-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from JetBrains/horis-canvas-control
Add canvascontrols package.
- Loading branch information
Showing
7 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
livemap/src/commonMain/kotlin/jetbrains/livemap/BaseLiveMap.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/CanvasContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
29 changes: 29 additions & 0 deletions
29
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/CanvasContentPresenter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/LiveMapContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
) | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/LiveMapPresenter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/MessageContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
|
||
} |
109 changes: 109 additions & 0 deletions
109
livemap/src/commonMain/kotlin/jetbrains/livemap/canvascontrols/SpinnerContent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |