mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Refactored the way Disposable is used and added QuestInfoWidget.
This commit is contained in:
parent
e75732ed9d
commit
b810e45fb3
@ -10,3 +10,18 @@ interface Disposable {
|
||||
*/
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given function on this disposable and then disposes it whether an exception is
|
||||
* thrown or not.
|
||||
*
|
||||
* @param block a function to process this [Disposable] resource.
|
||||
* @return the result of [block] invoked on this resource.
|
||||
*/
|
||||
inline fun <D : Disposable, R> D.use(block: (D) -> R): R {
|
||||
try {
|
||||
return block(this)
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,11 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
fun Scope.disposable(dispose: () -> Unit): Disposable = SimpleDisposable(this, dispose)
|
||||
private object StubDisposable : Disposable {
|
||||
override fun dispose() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
fun disposable(dispose: () -> Unit): Disposable = SimpleDisposable(dispose)
|
||||
|
||||
fun stubDisposable(): Disposable = StubDisposable
|
||||
|
@ -1,32 +1,25 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class DisposableScope(override val coroutineContext: CoroutineContext) : Scope, Disposable {
|
||||
private val disposables = mutableListOf<Disposable>()
|
||||
private var disposed = false
|
||||
class Disposer(vararg disposables: Disposable) : TrackedDisposable() {
|
||||
private val disposables = mutableListOf(*disposables)
|
||||
|
||||
/**
|
||||
* The amount of held disposables.
|
||||
*/
|
||||
val size: Int get() = disposables.size
|
||||
|
||||
override fun scope(): Scope = DisposableScope(coroutineContext + SupervisorJob()).also(::add)
|
||||
|
||||
override fun add(disposable: Disposable) {
|
||||
require(!disposed) { "Scope already disposed." }
|
||||
fun <T : Disposable> add(disposable: T): T {
|
||||
require(!disposed) { "Disposer already disposed." }
|
||||
|
||||
disposables.add(disposable)
|
||||
return disposable
|
||||
}
|
||||
|
||||
/**
|
||||
* Add 0 or more disposables.
|
||||
*/
|
||||
fun addAll(disposables: Iterable<Disposable>) {
|
||||
require(!disposed) { "Scope already disposed." }
|
||||
require(!disposed) { "Disposer already disposed." }
|
||||
|
||||
this.disposables.addAll(disposables)
|
||||
}
|
||||
@ -35,7 +28,7 @@ class DisposableScope(override val coroutineContext: CoroutineContext) : Scope,
|
||||
* Add 0 or more disposables.
|
||||
*/
|
||||
fun addAll(vararg disposables: Disposable) {
|
||||
require(!disposed) { "Scope already disposed." }
|
||||
require(!disposed) { "Disposer already disposed." }
|
||||
|
||||
this.disposables.addAll(disposables)
|
||||
}
|
||||
@ -67,15 +60,7 @@ class DisposableScope(override val coroutineContext: CoroutineContext) : Scope,
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (!disposed) {
|
||||
override fun internalDispose() {
|
||||
disposeAll()
|
||||
|
||||
if (coroutineContext[Job] != null) {
|
||||
cancel()
|
||||
}
|
||||
|
||||
disposed = true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Container for disposables. Takes ownership of all held disposables and automatically disposes
|
||||
* them when the Scope is disposed.
|
||||
*/
|
||||
interface Scope: CoroutineScope {
|
||||
fun add(disposable: Disposable)
|
||||
|
||||
/**
|
||||
* Creates a sub-scope of this scope.
|
||||
*/
|
||||
fun scope(): Scope
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
class SimpleDisposable(
|
||||
scope: Scope,
|
||||
private val dispose: () -> Unit,
|
||||
) : TrackedDisposable(scope) {
|
||||
) : TrackedDisposable() {
|
||||
override fun internalDispose() {
|
||||
// Use invoke to avoid calling the dispose method instead of the dispose property.
|
||||
dispose.invoke()
|
||||
|
@ -4,14 +4,11 @@ package world.phantasmal.core.disposable
|
||||
* A global count is kept of all undisposed instances of this class.
|
||||
* This count can be used to find memory leaks.
|
||||
*/
|
||||
abstract class TrackedDisposable(scope: Scope) : Disposable {
|
||||
abstract class TrackedDisposable : Disposable {
|
||||
var disposed = false
|
||||
private set
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
scope.add(this)
|
||||
|
||||
disposableCount++
|
||||
}
|
||||
|
||||
|
@ -1,46 +1,47 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.test.*
|
||||
|
||||
class DisposableScopeTests {
|
||||
class DisposerTests {
|
||||
@Test
|
||||
fun calling_add_or_addAll_increases_size_correctly() {
|
||||
TrackedDisposable.checkNoLeaks {
|
||||
val scope = DisposableScope(Job())
|
||||
assertEquals(scope.size, 0)
|
||||
val disposer = Disposer()
|
||||
assertEquals(disposer.size, 0)
|
||||
|
||||
scope.add(Dummy())
|
||||
assertEquals(scope.size, 1)
|
||||
disposer.add(StubDisposable())
|
||||
assertEquals(disposer.size, 1)
|
||||
|
||||
scope.addAll(Dummy(), Dummy())
|
||||
assertEquals(scope.size, 3)
|
||||
disposer.addAll(StubDisposable(),
|
||||
StubDisposable())
|
||||
assertEquals(disposer.size, 3)
|
||||
|
||||
scope.add(Dummy())
|
||||
assertEquals(scope.size, 4)
|
||||
disposer.add(StubDisposable())
|
||||
assertEquals(disposer.size, 4)
|
||||
|
||||
scope.addAll(Dummy(), Dummy())
|
||||
assertEquals(scope.size, 6)
|
||||
disposer.addAll(StubDisposable(),
|
||||
StubDisposable())
|
||||
assertEquals(disposer.size, 6)
|
||||
|
||||
scope.dispose()
|
||||
disposer.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disposes_all_its_disposables_when_disposed() {
|
||||
TrackedDisposable.checkNoLeaks {
|
||||
val scope = DisposableScope(Job())
|
||||
val disposer = Disposer()
|
||||
var disposablesDisposed = 0
|
||||
|
||||
for (i in 1..5) {
|
||||
scope.add(object : Disposable {
|
||||
disposer.add(object : Disposable {
|
||||
override fun dispose() {
|
||||
disposablesDisposed++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
scope.addAll((1..5).map {
|
||||
disposer.addAll((1..5).map {
|
||||
object : Disposable {
|
||||
override fun dispose() {
|
||||
disposablesDisposed++
|
||||
@ -48,7 +49,7 @@ class DisposableScopeTests {
|
||||
}
|
||||
})
|
||||
|
||||
scope.dispose()
|
||||
disposer.dispose()
|
||||
|
||||
assertEquals(10, disposablesDisposed)
|
||||
}
|
||||
@ -57,67 +58,67 @@ class DisposableScopeTests {
|
||||
@Test
|
||||
fun disposeAll_disposes_all_disposables() {
|
||||
TrackedDisposable.checkNoLeaks {
|
||||
val scope = DisposableScope(Job())
|
||||
val disposer = Disposer()
|
||||
|
||||
var disposablesDisposed = 0
|
||||
|
||||
for (i in 1..5) {
|
||||
scope.add(object : Disposable {
|
||||
disposer.add(object : Disposable {
|
||||
override fun dispose() {
|
||||
disposablesDisposed++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
scope.disposeAll()
|
||||
disposer.disposeAll()
|
||||
|
||||
assertEquals(5, disposablesDisposed)
|
||||
|
||||
scope.dispose()
|
||||
disposer.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun size_and_is_empty_should_correctly_reflect_the_contained_disposables() {
|
||||
TrackedDisposable.checkNoLeaks {
|
||||
val scope = DisposableScope(Job())
|
||||
val disposer = Disposer()
|
||||
|
||||
assertEquals(scope.size, 0)
|
||||
assertTrue(scope.isEmpty())
|
||||
assertEquals(disposer.size, 0)
|
||||
assertTrue(disposer.isEmpty())
|
||||
|
||||
for (i in 1..5) {
|
||||
scope.add(Dummy())
|
||||
disposer.add(StubDisposable())
|
||||
|
||||
assertEquals(scope.size, i)
|
||||
assertFalse(scope.isEmpty())
|
||||
assertEquals(disposer.size, i)
|
||||
assertFalse(disposer.isEmpty())
|
||||
}
|
||||
|
||||
scope.dispose()
|
||||
disposer.dispose()
|
||||
|
||||
assertEquals(scope.size, 0)
|
||||
assertTrue(scope.isEmpty())
|
||||
assertEquals(disposer.size, 0)
|
||||
assertTrue(disposer.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adding_disposables_after_being_disposed_throws() {
|
||||
TrackedDisposable.checkNoLeaks {
|
||||
val scope = DisposableScope(Job())
|
||||
scope.dispose()
|
||||
val disposer = Disposer()
|
||||
disposer.dispose()
|
||||
|
||||
for (i in 1..3) {
|
||||
assertFails {
|
||||
scope.add(Dummy())
|
||||
disposer.add(StubDisposable())
|
||||
}
|
||||
}
|
||||
|
||||
assertFails {
|
||||
scope.addAll((1..3).map { Dummy() })
|
||||
disposer.addAll((1..3).map { StubDisposable() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Dummy : Disposable {
|
||||
private class StubDisposable : Disposable {
|
||||
override fun dispose() {
|
||||
// Do nothing.
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||
import org.snakeyaml.engine.v2.api.Load
|
||||
import org.snakeyaml.engine.v2.api.LoadSettings
|
||||
import java.io.PrintWriter
|
||||
@ -16,7 +17,13 @@ val kotlinLoggingVersion: String by project.extra
|
||||
|
||||
kotlin {
|
||||
js {
|
||||
browser()
|
||||
browser {
|
||||
testTask {
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@ -166,6 +173,6 @@ fun paramsToCode(params: List<Map<String, Any>>, indent: Int): String {
|
||||
}
|
||||
}
|
||||
|
||||
val build by tasks.build
|
||||
|
||||
build.dependsOn(generateOpcodes)
|
||||
tasks.withType<AbstractKotlinCompile<*>> {
|
||||
dependsOn(generateOpcodes)
|
||||
}
|
||||
|
@ -19,10 +19,6 @@ class AssemblyProblem(
|
||||
val length: Int,
|
||||
) : Problem(severity, uiMessage, message, cause)
|
||||
|
||||
class AssemblySettings(
|
||||
val manualStack: Boolean,
|
||||
)
|
||||
|
||||
fun assemble(
|
||||
assembly: List<String>,
|
||||
manualStack: Boolean = false,
|
||||
|
@ -3,8 +3,19 @@ package world.phantasmal.lib.test
|
||||
import world.phantasmal.core.Success
|
||||
import world.phantasmal.lib.assembly.InstructionSegment
|
||||
import world.phantasmal.lib.assembly.assemble
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
|
||||
* actually returns a Promise. If this promise is not returned from the test function, the testing
|
||||
* framework won't wait for its completion. This is a workaround for issue
|
||||
* [https://youtrack.jetbrains.com/issue/KT-22228].
|
||||
*/
|
||||
expect fun asyncTest(block: suspend () -> Unit)
|
||||
|
||||
expect suspend fun readFile(path: String): Cursor
|
||||
|
||||
fun toInstructions(assembly: String): List<InstructionSegment> {
|
||||
val result = assemble(assembly.split('\n'))
|
||||
|
||||
|
18
lib/src/jsTest/kotlin/world/phantasmal/lib/test/TestUtils.kt
Normal file
18
lib/src/jsTest/kotlin/world/phantasmal/lib/test/TestUtils.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package world.phantasmal.lib.test
|
||||
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.coroutines.promise
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.ArrayBufferCursor
|
||||
import world.phantasmal.lib.cursor.Cursor
|
||||
|
||||
actual fun asyncTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }
|
||||
|
||||
actual suspend fun readFile(path: String): Cursor {
|
||||
return window.fetch(path)
|
||||
.then { it.arrayBuffer() }
|
||||
.then { ArrayBufferCursor(it, Endianness.Little) }
|
||||
.await()
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
|
||||
interface Observable<out T> {
|
||||
fun observe(scope: Scope, observer: Observer<T>)
|
||||
fun observe(observer: Observer<T>): Disposable
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
|
||||
class SimpleEmitter<T> : Emitter<T> {
|
||||
private val observers = mutableListOf<Observer<T>>()
|
||||
|
||||
override fun observe(scope: Scope, observer: Observer<T>) {
|
||||
override fun observe(observer: Observer<T>): Disposable {
|
||||
observers.add(observer)
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
observers.remove(observer)
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,23 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
abstract class AbstractVal<T> : Val<T> {
|
||||
protected val observers: MutableList<ValObserver<T>> = mutableListOf()
|
||||
|
||||
final override fun observe(scope: Scope, observer: Observer<T>) {
|
||||
observe(scope, callNow = false, observer)
|
||||
}
|
||||
final override fun observe(observer: Observer<T>): Disposable =
|
||||
observe(callNow = false, observer)
|
||||
|
||||
override fun observe(scope: Scope, callNow: Boolean, observer: ValObserver<T>) {
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
observers.add(observer)
|
||||
|
||||
if (callNow) {
|
||||
observer(ValChangeEvent(value, value))
|
||||
}
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
observers.remove(observer)
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,68 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.fastCast
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
class DependentVal<T>(
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
abstract class DependentVal<T>(
|
||||
private val dependencies: Iterable<Val<*>>,
|
||||
private val operation: () -> T,
|
||||
) : AbstractVal<T>() {
|
||||
private var dependencyScope = DisposableScope(EmptyCoroutineContext)
|
||||
private var internalValue: T? = null
|
||||
/**
|
||||
* Is either empty or has a disposable per dependency.
|
||||
*/
|
||||
private val dependencyObservers = mutableListOf<Disposable>()
|
||||
|
||||
protected var _value: T? = null
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
return if (dependencyScope.isEmpty()) {
|
||||
operation()
|
||||
} else {
|
||||
internalValue.fastCast()
|
||||
}
|
||||
if (hasNoObservers()) {
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
override fun observe(scope: Scope, callNow: Boolean, observer: ValObserver<T>) {
|
||||
if (dependencyScope.isEmpty()) {
|
||||
internalValue = operation()
|
||||
return _value.fastCast()
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
if (hasNoObservers()) {
|
||||
dependencies.forEach { dependency ->
|
||||
dependency.observe(dependencyScope) {
|
||||
val oldValue = internalValue
|
||||
internalValue = operation()
|
||||
dependencyObservers.add(
|
||||
dependency.observe {
|
||||
val oldValue = _value
|
||||
_value = computeValue()
|
||||
|
||||
if (_value != oldValue) {
|
||||
emit(oldValue.fastCast())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
super.observe(scope, callNow, observer)
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
scope.disposable {
|
||||
if (observers.isEmpty()) {
|
||||
dependencyScope.disposeAll()
|
||||
dependencyObservers.forEach { it.dispose() }
|
||||
dependencyObservers.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun hasObservers(): Boolean =
|
||||
dependencyObservers.isNotEmpty()
|
||||
|
||||
protected fun hasNoObservers(): Boolean =
|
||||
dependencyObservers.isEmpty()
|
||||
|
||||
protected abstract fun computeValue(): T
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.fastCast
|
||||
|
||||
class FlatTransformedVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
private val compute: () -> Val<T>,
|
||||
) : DependentVal<T>(dependencies) {
|
||||
private var computedVal: Val<T>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
return if (hasNoObservers()) {
|
||||
super.value
|
||||
} else {
|
||||
computedVal.fastCast<Val<T>>().value
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
if (hasNoObservers()) {
|
||||
computedValObserver?.dispose()
|
||||
computedValObserver = null
|
||||
computedVal = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun computeValue(): T {
|
||||
val computedVal = compute()
|
||||
this.computedVal = computedVal
|
||||
|
||||
computedValObserver?.dispose()
|
||||
|
||||
if (hasObservers()) {
|
||||
computedValObserver = computedVal.observe { (value) ->
|
||||
val oldValue = _value.fastCast<T>()
|
||||
_value = value
|
||||
emit(oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
return computedVal.value
|
||||
}
|
||||
}
|
@ -1,16 +1,17 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.stubDisposable
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
class StaticVal<T>(override val value: T) : Val<T> {
|
||||
override fun observe(scope: Scope, callNow: Boolean, observer: ValObserver<T>) {
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ValChangeEvent(value, value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
}
|
||||
|
||||
override fun observe(scope: Scope, observer: Observer<T>) {
|
||||
// Do nothing.
|
||||
}
|
||||
override fun observe(observer: Observer<T>): Disposable = stubDisposable()
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class TransformedVal<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
private val compute: () -> T,
|
||||
) : DependentVal<T>(dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -15,14 +15,14 @@ interface Val<out T> : Observable<T> {
|
||||
/**
|
||||
* @param callNow Call [observer] immediately with the current [mutableVal].
|
||||
*/
|
||||
fun observe(scope: Scope, callNow: Boolean = false, observer: ValObserver<T>)
|
||||
fun observe(callNow: Boolean = false, observer: ValObserver<T>): Disposable
|
||||
|
||||
fun <R> transform(transform: (T) -> R): Val<R> =
|
||||
DependentVal(listOf(this)) { transform(value) }
|
||||
TransformedVal(listOf(this)) { transform(value) }
|
||||
|
||||
fun <T2, R> transform(v2: Val<T2>, transform: (T, T2) -> R): Val<R> =
|
||||
DependentVal(listOf(this, v2)) { transform(value, v2.value) }
|
||||
TransformedVal(listOf(this, v2)) { transform(value, v2.value) }
|
||||
|
||||
fun <R> flatTransform(transform: (T) -> Val<R>): Val<R> =
|
||||
TODO()
|
||||
FlatTransformedVal(listOf(this)) { transform(value) }
|
||||
}
|
||||
|
@ -1,46 +1,47 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.fastCast
|
||||
import world.phantasmal.observable.value.AbstractVal
|
||||
import world.phantasmal.observable.value.ValObserver
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
class FoldedVal<T, R>(
|
||||
private val dependency: ListVal<T>,
|
||||
private val initial: R,
|
||||
private val operation: (R, T) -> R,
|
||||
) : AbstractVal<R>() {
|
||||
private var dependencyDisposable = DisposableScope(EmptyCoroutineContext)
|
||||
private var dependencyDisposable: Disposable? = null
|
||||
private var internalValue: R? = null
|
||||
|
||||
override val value: R
|
||||
get() {
|
||||
return if (dependencyDisposable.isEmpty()) {
|
||||
return if (dependencyDisposable == null) {
|
||||
computeValue()
|
||||
} else {
|
||||
internalValue.fastCast()
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe(scope: Scope, callNow: Boolean, observer: ValObserver<R>) {
|
||||
super.observe(scope, callNow, observer)
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<R>): Disposable {
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
if (dependencyDisposable.isEmpty()) {
|
||||
if (dependencyDisposable == null) {
|
||||
internalValue = computeValue()
|
||||
|
||||
dependency.observe(dependencyDisposable) {
|
||||
dependencyDisposable = dependency.observe {
|
||||
val oldValue = internalValue
|
||||
internalValue = computeValue()
|
||||
emit(oldValue.fastCast())
|
||||
}
|
||||
}
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
if (observers.isEmpty()) {
|
||||
dependencyDisposable.disposeAll()
|
||||
dependencyDisposable?.dispose()
|
||||
dependencyDisposable = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
interface ListVal<E> : Val<List<E>>, List<E> {
|
||||
val sizeVal: Val<Int>
|
||||
|
||||
fun observeList(scope: Scope, observer: ListValObserver<E>)
|
||||
fun observeList(observer: ListValObserver<E>): Disposable
|
||||
|
||||
fun sumBy(selector: (E) -> Int): Val<Int> =
|
||||
fold(0) { acc, el -> acc + selector(el) }
|
||||
|
@ -1,12 +1,10 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.*
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
typealias ObservablesExtractor<E> = (element: E) -> Array<Observable<*>>
|
||||
|
||||
@ -73,11 +71,10 @@ class SimpleListVal<E>(
|
||||
return removed
|
||||
}
|
||||
|
||||
override fun observe(scope: Scope, observer: Observer<List<E>>) {
|
||||
observe(scope, callNow = false, observer)
|
||||
}
|
||||
override fun observe(observer: Observer<List<E>>): Disposable =
|
||||
observe(callNow = false, observer)
|
||||
|
||||
override fun observe(scope: Scope, callNow: Boolean, observer: ValObserver<List<E>>) {
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<List<E>>): Disposable {
|
||||
if (elementObservers.isEmpty() && extractObservables != null) {
|
||||
replaceElementObservers(0, elementObservers.size, elements)
|
||||
}
|
||||
@ -88,20 +85,20 @@ class SimpleListVal<E>(
|
||||
observer(ValChangeEvent(value, value))
|
||||
}
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
observers.remove(observer)
|
||||
disposeElementObserversIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(scope: Scope, observer: ListValObserver<E>) {
|
||||
override fun observeList(observer: ListValObserver<E>): Disposable {
|
||||
if (elementObservers.isEmpty() && extractObservables != null) {
|
||||
replaceElementObservers(0, elementObservers.size, elements)
|
||||
}
|
||||
|
||||
listObservers.add(observer)
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
listObservers.remove(observer)
|
||||
disposeElementObserversIfNecessary()
|
||||
}
|
||||
@ -138,9 +135,7 @@ class SimpleListVal<E>(
|
||||
|
||||
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
|
||||
for (i in 1..amountRemoved) {
|
||||
elementObservers.removeAt(from).observers.forEach { observer ->
|
||||
observer.dispose()
|
||||
}
|
||||
elementObservers.removeAt(from).observers.forEach { it.dispose() }
|
||||
}
|
||||
|
||||
var index = from
|
||||
@ -166,9 +161,7 @@ class SimpleListVal<E>(
|
||||
private fun disposeElementObserversIfNecessary() {
|
||||
if (listObservers.isEmpty() && observers.isEmpty()) {
|
||||
elementObservers.forEach { elementObserver: ElementObserver ->
|
||||
elementObserver.observers.forEach { observer ->
|
||||
observer.dispose()
|
||||
}
|
||||
elementObserver.observers.forEach { it.dispose() }
|
||||
}
|
||||
|
||||
elementObservers.clear()
|
||||
@ -180,9 +173,8 @@ class SimpleListVal<E>(
|
||||
element: E,
|
||||
observables: Array<Observable<*>>,
|
||||
) {
|
||||
val observers: Array<DisposableScope> = Array(observables.size) {
|
||||
val scope = DisposableScope(EmptyCoroutineContext)
|
||||
observables[it].observe(scope) {
|
||||
val observers: Array<Disposable> = Array(observables.size) {
|
||||
observables[it].observe {
|
||||
finalizeUpdate(
|
||||
ListValChangeEvent.ElementChange(
|
||||
index,
|
||||
@ -190,7 +182,6 @@ class SimpleListVal<E>(
|
||||
)
|
||||
)
|
||||
}
|
||||
scope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
import world.phantasmal.observable.test.withScope
|
||||
import world.phantasmal.testUtils.TestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -12,49 +11,49 @@ typealias ObservableAndEmit = Pair<Observable<*>, () -> Unit>
|
||||
* [Observable] implementation.
|
||||
*/
|
||||
abstract class ObservableTests : TestSuite() {
|
||||
abstract fun create(): ObservableAndEmit
|
||||
protected abstract fun create(): ObservableAndEmit
|
||||
|
||||
@Test
|
||||
fun observable_calls_observers_when_events_are_emitted() {
|
||||
val (observable, emit) = create()
|
||||
val changes = mutableListOf<ChangeEvent<*>>()
|
||||
var changes = 0
|
||||
|
||||
withScope { scope ->
|
||||
observable.observe(scope) { c ->
|
||||
changes.add(c)
|
||||
disposer.add(
|
||||
observable.observe {
|
||||
changes++
|
||||
}
|
||||
)
|
||||
|
||||
emit()
|
||||
|
||||
assertEquals(1, changes.size)
|
||||
assertEquals(1, changes)
|
||||
|
||||
emit()
|
||||
emit()
|
||||
emit()
|
||||
|
||||
assertEquals(4, changes.size)
|
||||
}
|
||||
assertEquals(4, changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun observable_does_not_call_observers_after_they_are_disposed() {
|
||||
val (observable, emit) = create()
|
||||
val changes = mutableListOf<ChangeEvent<*>>()
|
||||
var changes = 0
|
||||
|
||||
withScope { scope ->
|
||||
observable.observe(scope) { c ->
|
||||
changes.add(c)
|
||||
val observer = observable.observe {
|
||||
changes++
|
||||
}
|
||||
|
||||
emit()
|
||||
|
||||
assertEquals(1, changes.size)
|
||||
assertEquals(1, changes)
|
||||
|
||||
observer.dispose()
|
||||
|
||||
emit()
|
||||
emit()
|
||||
emit()
|
||||
|
||||
assertEquals(4, changes.size)
|
||||
}
|
||||
assertEquals(1, changes)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package world.phantasmal.observable.test
|
||||
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
fun withScope(block: (Scope) -> Unit) {
|
||||
val scope = DisposableScope(EmptyCoroutineContext)
|
||||
|
||||
try {
|
||||
block(scope)
|
||||
} finally {
|
||||
scope.dispose()
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* In these tests the direct dependency of the [FlatTransformedVal] changes.
|
||||
*/
|
||||
class FlatTransformedValDependentValEmitsTests : RegularValTests() {
|
||||
/**
|
||||
* This is a regression test, it's important that this exact sequence of statements stays the
|
||||
* same.
|
||||
*/
|
||||
@Test
|
||||
fun emits_a_change_when_its_direct_val_dependency_changes() {
|
||||
val v = SimpleVal(SimpleVal(7))
|
||||
val fv = FlatTransformedVal(listOf(v)) { v.value }
|
||||
var observedValue: Int? = null
|
||||
|
||||
disposer.add(
|
||||
fv.observe { observedValue = it.value }
|
||||
)
|
||||
|
||||
assertNull(observedValue)
|
||||
|
||||
v.value.value = 99
|
||||
|
||||
assertEquals(99, observedValue)
|
||||
|
||||
v.value = SimpleVal(7)
|
||||
|
||||
assertEquals(7, observedValue)
|
||||
}
|
||||
|
||||
override fun create(): ValAndEmit<*> {
|
||||
val v = SimpleVal(SimpleVal(5))
|
||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
||||
return ValAndEmit(value) { v.value = SimpleVal(v.value.value + 5) }
|
||||
}
|
||||
|
||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||
val v = SimpleVal(SimpleVal(bool))
|
||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
||||
return ValAndEmit(value) { v.value = SimpleVal(!v.value.value) }
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
/**
|
||||
* In these tests the dependency of the [FlatTransformedVal]'s direct dependency changes.
|
||||
*/
|
||||
class FlatTransformedValNestedValEmitsTests : RegularValTests() {
|
||||
override fun create(): ValAndEmit<*> {
|
||||
val v = SimpleVal(SimpleVal(5))
|
||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
||||
return ValAndEmit(value) { v.value.value += 5 }
|
||||
}
|
||||
|
||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||
val v = SimpleVal(SimpleVal(bool))
|
||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
||||
return ValAndEmit(value) { v.value.value = !v.value.value }
|
||||
}
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.testUtils.TestSuite
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.test.Test
|
||||
|
||||
class StaticValTests : TestSuite() {
|
||||
@ -11,20 +8,8 @@ class StaticValTests : TestSuite() {
|
||||
fun observing_StaticVal_should_never_create_leaks() {
|
||||
val static = StaticVal("test value")
|
||||
|
||||
static.observe(DummyScope) {}
|
||||
static.observe(DummyScope, callNow = false) {}
|
||||
static.observe(DummyScope, callNow = true) {}
|
||||
}
|
||||
|
||||
private object DummyScope : Scope {
|
||||
override val coroutineContext = EmptyCoroutineContext
|
||||
|
||||
override fun add(disposable: Disposable) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun scope(): Scope {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
static.observe {}
|
||||
static.observe(callNow = false) {}
|
||||
static.observe(callNow = true) {}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class DependentValTests : RegularValTests() {
|
||||
class TransformedValTests : RegularValTests() {
|
||||
override fun create(): ValAndEmit<*> {
|
||||
val v = SimpleVal(0)
|
||||
val value = DependentVal(listOf(v)) { 2 * v.value }
|
||||
val value = TransformedVal(listOf(v)) { 2 * v.value }
|
||||
return ValAndEmit(value) { v.value += 2 }
|
||||
}
|
||||
|
||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||
val v = SimpleVal(bool)
|
||||
val value = DependentVal(listOf(v)) { v.value }
|
||||
val value = TransformedVal(listOf(v)) { v.value }
|
||||
return ValAndEmit(value) { v.value = !v.value }
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.core.disposable.use
|
||||
import world.phantasmal.observable.ObservableTests
|
||||
import world.phantasmal.observable.test.withScope
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -22,30 +21,26 @@ abstract class ValTests : ObservableTests() {
|
||||
@Test
|
||||
fun val_respects_call_now_argument() {
|
||||
val (value, emit) = create()
|
||||
val changes = mutableListOf<ChangeEvent<*>>()
|
||||
var changes = 0
|
||||
|
||||
withScope { scope ->
|
||||
// Test callNow = false
|
||||
value.observe(scope, callNow = false) { c ->
|
||||
changes.add(c)
|
||||
}
|
||||
|
||||
value.observe(callNow = false) {
|
||||
changes++
|
||||
}.use {
|
||||
emit()
|
||||
|
||||
assertEquals(1, changes.size)
|
||||
assertEquals(1, changes)
|
||||
}
|
||||
|
||||
withScope { scope ->
|
||||
// Test callNow = true
|
||||
changes.clear()
|
||||
|
||||
value.observe(scope, callNow = true) { c ->
|
||||
changes.add(c)
|
||||
}
|
||||
changes = 0
|
||||
|
||||
value.observe(callNow = true) {
|
||||
changes++
|
||||
}.use {
|
||||
emit()
|
||||
|
||||
assertEquals(2, changes.size)
|
||||
assertEquals(2, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.observable.test.withScope
|
||||
import world.phantasmal.observable.value.ValTests
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -22,8 +21,9 @@ abstract class ListValTests : ValTests() {
|
||||
|
||||
var observedSize = 0
|
||||
|
||||
withScope { scope ->
|
||||
list.sizeVal.observe(scope) { observedSize = it.value }
|
||||
disposer.add(
|
||||
list.sizeVal.observe { observedSize = it.value }
|
||||
)
|
||||
|
||||
for (i in 1..3) {
|
||||
add()
|
||||
@ -32,5 +32,4 @@ abstract class ListValTests : ValTests() {
|
||||
assertEquals(i, observedSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.testUtils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
@ -10,19 +10,23 @@ import kotlin.test.assertEquals
|
||||
|
||||
abstract class TestSuite {
|
||||
private var initialDisposableCount: Int = 0
|
||||
private var _scope: DisposableScope? = null
|
||||
private var _disposer: Disposer? = null
|
||||
|
||||
protected val scope: Scope get() = _scope!!
|
||||
protected val disposer: Disposer get() = _disposer!!
|
||||
|
||||
protected val scope: CoroutineScope = object : CoroutineScope {
|
||||
override val coroutineContext = Job()
|
||||
}
|
||||
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
initialDisposableCount = TrackedDisposable.disposableCount
|
||||
_scope = DisposableScope(Job())
|
||||
_disposer = Disposer()
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun after() {
|
||||
_scope!!.dispose()
|
||||
_disposer!!.dispose()
|
||||
|
||||
val leakCount = TrackedDisposable.disposableCount - initialDisposableCount
|
||||
assertEquals(0, leakCount, "TrackedDisposables were leaked")
|
@ -5,16 +5,17 @@ import io.ktor.client.features.json.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.w3c.dom.PopStateEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.application.Application
|
||||
import world.phantasmal.web.core.HttpAssetLoader
|
||||
import world.phantasmal.web.core.UiDispatcher
|
||||
import world.phantasmal.web.core.stores.ApplicationUrl
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
@ -29,7 +30,7 @@ fun main() {
|
||||
}
|
||||
|
||||
private fun init(): Disposable {
|
||||
val scope = DisposableScope(UiDispatcher)
|
||||
val disposer = Disposer()
|
||||
|
||||
val rootElement = document.body!!.root()
|
||||
|
||||
@ -40,32 +41,39 @@ private fun init(): Disposable {
|
||||
})
|
||||
}
|
||||
}
|
||||
scope.disposable { httpClient.cancel() }
|
||||
disposer.add(disposable { httpClient.cancel() })
|
||||
|
||||
val pathname = window.location.pathname
|
||||
val basePath = window.location.origin +
|
||||
(if (pathname.lastOrNull() == '/') pathname.dropLast(1) else pathname)
|
||||
|
||||
val scope = CoroutineScope(Job())
|
||||
disposer.add(disposable { scope.cancel() })
|
||||
|
||||
disposer.add(
|
||||
Application(
|
||||
scope,
|
||||
rootElement,
|
||||
HttpAssetLoader(httpClient, basePath),
|
||||
HistoryApplicationUrl(scope),
|
||||
disposer.add(HistoryApplicationUrl()),
|
||||
createEngine = { Engine(it) }
|
||||
)
|
||||
)
|
||||
|
||||
return scope
|
||||
return disposer
|
||||
}
|
||||
|
||||
class HistoryApplicationUrl(scope: Scope) : ApplicationUrl {
|
||||
class HistoryApplicationUrl : TrackedDisposable(), ApplicationUrl {
|
||||
private val path: String get() = window.location.pathname
|
||||
|
||||
override val url = mutableVal(window.location.hash.substring(1))
|
||||
|
||||
init {
|
||||
disposableListener<PopStateEvent>(scope, window, "popstate", {
|
||||
private val popStateListener = disposableListener<PopStateEvent>(window, "popstate", {
|
||||
url.value = window.location.hash.substring(1)
|
||||
})
|
||||
|
||||
override fun internalDispose() {
|
||||
popStateListener.dispose()
|
||||
}
|
||||
|
||||
override fun pushUrl(url: String) {
|
||||
|
@ -7,7 +7,6 @@ import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.application.controllers.MainContentController
|
||||
import world.phantasmal.web.application.controllers.NavigationController
|
||||
import world.phantasmal.web.application.widgets.ApplicationWidget
|
||||
@ -20,47 +19,52 @@ import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.web.huntOptimizer.HuntOptimizer
|
||||
import world.phantasmal.web.questEditor.QuestEditor
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
class Application(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
rootElement: HTMLElement,
|
||||
assetLoader: AssetLoader,
|
||||
applicationUrl: ApplicationUrl,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) {
|
||||
) : DisposableContainer() {
|
||||
init {
|
||||
addDisposables(
|
||||
// Disable native undo/redo.
|
||||
disposableListener(scope, document, "beforeinput", ::beforeInput)
|
||||
disposableListener(document, "beforeinput", ::beforeInput),
|
||||
// Work-around for FireFox:
|
||||
disposableListener(scope, document, "keydown", ::keydown)
|
||||
disposableListener(document, "keydown", ::keydown),
|
||||
|
||||
// Disable native drag-and-drop to avoid users dragging in unsupported file formats and
|
||||
// leaving the application unexpectedly.
|
||||
disposableListener(scope, document, "dragenter", ::dragenter)
|
||||
disposableListener(scope, document, "dragover", ::dragover)
|
||||
disposableListener(scope, document, "drop", ::drop)
|
||||
disposableListener(document, "dragenter", ::dragenter),
|
||||
disposableListener(document, "dragover", ::dragover),
|
||||
disposableListener(document, "drop", ::drop),
|
||||
)
|
||||
|
||||
// Initialize core stores shared by several submodules.
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = addDisposable(UiStore(scope, applicationUrl))
|
||||
|
||||
// Controllers.
|
||||
val navigationController = NavigationController(scope, uiStore)
|
||||
val mainContentController = MainContentController(scope, uiStore)
|
||||
val navigationController = addDisposable(NavigationController(scope, uiStore))
|
||||
val mainContentController = addDisposable(MainContentController(scope, uiStore))
|
||||
|
||||
// Initialize application view.
|
||||
val applicationWidget = ApplicationWidget(
|
||||
val applicationWidget = addDisposable(
|
||||
ApplicationWidget(
|
||||
scope,
|
||||
NavigationWidget(scope, navigationController),
|
||||
MainContentWidget(scope, mainContentController, mapOf(
|
||||
PwTool.QuestEditor to { s ->
|
||||
QuestEditor(s, uiStore, createEngine).widget
|
||||
addDisposable(QuestEditor(s, uiStore, createEngine)).createWidget()
|
||||
},
|
||||
PwTool.HuntOptimizer to { s ->
|
||||
HuntOptimizer(s, assetLoader, uiStore).widget
|
||||
addDisposable(HuntOptimizer(s, assetLoader, uiStore)).createWidget()
|
||||
},
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
rootElement.appendChild(applicationWidget.element)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.web.application.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class MainContentController(scope: Scope, uiStore: UiStore) : Controller(scope) {
|
||||
class MainContentController(scope: CoroutineScope, uiStore: UiStore) : Controller(scope) {
|
||||
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
package world.phantasmal.web.application.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class NavigationController(scope: Scope, private val uiStore: UiStore) : Controller(scope) {
|
||||
class NavigationController(
|
||||
scope: CoroutineScope,
|
||||
private val uiStore: UiStore,
|
||||
) : Controller(scope) {
|
||||
val tools: Map<PwTool, Val<Boolean>> = uiStore.toolToActive
|
||||
|
||||
fun setCurrentTool(tool: PwTool) {
|
||||
|
@ -1,15 +1,15 @@
|
||||
package world.phantasmal.web.application.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class ApplicationWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val navigationWidget: NavigationWidget,
|
||||
private val mainContentWidget: MainContentWidget,
|
||||
) : Widget(scope, ::style) {
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-application-application") {
|
||||
addChild(navigationWidget)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.application.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.not
|
||||
import world.phantasmal.web.application.controllers.MainContentController
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
@ -10,11 +10,12 @@ import world.phantasmal.webui.widgets.LazyLoader
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class MainContentWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: MainContentController,
|
||||
private val toolViews: Map<PwTool, (Scope) -> Widget>,
|
||||
) : Widget(scope, ::style) {
|
||||
override fun Node.createElement() = div(className = "pw-application-main-content") {
|
||||
private val toolViews: Map<PwTool, (CoroutineScope) -> Widget>,
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-application-main-content") {
|
||||
ctrl.tools.forEach { (tool, active) ->
|
||||
toolViews[tool]?.let { createWidget ->
|
||||
addChild(LazyLoader(scope, hidden = !active, createWidget = createWidget))
|
||||
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.web.application.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.application.controllers.NavigationController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class NavigationWidget(scope: Scope, private val ctrl: NavigationController) :
|
||||
Widget(scope, ::style) {
|
||||
class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationController) :
|
||||
Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-application-navigation") {
|
||||
ctrl.tools.forEach { (tool, active) ->
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.application.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.webui.dom.input
|
||||
@ -10,18 +10,18 @@ import world.phantasmal.webui.dom.span
|
||||
import world.phantasmal.webui.widgets.Control
|
||||
|
||||
class PwToolButton(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val tool: PwTool,
|
||||
private val toggled: Observable<Boolean>,
|
||||
private val mouseDown: () -> Unit,
|
||||
) : Control(scope, ::style) {
|
||||
) : Control(scope, listOf(::style)) {
|
||||
private val inputId = "pw-application-pw-tool-button-${tool.name.toLowerCase()}"
|
||||
|
||||
override fun Node.createElement() =
|
||||
span(className = "pw-application-pw-tool-button") {
|
||||
input(type = "radio", id = inputId) {
|
||||
name = "pw-application-pw-tool-button"
|
||||
toggled.observe { checked = it }
|
||||
observe(toggled) { checked = it }
|
||||
}
|
||||
label(htmlFor = inputId) {
|
||||
textContent = tool.uiName
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.web.core.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Tab
|
||||
@ -9,13 +9,13 @@ import world.phantasmal.webui.controllers.TabController
|
||||
open class PathAwareTab(override val title: String, val path: String) : Tab
|
||||
|
||||
open class PathAwareTabController<T : PathAwareTab>(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val uiStore: UiStore,
|
||||
private val tool: PwTool,
|
||||
tabs: List<T>,
|
||||
) : TabController<T>(scope, tabs) {
|
||||
init {
|
||||
uiStore.path.observe(scope, callNow = true) { (path) ->
|
||||
observe(uiStore.path) { path ->
|
||||
if (uiStore.currentTool.value == tool) {
|
||||
tabs.find { path.startsWith(it.path) }?.let {
|
||||
setActiveTab(it, replaceUrl = true)
|
||||
|
@ -1,16 +1,14 @@
|
||||
package world.phantasmal.web.core.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.web.externals.Scene
|
||||
|
||||
abstract class Renderer(
|
||||
scope: Scope,
|
||||
protected val canvas: HTMLCanvasElement,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : TrackedDisposable(scope) {
|
||||
) : TrackedDisposable() {
|
||||
protected val engine = createEngine(canvas)
|
||||
protected val scene = Scene(engine)
|
||||
|
||||
@ -23,5 +21,6 @@ abstract class Renderer(
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
// TODO: Clean up Babylon resources.
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.core.stores
|
||||
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
@ -27,7 +27,7 @@ interface ApplicationUrl {
|
||||
fun replaceUrl(url: String)
|
||||
}
|
||||
|
||||
class UiStore(scope: Scope, private val applicationUrl: ApplicationUrl) : Store(scope) {
|
||||
class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl) : Store(scope) {
|
||||
private val _currentTool: MutableVal<PwTool>
|
||||
|
||||
private val _path = mutableVal("")
|
||||
@ -85,8 +85,11 @@ class UiStore(scope: Scope, private val applicationUrl: ApplicationUrl) : Store(
|
||||
}
|
||||
.toMap()
|
||||
|
||||
disposableListener(scope, window, "keydown", ::dispatchGlobalKeydown)
|
||||
applicationUrl.url.observe(scope, callNow = true) { setDataFromUrl(it.value) }
|
||||
addDisposables(
|
||||
disposableListener(window, "keydown", ::dispatchGlobalKeydown),
|
||||
)
|
||||
|
||||
observe(applicationUrl.url) { setDataFromUrl(it) }
|
||||
}
|
||||
|
||||
fun setCurrentTool(tool: PwTool) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.core.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.web.core.newJsObject
|
||||
@ -35,33 +35,29 @@ class DockedStack(
|
||||
items: List<DockedItem> = emptyList(),
|
||||
) : DockedContainer(flex, items)
|
||||
|
||||
class DocketWidget(
|
||||
class DockedWidget(
|
||||
val id: String,
|
||||
val title: String,
|
||||
flex: Int? = null,
|
||||
val createWidget: (Scope) -> Widget,
|
||||
val createWidget: (CoroutineScope) -> Widget,
|
||||
) : DockedItem(flex)
|
||||
|
||||
class DockWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
private val item: DockedItem,
|
||||
) : Widget(scope, ::style, hidden) {
|
||||
) : Widget(scope, listOf(::style), hidden) {
|
||||
private lateinit var goldenLayout: GoldenLayout
|
||||
|
||||
init {
|
||||
try {
|
||||
// Importing the base CSS fails during unit tests.
|
||||
js("""require("golden-layout/src/css/goldenlayout-base.css");""")
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
observeResize()
|
||||
}
|
||||
|
||||
override fun Node.createElement() = div(className = "pw-core-dock") {
|
||||
val idToCreate = mutableMapOf<String, (Scope) -> Widget>()
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-core-dock") {
|
||||
val idToCreate = mutableMapOf<String, (CoroutineScope) -> Widget>()
|
||||
|
||||
val config = newJsObject<GoldenLayout.Config> {
|
||||
settings = newJsObject<GoldenLayout.Settings> {
|
||||
@ -85,7 +81,8 @@ class DockWidget(
|
||||
|
||||
idToCreate.forEach { (id, create) ->
|
||||
goldenLayout.registerComponent(id) { container: GoldenLayout.Container ->
|
||||
container.getElement().append(create(scope).element)
|
||||
val node = container.getElement()[0] as Node
|
||||
node.addChild(create(scope))
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,17 +103,17 @@ class DockWidget(
|
||||
|
||||
private fun toConfigContent(
|
||||
item: DockedItem,
|
||||
idToCreate: MutableMap<String, (Scope) -> Widget>,
|
||||
idToCreate: MutableMap<String, (CoroutineScope) -> Widget>,
|
||||
): GoldenLayout.ItemConfig {
|
||||
val itemType = when (item) {
|
||||
is DockedRow -> "row"
|
||||
is DockedColumn -> "column"
|
||||
is DockedStack -> "stack"
|
||||
is DocketWidget -> "component"
|
||||
is DockedWidget -> "component"
|
||||
}
|
||||
|
||||
return when (item) {
|
||||
is DocketWidget -> {
|
||||
is DockedWidget -> {
|
||||
idToCreate[item.id] = item.createWidget
|
||||
|
||||
newJsObject<GoldenLayout.ComponentConfig> {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.core.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
import world.phantasmal.webui.dom.canvas
|
||||
@ -10,12 +10,13 @@ import world.phantasmal.webui.widgets.Widget
|
||||
import kotlin.math.floor
|
||||
|
||||
class RendererWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : Widget(scope, ::style) {
|
||||
override fun Node.createElement() = canvas(className = "pw-core-renderer") {
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
canvas(className = "pw-core-renderer") {
|
||||
observeResize()
|
||||
QuestRenderer(scope, this, createEngine)
|
||||
addDisposable(QuestRenderer(this, createEngine))
|
||||
}
|
||||
|
||||
override fun resized(width: Double, height: Double) {
|
||||
|
@ -4,7 +4,7 @@ import org.w3c.dom.Element
|
||||
|
||||
@JsModule("golden-layout")
|
||||
@JsNonModule
|
||||
external open class GoldenLayout(configuration: Config, container: Element = definedExternally) {
|
||||
open external class GoldenLayout(configuration: Config, container: Element = definedExternally) {
|
||||
open fun init()
|
||||
open fun updateSize(width: Double, height: Double)
|
||||
open fun registerComponent(name: String, component: Any)
|
||||
@ -12,128 +12,62 @@ external open class GoldenLayout(configuration: Config, container: Element = def
|
||||
|
||||
interface Settings {
|
||||
var hasHeaders: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var constrainDragToContainer: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var reorderEnabled: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var selectionEnabled: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var popoutWholeStack: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var blockedPopoutsThrowError: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var closePopoutsOnUnload: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var showPopoutIcon: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var showMaximiseIcon: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var showCloseIcon: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface Dimensions {
|
||||
var borderWidth: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var minItemHeight: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var minItemWidth: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var headerHeight: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var dragProxyWidth: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var dragProxyHeight: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface Labels {
|
||||
var close: String?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var maximise: String?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var minimise: String?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var popout: String?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface ItemConfig {
|
||||
var type: String
|
||||
var content: Array<ItemConfig>?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var width: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var height: Number?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var id: dynamic /* String? | Array<String>? */
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var isClosable: Boolean?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var title: String?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface ComponentConfig : ItemConfig {
|
||||
var componentName: String
|
||||
var componentState: Any?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface ReactComponentConfig : ItemConfig {
|
||||
var component: String
|
||||
var props: Any?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
}
|
||||
|
||||
interface Config {
|
||||
var settings: Settings?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var dimensions: Dimensions?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var labels: Labels?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var content: Array<dynamic /* ItemConfig | ComponentConfig | ReactComponentConfig */>?
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var content: Array<ItemConfig>?
|
||||
}
|
||||
|
||||
interface ContentItem : EventEmitter {
|
||||
var config: dynamic /* ItemConfig | ComponentConfig | ReactComponentConfig */
|
||||
get() = definedExternally
|
||||
set(value) = definedExternally
|
||||
var config: ItemConfig
|
||||
var type: String
|
||||
var contentItems: Array<ContentItem>
|
||||
var parent: ContentItem
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.web.huntOptimizer
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.web.core.AssetLoader
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
||||
@ -8,18 +8,22 @@ import world.phantasmal.web.huntOptimizer.controllers.MethodsController
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntMethodStore
|
||||
import world.phantasmal.web.huntOptimizer.widgets.HuntOptimizerWidget
|
||||
import world.phantasmal.web.huntOptimizer.widgets.MethodsWidget
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class HuntOptimizer(
|
||||
scope: Scope,
|
||||
private val scope: CoroutineScope,
|
||||
assetLoader: AssetLoader,
|
||||
uiStore: UiStore,
|
||||
) {
|
||||
private val huntMethodStore = HuntMethodStore(scope, uiStore, assetLoader)
|
||||
) : DisposableContainer() {
|
||||
private val huntMethodStore = addDisposable(HuntMethodStore(scope, uiStore, assetLoader))
|
||||
|
||||
private val huntOptimizerController = HuntOptimizerController(scope, uiStore)
|
||||
private val methodsController = MethodsController(scope, uiStore, huntMethodStore)
|
||||
private val huntOptimizerController = addDisposable(HuntOptimizerController(scope, uiStore))
|
||||
private val methodsController =
|
||||
addDisposable(MethodsController(scope, uiStore, huntMethodStore))
|
||||
|
||||
val widget = HuntOptimizerWidget(
|
||||
fun createWidget(): Widget =
|
||||
HuntOptimizerWidget(
|
||||
scope,
|
||||
ctrl = huntOptimizerController,
|
||||
createMethodsWidget = { scope -> MethodsWidget(scope, methodsController) }
|
||||
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.web.core.controllers.PathAwareTab
|
||||
import world.phantasmal.web.core.controllers.PathAwareTabController
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
||||
|
||||
class HuntOptimizerController(scope: Scope, uiStore: UiStore) :
|
||||
class HuntOptimizerController(scope: CoroutineScope, uiStore: UiStore) :
|
||||
PathAwareTabController<PathAwareTab>(
|
||||
scope,
|
||||
uiStore,
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.MutableListVal
|
||||
@ -16,7 +16,7 @@ import world.phantasmal.web.huntOptimizer.stores.HuntMethodStore
|
||||
class MethodsTab(title: String, path: String, val episode: Episode) : PathAwareTab(title, path)
|
||||
|
||||
class MethodsController(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
uiStore: UiStore,
|
||||
huntMethodStore: HuntMethodStore,
|
||||
) : PathAwareTabController<MethodsTab>(
|
||||
@ -35,7 +35,7 @@ class MethodsController(
|
||||
|
||||
init {
|
||||
// TODO: Use filtered ListVals.
|
||||
huntMethodStore.methods.observe(scope, callNow = true) { (methods) ->
|
||||
observe(huntMethodStore.methods) { methods ->
|
||||
val ep1 = _episodeToMethods.getOrPut(Episode.I) { mutableListVal() }
|
||||
val ep2 = _episodeToMethods.getOrPut(Episode.II) { mutableListVal() }
|
||||
val ep4 = _episodeToMethods.getOrPut(Episode.IV) { mutableListVal() }
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.huntOptimizer.stores
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
@ -21,14 +21,14 @@ import kotlin.collections.set
|
||||
import kotlin.time.minutes
|
||||
|
||||
class HuntMethodStore(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
uiStore: UiStore,
|
||||
private val assetLoader: AssetLoader,
|
||||
) : Store(scope) {
|
||||
private val _methods = mutableListVal<HuntMethodModel>()
|
||||
|
||||
val methods: ListVal<HuntMethodModel> by lazy {
|
||||
uiStore.server.observe(scope, callNow = true) { loadMethods(it.value) }
|
||||
observe(uiStore.server) { loadMethods(it) }
|
||||
_methods
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
package world.phantasmal.web.huntOptimizer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.dom.p
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class HelpWidget(scope: Scope) : Widget(scope, ::style) {
|
||||
override fun Node.createElement() = div(className = "pw-hunt-optimizer-help") {
|
||||
class HelpWidget(scope: CoroutineScope) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-hunt-optimizer-help") {
|
||||
p {
|
||||
textContent =
|
||||
"Add some items with the combo box on the left to see the optimal combination of hunt methods on the right."
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.huntOptimizer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.huntOptimizer.HuntOptimizerUrls
|
||||
import world.phantasmal.web.huntOptimizer.controllers.HuntOptimizerController
|
||||
import world.phantasmal.webui.dom.div
|
||||
@ -9,10 +9,10 @@ import world.phantasmal.webui.widgets.TabContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class HuntOptimizerWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: HuntOptimizerController,
|
||||
private val createMethodsWidget: (Scope) -> MethodsWidget,
|
||||
) : Widget(scope, ::style) {
|
||||
private val createMethodsWidget: (CoroutineScope) -> MethodsWidget,
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() = div(className = "pw-hunt-optimizer-hunt-optimizer") {
|
||||
addChild(TabContainer(
|
||||
scope,
|
||||
|
@ -1,21 +1,20 @@
|
||||
package world.phantasmal.web.huntOptimizer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.web.huntOptimizer.controllers.MethodsController
|
||||
import world.phantasmal.webui.dom.bindChildrenTo
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class MethodsForEpisodeWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: MethodsController,
|
||||
private val episode: Episode,
|
||||
) : Widget(scope, ::style) {
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-hunt-optimizer-methods-for-episode") {
|
||||
bindChildrenTo(scope, ctrl.episodeToMethods.getValue(episode)) { method, _ ->
|
||||
bindChildrenTo(ctrl.episodeToMethods.getValue(episode)) { method, _ ->
|
||||
div { textContent = method.name }
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
package world.phantasmal.web.huntOptimizer.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.huntOptimizer.controllers.MethodsController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.TabContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class MethodsWidget(scope: Scope, private val ctrl: MethodsController) : Widget(scope, ::style) {
|
||||
override fun Node.createElement() = div(className = "pw-hunt-optimizer-methods") {
|
||||
class MethodsWidget(
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: MethodsController,
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-hunt-optimizer-methods") {
|
||||
addChild(TabContainer(scope, ctrl = ctrl, createWidget = { scope, tab ->
|
||||
MethodsForEpisodeWidget(scope, ctrl, tab.episode)
|
||||
}))
|
||||
|
@ -1,24 +1,35 @@
|
||||
package world.phantasmal.web.questEditor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
|
||||
import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.web.questEditor.widgets.QuestEditorRendererWidget
|
||||
import world.phantasmal.web.questEditor.widgets.QuestEditorToolbar
|
||||
import world.phantasmal.web.questEditor.widgets.QuestEditorWidget
|
||||
import world.phantasmal.web.questEditor.widgets.QuestInfoWidget
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class QuestEditor(
|
||||
scope: Scope,
|
||||
private val scope: CoroutineScope,
|
||||
uiStore: UiStore,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) {
|
||||
private val toolbarController = QuestEditorToolbarController(scope)
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : DisposableContainer() {
|
||||
private val questEditorStore = addDisposable(QuestEditorStore(scope))
|
||||
|
||||
val widget = QuestEditorWidget(
|
||||
private val toolbarController =
|
||||
addDisposable(QuestEditorToolbarController(scope, questEditorStore))
|
||||
private val questInfoController = addDisposable(QuestInfoController(scope, questEditorStore))
|
||||
|
||||
fun createWidget(): Widget =
|
||||
QuestEditorWidget(
|
||||
scope,
|
||||
QuestEditorToolbar(scope, toolbarController),
|
||||
{ scope -> QuestInfoWidget(scope, questInfoController) },
|
||||
{ scope -> QuestEditorRendererWidget(scope, createEngine) }
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,31 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.w3c.files.File
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.*
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.cursor.ArrayBufferCursor
|
||||
import world.phantasmal.lib.fileFormats.quest.Quest
|
||||
import world.phantasmal.lib.fileFormats.quest.parseBinDatToQuest
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.web.questEditor.stores.convertQuestToModel
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
import world.phantasmal.webui.readFile
|
||||
|
||||
class QuestEditorToolbarController(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val questEditorStore: QuestEditorStore
|
||||
) : Controller(scope) {
|
||||
fun filesOpened(files: List<File>) {
|
||||
private val _resultDialogVisible = mutableVal(false)
|
||||
private val _result = mutableVal<PwResult<*>?>(null)
|
||||
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
|
||||
fun openFiles(files: List<File>) {
|
||||
launch {
|
||||
if (files.isEmpty()) return@launch
|
||||
|
||||
@ -22,12 +38,38 @@ class QuestEditorToolbarController(
|
||||
val bin = files.find { it.name.endsWith(".bin", ignoreCase = true) }
|
||||
val dat = files.find { it.name.endsWith(".dat", ignoreCase = true) }
|
||||
|
||||
if (bin != null && dat != null) {
|
||||
if (bin == null || dat == null) {
|
||||
setResult(Failure(listOf(Problem(
|
||||
Severity.Error,
|
||||
"Please select a .qst file or one .bin and one .dat file."
|
||||
))))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val binBuffer = readFile(bin)
|
||||
val datBuffer = readFile(dat)
|
||||
// TODO: Parse bin and dat.
|
||||
val parseResult = parseBinDatToQuest(
|
||||
ArrayBufferCursor(binBuffer, Endianness.Little),
|
||||
ArrayBufferCursor(datBuffer, Endianness.Little)
|
||||
)
|
||||
setResult(parseResult)
|
||||
|
||||
if (parseResult is Success) {
|
||||
setCurrentQuest(parseResult.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCurrentQuest(quest: Quest) {
|
||||
questEditorStore.setCurrentQuest(convertQuestToModel(quest))
|
||||
}
|
||||
|
||||
private fun setResult(result: PwResult<*>) {
|
||||
_result.value = result
|
||||
|
||||
if (result.problems.isNotEmpty()) {
|
||||
_resultDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class QuestInfoController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) {
|
||||
val id: Val<Int> = store.currentQuest.flatTransform { it?.id ?: value(0) }
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
|
||||
class QuestModel(
|
||||
id: Int,
|
||||
language: Int,
|
||||
name: String,
|
||||
shortDescription: String,
|
||||
longDescription: String,
|
||||
) {
|
||||
private val _id = mutableVal(0)
|
||||
private val _language = mutableVal(0)
|
||||
private val _name = mutableVal("")
|
||||
private val _shortDescription = mutableVal("")
|
||||
private val _longDescription = mutableVal("")
|
||||
|
||||
val id: Val<Int> = _id
|
||||
val language: Val<Int> = _language
|
||||
val name: Val<String> = _name
|
||||
val shortDescription: Val<String> = _shortDescription
|
||||
val longDescription: Val<String> = _longDescription
|
||||
|
||||
init {
|
||||
setId(id)
|
||||
setLanguage(language)
|
||||
setName(name)
|
||||
setShortDescription(shortDescription)
|
||||
setLongDescription(longDescription)
|
||||
}
|
||||
|
||||
fun setId(id: Int): QuestModel {
|
||||
require(id >= 0) { "id should be greater than or equal to 0, was ${id}." }
|
||||
|
||||
_id.value = id
|
||||
return this
|
||||
}
|
||||
|
||||
fun setLanguage(language: Int): QuestModel {
|
||||
require(language >= 0) { "language should be greater than or equal to 0, was ${language}." }
|
||||
|
||||
_language.value = language
|
||||
return this
|
||||
}
|
||||
|
||||
fun setName(name: String): QuestModel {
|
||||
require(name.length <= 32) { """name can't be longer than 32 characters, got "$name".""" }
|
||||
|
||||
_name.value = name
|
||||
return this
|
||||
}
|
||||
|
||||
fun setShortDescription(shortDescription: String): QuestModel {
|
||||
require(shortDescription.length <= 128) {
|
||||
"""shortDescription can't be longer than 128 characters, got "$shortDescription"."""
|
||||
}
|
||||
|
||||
_shortDescription.value = shortDescription
|
||||
return this
|
||||
}
|
||||
|
||||
fun setLongDescription(longDescription: String): QuestModel {
|
||||
require(longDescription.length <= 288) {
|
||||
"""longDescription can't be longer than 288 characters, got "$longDescription"."""
|
||||
}
|
||||
|
||||
_longDescription.value = longDescription
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.core.newJsObject
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.externals.*
|
||||
import kotlin.math.PI
|
||||
|
||||
class QuestRenderer(
|
||||
scope: Scope,
|
||||
canvas: HTMLCanvasElement,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : Renderer(scope, canvas, createEngine) {
|
||||
) : Renderer(canvas, createEngine) {
|
||||
private val camera = ArcRotateCamera("Camera", PI / 2, PI / 2, 2.0, Vector3.Zero(), scene)
|
||||
private val light = HemisphericLight("Light", Vector3(1.0, 1.0, 0.0), scene)
|
||||
private val cylinder =
|
||||
|
@ -0,0 +1,14 @@
|
||||
package world.phantasmal.web.questEditor.stores
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.Quest
|
||||
import world.phantasmal.web.questEditor.models.QuestModel
|
||||
|
||||
fun convertQuestToModel(quest: Quest): QuestModel {
|
||||
return QuestModel(
|
||||
quest.id,
|
||||
quest.language,
|
||||
quest.name,
|
||||
quest.shortDescription,
|
||||
quest.longDescription
|
||||
)
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package world.phantasmal.web.questEditor.stores
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.web.questEditor.models.QuestModel
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
class QuestEditorStore(scope: CoroutineScope) : Store(scope) {
|
||||
private val _currentQuest = mutableVal<QuestModel?>(null)
|
||||
|
||||
val currentQuest: Val<QuestModel?> = _currentQuest
|
||||
|
||||
fun setCurrentQuest(quest: QuestModel?) {
|
||||
_currentQuest.value = quest
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.externals.Engine
|
||||
|
||||
class QuestEditorRendererWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : QuestRendererWidget(scope, createEngine) {
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.FileButton
|
||||
@ -9,7 +9,7 @@ import world.phantasmal.webui.widgets.Toolbar
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class QuestEditorToolbar(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: QuestEditorToolbarController,
|
||||
) : Widget(scope) {
|
||||
override fun Node.createElement() = div(className = "pw-quest-editor-toolbar") {
|
||||
@ -21,7 +21,7 @@ class QuestEditorToolbar(
|
||||
text = "Open file...",
|
||||
accept = ".bin, .dat, .qst",
|
||||
multiple = true,
|
||||
filesSelected = ctrl::filesOpened
|
||||
filesSelected = ctrl::openFiles
|
||||
)
|
||||
)
|
||||
))
|
||||
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.core.widgets.*
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
// TODO: Remove TestWidget.
|
||||
private class TestWidget(scope: Scope) : Widget(scope) {
|
||||
private class TestWidget(scope: CoroutineScope) : Widget(scope) {
|
||||
override fun Node.createElement() = div {
|
||||
textContent = "Test ${++count}"
|
||||
}
|
||||
@ -18,10 +18,11 @@ private class TestWidget(scope: Scope) : Widget(scope) {
|
||||
}
|
||||
|
||||
open class QuestEditorWidget(
|
||||
scope: Scope,
|
||||
private val toolbar: QuestEditorToolbar,
|
||||
private val createQuestRendererWidget: (Scope) -> Widget,
|
||||
) : Widget(scope, ::style) {
|
||||
scope: CoroutineScope,
|
||||
private val toolbar: Widget,
|
||||
private val createQuestInfoWidget: (CoroutineScope) -> Widget,
|
||||
private val createQuestRendererWidget: (CoroutineScope) -> Widget,
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-quest-editor-quest-editor") {
|
||||
addChild(toolbar)
|
||||
@ -34,19 +35,19 @@ open class QuestEditorWidget(
|
||||
items = listOf(
|
||||
DockedStack(
|
||||
items = listOf(
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "Info",
|
||||
id = "info",
|
||||
createWidget = ::TestWidget
|
||||
createWidget = createQuestInfoWidget
|
||||
),
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "NPC Counts",
|
||||
id = "npc_counts",
|
||||
createWidget = ::TestWidget
|
||||
),
|
||||
)
|
||||
),
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "Entity",
|
||||
id = "entity_info",
|
||||
createWidget = ::TestWidget
|
||||
@ -56,12 +57,12 @@ open class QuestEditorWidget(
|
||||
DockedStack(
|
||||
flex = 9,
|
||||
items = listOf(
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "3D View",
|
||||
id = "quest_renderer",
|
||||
createWidget = createQuestRendererWidget
|
||||
),
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "Script",
|
||||
id = "asm_editor",
|
||||
createWidget = ::TestWidget
|
||||
@ -71,17 +72,17 @@ open class QuestEditorWidget(
|
||||
DockedStack(
|
||||
flex = 2,
|
||||
items = listOf(
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "NPCs",
|
||||
id = "npc_list_view",
|
||||
createWidget = ::TestWidget
|
||||
),
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "Objects",
|
||||
id = "object_list_view",
|
||||
createWidget = ::TestWidget
|
||||
),
|
||||
DocketWidget(
|
||||
DockedWidget(
|
||||
title = "Events",
|
||||
id = "events_view",
|
||||
createWidget = ::TestWidget
|
||||
|
@ -0,0 +1,65 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
||||
import world.phantasmal.webui.dom.*
|
||||
import world.phantasmal.webui.widgets.IntInput
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class QuestInfoWidget(
|
||||
scope: CoroutineScope,
|
||||
private val ctrl: QuestInfoController,
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-quest-editor-quest-info", tabIndex = -1) {
|
||||
table {
|
||||
tr {
|
||||
th { textContent = "Episode:" }
|
||||
td()
|
||||
}
|
||||
tr {
|
||||
th { textContent = "ID:" }
|
||||
td {
|
||||
addChild(IntInput(
|
||||
this@QuestInfoWidget.scope,
|
||||
valueVal = ctrl.id,
|
||||
min = 0,
|
||||
step = 0
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
private fun style() = """
|
||||
.pw-quest-editor-quest-info {
|
||||
box-sizing: border-box;
|
||||
padding: 3px;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pw-quest-editor-quest-info table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pw-quest-editor-quest-info th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pw-quest-editor-quest-info .pw-text-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pw-quest-editor-quest-info .pw-text-area {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pw-quest-editor-quest-info textarea {
|
||||
width: 100%;
|
||||
}
|
||||
"""
|
@ -1,17 +1,17 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.web.core.widgets.RendererWidget
|
||||
import world.phantasmal.web.externals.Engine
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
abstract class QuestRendererWidget(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
private val createEngine: (HTMLCanvasElement) -> Engine,
|
||||
) : Widget(scope, ::style) {
|
||||
) : Widget(scope, listOf(::style)) {
|
||||
override fun Node.createElement() = div(className = "pw-quest-editor-quest-renderer") {
|
||||
addChild(RendererWidget(scope, createEngine))
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import io.ktor.client.*
|
||||
import io.ktor.client.features.json.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import world.phantasmal.core.disposable.DisposableScope
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.disposable.use
|
||||
import world.phantasmal.testUtils.TestSuite
|
||||
import world.phantasmal.web.core.HttpAssetLoader
|
||||
import world.phantasmal.web.core.stores.PwTool
|
||||
@ -19,9 +19,7 @@ class ApplicationTests : TestSuite() {
|
||||
@Test
|
||||
fun initialization_and_shutdown_should_succeed_without_throwing() {
|
||||
(listOf(null) + PwTool.values().toList()).forEach { tool ->
|
||||
val scope = DisposableScope(Job())
|
||||
|
||||
try {
|
||||
Disposer().use { disposer ->
|
||||
val httpClient = HttpClient {
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
|
||||
@ -29,17 +27,19 @@ class ApplicationTests : TestSuite() {
|
||||
})
|
||||
}
|
||||
}
|
||||
scope.disposable { httpClient.cancel() }
|
||||
disposer.add(disposable { httpClient.cancel() })
|
||||
|
||||
val appUrl = TestApplicationUrl(if (tool == null) "" else "/${tool.slug}")
|
||||
|
||||
disposer.add(
|
||||
Application(
|
||||
scope,
|
||||
rootElement = document.body!!,
|
||||
assetLoader = HttpAssetLoader(httpClient, basePath = ""),
|
||||
applicationUrl = TestApplicationUrl(if (tool == null) "" else "/${tool.slug}"),
|
||||
applicationUrl = appUrl,
|
||||
createEngine = { Engine(it) }
|
||||
)
|
||||
} finally {
|
||||
scope.dispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,13 +41,15 @@ class PathAwareTabControllerTests : TestSuite() {
|
||||
@Test
|
||||
fun applicationUrl_changes_when_switch_to_tool_with_tabs() {
|
||||
val appUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, appUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, appUrl))
|
||||
|
||||
disposer.add(
|
||||
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf(
|
||||
PathAwareTab("A", "/a"),
|
||||
PathAwareTab("B", "/b"),
|
||||
PathAwareTab("C", "/c"),
|
||||
))
|
||||
)
|
||||
|
||||
assertFalse(appUrl.canGoBack)
|
||||
assertFalse(appUrl.canGoForward)
|
||||
@ -68,14 +70,16 @@ class PathAwareTabControllerTests : TestSuite() {
|
||||
block: (PathAwareTabController<PathAwareTab>, applicationUrl: TestApplicationUrl) -> Unit,
|
||||
) {
|
||||
val applicationUrl = TestApplicationUrl("/${PwTool.HuntOptimizer.slug}/b")
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, applicationUrl))
|
||||
uiStore.setCurrentTool(PwTool.HuntOptimizer)
|
||||
|
||||
val ctrl = PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf(
|
||||
val ctrl = disposer.add(
|
||||
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf(
|
||||
PathAwareTab("A", "/a"),
|
||||
PathAwareTab("B", "/b"),
|
||||
PathAwareTab("C", "/c"),
|
||||
))
|
||||
)
|
||||
|
||||
block(ctrl, applicationUrl)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class UiStoreTests : TestSuite() {
|
||||
@Test
|
||||
fun applicationUrl_is_initialized_correctly() {
|
||||
val applicationUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, applicationUrl))
|
||||
|
||||
assertEquals(PwTool.Viewer, uiStore.currentTool.value)
|
||||
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value)
|
||||
@ -20,7 +20,7 @@ class UiStoreTests : TestSuite() {
|
||||
@Test
|
||||
fun applicationUrl_changes_when_tool_changes() {
|
||||
val applicationUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, applicationUrl))
|
||||
|
||||
PwTool.values().forEach { tool ->
|
||||
uiStore.setCurrentTool(tool)
|
||||
@ -33,7 +33,7 @@ class UiStoreTests : TestSuite() {
|
||||
@Test
|
||||
fun applicationUrl_changes_when_path_changes() {
|
||||
val applicationUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, applicationUrl))
|
||||
|
||||
assertEquals(PwTool.Viewer, uiStore.currentTool.value)
|
||||
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value)
|
||||
@ -48,7 +48,7 @@ class UiStoreTests : TestSuite() {
|
||||
@Test
|
||||
fun currentTool_and_path_change_when_applicationUrl_changes() {
|
||||
val applicationUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, applicationUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, applicationUrl))
|
||||
|
||||
PwTool.values().forEach { tool ->
|
||||
listOf("/a", "/b", "/c").forEach { path ->
|
||||
@ -63,7 +63,7 @@ class UiStoreTests : TestSuite() {
|
||||
@Test
|
||||
fun browser_navigation_stack_is_manipulated_correctly() {
|
||||
val appUrl = TestApplicationUrl("/")
|
||||
val uiStore = UiStore(scope, appUrl)
|
||||
val uiStore = disposer.add(UiStore(scope, appUrl))
|
||||
|
||||
assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value)
|
||||
|
||||
|
@ -22,12 +22,16 @@ class HuntOptimizerTests : TestSuite() {
|
||||
})
|
||||
}
|
||||
}
|
||||
scope.disposable { httpClient.cancel() }
|
||||
disposer.add(disposable { httpClient.cancel() })
|
||||
|
||||
val uiStore = disposer.add(UiStore(scope, TestApplicationUrl("/${PwTool.HuntOptimizer}")))
|
||||
|
||||
disposer.add(
|
||||
HuntOptimizer(
|
||||
scope,
|
||||
assetLoader = HttpAssetLoader(httpClient, basePath = ""),
|
||||
uiStore = UiStore(scope, TestApplicationUrl("/${PwTool.HuntOptimizer}"))
|
||||
uiStore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,16 @@ class QuestEditorTests : TestSuite() {
|
||||
})
|
||||
}
|
||||
}
|
||||
scope.disposable { httpClient.cancel() }
|
||||
disposer.add(disposable { httpClient.cancel() })
|
||||
|
||||
val uiStore = disposer.add(UiStore(scope, TestApplicationUrl("/${PwTool.QuestEditor}")))
|
||||
|
||||
disposer.add(
|
||||
QuestEditor(
|
||||
scope,
|
||||
uiStore = UiStore(scope, TestApplicationUrl("/${PwTool.QuestEditor}")),
|
||||
uiStore,
|
||||
createEngine = { Engine(it) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
package world.phantasmal.webui
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
abstract class DisposableContainer : TrackedDisposable() {
|
||||
private val disposer = Disposer()
|
||||
|
||||
override fun internalDispose() {
|
||||
disposer.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
protected fun <T : Disposable> addDisposable(disposable: T): T =
|
||||
disposer.add(disposable)
|
||||
|
||||
protected fun addDisposables(vararg disposables: Disposable) {
|
||||
disposer.addAll(*disposables)
|
||||
}
|
||||
|
||||
protected fun <V1> observe(observable: Observable<V1>, operation: (V1) -> Unit) {
|
||||
addDisposable(
|
||||
if (observable is Val<V1>) {
|
||||
observable.observe(callNow = true) { operation(it.value) }
|
||||
} else {
|
||||
observable.observe { operation(it.value) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected fun <V1, V2> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
operation: (V1, V2) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value)
|
||||
}
|
||||
addDisposables(
|
||||
v1.observe(observer),
|
||||
v2.observe(observer),
|
||||
)
|
||||
operation(v1.value, v2.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
operation: (V1, V2, V3) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value)
|
||||
}
|
||||
addDisposables(
|
||||
v1.observe(observer),
|
||||
v2.observe(observer),
|
||||
v3.observe(observer),
|
||||
)
|
||||
operation(v1.value, v2.value, v3.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3, V4> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
v4: Val<V4>,
|
||||
operation: (V1, V2, V3, V4) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value, v4.value)
|
||||
}
|
||||
addDisposables(
|
||||
v1.observe(observer),
|
||||
v2.observe(observer),
|
||||
v3.observe(observer),
|
||||
v4.observe(observer),
|
||||
)
|
||||
operation(v1.value, v2.value, v3.value, v4.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3, V4, V5> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
v4: Val<V4>,
|
||||
v5: Val<V5>,
|
||||
operation: (V1, V2, V3, V4, V5) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value, v4.value, v5.value)
|
||||
}
|
||||
addDisposables(
|
||||
v1.observe(observer),
|
||||
v2.observe(observer),
|
||||
v3.observe(observer),
|
||||
v4.observe(observer),
|
||||
v5.observe(observer),
|
||||
)
|
||||
operation(v1.value, v2.value, v3.value, v4.value, v5.value)
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package world.phantasmal.webui.controllers
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
abstract class Controller(protected val scope: Scope) :
|
||||
TrackedDisposable(scope.scope()),
|
||||
abstract class Controller(protected val scope: CoroutineScope) :
|
||||
DisposableContainer(),
|
||||
CoroutineScope by scope
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.webui.controllers
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
@ -9,7 +9,7 @@ interface Tab {
|
||||
val title: String
|
||||
}
|
||||
|
||||
open class TabController<T : Tab>(scope: Scope, val tabs: List<T>) : Controller(scope) {
|
||||
open class TabController<T : Tab>(scope: CoroutineScope, val tabs: List<T>) : Controller(scope) {
|
||||
private val _activeTab: MutableVal<T?> = mutableVal(tabs.firstOrNull())
|
||||
|
||||
val activeTab: Val<T?> = _activeTab
|
||||
|
@ -6,22 +6,21 @@ import kotlinx.dom.clear
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventTarget
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.ListValChangeEvent
|
||||
|
||||
fun <E : Event> disposableListener(
|
||||
scope: Scope,
|
||||
target: EventTarget,
|
||||
type: String,
|
||||
listener: (E) -> Unit,
|
||||
options: AddEventListenerOptions? = null,
|
||||
) {
|
||||
): Disposable {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
target.addEventListener(type, listener as (Event) -> Unit, options)
|
||||
|
||||
scope.disposable {
|
||||
return disposable {
|
||||
target.removeEventListener(type, listener)
|
||||
}
|
||||
}
|
||||
@ -35,44 +34,3 @@ fun HTMLElement.root(): HTMLElement {
|
||||
id = "pw-root"
|
||||
return this
|
||||
}
|
||||
|
||||
fun <T> Node.bindChildrenTo(
|
||||
scope: Scope,
|
||||
list: ListVal<T>,
|
||||
createChild: (T, Int) -> Node,
|
||||
) {
|
||||
fun spliceChildren(index: Int, removedCount: Int, inserted: List<T>) {
|
||||
for (i in 1..removedCount) {
|
||||
removeChild(childNodes[index].unsafeCast<Node>())
|
||||
}
|
||||
|
||||
val frag = document.createDocumentFragment()
|
||||
|
||||
inserted.forEachIndexed { i, value ->
|
||||
val child = createChild(value, index + i)
|
||||
|
||||
frag.append(child)
|
||||
}
|
||||
|
||||
if (index >= childNodes.length) {
|
||||
appendChild(frag)
|
||||
} else {
|
||||
insertBefore(frag, childNodes[index])
|
||||
}
|
||||
}
|
||||
|
||||
list.observeList(scope) { change: ListValChangeEvent<T> ->
|
||||
when (change) {
|
||||
is ListValChangeEvent.Change -> {
|
||||
spliceChildren(change.index, change.removed.size, change.inserted)
|
||||
}
|
||||
is ListValChangeEvent.ElementChange -> {
|
||||
// TODO: Update children.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spliceChildren(0, 0, list.value)
|
||||
|
||||
scope.disposable { clear() }
|
||||
}
|
||||
|
@ -3,17 +3,15 @@ package world.phantasmal.webui.dom
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
|
||||
fun template(block: DocumentFragment.() -> Unit = {}): HTMLTemplateElement =
|
||||
newHtmlEl("TEMPLATE") { content.block() }
|
||||
|
||||
fun Node.a(
|
||||
href: String? = null,
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLAnchorElement.() -> Unit = {},
|
||||
): HTMLAnchorElement =
|
||||
appendHtmlEl("A", id, className, title) {
|
||||
appendHtmlEl("A", id, className, title, tabIndex) {
|
||||
if (href != null) this.href = href
|
||||
block()
|
||||
}
|
||||
@ -23,9 +21,10 @@ fun Node.button(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLButtonElement.() -> Unit = {},
|
||||
): HTMLButtonElement =
|
||||
appendHtmlEl("BUTTON", id, className, title) {
|
||||
appendHtmlEl("BUTTON", id, className, title, tabIndex) {
|
||||
if (type != null) this.type = type
|
||||
block()
|
||||
}
|
||||
@ -34,49 +33,55 @@ fun Node.canvas(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLCanvasElement.() -> Unit = {},
|
||||
): HTMLCanvasElement =
|
||||
appendHtmlEl("CANVAS", id, className, title, block)
|
||||
appendHtmlEl("CANVAS", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.div(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
block: HTMLDivElement.() -> Unit = {},
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLDivElement .() -> Unit = {},
|
||||
): HTMLDivElement =
|
||||
appendHtmlEl("DIV", id, className, title, block)
|
||||
appendHtmlEl("DIV", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.form(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLFormElement.() -> Unit = {},
|
||||
): HTMLFormElement =
|
||||
appendHtmlEl("FORM", id, className, title, block)
|
||||
appendHtmlEl("FORM", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.h1(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLHeadingElement.() -> Unit = {},
|
||||
): HTMLHeadingElement =
|
||||
appendHtmlEl("H1", id, className, title, block)
|
||||
appendHtmlEl("H1", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.h2(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLHeadingElement.() -> Unit = {},
|
||||
): HTMLHeadingElement =
|
||||
appendHtmlEl("H2", id, className, title, block)
|
||||
appendHtmlEl("H2", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.header(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLElement.() -> Unit = {},
|
||||
): HTMLElement =
|
||||
appendHtmlEl("HEADER", id, className, title, block)
|
||||
appendHtmlEl("HEADER", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.img(
|
||||
src: String? = null,
|
||||
@ -86,9 +91,10 @@ fun Node.img(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLImageElement.() -> Unit = {},
|
||||
): HTMLImageElement =
|
||||
appendHtmlEl("IMG", id, className, title) {
|
||||
appendHtmlEl("IMG", id, className, title, tabIndex) {
|
||||
if (src != null) this.src = src
|
||||
if (width != null) this.width = width
|
||||
if (height != null) this.height = height
|
||||
@ -101,9 +107,10 @@ fun Node.input(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLInputElement.() -> Unit = {},
|
||||
): HTMLInputElement =
|
||||
appendHtmlEl("INPUT", id, className, title) {
|
||||
appendHtmlEl("INPUT", id, className, title, tabIndex) {
|
||||
if (type != null) this.type = type
|
||||
block()
|
||||
}
|
||||
@ -113,9 +120,10 @@ fun Node.label(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLLabelElement.() -> Unit = {},
|
||||
): HTMLLabelElement =
|
||||
appendHtmlEl("LABEL", id, className, title) {
|
||||
appendHtmlEl("LABEL", id, className, title, tabIndex) {
|
||||
if (htmlFor != null) this.htmlFor = htmlFor
|
||||
block()
|
||||
}
|
||||
@ -124,85 +132,86 @@ fun Node.main(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLElement.() -> Unit = {},
|
||||
): HTMLElement =
|
||||
appendHtmlEl("MAIN", id, className, title, block)
|
||||
appendHtmlEl("MAIN", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.p(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLParagraphElement.() -> Unit = {},
|
||||
): HTMLParagraphElement =
|
||||
appendHtmlEl("P", id, className, title, block)
|
||||
appendHtmlEl("P", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.span(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLSpanElement.() -> Unit = {},
|
||||
): HTMLSpanElement =
|
||||
appendHtmlEl("SPAN", id, className, title, block)
|
||||
|
||||
fun Node.slot(
|
||||
name: String? = null,
|
||||
block: HTMLSlotElement.() -> Unit = {},
|
||||
): HTMLSlotElement =
|
||||
appendHtmlEl("SLOT") {
|
||||
if (name != null) this.name = name
|
||||
block()
|
||||
}
|
||||
appendHtmlEl("SPAN", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.table(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLTableElement.() -> Unit = {},
|
||||
): HTMLTableElement =
|
||||
appendHtmlEl("TABLE", id, className, title, block)
|
||||
appendHtmlEl("TABLE", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.td(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLTableCellElement.() -> Unit = {},
|
||||
): HTMLTableCellElement =
|
||||
appendHtmlEl("TD", id, className, title, block)
|
||||
appendHtmlEl("TD", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.th(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLTableCellElement.() -> Unit = {},
|
||||
): HTMLTableCellElement =
|
||||
appendHtmlEl("TH", id, className, title, block)
|
||||
appendHtmlEl("TH", id, className, title, tabIndex, block)
|
||||
|
||||
fun Node.tr(
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: HTMLTableRowElement.() -> Unit = {},
|
||||
): HTMLTableRowElement =
|
||||
appendHtmlEl("TR", id, className, title, block)
|
||||
appendHtmlEl("TR", id, className, title, tabIndex, block)
|
||||
|
||||
fun <T : HTMLElement> Node.appendHtmlEl(
|
||||
tagName: String,
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: T.() -> Unit,
|
||||
): T =
|
||||
appendChild(newHtmlEl(tagName, id, className, title, block)).unsafeCast<T>()
|
||||
appendChild(newHtmlEl(tagName, id, className, title, tabIndex, block)).unsafeCast<T>()
|
||||
|
||||
fun <T : HTMLElement> newHtmlEl(
|
||||
tagName: String,
|
||||
id: String? = null,
|
||||
className: String? = null,
|
||||
title: String? = null,
|
||||
tabIndex: Int? = null,
|
||||
block: T.() -> Unit,
|
||||
): T =
|
||||
newEl(tagName, id, className) {
|
||||
if (title != null) this.title = title
|
||||
if (tabIndex != null) this.tabIndex = tabIndex
|
||||
block()
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
package world.phantasmal.webui.stores
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
abstract class Store(scope: Scope) : TrackedDisposable(scope.scope()), CoroutineScope by scope {
|
||||
override fun internalDispose() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
abstract class Store(protected val scope: CoroutineScope) :
|
||||
DisposableContainer(),
|
||||
CoroutineScope by scope
|
||||
|
@ -1,21 +1,21 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.dom.button
|
||||
import world.phantasmal.webui.dom.span
|
||||
|
||||
open class Button(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
private val text: String? = null,
|
||||
private val textVal: Val<String>? = null,
|
||||
private val onclick: ((MouseEvent) -> Unit)? = null,
|
||||
) : Control(scope, ::style, hidden, disabled) {
|
||||
) : Control(scope, listOf(::style), hidden, disabled) {
|
||||
override fun Node.createElement() =
|
||||
button(className = "pw-button") {
|
||||
onclick = this@Button.onclick
|
||||
@ -23,7 +23,7 @@ open class Button(
|
||||
span(className = "pw-button-inner") {
|
||||
span(className = "pw-button-center") {
|
||||
if (textVal != null) {
|
||||
textVal.observe {
|
||||
observe(textVal) {
|
||||
textContent = it
|
||||
hidden = it.isEmpty()
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
|
||||
@ -9,8 +9,8 @@ import world.phantasmal.observable.value.falseVal
|
||||
* etc. Controls are typically leaf nodes and thus typically don't have children.
|
||||
*/
|
||||
abstract class Control(
|
||||
scope: Scope,
|
||||
style: () -> String,
|
||||
scope: CoroutineScope,
|
||||
styles: List<() -> String>,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
) : Widget(scope, style, hidden, disabled)
|
||||
) : Widget(scope, styles, hidden, disabled)
|
||||
|
@ -0,0 +1,43 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.round
|
||||
|
||||
class DoubleInput(
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
label: String? = null,
|
||||
labelVal: Val<String>? = null,
|
||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||
value: Double? = null,
|
||||
valueVal: Val<Double>? = null,
|
||||
setValue: ((Double) -> Unit)? = null,
|
||||
roundTo: Int = 2,
|
||||
) : NumberInput<Double>(
|
||||
scope,
|
||||
hidden,
|
||||
disabled,
|
||||
label,
|
||||
labelVal,
|
||||
preferredLabelPosition,
|
||||
value,
|
||||
valueVal,
|
||||
setValue,
|
||||
min = null,
|
||||
max = null,
|
||||
step = null,
|
||||
) {
|
||||
private val roundingFactor: Double =
|
||||
if (roundTo < 0) 1.0 else (10.0).pow(roundTo)
|
||||
|
||||
override fun getInputValue(input: HTMLInputElement): Double = input.valueAsNumber
|
||||
|
||||
override fun setInputValue(input: HTMLInputElement, value: Double) {
|
||||
input.valueAsNumber = round(value * roundingFactor) / roundingFactor
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.files.File
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.openFiles
|
||||
|
||||
class FileButton(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
text: String? = null,
|
||||
|
118
webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt
Normal file
118
webui/src/main/kotlin/world/phantasmal/webui/widgets/Input.kt
Normal file
@ -0,0 +1,118 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.webui.dom.input
|
||||
import world.phantasmal.webui.dom.span
|
||||
|
||||
abstract class Input<T>(
|
||||
scope: CoroutineScope,
|
||||
styles: List<() -> String>,
|
||||
hidden: Val<Boolean>,
|
||||
disabled: Val<Boolean>,
|
||||
label: String?,
|
||||
labelVal: Val<String>?,
|
||||
preferredLabelPosition: LabelPosition,
|
||||
private val className: String,
|
||||
private val inputClassName: String,
|
||||
private val inputType: String,
|
||||
private val value: T?,
|
||||
private val valueVal: Val<T>?,
|
||||
private val setValue: ((T) -> Unit)?,
|
||||
private val min: Int?,
|
||||
private val max: Int?,
|
||||
private val step: Int?,
|
||||
) : LabelledControl(
|
||||
scope,
|
||||
styles + ::style,
|
||||
hidden,
|
||||
disabled,
|
||||
label,
|
||||
labelVal,
|
||||
preferredLabelPosition,
|
||||
) {
|
||||
override fun Node.createElement() =
|
||||
span(className = "pw-input") {
|
||||
classList.add(className)
|
||||
|
||||
input(className = "pw-input-inner", type = inputType) {
|
||||
classList.add(inputClassName)
|
||||
|
||||
observe(this@Input.disabled) { disabled = it }
|
||||
|
||||
if (setValue != null) {
|
||||
onchange = { setValue.invoke(getInputValue(this)) }
|
||||
|
||||
onkeydown = { e ->
|
||||
if (e.key == "Enter") {
|
||||
setValue.invoke(getInputValue(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valueVal != null) {
|
||||
observe(valueVal) { setInputValue(this, it) }
|
||||
} else if (this@Input.value != null) {
|
||||
setInputValue(this, this@Input.value)
|
||||
}
|
||||
|
||||
if (this@Input.min != null) {
|
||||
min = this@Input.min.toString()
|
||||
}
|
||||
|
||||
if (this@Input.max != null) {
|
||||
max = this@Input.max.toString()
|
||||
}
|
||||
|
||||
if (this@Input.step != null) {
|
||||
step = this@Input.step.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun getInputValue(input: HTMLInputElement): T
|
||||
|
||||
protected abstract fun setInputValue(input: HTMLInputElement, value: T)
|
||||
}
|
||||
|
||||
@Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty")
|
||||
// language=css
|
||||
private fun style() = """
|
||||
.pw-input {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 24px;
|
||||
border: var(--pw-input-border);
|
||||
}
|
||||
|
||||
.pw-input .pw-input-inner {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 3px;
|
||||
border: var(--pw-input-inner-border);
|
||||
background-color: var(--pw-input-bg-color);
|
||||
color: var(--pw-input-text-color);
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pw-input:hover {
|
||||
border: var(--pw-input-border-hover);
|
||||
}
|
||||
|
||||
.pw-input:focus-within {
|
||||
border: var(--pw-input-border-focus);
|
||||
}
|
||||
|
||||
.pw-input.disabled {
|
||||
border: var(--pw-input-border-disabled);
|
||||
}
|
||||
|
||||
.pw-input.disabled .pw-input-inner {
|
||||
color: var(--pw-input-text-color-disabled);
|
||||
background-color: var(--pw-input-bg-color-disabled);
|
||||
}
|
||||
"""
|
@ -0,0 +1,40 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
|
||||
class IntInput(
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
label: String? = null,
|
||||
labelVal: Val<String>? = null,
|
||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||
value: Int? = null,
|
||||
valueVal: Val<Int>? = null,
|
||||
setValue: ((Int) -> Unit)? = null,
|
||||
min: Int? = null,
|
||||
max: Int? = null,
|
||||
step: Int? = null,
|
||||
) : NumberInput<Int>(
|
||||
scope,
|
||||
hidden,
|
||||
disabled,
|
||||
label,
|
||||
labelVal,
|
||||
preferredLabelPosition,
|
||||
value,
|
||||
valueVal,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
) {
|
||||
override fun getInputValue(input: HTMLInputElement): Int = input.valueAsNumber.toInt()
|
||||
|
||||
override fun setInputValue(input: HTMLInputElement, value: Int) {
|
||||
input.valueAsNumber = value.toDouble()
|
||||
}
|
||||
}
|
@ -1,23 +1,23 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.dom.label
|
||||
|
||||
class Label(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
private val text: String? = null,
|
||||
private val textVal: Val<String>? = null,
|
||||
private val htmlFor: String?,
|
||||
) : Widget(scope, ::style, hidden, disabled) {
|
||||
) : Widget(scope, listOf(::style), hidden, disabled) {
|
||||
override fun Node.createElement() =
|
||||
label(htmlFor) {
|
||||
if (textVal != null) {
|
||||
textVal.observe { textContent = it }
|
||||
observe(textVal) { textContent = it }
|
||||
} else if (text != null) {
|
||||
textContent = text
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
|
||||
@ -10,14 +10,14 @@ enum class LabelPosition {
|
||||
}
|
||||
|
||||
abstract class LabelledControl(
|
||||
scope: Scope,
|
||||
style: () -> String,
|
||||
scope: CoroutineScope,
|
||||
styles: List<() -> String>,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
label: String? = null,
|
||||
labelVal: Val<String>? = null,
|
||||
val preferredLabelPosition: LabelPosition,
|
||||
) : Control(scope, style, hidden, disabled) {
|
||||
) : Control(scope, styles, hidden, disabled) {
|
||||
val label: Label? by lazy {
|
||||
if (label == null && labelVal == null) {
|
||||
null
|
||||
|
@ -1,21 +1,22 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.dom.div
|
||||
|
||||
class LazyLoader(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
private val createWidget: (Scope) -> Widget,
|
||||
) : Widget(scope, ::style, hidden, disabled) {
|
||||
private val createWidget: (CoroutineScope) -> Widget,
|
||||
) : Widget(scope, listOf(::style), hidden, disabled) {
|
||||
private var initialized = false
|
||||
|
||||
override fun Node.createElement() = div(className = "pw-lazy-loader") {
|
||||
this@LazyLoader.hidden.observe { h ->
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-lazy-loader") {
|
||||
observe(this@LazyLoader.hidden) { h ->
|
||||
if (!h && !initialized) {
|
||||
initialized = true
|
||||
addChild(createWidget(scope))
|
||||
|
@ -0,0 +1,48 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
abstract class NumberInput<T : Number>(
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean>,
|
||||
disabled: Val<Boolean>,
|
||||
label: String?,
|
||||
labelVal: Val<String>?,
|
||||
preferredLabelPosition: LabelPosition,
|
||||
value: T?,
|
||||
valueVal: Val<T>?,
|
||||
setValue: ((T) -> Unit)?,
|
||||
min: Int?,
|
||||
max: Int?,
|
||||
step: Int?,
|
||||
) : Input<T>(
|
||||
scope,
|
||||
listOf(::style),
|
||||
hidden,
|
||||
disabled,
|
||||
label,
|
||||
labelVal,
|
||||
preferredLabelPosition,
|
||||
className = "pw-number-input",
|
||||
inputClassName = "pw-number-input-inner",
|
||||
inputType = "number",
|
||||
value,
|
||||
valueVal,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
)
|
||||
|
||||
@Suppress("CssUnusedSymbol")
|
||||
// language=css
|
||||
private fun style() = """
|
||||
.pw-number-input {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.pw-number-input .pw-number-input-inner {
|
||||
padding-right: 1px;
|
||||
}
|
||||
"""
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.controllers.Tab
|
||||
@ -10,12 +10,12 @@ import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.dom.span
|
||||
|
||||
class TabContainer<T : Tab>(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
private val ctrl: TabController<T>,
|
||||
private val createWidget: (Scope, T) -> Widget,
|
||||
) : Widget(scope, ::style, hidden, disabled) {
|
||||
private val createWidget: (CoroutineScope, T) -> Widget,
|
||||
) : Widget(scope, listOf(::style), hidden, disabled) {
|
||||
override fun Node.createElement() =
|
||||
div(className = "pw-tab-container") {
|
||||
div(className = "pw-tab-container-bar") {
|
||||
@ -26,7 +26,7 @@ class TabContainer<T : Tab>(
|
||||
) {
|
||||
textContent = tab.title
|
||||
|
||||
ctrl.activeTab.observe {
|
||||
observe(ctrl.activeTab) {
|
||||
if (it == tab) {
|
||||
classList.add(ACTIVE_CLASS)
|
||||
} else {
|
||||
@ -52,7 +52,7 @@ class TabContainer<T : Tab>(
|
||||
}
|
||||
|
||||
init {
|
||||
selfOrAncestorHidden.observe(ctrl::hiddenChanged)
|
||||
observe(selfOrAncestorHidden, ctrl::hiddenChanged)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,17 +1,17 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.webui.dom.div
|
||||
|
||||
class Toolbar(
|
||||
scope: Scope,
|
||||
scope: CoroutineScope,
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
disabled: Val<Boolean> = falseVal(),
|
||||
children: List<Widget>,
|
||||
) : Widget(scope, ::style, hidden, disabled) {
|
||||
) : Widget(scope, listOf(::style), hidden, disabled) {
|
||||
private val childWidgets = children
|
||||
|
||||
override fun Node.createElement() =
|
||||
|
@ -1,25 +1,22 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.dom.appendText
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLStyleElement
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import kotlinx.dom.clear
|
||||
import org.w3c.dom.*
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.ListValChangeEvent
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.value.or
|
||||
import kotlin.reflect.KClass
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
abstract class Widget(
|
||||
protected val scope: Scope,
|
||||
style: () -> String = NO_STYLE,
|
||||
protected val scope: CoroutineScope,
|
||||
private val styles: List<() -> String> = emptyList(),
|
||||
/**
|
||||
* By default determines the hidden attribute of its [element].
|
||||
*/
|
||||
@ -29,27 +26,28 @@ abstract class Widget(
|
||||
* `pw-disabled` class is added.
|
||||
*/
|
||||
val disabled: Val<Boolean> = falseVal(),
|
||||
) : TrackedDisposable(scope.scope()) {
|
||||
) : DisposableContainer() {
|
||||
private val _ancestorHidden = mutableVal(false)
|
||||
private val _children = mutableListOf<Widget>()
|
||||
private var initResizeObserverRequested = false
|
||||
private var resizeObserverInitialized = false
|
||||
|
||||
private val elementDelegate = lazy {
|
||||
// Add CSS declarations to stylesheet if this is the first time we're instantiating this
|
||||
// widget.
|
||||
if (style !== NO_STYLE && STYLES_ADDED.add(this::class)) {
|
||||
// Add CSS declarations to stylesheet if this is the first time we're encountering them.
|
||||
styles.forEach { style ->
|
||||
if (STYLES_ADDED.add(style)) {
|
||||
STYLE_EL.appendText(style())
|
||||
}
|
||||
}
|
||||
|
||||
val el = document.createDocumentFragment().createElement()
|
||||
|
||||
hidden.observe { hidden ->
|
||||
observe(hidden) { hidden ->
|
||||
el.hidden = hidden
|
||||
children.forEach { setAncestorHidden(it, hidden || ancestorHidden.value) }
|
||||
}
|
||||
|
||||
disabled.observe { disabled ->
|
||||
observe(disabled) { disabled ->
|
||||
if (disabled) {
|
||||
el.setAttribute("disabled", "")
|
||||
el.classList.add("pw-disabled")
|
||||
@ -100,90 +98,65 @@ abstract class Widget(
|
||||
}
|
||||
|
||||
_children.clear()
|
||||
}
|
||||
|
||||
protected fun <V1> Observable<V1>.observe(operation: (V1) -> Unit) {
|
||||
if (this is Val<V1>) {
|
||||
this.observe(scope, callNow = true) { operation(it.value) }
|
||||
} else {
|
||||
this.observe(scope) { operation(it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <V1, V2> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
operation: (V1, V2) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value)
|
||||
}
|
||||
v1.observe(scope, observer)
|
||||
v2.observe(scope, observer)
|
||||
operation(v1.value, v2.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
operation: (V1, V2, V3) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value)
|
||||
}
|
||||
v1.observe(scope, observer)
|
||||
v2.observe(scope, observer)
|
||||
v3.observe(scope, observer)
|
||||
operation(v1.value, v2.value, v3.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3, V4> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
v4: Val<V4>,
|
||||
operation: (V1, V2, V3, V4) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value, v4.value)
|
||||
}
|
||||
v1.observe(scope, observer)
|
||||
v2.observe(scope, observer)
|
||||
v3.observe(scope, observer)
|
||||
v4.observe(scope, observer)
|
||||
operation(v1.value, v2.value, v3.value, v4.value)
|
||||
}
|
||||
|
||||
protected fun <V1, V2, V3, V4, V5> observe(
|
||||
v1: Val<V1>,
|
||||
v2: Val<V2>,
|
||||
v3: Val<V3>,
|
||||
v4: Val<V4>,
|
||||
v5: Val<V5>,
|
||||
operation: (V1, V2, V3, V4, V5) -> Unit,
|
||||
) {
|
||||
val observer: Observer<*> = {
|
||||
operation(v1.value, v2.value, v3.value, v4.value, v5.value)
|
||||
}
|
||||
v1.observe(scope, observer)
|
||||
v2.observe(scope, observer)
|
||||
v3.observe(scope, observer)
|
||||
v4.observe(scope, observer)
|
||||
v5.observe(scope, observer)
|
||||
operation(v1.value, v2.value, v3.value, v4.value, v5.value)
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child widget to [children].
|
||||
* Adds a child widget to [children] and appends its element to the receiving node.
|
||||
*/
|
||||
protected fun <T : Widget> Node.addChild(child: T): T {
|
||||
addDisposable(child)
|
||||
_children.add(child)
|
||||
setAncestorHidden(child, selfOrAncestorHidden.value)
|
||||
appendChild(child.element)
|
||||
return child
|
||||
}
|
||||
|
||||
fun <T> Node.bindChildrenTo(
|
||||
list: ListVal<T>,
|
||||
createChild: (T, Int) -> Node,
|
||||
) {
|
||||
fun spliceChildren(index: Int, removedCount: Int, inserted: List<T>) {
|
||||
for (i in 1..removedCount) {
|
||||
removeChild(childNodes[index].unsafeCast<Node>())
|
||||
}
|
||||
|
||||
val frag = document.createDocumentFragment()
|
||||
|
||||
inserted.forEachIndexed { i, value ->
|
||||
val child = createChild(value, index + i)
|
||||
|
||||
frag.append(child)
|
||||
}
|
||||
|
||||
if (index >= childNodes.length) {
|
||||
appendChild(frag)
|
||||
} else {
|
||||
insertBefore(frag, childNodes[index])
|
||||
}
|
||||
}
|
||||
|
||||
val observer = list.observeList { change: ListValChangeEvent<T> ->
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever [element] is resized.
|
||||
* Must be initialized with [observeResize].
|
||||
@ -206,7 +179,7 @@ abstract class Widget(
|
||||
val resize = ::resizeCallback
|
||||
val observer = js("new ResizeObserver(resize);")
|
||||
observer.observe(element)
|
||||
scope.disposable { observer.disconnect().unsafeCast<Unit>() }
|
||||
addDisposable(disposable { observer.disconnect().unsafeCast<Unit>() })
|
||||
}
|
||||
|
||||
private fun resizeCallback(entries: Array<dynamic>) {
|
||||
@ -225,9 +198,7 @@ abstract class Widget(
|
||||
document.head!!.append(el)
|
||||
el
|
||||
}
|
||||
private val STYLES_ADDED: MutableSet<KClass<out Widget>> = mutableSetOf()
|
||||
|
||||
protected val NO_STYLE = { "" }
|
||||
private val STYLES_ADDED: MutableSet<() -> String> = mutableSetOf()
|
||||
|
||||
protected fun setAncestorHidden(widget: Widget, hidden: Boolean) {
|
||||
widget._ancestorHidden.value = hidden
|
||||
|
@ -1,7 +1,6 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.Scope
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
@ -17,9 +16,9 @@ class WidgetTests : TestSuite() {
|
||||
fun ancestorHidden_and_selfOrAncestorHidden_should_update_when_hidden_changes() {
|
||||
val parentHidden = mutableVal(false)
|
||||
val childHidden = mutableVal(false)
|
||||
val grandChild = DummyWidget(scope)
|
||||
val child = DummyWidget(scope, childHidden, grandChild)
|
||||
val parent = DummyWidget(scope, parentHidden, child)
|
||||
val grandChild = DummyWidget()
|
||||
val child = DummyWidget(childHidden, grandChild)
|
||||
val parent = disposer.add(DummyWidget(parentHidden, child))
|
||||
|
||||
parent.element // Ensure widgets are fully initialized.
|
||||
|
||||
@ -52,8 +51,8 @@ class WidgetTests : TestSuite() {
|
||||
|
||||
@Test
|
||||
fun added_child_widgets_should_have_ancestorHidden_and_selfOrAncestorHidden_set_correctly() {
|
||||
val parent = DummyWidget(scope, hidden = trueVal())
|
||||
val child = parent.addChild(DummyWidget(scope))
|
||||
val parent = disposer.add(DummyWidget(hidden = trueVal()))
|
||||
val child = parent.addChild(DummyWidget())
|
||||
|
||||
assertFalse(parent.ancestorHidden.value)
|
||||
assertTrue(parent.selfOrAncestorHidden.value)
|
||||
@ -61,11 +60,10 @@ class WidgetTests : TestSuite() {
|
||||
assertTrue(child.selfOrAncestorHidden.value)
|
||||
}
|
||||
|
||||
private class DummyWidget(
|
||||
scope: Scope,
|
||||
private inner class DummyWidget(
|
||||
hidden: Val<Boolean> = falseVal(),
|
||||
private val child: Widget? = null,
|
||||
) : Widget(scope, NO_STYLE, hidden) {
|
||||
) : Widget(scope, hidden = hidden) {
|
||||
override fun Node.createElement() = div {
|
||||
child?.let { addChild(it) }
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user