Skip to content

Commit

Permalink
Merge pull request #9 from JetBrains/horis-canvas-control
Browse files Browse the repository at this point in the history
Add canvascontrols package.
  • Loading branch information
ISeleznev-HORIS committed Aug 6, 2019
2 parents 136b72a + 61e563d commit 2665f71
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 0 deletions.
25 changes: 25 additions & 0 deletions livemap/src/commonMain/kotlin/jetbrains/livemap/BaseLiveMap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jetbrains.livemap

import jetbrains.datalore.base.observable.event.EventHandler
import jetbrains.datalore.base.observable.event.EventSource
import jetbrains.datalore.base.observable.event.SimpleEventSource
import jetbrains.datalore.base.observable.property.Property
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.registration.Disposable
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl

abstract class BaseLiveMap : EventSource<Throwable>, Disposable {
private val throwableSource = SimpleEventSource<Throwable>()
val isLoading: Property<Boolean> = ValueProperty(true)

abstract fun draw(canvasControl: CanvasControl)

override fun addHandler(handler: EventHandler<in Throwable>): Registration {
return throwableSource.addHandler(handler)
}

protected fun fireThrowable(throwable: Throwable) {
throwableSource.fire(throwable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.visualization.base.canvas.CanvasControl

internal interface CanvasContent {
fun show(parentControl: CanvasControl)
fun hide()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.visualization.base.canvas.CanvasControl

internal class CanvasContentPresenter {
lateinit var canvasControl: CanvasControl
private var canvasContent: CanvasContent = EMPTY_CANVAS_CONTENT


fun show(content: CanvasContent) {
canvasContent.hide()
canvasContent = content
canvasContent.show(canvasControl)
}

fun clear() {
show(EMPTY_CANVAS_CONTENT)
}

private class EmptyContent : CanvasContent {
override fun show(parentControl: CanvasControl) {}

override fun hide() {}
}

companion object {
private val EMPTY_CANVAS_CONTENT = EmptyContent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.observable.event.EventHandler
import jetbrains.datalore.base.observable.event.EventSource
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.livemap.BaseLiveMap

class LiveMapContent(private val liveMap: BaseLiveMap) : CanvasContent, EventSource<Throwable> {

override fun show(parentControl: CanvasControl) {
liveMap.draw(parentControl)
}

override fun hide() {
liveMap.dispose()
}

override fun addHandler(handler: EventHandler<Throwable>): Registration {
return liveMap.addHandler(handler)
}

fun addHandler(handler: (Throwable) -> Unit): Registration {
return liveMap.addHandler(
object : EventHandler<Throwable> {
override fun onEvent(event: Throwable) {
handler(event)
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.async.Async
import jetbrains.datalore.base.observable.property.Properties
import jetbrains.datalore.base.observable.property.PropertyBinding
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.registration.Disposable
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.livemap.BaseLiveMap

class LiveMapPresenter : Disposable {
private val contentPresenter: CanvasContentPresenter
private var registration = Registration.EMPTY
private var isLoadingLiveMapRegistration = Registration.EMPTY
private var removed = false

private val initializing = ValueProperty(true)
private val liveMapIsLoading = ValueProperty(true)
val isLoading = Properties.or(initializing, liveMapIsLoading)

constructor() {
contentPresenter = CanvasContentPresenter()
}

// for tests
internal constructor(presenter: CanvasContentPresenter) {
contentPresenter = presenter
}

fun render(canvasControl: CanvasControl, liveMap: Async<BaseLiveMap>) {
contentPresenter.canvasControl = canvasControl

showSpinner()
liveMap.onResult(::showLiveMap, ::showError)
}

private fun showLiveMap(liveMap: BaseLiveMap) {
if (isLoadingLiveMapRegistration !== Registration.EMPTY) {
throw IllegalStateException("Unexpected")
}

initializing.set(false)
isLoadingLiveMapRegistration = PropertyBinding.bindOneWay(liveMap.isLoading, liveMapIsLoading)

setContent {
LiveMapContent(liveMap).also {
registration = it.addHandler(::showError)
}
}
}

private fun showSpinner() {
initializing.set(true)
setContent(::SpinnerContent)
}

private fun showError(throwable: Throwable) {
initializing.set(false)
liveMapIsLoading.set(false)
val message = throwable.message
setContent { MessageContent(message ?: "Undefined exception") }
}

private fun setContent(canvasContentSupplier: () -> CanvasContent) {
if (removed) {
return
}

contentPresenter.show(canvasContentSupplier())
}

override fun dispose() {
removed = true
registration.dispose()
isLoadingLiveMapRegistration.dispose()
contentPresenter.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.datalore.visualization.base.canvas.CanvasControlUtil.drawLater
import jetbrains.datalore.visualization.base.canvas.Context2d
import jetbrains.datalore.visualization.base.canvas.Context2d.TextAlign
import jetbrains.datalore.visualization.base.canvas.Context2d.TextBaseline
import jetbrains.datalore.visualization.base.canvas.SingleCanvasControl
import kotlin.math.max

internal class MessageContent(private val message: String) : CanvasContent {
private lateinit var canvasControl: SingleCanvasControl

override fun show(parentControl: CanvasControl) {
canvasControl = SingleCanvasControl(parentControl)

with(canvasControl.createCanvas()) {
drawText(context2d, DoubleVector(size.x.toDouble(), size.y.toDouble()))

takeSnapshot()
.onSuccess { snapshot ->
drawLater(parentControl) {
canvasControl.context.drawImage(
snapshot,
0.0,
0.0
)
}
}
}
}

override fun hide() {
canvasControl.dispose()
}

private fun drawText(context: Context2d, dimension: DoubleVector) =
with(context) {
val lines: List<String> = message.split("\n")

save()

setFillColor(BACKGROUND_COLOR)
fillRect(0.0, 0.0, dimension.x, dimension.y)

setTextBaseline(TextBaseline.TOP)
setTextAlign(TextAlign.LEFT)
setFillColor(FONT_COLOR)
setFont("400 " + FONT_SIZE + "px/" + FONT_HEIGHT + "px Helvetica, Arial, sans-serif")

val height = FONT_HEIGHT * lines.size
var width = 0.0

lines.forEach { width = max(width, measureText(it)) }

lines.indices.forEach {
fillText(lines[it], (dimension.x - width) / 2, (dimension.y - height) / 2 + it * FONT_HEIGHT)
}

restore()
}

companion object {
private const val FONT_SIZE = 17.0
private const val FONT_HEIGHT = 21.25
private const val FONT_COLOR = "#B3B3B3"
private const val BACKGROUND_COLOR = "#FFFFFF"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.datalore.visualization.base.canvas.CanvasControl.AnimationEventHandler
import jetbrains.datalore.visualization.base.canvas.CanvasControlUtil.setAnimationHandler
import jetbrains.datalore.visualization.base.canvas.Context2d
import jetbrains.datalore.visualization.base.canvas.SingleCanvasControl
import kotlin.math.PI

internal class SpinnerContent : CanvasContent {

private lateinit var registration: Registration
private lateinit var canvasControl: SingleCanvasControl
private lateinit var spinnerCenter: DoubleVector

override fun show(parentControl: CanvasControl) {
canvasControl = SingleCanvasControl(parentControl)

with(canvasControl.createCanvas()) {
context2d.drawStaticElements()

registration = setAnimationHandler(
parentControl,
object : AnimationEventHandler {
override fun onEvent(millisTime: Long): Boolean {
context2d.drawSpinner(millisTime)

takeSnapshot()
.onSuccess { canvasControl.context.drawImage(it, 0.0, 0.0) }
return true
}
}
)
}


}

override fun hide() {
canvasControl.dispose()
registration.dispose()
}

private fun Context2d.drawStaticElements() {
save()

setFont("400 " + FONT_SIZE + "px Helvetica, Arial, sans-serif")
val textWidth = measureText(LOADING_TEXT)

val spinnerWidth = 2 * RADIUS + LINE_WIDTH
val width = spinnerWidth + SPACE + textWidth
val dimension = canvasControl.size
spinnerCenter = DoubleVector((dimension.x - width) / 2 + spinnerWidth / 2, dimension.y / 2.0)

setFillColor(BACKGROUND_COLOR)
fillRect(0.0, 0.0, dimension.x.toDouble(), dimension.y.toDouble())

setTextBaseline(Context2d.TextBaseline.MIDDLE)
setTextAlign(Context2d.TextAlign.LEFT)
setFillColor(FONT_COLOR)
fillText(LOADING_TEXT, (dimension.x + width) / 2 - textWidth, dimension.y / 2.0)

restore()
}

private fun Context2d.drawSpinner(time: Long) {
save()

setFillColor(BACKGROUND_COLOR)
fillRect(
spinnerCenter.x - BACK_RADIUS,
spinnerCenter.y - BACK_RADIUS,
2 * BACK_RADIUS,
2 * BACK_RADIUS
)

drawSpinnerArc(CIRCLE_COLOR, 0.0, 2 * PI)

val angle = 2.0 * PI * (time % LOOP_DURATION).toDouble() / LOOP_DURATION
drawSpinnerArc(ARC_COLOR, angle, ARC_LENGTH)

restore()
}

private fun Context2d.drawSpinnerArc(color: String, startAngle: Double, arcAngle: Double) {
setLineWidth(LINE_WIDTH)
setStrokeColor(color)
beginPath()
arc(spinnerCenter.x, spinnerCenter.y, RADIUS, startAngle, startAngle + arcAngle)
stroke()
}

companion object {
private const val BACKGROUND_COLOR = "#FFFFFF"
private const val LINE_WIDTH = 0.9
private const val RADIUS = 11.5
private const val BACK_RADIUS = RADIUS + LINE_WIDTH
private const val CIRCLE_COLOR = "#E8E8E8"
private const val ARC_COLOR = "#00BFFF"
private const val ARC_LENGTH = PI / 2
private const val LOOP_DURATION: Long = 1000
private const val SPACE = 15.0
private const val LOADING_TEXT = "Loading..."
private const val FONT_SIZE = 12.0
private const val FONT_COLOR = "#616161"
}
}

0 comments on commit 2665f71

Please sign in to comment.