From 1bb52206a0cb8877ecdd16862213af4a586859c0 Mon Sep 17 00:00:00 2001 From: soywiz Date: Sat, 7 Dec 2019 23:51:57 +0100 Subject: [PATCH] More work on UI --- .../kotlin/{UI.kt => DefaultUISkin.kt} | 101 ------------- sample-ui/src/commonMain/kotlin/MouseDrag.kt | 85 +++++++++++ sample-ui/src/commonMain/kotlin/SampleUi.kt | 139 +++++++++++++++++- sample-ui/src/commonMain/kotlin/UIButton.kt | 75 ++++++++++ sample-ui/src/commonMain/kotlin/UISkin.kt | 7 + sample-ui/src/commonMain/kotlin/UIView.kt | 41 ++++++ 6 files changed, 343 insertions(+), 105 deletions(-) rename sample-ui/src/commonMain/kotlin/{UI.kt => DefaultUISkin.kt} (54%) create mode 100644 sample-ui/src/commonMain/kotlin/MouseDrag.kt create mode 100644 sample-ui/src/commonMain/kotlin/UIButton.kt create mode 100644 sample-ui/src/commonMain/kotlin/UISkin.kt create mode 100644 sample-ui/src/commonMain/kotlin/UIView.kt diff --git a/sample-ui/src/commonMain/kotlin/UI.kt b/sample-ui/src/commonMain/kotlin/DefaultUISkin.kt similarity index 54% rename from sample-ui/src/commonMain/kotlin/UI.kt rename to sample-ui/src/commonMain/kotlin/DefaultUISkin.kt index 06541eb..b255268 100644 --- a/sample-ui/src/commonMain/kotlin/UI.kt +++ b/sample-ui/src/commonMain/kotlin/DefaultUISkin.kt @@ -1,21 +1,6 @@ -import com.soywiz.kds.* -import com.soywiz.korag.* -import com.soywiz.korge.component.* -import com.soywiz.korge.html.* -import com.soywiz.korge.input.* -import com.soywiz.korge.render.* -import com.soywiz.korge.view.* import com.soywiz.korim.bitmap.* import com.soywiz.korim.format.* import com.soywiz.korio.util.encoding.* -import com.soywiz.korma.geom.* -import kotlin.properties.* - -open class UISkin( - val normal: BmpSlice, - val hover: BmpSlice, - val down: BmpSlice -) private val DEFAULT_UI_SKIN_IMG by lazy { PNG.decode( @@ -29,89 +14,3 @@ object DefaultUISkin : UISkin( down = DEFAULT_UI_SKIN_IMG.sliceWithSize(127, 0, 64, 64) ) -open class UIButton( - width: Double = 128.0, - height: Double = 64.0, - label: String = "Button", - skin: UISkin = DefaultUISkin -) : UIView() { - var skin: UISkin by Delegates.observable(skin) { _, _, _ -> updateState() } - var label by Delegates.observable(label) { _, _, _ -> updateState() } - private val rect = ninePatch(skin.normal, width, height, 16.0 / 64.0, 16.0 / 64.0, (64.0 - 16.0) / 64.0, (64.0 - 16.0) / 64.0) {} - private val text = text(label) - private var bover by Delegates.observable(false) { _, _, _ -> updateState() } - private var bpressing by Delegates.observable(false) { _, _, _ -> updateState() } - init { - this.width = width - this.height = height - mouse { - onOver { - bover = true - } - onOut { - bover = false - } - onDown { - bpressing = true - } - onUpAnywhere { - bpressing = false - } - } - updateState() - } - - private fun updateState() { - when { - bpressing -> { - rect.tex = skin.down - } - bover -> { - rect.tex = skin.hover - } - else -> { - rect.tex = skin.normal - } - } - text.format = Html.Format(align = Html.Alignment.MIDDLE_CENTER) - text.setTextBounds(Rectangle(0, 0, width, height)) - text.setText(label) - } - - override fun updatedSize() { - super.updatedSize() - rect.width = width - rect.height = height - } -} - -open class UIView : Container() { - override var width: Double by Delegates.observable(96.0) { prop, old, new -> updatedSize() } - override var height: Double by Delegates.observable(32.0) { prop, old, new -> updatedSize() } - - protected open fun updatedSize() { - } - - override fun renderInternal(ctx: RenderContext) { - registerUISupportOnce(stage) - super.renderInternal(ctx) - } -} - -fun registerUISupportOnce(stage: Stage?) { - if (stage == null) return - if (stage.getExtra("uiSupport") == true) return - stage.setExtra("uiSupport", true) - stage.keys { - onKeyDown { - - } - } - stage?.getOrCreateComponent { stage -> - object : UpdateComponentWithViews { - override val view: View = stage - override fun update(views: Views, ms: Double) { - } - } - } -} diff --git a/sample-ui/src/commonMain/kotlin/MouseDrag.kt b/sample-ui/src/commonMain/kotlin/MouseDrag.kt new file mode 100644 index 0000000..fd40478 --- /dev/null +++ b/sample-ui/src/commonMain/kotlin/MouseDrag.kt @@ -0,0 +1,85 @@ +import com.soywiz.korge.input.* +import com.soywiz.korge.view.* +import com.soywiz.korma.geom.* + +// @TODO: Move to Korge +data class MouseDragInfo( + var dx: Double = 0.0, + var dy: Double = 0.0, + var start: Boolean = false, + var end: Boolean = false +) { + fun set(dx: Double, dy: Double, start: Boolean, end: Boolean) = this.apply { + this.dx = dx + this.dy = dy + this.start = start + this.end = end + } +} + +fun T.onMouseDrag(callback: Views.(MouseDragInfo) -> Unit): T { + var dragging = false + var sx = 0.0 + var sy = 0.0 + var cx = 0.0 + var cy = 0.0 + val view = this + + val info = MouseDragInfo() + val mousePos = Point() + + fun views() = view.stage!!.views + + fun updateMouse() { + val views = views() + mousePos.setTo( + views.nativeMouseX, + views.nativeMouseY + ) + } + + this.mouse { + onDown { + updateMouse() + dragging = true + sx = mousePos.x + sy = mousePos.y + info.dx = 0.0 + info.dy = 0.0 + callback(views(), info.set(0.0, 0.0, true, false)) + } + onUpAnywhere { + if (dragging) { + updateMouse() + dragging = false + cx = mousePos.x + cy = mousePos.y + callback(views(), info.set(cx - sx, cy - sy, false, true)) + } + } + onMoveAnywhere { + if (dragging) { + updateMouse() + cx = mousePos.x + cy = mousePos.y + callback(views(), info.set(cx - sx, cy - sy, false, false)) + } + } + } + return this +} + +fun T.draggable(): T = this.apply { + val view = this + var sx = 0.0 + var sy = 0.0 + onMouseDrag { info -> + if (info.start) { + sx = view.x + sy = view.y + } + view.x = sx + info.dx + view.y = sy + info.dy + //println("DRAG: $dx, $dy, $start, $end") + } +} diff --git a/sample-ui/src/commonMain/kotlin/SampleUi.kt b/sample-ui/src/commonMain/kotlin/SampleUi.kt index 0f25ed2..657cf65 100644 --- a/sample-ui/src/commonMain/kotlin/SampleUi.kt +++ b/sample-ui/src/commonMain/kotlin/SampleUi.kt @@ -1,10 +1,141 @@ +import com.soywiz.kmem.* import com.soywiz.korge.* +import com.soywiz.korge.input.* +import com.soywiz.korge.view.* import com.soywiz.korgw.* -import kotlin.jvm.* +import com.soywiz.korim.color.* +import com.soywiz.korio.async.* +import com.soywiz.korma.geom.* +import kotlin.math.* +import kotlin.properties.* suspend fun main() = Korge(quality = GameWindow.Quality.PERFORMANCE, title = "UI") { - addChild(UIButton(256.0, 32.0).apply { - x = 64.0 - y = 64.0 + /* + uiButton(256.0, 32.0) { + position(64, 64) + onClick { + println("CLICKED!") + } + draggable() + } + */ + addChild(UIScrollBar(256.0, 32.0, 0.0, 16.0, 64.0).position(64, 64).apply { + onChange { + println(it.ratio) + } + }) + addChild(UIScrollBar(32.0, 256.0, 0.0, 16.0, 64.0).position(64, 128).apply { + onChange { + println(it.ratio) + } }) } + +open class UIScrollBar( + width: Double, + height: Double, + current: Double, + pageSize: Double, + totalSize: Double, + buttonSize: Double = 32.0, + direction: Direction = if (width > height) Direction.Horizontal else Direction.Vertical, + var stepSize: Double = pageSize / 10.0 +) : UIView() { + val onChange = Signal() + enum class Direction { Vertical, Horizontal } + var current by Delegates.observable(current) { _, _, _ -> updatedPos() } + var pageSize by Delegates.observable(pageSize) { _, _, _ -> updatedPos() } + var totalSize by Delegates.observable(totalSize) { _, _, _ -> updatedPos() } + var direction by Delegates.observable(direction) { _, _, _ -> reshape() } + val isHorizontal get() = direction == Direction.Horizontal + val isVertical get() = direction == Direction.Vertical + override var ratio: Double + set(value) = run { current = value.clamp01() * (totalSize - pageSize) } + get() = (current / (totalSize - pageSize)).clamp(0.0, 1.0) + override var width: Double by Delegates.observable(width) { _, _, _ -> reshape() } + override var height: Double by Delegates.observable(height) { _, _, _ -> reshape() } + var buttonSize by Delegates.observable(buttonSize) { _, _, _ -> reshape() } + val buttonWidth get() = if (isHorizontal) buttonSize else width + val buttonHeight get() = if (isHorizontal) height else buttonSize + val clientWidth get() = if (isHorizontal) width - buttonWidth * 2 else width + val clientHeight get() = if (isHorizontal) height else height - buttonHeight * 2 + + protected val background = solidRect(100, 100, Colors.DARKGREY) + protected val lessButton = uiButton(16, 16, "-") + protected val moreButton = uiButton(16, 16, "+") + protected val caretButton = uiButton(16, 16, "") + + protected val views get() = stage?.views + + init { + reshape() + + var slx: Double = 0.0 + var sly: Double = 0.0 + var iratio: Double = 0.0 + var sratio: Double = 0.0 + val tempP = Point() + + lessButton.onDown { + deltaCurrent(-stepSize) + reshape() + } + moreButton.onDown { + deltaCurrent(+stepSize) + reshape() + } + background.onClick { + val pos = if (isHorizontal) caretButton.localMouseX(views!!) else caretButton.localMouseY(views!!) + deltaCurrent(pageSize * pos.sign) + } + caretButton.onMouseDrag { + val lmouse = background.localMouseXY(views, tempP) + val lx = lmouse.x + val ly = lmouse.y + val cratio = if (isHorizontal) lmouse.x / background.width else lmouse.y / background.height + if (it.start) { + slx = lx + sly = ly + iratio = ratio + sratio = cratio + } + val dratio = cratio - sratio + ratio = iratio + dratio + reshape() + } + } + + private fun deltaCurrent(value: Double) { + current = (current + value).clamp(0.0, totalSize) + } + + private fun reshape() { + if (isHorizontal) { + background.position(buttonWidth, 0).size(clientWidth, clientHeight) + lessButton.position(0, 0).size(buttonWidth, buttonHeight) + moreButton.position(width - buttonWidth, 0).size(buttonWidth, buttonHeight) + val caretWidth = clientWidth * (pageSize / totalSize) + caretButton.position(buttonWidth + (clientWidth - caretWidth) * ratio, 0).size(caretWidth, buttonHeight) + } else { + background.position(0, buttonHeight).size(clientWidth, clientHeight) + lessButton.position(0, 0).size(buttonWidth, buttonHeight) + moreButton.position(0, height - buttonHeight).size(buttonWidth, buttonHeight) + val caretHeight = clientHeight * (pageSize / totalSize) + caretButton.position(0, buttonHeight + (clientHeight - caretHeight) * ratio).size(buttonWidth, caretHeight) + } + } + + private fun updatedPos() { + reshape() + onChange(this) + } +} + +// @TODO: Move to Korge +inline fun T.size(width: Number, height: Number): T = this.apply { + this.width = width.toDouble() + this.height = height.toDouble() +} + +// @TODO: Move to Korge +inline fun T.localMouseXY(views: Views, target: Point = Point()): Point = target.setTo(localMouseX(views), localMouseY(views)) diff --git a/sample-ui/src/commonMain/kotlin/UIButton.kt b/sample-ui/src/commonMain/kotlin/UIButton.kt new file mode 100644 index 0000000..927998d --- /dev/null +++ b/sample-ui/src/commonMain/kotlin/UIButton.kt @@ -0,0 +1,75 @@ +import com.soywiz.korge.html.* +import com.soywiz.korge.input.* +import com.soywiz.korge.view.* +import com.soywiz.korim.color.* +import com.soywiz.korma.geom.* +import kotlin.properties.* + +inline fun Container.uiButton( + width: Number = 128, + height: Number = 64, + label: String = "Button", + skin: UISkin = DefaultUISkin, + block: UIButton.() -> Unit = {} +): UIButton = UIButton(width.toDouble(), height.toDouble(), label, skin).also { addChild(it) }.apply(block) + +open class UIButton( + width: Double = 128.0, + height: Double = 64.0, + label: String = "Button", + skin: UISkin = DefaultUISkin +) : UIView() { + var skin: UISkin by Delegates.observable(skin) { _, _, _ -> updateState() } + var label by Delegates.observable(label) { _, _, _ -> updateState() } + private val rect = ninePatch(skin.normal, width, height, 16.0 / 64.0, 16.0 / 64.0, (64.0 - 16.0) / 64.0, (64.0 - 16.0) / 64.0) {} + private val textShadow = text(label).also { it.position(1, 1) } + private val text = text(label) + private var bover by Delegates.observable(false) { _, _, _ -> updateState() } + private var bpressing by Delegates.observable(false) { _, _, _ -> updateState() } + init { + this.width = width + this.height = height + mouse { + onOver { + bover = true + } + onOut { + bover = false + } + onDown { + bpressing = true + } + onUpAnywhere { + bpressing = false + } + } + updateState() + } + + private fun updateState() { + when { + bpressing -> { + rect.tex = skin.down + } + bover -> { + rect.tex = skin.hover + } + else -> { + rect.tex = skin.normal + } + } + text.format = Html.Format(align = Html.Alignment.MIDDLE_CENTER) + text.setTextBounds(Rectangle(0, 0, width, height)) + text.setText(label) + textShadow.format = Html.Format(align = Html.Alignment.MIDDLE_CENTER, color = Colors.BLACK.withA(64)) + textShadow.setTextBounds(Rectangle(0, 0, width, height)) + textShadow.setText(label) + } + + override fun updatedSize() { + super.updatedSize() + rect.width = width + rect.height = height + updateState() + } +} diff --git a/sample-ui/src/commonMain/kotlin/UISkin.kt b/sample-ui/src/commonMain/kotlin/UISkin.kt new file mode 100644 index 0000000..e4386d8 --- /dev/null +++ b/sample-ui/src/commonMain/kotlin/UISkin.kt @@ -0,0 +1,7 @@ +import com.soywiz.korim.bitmap.* + +open class UISkin( + val normal: BmpSlice, + val hover: BmpSlice, + val down: BmpSlice +) diff --git a/sample-ui/src/commonMain/kotlin/UIView.kt b/sample-ui/src/commonMain/kotlin/UIView.kt new file mode 100644 index 0000000..e11d259 --- /dev/null +++ b/sample-ui/src/commonMain/kotlin/UIView.kt @@ -0,0 +1,41 @@ +import com.soywiz.kds.* +import com.soywiz.korge.component.* +import com.soywiz.korge.input.* +import com.soywiz.korge.render.* +import com.soywiz.korge.view.* +import kotlin.properties.* + +open class UIView : Container() { + override var width: Double by Delegates.observable(96.0) { prop, old, new -> updatedSize() } + override var height: Double by Delegates.observable(32.0) { prop, old, new -> updatedSize() } + + protected open fun updatedSize() { + } + + override fun renderInternal(ctx: RenderContext) { + registerUISupportOnce() + super.renderInternal(ctx) + } + + private var registered = false + private fun registerUISupportOnce() { + if (registered) return + val stage = stage ?: return + registered = true + if (stage.getExtra("uiSupport") == true) return + stage.setExtra("uiSupport", true) + stage.keys { + onKeyDown { + + } + } + stage?.getOrCreateComponent { stage -> + object : UpdateComponentWithViews { + override val view: View = stage + override fun update(views: Views, ms: Double) { + } + } + } + } + +}