From c028c09ac92e5c39733d972016b8f226d1376bb7 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 30 Oct 2020 21:42:29 +0100 Subject: [PATCH] Added TextArea, Menu and Select. Added some fields to InfoWidget and added the server select widget. --- .../observable/value/DependentVal.kt | 2 +- ...FlatTransformedVal.kt => FlatMappedVal.kt} | 2 +- .../value/{TransformedVal.kt => MappedVal.kt} | 2 +- .../world/phantasmal/observable/value/Val.kt | 12 +- .../observable/value/ValExtensions.kt | 8 +- .../observable/value/list/ListVal.kt | 2 +- .../observable/value/list/MutableListVal.kt | 15 +- .../observable/value/list/SimpleListVal.kt | 25 +- ...=> FlatMappedValDependentValEmitsTests.kt} | 10 +- ...kt => FlatMappedValNestedValEmitsTests.kt} | 8 +- ...ansformedValTests.kt => MappedValTests.kt} | 6 +- .../observable/value/list/ListValTests.kt | 2 +- .../application/widgets/NavigationWidget.kt | 37 ++- .../phantasmal/web/core/stores/UiStore.kt | 2 +- .../phantasmal/web/core/widgets/DockWidget.kt | 6 +- .../huntOptimizer/models/HuntMethodModel.kt | 2 +- .../huntOptimizer/stores/HuntMethodStore.kt | 4 +- .../controllers/QuestInfoController.kt | 14 +- .../questEditor/rendering/QuestRenderer.kt | 2 +- .../questEditor/stores/QuestEditorStore.kt | 3 + .../questEditor/widgets/QuestInfoWidget.kt | 49 +++- .../main/kotlin/world/phantasmal/webui}/Js.kt | 2 +- .../world/phantasmal/webui/dom/DomCreation.kt | 3 + .../world/phantasmal/webui/widgets/Button.kt | 15 +- .../world/phantasmal/webui/widgets/Control.kt | 3 +- .../phantasmal/webui/widgets/DoubleInput.kt | 6 +- .../world/phantasmal/webui/widgets/Input.kt | 14 +- .../phantasmal/webui/widgets/IntInput.kt | 6 +- .../webui/widgets/LabelledControl.kt | 12 +- .../world/phantasmal/webui/widgets/Menu.kt | 220 ++++++++++++++++++ .../phantasmal/webui/widgets/NumberInput.kt | 6 +- .../world/phantasmal/webui/widgets/Select.kt | 161 +++++++++++++ .../phantasmal/webui/widgets/TabContainer.kt | 2 +- .../phantasmal/webui/widgets/TextArea.kt | 102 ++++++++ .../phantasmal/webui/widgets/TextInput.kt | 8 +- .../world/phantasmal/webui/widgets/Widget.kt | 66 ++++-- 36 files changed, 729 insertions(+), 110 deletions(-) rename observable/src/commonMain/kotlin/world/phantasmal/observable/value/{FlatTransformedVal.kt => FlatMappedVal.kt} (98%) rename observable/src/commonMain/kotlin/world/phantasmal/observable/value/{TransformedVal.kt => MappedVal.kt} (88%) rename observable/src/commonTest/kotlin/world/phantasmal/observable/value/{FlatTransformedValDependentValEmitsTests.kt => FlatMappedValDependentValEmitsTests.kt} (76%) rename observable/src/commonTest/kotlin/world/phantasmal/observable/value/{FlatTransformedValNestedValEmitsTests.kt => FlatMappedValNestedValEmitsTests.kt} (57%) rename observable/src/commonTest/kotlin/world/phantasmal/observable/value/{TransformedValTests.kt => MappedValTests.kt} (66%) rename {web/src/main/kotlin/world/phantasmal/web/core => webui/src/main/kotlin/world/phantasmal/webui}/Js.kt (72%) create mode 100644 webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt create mode 100644 webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt create mode 100644 webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt index 0a14cd56..bd6bef8b 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt @@ -7,7 +7,7 @@ import world.phantasmal.core.unsafeToNonNull /** * Starts observing its dependencies when the first observer on this val is registered. Stops * observing its dependencies when the last observer on this val is disposed. This way no extra - * disposables need to be managed when e.g. [transform] is used. + * disposables need to be managed when e.g. [map] is used. */ abstract class DependentVal( private val dependencies: Iterable>, diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatTransformedVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt similarity index 98% rename from observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatTransformedVal.kt rename to observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt index bb7c23cc..b3f14d64 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatTransformedVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt @@ -4,7 +4,7 @@ import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.disposable import world.phantasmal.core.unsafeToNonNull -class FlatTransformedVal( +class FlatMappedVal( dependencies: Iterable>, private val compute: () -> Val, ) : DependentVal(dependencies) { diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/TransformedVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/MappedVal.kt similarity index 88% rename from observable/src/commonMain/kotlin/world/phantasmal/observable/value/TransformedVal.kt rename to observable/src/commonMain/kotlin/world/phantasmal/observable/value/MappedVal.kt index 28da0a79..4200fd82 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/TransformedVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/MappedVal.kt @@ -1,6 +1,6 @@ package world.phantasmal.observable.value -class TransformedVal( +class MappedVal( dependencies: Iterable>, private val compute: () -> T, ) : DependentVal(dependencies) { diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt index 1723eeb2..a1800a59 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt @@ -17,12 +17,12 @@ interface Val : Observable { */ fun observe(callNow: Boolean = false, observer: ValObserver): Disposable - fun transform(transform: (T) -> R): Val = - TransformedVal(listOf(this)) { transform(value) } + fun map(transform: (T) -> R): Val = + MappedVal(listOf(this)) { transform(value) } - fun transform(v2: Val, transform: (T, T2) -> R): Val = - TransformedVal(listOf(this, v2)) { transform(value, v2.value) } + fun map(v2: Val, transform: (T, T2) -> R): Val = + MappedVal(listOf(this, v2)) { transform(value, v2.value) } - fun flatTransform(transform: (T) -> Val): Val = - FlatTransformedVal(listOf(this)) { transform(value) } + fun flatMap(transform: (T) -> Val): Val = + FlatMappedVal(listOf(this)) { transform(value) } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt index 6473d044..3b490c7c 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValExtensions.kt @@ -1,13 +1,13 @@ package world.phantasmal.observable.value infix fun Val.and(other: Val): Val = - transform(other) { a, b -> a && b } + map(other) { a, b -> a && b } infix fun Val.or(other: Val): Val = - transform(other) { a, b -> a || b } + map(other) { a, b -> a || b } // Use != because of https://youtrack.jetbrains.com/issue/KT-31277. infix fun Val.xor(other: Val): Val = - transform(other) { a, b -> a != b } + map(other) { a, b -> a != b } -operator fun Val.not(): Val = transform { !it } +operator fun Val.not(): Val = map { !it } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt index 4eb58d03..3a1c00ec 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt @@ -3,7 +3,7 @@ package world.phantasmal.observable.value.list import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.value.Val -interface ListVal : Val>, List { +interface ListVal : Val> { val sizeVal: Val fun observeList(observer: ListValObserver): Disposable diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt index e179feae..e7e9184e 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/MutableListVal.kt @@ -1,8 +1,17 @@ package world.phantasmal.observable.value.list import world.phantasmal.observable.value.MutableVal -import kotlin.reflect.KProperty -interface MutableListVal : ListVal, MutableVal>, MutableList { - override operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableList = this +interface MutableListVal : ListVal, MutableVal> { + fun set(index: Int, element: E): E + + fun add(element: E) + + fun add(index: Int, element: E) + + fun removeAt(index: Int): E + + fun replaceAll(elements: Sequence) + + fun clear() } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt index 315834ea..c46b5e19 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt @@ -15,7 +15,7 @@ class SimpleListVal( * will be propagated via ElementChange events. */ private val extractObservables: ObservablesExtractor? = null, -) : AbstractMutableList(), MutableListVal { +) : MutableListVal { override var value: List = elements set(value) { val removed = ArrayList(elements) @@ -34,8 +34,6 @@ class SimpleListVal( override val sizeVal: Val = mutableSizeVal - override val size: Int by sizeVal - /** * Internal observers which observe observables related to this list's elements so that their * changes can be propagated via ElementChange events. @@ -52,14 +50,18 @@ class SimpleListVal( */ private val observers = mutableListOf>>() - override fun get(index: Int): E = elements[index] - override fun set(index: Int, element: E): E { val removed = elements.set(index, element) finalizeUpdate(ListValChangeEvent.Change(index, listOf(removed), listOf(element))) return removed } + override fun add(element: E) { + val index = elements.size + elements.add(element) + finalizeUpdate(ListValChangeEvent.Change(index, emptyList(), listOf(element))) + } + override fun add(index: Int, element: E) { elements.add(index, element) finalizeUpdate(ListValChangeEvent.Change(index, emptyList(), listOf(element))) @@ -71,6 +73,19 @@ class SimpleListVal( return removed } + override fun replaceAll(elements: Sequence) { + val removed = ArrayList(this.elements) + this.elements.clear() + this.elements.addAll(elements) + finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements)) + } + + override fun clear() { + val removed = ArrayList(elements) + elements.clear() + finalizeUpdate(ListValChangeEvent.Change(0, removed, emptyList())) + } + override fun observe(observer: Observer>): Disposable = observe(callNow = false, observer) diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValDependentValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValDependentValEmitsTests.kt similarity index 76% rename from observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValDependentValEmitsTests.kt rename to observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValDependentValEmitsTests.kt index 8ef83a11..7e36364f 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValDependentValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValDependentValEmitsTests.kt @@ -5,9 +5,9 @@ import kotlin.test.assertEquals import kotlin.test.assertNull /** - * In these tests the direct dependency of the [FlatTransformedVal] changes. + * In these tests the direct dependency of the [FlatMappedVal] changes. */ -class FlatTransformedValDependentValEmitsTests : RegularValTests() { +class FlatMappedValDependentValEmitsTests : RegularValTests() { /** * This is a regression test, it's important that this exact sequence of statements stays the * same. @@ -15,7 +15,7 @@ class FlatTransformedValDependentValEmitsTests : RegularValTests() { @Test fun emits_a_change_when_its_direct_val_dependency_changes() = test { val v = SimpleVal(SimpleVal(7)) - val fv = FlatTransformedVal(listOf(v)) { v.value } + val fv = FlatMappedVal(listOf(v)) { v.value } var observedValue: Int? = null disposer.add( @@ -35,13 +35,13 @@ class FlatTransformedValDependentValEmitsTests : RegularValTests() { override fun create(): ValAndEmit<*> { val v = SimpleVal(SimpleVal(5)) - val value = FlatTransformedVal(listOf(v)) { v.value } + val value = FlatMappedVal(listOf(v)) { v.value } return ValAndEmit(value) { v.value = SimpleVal(v.value.value + 5) } } override fun createBoolean(bool: Boolean): ValAndEmit { val v = SimpleVal(SimpleVal(bool)) - val value = FlatTransformedVal(listOf(v)) { v.value } + val value = FlatMappedVal(listOf(v)) { v.value } return ValAndEmit(value) { v.value = SimpleVal(!v.value.value) } } } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValNestedValEmitsTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValNestedValEmitsTests.kt similarity index 57% rename from observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValNestedValEmitsTests.kt rename to observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValNestedValEmitsTests.kt index 44ba2c4c..a4db9dec 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatTransformedValNestedValEmitsTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/FlatMappedValNestedValEmitsTests.kt @@ -1,18 +1,18 @@ package world.phantasmal.observable.value /** - * In these tests the dependency of the [FlatTransformedVal]'s direct dependency changes. + * In these tests the dependency of the [FlatMappedVal]'s direct dependency changes. */ -class FlatTransformedValNestedValEmitsTests : RegularValTests() { +class FlatMappedValNestedValEmitsTests : RegularValTests() { override fun create(): ValAndEmit<*> { val v = SimpleVal(SimpleVal(5)) - val value = FlatTransformedVal(listOf(v)) { v.value } + val value = FlatMappedVal(listOf(v)) { v.value } return ValAndEmit(value) { v.value.value += 5 } } override fun createBoolean(bool: Boolean): ValAndEmit { val v = SimpleVal(SimpleVal(bool)) - val value = FlatTransformedVal(listOf(v)) { v.value } + val value = FlatMappedVal(listOf(v)) { v.value } return ValAndEmit(value) { v.value.value = !v.value.value } } } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/TransformedValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/MappedValTests.kt similarity index 66% rename from observable/src/commonTest/kotlin/world/phantasmal/observable/value/TransformedValTests.kt rename to observable/src/commonTest/kotlin/world/phantasmal/observable/value/MappedValTests.kt index 03dea243..af0c7a5b 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/TransformedValTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/MappedValTests.kt @@ -1,15 +1,15 @@ package world.phantasmal.observable.value -class TransformedValTests : RegularValTests() { +class MappedValTests : RegularValTests() { override fun create(): ValAndEmit<*> { val v = SimpleVal(0) - val value = TransformedVal(listOf(v)) { 2 * v.value } + val value = MappedVal(listOf(v)) { 2 * v.value } return ValAndEmit(value) { v.value += 2 } } override fun createBoolean(bool: Boolean): ValAndEmit { val v = SimpleVal(bool) - val value = TransformedVal(listOf(v)) { v.value } + val value = MappedVal(listOf(v)) { v.value } return ValAndEmit(value) { v.value = !v.value } } } diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/ListValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/ListValTests.kt index c3b97256..c723b5c6 100644 --- a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/ListValTests.kt +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/ListValTests.kt @@ -15,7 +15,7 @@ abstract class ListValTests : ValTests() { @Test fun listVal_updates_sizeVal_correctly() = test { - val (list: List<*>, add) = create() + val (list: ListVal<*>, add) = create() assertEquals(0, list.sizeVal.value) diff --git a/web/src/main/kotlin/world/phantasmal/web/application/widgets/NavigationWidget.kt b/web/src/main/kotlin/world/phantasmal/web/application/widgets/NavigationWidget.kt index 4964dec5..b99c5164 100644 --- a/web/src/main/kotlin/world/phantasmal/web/application/widgets/NavigationWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/application/widgets/NavigationWidget.kt @@ -2,12 +2,16 @@ package world.phantasmal.web.application.widgets import kotlinx.coroutines.CoroutineScope import org.w3c.dom.Node +import world.phantasmal.observable.value.trueVal import world.phantasmal.web.application.controllers.NavigationController import world.phantasmal.webui.dom.div +import world.phantasmal.webui.widgets.Select import world.phantasmal.webui.widgets.Widget -class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationController) : - Widget(scope) { +class NavigationWidget( + scope: CoroutineScope, + private val ctrl: NavigationController, +) : Widget(scope) { override fun Node.createElement() = div { className = "pw-application-navigation" @@ -15,6 +19,24 @@ class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationContro ctrl.tools.forEach { (tool, active) -> addChild(PwToolButton(scope, tool, active) { ctrl.setCurrentTool(tool) }) } + + div { + className = "pw-application-navigation-spacer" + } + div { + className = "pw-application-navigation-right" + + val serverSelect = Select( + scope, + disabled = trueVal(), + label = "Server:", + items = listOf("Ephinea"), + selected = "Ephinea", + tooltip = "Only Ephinea is supported at the moment", + ) + addWidget(serverSelect.label!!) + addChild(serverSelect) + } } companion object { @@ -35,18 +57,13 @@ class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationContro flex-grow: 1; } - .pw-application-navigation-server { + .pw-application-navigation-right { display: flex; align-items: center; } - .pw-application-navigation-server > * { - margin: 0 2px; - } - - .pw-application-navigation-time { - display: flex; - align-items: center; + .pw-application-navigation-right > * { + margin: 1px 2px; } .pw-application-navigation-github { diff --git a/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt b/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt index bb90cfc6..e225059b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/stores/UiStore.kt @@ -81,7 +81,7 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) toolToActive = tools .map { tool -> - tool to currentTool.transform { it == tool } + tool to currentTool.map { it == tool } } .toMap() diff --git a/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt b/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt index 54dab99b..cb95cce0 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/core/widgets/DockWidget.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineScope import org.w3c.dom.Node import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.falseVal -import world.phantasmal.web.core.newJsObject +import world.phantasmal.webui.newJsObject import world.phantasmal.web.externals.GoldenLayout import world.phantasmal.webui.dom.div import world.phantasmal.webui.widgets.Widget @@ -84,7 +84,9 @@ class DockWidget( idToCreate.forEach { (id, create) -> goldenLayout.registerComponent(id) { container: GoldenLayout.Container -> val node = container.getElement()[0] as Node - node.addChild(create(scope)) + val widget = create(scope) + node.addChild(widget) + widget.focus() } } diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt index abe5fea5..7d83d4b4 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/models/HuntMethodModel.kt @@ -26,7 +26,7 @@ class HuntMethodModel( */ val userTime: Val = _userTime - val time: Val = userTime.transform { it ?: defaultTime } + val time: Val = userTime.map { it ?: defaultTime } fun setUserTime(userTime: Duration?): HuntMethodModel { _userTime.value = userTime diff --git a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/stores/HuntMethodStore.kt b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/stores/HuntMethodStore.kt index cc617c9e..c1dcabf2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/stores/HuntMethodStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/huntOptimizer/stores/HuntMethodStore.kt @@ -93,9 +93,7 @@ class HuntMethodStore( } withContext(UiDispatcher) { - // TODO: Add more performant replaceAll method. - _methods.clear() - _methods.addAll(methods) + _methods.replaceAll(methods) } } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestInfoController.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestInfoController.kt index 06a28673..ef99947f 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestInfoController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestInfoController.kt @@ -7,8 +7,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.webui.controllers.Controller class QuestInfoController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) { - val unavailable = store.currentQuest.transform { it == null } - val episode: Val = store.currentQuest.transform { it?.episode?.name ?: "" } - val id: Val = store.currentQuest.flatTransform { it?.id ?: value(0) } - val name: Val = store.currentQuest.flatTransform { it?.name ?: value("") } + val unavailable: Val = store.currentQuest.map { it == null } + val disabled: Val = store.questEditingDisabled + + val episode: Val = store.currentQuest.map { it?.episode?.name ?: "" } + val id: Val = store.currentQuest.flatMap { it?.id ?: value(0) } + val name: Val = store.currentQuest.flatMap { it?.name ?: value("") } + val shortDescription: Val = + store.currentQuest.flatMap { it?.shortDescription ?: value("") } + val longDescription: Val = + store.currentQuest.flatMap { it?.longDescription ?: value("") } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt index 76fc0274..3799b455 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestRenderer.kt @@ -1,7 +1,7 @@ package world.phantasmal.web.questEditor.rendering import org.w3c.dom.HTMLCanvasElement -import world.phantasmal.web.core.newJsObject +import world.phantasmal.webui.newJsObject import world.phantasmal.web.core.rendering.Renderer import world.phantasmal.web.externals.* import kotlin.math.PI diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt index 1b8ee6b1..15688918 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt @@ -11,6 +11,9 @@ class QuestEditorStore(scope: CoroutineScope) : Store(scope) { val currentQuest: Val = _currentQuest + // TODO: Take into account whether we're debugging or not. + val questEditingDisabled: Val = currentQuest.map { it == null } + fun setCurrentQuest(quest: QuestModel?) { _currentQuest.value = quest } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestInfoWidget.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestInfoWidget.kt index 63e752be..c167fb14 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestInfoWidget.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestInfoWidget.kt @@ -7,13 +7,14 @@ import world.phantasmal.web.core.widgets.UnavailableWidget import world.phantasmal.web.questEditor.controllers.QuestInfoController import world.phantasmal.webui.dom.* import world.phantasmal.webui.widgets.IntInput +import world.phantasmal.webui.widgets.TextArea import world.phantasmal.webui.widgets.TextInput import world.phantasmal.webui.widgets.Widget class QuestInfoWidget( scope: CoroutineScope, private val ctrl: QuestInfoController, -) : Widget(scope) { +) : Widget(scope, disabled = ctrl.disabled) { override fun Node.createElement() = div { className = "pw-quest-editor-quest-info" @@ -31,9 +32,10 @@ class QuestInfoWidget( td { addChild(IntInput( this@QuestInfoWidget.scope, + disabled = ctrl.disabled, valueVal = ctrl.id, min = 0, - step = 1 + step = 1, )) } } @@ -42,8 +44,49 @@ class QuestInfoWidget( td { addChild(TextInput( this@QuestInfoWidget.scope, + disabled = ctrl.disabled, valueVal = ctrl.name, - maxLength = 32 + maxLength = 32, + )) + } + } + tr { + th { + colSpan = 2 + textContent = "Short description:" + } + } + tr { + td { + colSpan = 2 + addChild(TextArea( + this@QuestInfoWidget.scope, + disabled = ctrl.disabled, + valueVal = ctrl.shortDescription, + maxLength = 128, + fontFamily = "\"Courier New\", monospace", + cols = 25, + rows = 5, + )) + } + } + tr { + th { + colSpan = 2 + textContent = "Long description:" + } + } + tr { + td { + colSpan = 2 + addChild(TextArea( + this@QuestInfoWidget.scope, + disabled = ctrl.disabled, + valueVal = ctrl.longDescription, + maxLength = 288, + fontFamily = "\"Courier New\", monospace", + cols = 25, + rows = 10, )) } } diff --git a/web/src/main/kotlin/world/phantasmal/web/core/Js.kt b/webui/src/main/kotlin/world/phantasmal/webui/Js.kt similarity index 72% rename from web/src/main/kotlin/world/phantasmal/web/core/Js.kt rename to webui/src/main/kotlin/world/phantasmal/webui/Js.kt index 0f492a31..0d37ca5f 100644 --- a/web/src/main/kotlin/world/phantasmal/web/core/Js.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/Js.kt @@ -1,4 +1,4 @@ -package world.phantasmal.web.core +package world.phantasmal.webui fun newJsObject(block: T.() -> Unit): T = js("{}").unsafeCast().apply(block) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/dom/DomCreation.kt b/webui/src/main/kotlin/world/phantasmal/webui/dom/DomCreation.kt index a23fbf64..5716d3c2 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/dom/DomCreation.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/dom/DomCreation.kt @@ -51,6 +51,9 @@ fun Node.table(block: HTMLTableElement.() -> Unit = {}): HTMLTableElement = fun Node.td(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement = appendHtmlEl("TD", block) +fun Node.textarea(block: HTMLTextAreaElement.() -> Unit = {}): HTMLTextAreaElement = + appendHtmlEl("TEXTAREA", block) + fun Node.th(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement = appendHtmlEl("TH", block) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Button.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Button.kt index 3354bde8..cf68f1bc 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Button.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Button.kt @@ -2,6 +2,7 @@ package world.phantasmal.webui.widgets import kotlinx.coroutines.CoroutineScope import org.w3c.dom.Node +import org.w3c.dom.events.KeyboardEvent import org.w3c.dom.events.MouseEvent import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.falseVal @@ -14,12 +15,22 @@ open class Button( disabled: Val = falseVal(), private val text: String? = null, private val textVal: Val? = null, - private val onclick: ((MouseEvent) -> Unit)? = null, + private val onMouseDown: ((MouseEvent) -> Unit)? = null, + private val onMouseUp: ((MouseEvent) -> Unit)? = null, + private val onClick: ((MouseEvent) -> Unit)? = null, + private val onKeyDown: ((KeyboardEvent) -> Unit)? = null, + private val onKeyUp: ((KeyboardEvent) -> Unit)? = null, + private val onKeyPress: ((KeyboardEvent) -> Unit)? = null, ) : Control(scope, hidden, disabled) { override fun Node.createElement() = button { className = "pw-button" - onclick = this@Button.onclick + onmousedown = onMouseDown + onmouseup = onMouseUp + onclick = onClick + onkeydown = onKeyDown + onkeyup = onKeyUp + onkeypress = onKeyPress span { className = "pw-button-inner" diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Control.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Control.kt index 6f32f06e..6e5820a1 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Control.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Control.kt @@ -12,4 +12,5 @@ abstract class Control( scope: CoroutineScope, hidden: Val = falseVal(), disabled: Val = falseVal(), -) : Widget(scope, hidden, disabled) + tooltip: String? = null, +) : Widget(scope, hidden, disabled, tooltip) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/DoubleInput.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/DoubleInput.kt index 8c791bf5..7515096e 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/DoubleInput.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/DoubleInput.kt @@ -11,23 +11,25 @@ class DoubleInput( scope: CoroutineScope, hidden: Val = falseVal(), disabled: Val = falseVal(), + tooltip: String? = null, label: String? = null, labelVal: Val? = null, preferredLabelPosition: LabelPosition = LabelPosition.Before, value: Double? = null, valueVal: Val? = null, - setValue: ((Double) -> Unit)? = null, + onChange: (Double) -> Unit = {}, roundTo: Int = 2, ) : NumberInput( scope, hidden, disabled, + tooltip, label, labelVal, preferredLabelPosition, value, valueVal, - setValue, + onChange, min = null, max = null, step = null, diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt index 04a8fa47..c2173555 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt @@ -11,6 +11,7 @@ abstract class Input( scope: CoroutineScope, hidden: Val, disabled: Val, + tooltip: String?, label: String?, labelVal: Val?, preferredLabelPosition: LabelPosition, @@ -19,7 +20,7 @@ abstract class Input( private val inputType: String, private val value: T?, private val valueVal: Val?, - private val setValue: ((T) -> Unit)?, + private val onChange: (T) -> Unit, private val maxLength: Int?, private val min: Int?, private val max: Int?, @@ -28,6 +29,7 @@ abstract class Input( scope, hidden, disabled, + tooltip, label, labelVal, preferredLabelPosition, @@ -42,13 +44,11 @@ abstract class Input( observe(this@Input.disabled) { disabled = it } - if (setValue != null) { - onchange = { setValue.invoke(getInputValue(this)) } + onchange = { onChange(getInputValue(this)) } - onkeydown = { e -> - if (e.key == "Enter") { - setValue.invoke(getInputValue(this)) - } + onkeydown = { e -> + if (e.key == "Enter") { + onChange(getInputValue(this)) } } diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/IntInput.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/IntInput.kt index 84770252..d73a7a36 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/IntInput.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/IntInput.kt @@ -9,12 +9,13 @@ class IntInput( scope: CoroutineScope, hidden: Val = falseVal(), disabled: Val = falseVal(), + tooltip: String? = null, label: String? = null, labelVal: Val? = null, preferredLabelPosition: LabelPosition = LabelPosition.Before, value: Int? = null, valueVal: Val? = null, - setValue: ((Int) -> Unit)? = null, + onChange: (Int) -> Unit = {}, min: Int? = null, max: Int? = null, step: Int? = null, @@ -22,12 +23,13 @@ class IntInput( scope, hidden, disabled, + tooltip, label, labelVal, preferredLabelPosition, value, valueVal, - setValue, + onChange, min, max, step, diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/LabelledControl.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/LabelledControl.kt index df6782f3..5f1134e8 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/LabelledControl.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/LabelledControl.kt @@ -2,7 +2,6 @@ package world.phantasmal.webui.widgets import kotlinx.coroutines.CoroutineScope import world.phantasmal.observable.value.Val -import world.phantasmal.observable.value.falseVal enum class LabelPosition { Before, @@ -11,12 +10,13 @@ enum class LabelPosition { abstract class LabelledControl( scope: CoroutineScope, - hidden: Val = falseVal(), - disabled: Val = falseVal(), - label: String? = null, - labelVal: Val? = null, + hidden: Val, + disabled: Val, + tooltip: String? = null, + label: String?, + labelVal: Val?, val preferredLabelPosition: LabelPosition, -) : Control(scope, hidden, disabled) { +) : Control(scope, hidden, disabled, tooltip) { val label: Label? by lazy { if (label == null && labelVal == null) { null diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt new file mode 100644 index 00000000..b1d80bce --- /dev/null +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt @@ -0,0 +1,220 @@ +package world.phantasmal.webui.widgets + +import kotlinx.browser.document +import kotlinx.coroutines.CoroutineScope +import org.w3c.dom.* +import org.w3c.dom.events.Event +import org.w3c.dom.events.KeyboardEvent +import org.w3c.dom.events.MouseEvent +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.falseVal +import world.phantasmal.observable.value.value +import world.phantasmal.webui.dom.disposableListener +import world.phantasmal.webui.dom.div +import world.phantasmal.webui.newJsObject + +class Menu( + scope: CoroutineScope, + hidden: Val = falseVal(), + disabled: Val = falseVal(), + tooltip: String? = null, + items: List? = null, + itemsVal: Val>? = null, + private val itemToString: (T) -> String = Any::toString, + private val onSelect: (T) -> Unit = {}, + private val onCancel: () -> Unit = {}, +) : Widget( + scope, + hidden, + disabled, + tooltip, +) { + private val items: Val> = itemsVal ?: value(items ?: emptyList()) + private lateinit var innerElement: HTMLElement + private var highlightedIndex: Int? = null + private var highlightedElement: Element? = null + private var previouslyFocusedElement: Element? = null + + override fun Node.createElement() = + div { + className = "pw-menu" + tabIndex = -1 + onmouseup = ::onMouseUp + onkeydown = ::onKeyDown + onblur = { onBlur() } + + innerElement = div { + className = "pw-menu-inner" + onmouseover = ::innerMouseOver + + bindChildrenTo(items) { item, index -> + div { + dataset["index"] = index.toString() + textContent = itemToString(item) + } + } + } + + observe(this@Menu.hidden) { + if (it) { + document.removeEventListener("mousedown", ::onDocumentMouseDown) + clearHighlightItem() + + (previouslyFocusedElement as HTMLElement?)?.focus() + } else { + document.addEventListener("mousedown", ::onDocumentMouseDown) + } + } + + observe(disabled) { + if (it) { + clearHighlightItem() + } + } + + disposableListener(document, "keydown", ::onDocumentKeyDown) + } + + override fun internalDispose() { + document.removeEventListener("mousedown", ::onDocumentMouseDown) + super.internalDispose() + } + + override fun focus() { + previouslyFocusedElement = document.activeElement + super.focus() + } + + fun highlightItem(item: T) { + val idx = items.value.indexOf(item) + + if (idx != -1) { + highlightItemAt(idx) + } + } + + private fun onMouseUp(e: MouseEvent) { + val target = e.target + + if (target !is HTMLElement) return + + target.dataset["index"]?.toIntOrNull()?.let(::selectItem) + } + + private fun onKeyDown(e: KeyboardEvent) { + when (e.key) { + "ArrowDown" -> { + e.preventDefault() + highlightItemAt( + when (val idx = highlightedIndex) { + null, items.value.lastIndex -> 0 + else -> idx + 1 + } + ) + } + + "ArrowUp" -> { + e.preventDefault() + highlightItemAt( + when (val idx = highlightedIndex) { + null, 0 -> items.value.lastIndex + else -> idx - 1 + } + ) + } + + "Enter", " " -> { + e.preventDefault() + e.stopPropagation() + highlightedIndex?.let(::selectItem) + } + } + } + + private fun onBlur() { + onCancel() + } + + private fun innerMouseOver(e: MouseEvent) { + val target = e.target + + if (target is HTMLElement) { + target.dataset["index"]?.toIntOrNull()?.let(::highlightItemAt) + } + } + + private fun onDocumentMouseDown(e: Event) { + val target = e.target + + if (target !is Node || !element.contains(target)) { + onCancel() + } + } + + private fun onDocumentKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + onCancel() + } + } + + private fun clearHighlightItem() { + highlightedElement?.classList?.remove("pw-menu-highlighted") + highlightedIndex = null + highlightedElement = null + } + + private fun highlightItemAt(index: Int) { + highlightedElement?.classList?.remove("pw-menu-highlighted") + + if (disabled.value) return + + highlightedElement = innerElement.children.item(index) + + highlightedElement?.let { + highlightedIndex = index + it.classList.add("pw-menu-highlighted") + it.scrollIntoView(newJsObject { block = "nearest" }) + } + } + + private fun selectItem(index: Int) { + if (disabled.value) return + + items.value.getOrNull(index)?.let(onSelect) + } + + companion object { + init { + @Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty") + // language=css + style(""" + .pw-menu { + z-index: 1000; + position: absolute; + box-sizing: border-box; + outline: none; + border: var(--pw-control-border); + --scrollbar-color: hsl(0, 0%, 18%); + --scrollbar-thumb-color: hsl(0, 0%, 22%); + } + + .pw-menu > .pw-menu-inner { + overflow: auto; + background-color: var(--pw-control-bg-color); + max-height: 500px; + border: var(--pw-control-inner-border); + } + + .pw-menu > .pw-menu-inner > * { + padding: 4px 8px; + white-space: nowrap; + } + + .pw-menu > .pw-menu-inner > .pw-menu-highlighted { + background-color: var(--pw-control-bg-color-hover); + color: var(--pw-control-text-color-hover); + } + """.trimIndent()) + } + } +} diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/NumberInput.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/NumberInput.kt index ffcfdca7..f0188fb9 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/NumberInput.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/NumberInput.kt @@ -7,12 +7,13 @@ abstract class NumberInput( scope: CoroutineScope, hidden: Val, disabled: Val, + tooltip: String?, label: String?, labelVal: Val?, preferredLabelPosition: LabelPosition, value: T?, valueVal: Val?, - setValue: ((T) -> Unit)?, + onChange: (T) -> Unit, min: Int?, max: Int?, step: Int?, @@ -20,6 +21,7 @@ abstract class NumberInput( scope, hidden, disabled, + tooltip, label, labelVal, preferredLabelPosition, @@ -28,7 +30,7 @@ abstract class NumberInput( inputType = "number", value, valueVal, - setValue, + onChange, maxLength = null, min, max, diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt new file mode 100644 index 00000000..6472a7c1 --- /dev/null +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt @@ -0,0 +1,161 @@ +package world.phantasmal.webui.widgets + +import kotlinx.coroutines.CoroutineScope +import org.w3c.dom.Node +import org.w3c.dom.events.KeyboardEvent +import org.w3c.dom.events.MouseEvent +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.falseVal +import world.phantasmal.observable.value.mutableVal +import world.phantasmal.observable.value.value +import world.phantasmal.webui.dom.div + +class Select( + scope: CoroutineScope, + hidden: Val = falseVal(), + disabled: Val = falseVal(), + tooltip: String? = null, + label: String? = null, + labelVal: Val? = null, + preferredLabelPosition: LabelPosition = LabelPosition.Before, + items: List? = null, + itemsVal: Val>? = null, + private val itemToString: (T) -> String = Any::toString, + selected: T? = null, + selectedVal: Val? = null, + private val onSelect: (T) -> Unit = {}, +) : LabelledControl( + scope, + hidden, + disabled, + tooltip, + label, + labelVal, + preferredLabelPosition, +) { + private val items: Val> = itemsVal ?: value(items ?: emptyList()) + private val selected: Val = selectedVal ?: value(selected) + + // Default to a single space so the inner text part won't be hidden. + private val buttonText = mutableVal(this.selected.value?.let(itemToString) ?: " ") + private val menuHidden = mutableVal(true) + + private lateinit var menu: Menu + private var justOpened = false + + override fun Node.createElement() = + div { + className = "pw-select" + + addWidget(Button( + scope, + disabled = disabled, + textVal = buttonText, + onMouseDown = ::onButtonMouseDown, + onMouseUp = { onButtonMouseUp() }, + onKeyDown = ::onButtonKeyDown, + )) + menu = addWidget(Menu( + scope, + hidden = menuHidden, + disabled = disabled, + itemsVal = items, + itemToString = itemToString, + onSelect = ::select, + onCancel = { menuHidden.value = true }, + )) + } + + private fun onButtonMouseDown(e: MouseEvent) { + e.stopPropagation() + justOpened = menuHidden.value + menuHidden.value = false + selected.value?.let(menu::highlightItem) + } + + private fun onButtonMouseUp() { + if (justOpened) { + menu.focus() + } else { + menuHidden.value = true + } + + justOpened = false + } + + private fun onButtonKeyDown(e: KeyboardEvent) { + when (e.key) { + "Enter", " " -> { + e.preventDefault() + e.stopPropagation() + + justOpened = menuHidden.value + menuHidden.value = false + selected.value?.let(menu::highlightItem) + menu.focus() + } + + "ArrowUp" -> { + if (items.value.isNotEmpty()) { + if (selected.value == null) { + select(items.value.last()) + } else { + val index = items.value.indexOf(selected.value) - 1 + + if (index < 0) { + select(items.value.last()) + } else { + select(items.value[index]) + } + } + } + } + + "ArrowDown" -> { + if (items.value.isNotEmpty()) { + if (selected.value == null) { + select(items.value.first()) + } else { + val index = items.value.indexOf(selected.value) + 1 + + if (index >= items.value.size) { + select(items.value.first()) + } else { + select(items.value[index]) + } + } + } + } + } + } + + private fun select(item: T) { + menuHidden.value = true + buttonText.value = itemToString(item) + onSelect(item) + } + + companion object { + init { + @Suppress("CssUnusedSymbol") + // language=css + style(""" + .pw-select { + position: relative; + display: inline-flex; + width: 160px; + } + + .pw-select .pw-button { + flex: 1; + } + + .pw-select .pw-menu { + top: 25px; + left: 0; + min-width: 100%; + } + """.trimIndent()) + } + } +} diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/TabContainer.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TabContainer.kt index e0c62031..294931e2 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/TabContainer.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TabContainer.kt @@ -48,7 +48,7 @@ class TabContainer( addChild( LazyLoader( scope, - hidden = ctrl.activeTab.transform { it != tab }, + hidden = ctrl.activeTab.map { it != tab }, createWidget = { scope -> createWidget(scope, tab) } ) ) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt new file mode 100644 index 00000000..be97db85 --- /dev/null +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt @@ -0,0 +1,102 @@ +package world.phantasmal.webui.widgets + +import kotlinx.coroutines.CoroutineScope +import org.w3c.dom.Node +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.falseVal +import world.phantasmal.webui.dom.div +import world.phantasmal.webui.dom.textarea + +class TextArea( + scope: CoroutineScope, + hidden: Val = falseVal(), + disabled: Val = falseVal(), + tooltip: String? = null, + label: String? = null, + labelVal: Val? = null, + preferredLabelPosition: LabelPosition = LabelPosition.Before, + private val value: String? = null, + private val valueVal: Val? = null, + private val setValue: ((String) -> Unit)? = null, + private val maxLength: Int? = null, + private val fontFamily: String? = null, + private val rows: Int? = null, + private val cols: Int? = null, +) : LabelledControl( + scope, + hidden, + disabled, + tooltip, + label, + labelVal, + preferredLabelPosition, +) { + override fun Node.createElement() = + div { + className = "pw-text-area" + + textarea { + className = "pw-text-area-inner" + + observe(this@TextArea.disabled) { disabled = it } + + if (setValue != null) { + onchange = { setValue.invoke(value) } + } + + if (valueVal != null) { + observe(valueVal) { value = it } + } else if (this@TextArea.value != null) { + value = this@TextArea.value + } + + this@TextArea.maxLength?.let { maxLength = it } + fontFamily?.let { style.fontFamily = it } + this@TextArea.rows?.let { rows = it } + this@TextArea.cols?.let { cols = it } + } + } + + companion object { + init { + @Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty") + // language=css + style(""" + .pw-text-area { + box-sizing: border-box; + display: inline-block; + border: var(--pw-input-border); + } + + .pw-text-area .pw-text-area-inner { + box-sizing: border-box; + vertical-align: top; + padding: 3px; + border: var(--pw-input-inner-border); + margin: 0; + background-color: var(--pw-input-bg-color); + color: var(--pw-input-text-color); + outline: none; + font-size: 13px; + } + + .pw-text-area:hover { + border: var(--pw-input-border-hover); + } + + .pw-text-area:focus-within { + border: var(--pw-input-border-focus); + } + + .pw-text-area.disabled { + border: var(--pw-input-border-disabled); + } + + .pw-text-area.disabled .pw-text-area-inner { + color: var(--pw-input-text-color-disabled); + background-color: var(--pw-input-bg-color-disabled); + } + """.trimIndent()) + } + } +} diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextInput.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextInput.kt index 0c56592d..da53392b 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextInput.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/TextInput.kt @@ -9,17 +9,19 @@ class TextInput( scope: CoroutineScope, hidden: Val = falseVal(), disabled: Val = falseVal(), + tooltip: String? = null, label: String? = null, labelVal: Val? = null, preferredLabelPosition: LabelPosition = LabelPosition.Before, value: String? = null, valueVal: Val? = null, - setValue: ((String) -> Unit)? = null, - maxLength: Int? = null + onChange: (String) -> Unit = {}, + maxLength: Int? = null, ) : Input( scope, hidden, disabled, + tooltip, label, labelVal, preferredLabelPosition, @@ -28,7 +30,7 @@ class TextInput( inputType = "text", value, valueVal, - setValue, + onChange, maxLength, min = null, max = null, diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt index 7427dc18..cc3925bc 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Widget.kt @@ -2,7 +2,6 @@ package world.phantasmal.webui.widgets import kotlinx.browser.document import kotlinx.coroutines.CoroutineScope -import kotlinx.dom.clear import org.w3c.dom.* import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.Observable @@ -22,9 +21,10 @@ abstract class Widget( val hidden: Val = falseVal(), /** * By default determines the disabled attribute of its [element] and whether or not the - * `pw-disabled` class is added. + * "pw-disabled" class is added. */ val disabled: Val = falseVal(), + val tooltip: String? = null, ) : DisposableContainer() { private val _ancestorHidden = mutableVal(false) private val _children = mutableListOf() @@ -49,6 +49,8 @@ abstract class Widget( } } + tooltip?.let { el.title = it } + if (initResizeObserverRequested) { initResizeObserver(el) } @@ -74,6 +76,10 @@ abstract class Widget( val children: List = _children + open fun focus() { + element.focus() + } + /** * Called to initialize [element] when it is first accessed. */ @@ -121,9 +127,30 @@ abstract class Widget( return child } - protected fun Node.bindChildrenTo( + protected fun Element.bindChildrenTo( + list: Val>, + createChild: Node.(T, Int) -> Node, + ) { + if (list is ListVal) { + bindChildrenTo(list, createChild) + } else { + observe(list) { items -> + innerHTML = "" + + val frag = document.createDocumentFragment() + + items.forEachIndexed { i, item -> + frag.createChild(item, i) + } + + appendChild(frag) + } + } + } + + protected fun Element.bindChildrenTo( list: ListVal, - createChild: (T, Int) -> Node, + createChild: Node.(T, Int) -> Node, ) { fun spliceChildren(index: Int, removedCount: Int, inserted: List) { for (i in 1..removedCount) { @@ -133,9 +160,7 @@ abstract class Widget( val frag = document.createDocumentFragment() inserted.forEachIndexed { i, value -> - val child = createChild(value, index + i) - - frag.append(child) + frag.createChild(value, index + i) } if (index >= childNodes.length) { @@ -145,25 +170,20 @@ abstract class Widget( } } - val observer = list.observeList { change: ListValChangeEvent -> - when (change) { - is ListValChangeEvent.Change -> { - spliceChildren(change.index, change.removed.size, change.inserted) - } - is ListValChangeEvent.ElementChange -> { - // TODO: Update children. - } - } - } - - spliceChildren(0, 0, list.value) - addDisposable( - disposable { - observer.dispose() - clear() + list.observeList { change: ListValChangeEvent -> + when (change) { + is ListValChangeEvent.Change -> { + spliceChildren(change.index, change.removed.size, change.inserted) + } + is ListValChangeEvent.ElementChange -> { + // TODO: Update children. + } + } } ) + + spliceChildren(0, 0, list.value) } /**