More work on UI

This commit is contained in:
soywiz
2019-12-07 23:51:57 +01:00
parent 07e35acfb2
commit 1bb52206a0
6 changed files with 343 additions and 105 deletions

View File

@@ -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) {
}
}
}
}

View File

@@ -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 : View> 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 : View> 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")
}
}

View File

@@ -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<UIScrollBar>()
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 : View> T.size(width: Number, height: Number): T = this.apply {
this.width = width.toDouble()
this.height = height.toDouble()
}
// @TODO: Move to Korge
inline fun <T : View> T.localMouseXY(views: Views, target: Point = Point()): Point = target.setTo(localMouseX(views), localMouseY(views))

View File

@@ -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()
}
}

View File

@@ -0,0 +1,7 @@
import com.soywiz.korim.bitmap.*
open class UISkin(
val normal: BmpSlice,
val hover: BmpSlice,
val down: BmpSlice
)

View File

@@ -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) {
}
}
}
}
}