Improved observable API and slightly simplified implementation of some observables.

This commit is contained in:
Daan Vanden Bosch 2021-12-04 20:55:12 +01:00
parent b84ef99e22
commit 89ea739c65
66 changed files with 453 additions and 413 deletions

View File

@ -0,0 +1,33 @@
package world.phantasmal.observable
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.core.unsafe.unsafeCast
/**
* Calls [callback] when [dependency] changes.
*/
class CallbackChangeObserver<T, E : ChangeEvent<T>>(
private val dependency: Dependency,
// We don't use Observer<T> because of type system limitations. It would break e.g.
// AbstractListCell.observeListChange.
private val callback: (E) -> Unit,
) : TrackedDisposable(), Dependent {
init {
dependency.addDependent(this)
}
override fun dispose() {
dependency.removeDependent(this)
super.dispose()
}
override fun dependencyMightChange() {
// Do nothing.
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if (event != null) {
callback(unsafeCast(event))
}
}
}

View File

@ -1,28 +1,46 @@
package world.phantasmal.observable
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.core.unsafe.unsafeCast
class CallbackObserver<T, E : ChangeEvent<T>>(
private val dependency: Dependency,
private val callback: (E) -> Unit,
/**
* Calls [callback] when one or more dependency in [dependencies] changes.
*/
class CallbackObserver(
private vararg val dependencies: Dependency,
private val callback: () -> Unit,
) : TrackedDisposable(), Dependent {
private var changingDependencies = 0
private var dependenciesActuallyChanged = false
init {
dependency.addDependent(this)
for (dependency in dependencies) {
dependency.addDependent(this)
}
}
override fun dispose() {
dependency.removeDependent(this)
for (dependency in dependencies) {
dependency.removeDependent(this)
}
super.dispose()
}
override fun dependencyMightChange() {
// Do nothing.
changingDependencies++
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if (event != null) {
callback(unsafeCast(event))
dependenciesActuallyChanged = true
}
changingDependencies--
if (changingDependencies == 0 && dependenciesActuallyChanged) {
dependenciesActuallyChanged = false
callback()
}
}
}

View File

@ -1,5 +1,7 @@
package world.phantasmal.observable
typealias ChangeObserver<T> = (ChangeEvent<T>) -> Unit
open class ChangeEvent<out T>(
/**
* The observable's new value.
@ -8,5 +10,3 @@ open class ChangeEvent<out T>(
) {
operator fun component1() = value
}
typealias Observer<T> = (ChangeEvent<T>) -> Unit

View File

@ -3,7 +3,7 @@ package world.phantasmal.observable
interface Dependency {
/**
* This method is not meant to be called from typical application code. Usually you'll want to
* use [Observable.observe].
* use [Observable.observeChange].
*/
fun addDependent(dependent: Dependent)

View File

@ -3,5 +3,8 @@ package world.phantasmal.observable
import world.phantasmal.core.disposable.Disposable
interface Observable<out T> : Dependency {
fun observe(observer: Observer<T>): Disposable
/**
* [observer] will be called whenever this observable changes.
*/
fun observeChange(observer: ChangeObserver<T>): Disposable
}

View File

@ -1,3 +0,0 @@
package world.phantasmal.observable
fun <T> emitter(): Emitter<T> = SimpleEmitter()

View File

@ -0,0 +1,8 @@
package world.phantasmal.observable
import world.phantasmal.core.disposable.Disposable
fun <T> emitter(): Emitter<T> = SimpleEmitter()
fun <T> Observable<T>.observe(observer: (T) -> Unit): Disposable =
observeChange { observer(it.value) }

View File

@ -15,8 +15,8 @@ class SimpleEmitter<T> : AbstractDependency(), Emitter<T> {
ChangeManager.changed(this)
}
override fun observe(observer: Observer<T>): Disposable =
CallbackObserver(this, observer)
override fun observeChange(observer: ChangeObserver<T>): Disposable =
CallbackChangeObserver(this, observer)
override fun emitDependencyChanged() {
if (event != null) {

View File

@ -2,25 +2,15 @@ package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.CallbackChangeObserver
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer
import world.phantasmal.observable.ChangeObserver
abstract class AbstractCell<T> : AbstractDependency(), Cell<T> {
private var mightChangeEmitted = false
final override fun observe(observer: Observer<T>): Disposable =
observe(callNow = false, observer)
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
val observingCell = CallbackObserver(this, observer)
if (callNow) {
observer(ChangeEvent(value))
}
return observingCell
}
override fun observeChange(observer: ChangeObserver<T>): Disposable =
CallbackChangeObserver(this, observer)
protected fun emitMightChange() {
if (!mightChangeEmitted) {

View File

@ -1,10 +1,6 @@
package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.Observable
import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.list.DependentListCell
import world.phantasmal.observable.cell.list.ListCell
import kotlin.reflect.KProperty
/**
@ -14,9 +10,4 @@ interface Cell<out T> : Observable<T> {
val value: T
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
/**
* @param callNow Call [observer] immediately with the current [value].
*/
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable
}

View File

@ -1,5 +1,8 @@
package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.CallbackObserver
private val TRUE_CELL: Cell<Boolean> = ImmutableCell(true)
private val FALSE_CELL: Cell<Boolean> = ImmutableCell(false)
private val NULL_CELL: Cell<Nothing?> = ImmutableCell(null)
@ -36,6 +39,62 @@ fun <T> mutableCell(value: T): MutableCell<T> = SimpleCell(value)
fun <T> mutableCell(getter: () -> T, setter: (T) -> Unit): MutableCell<T> =
DelegatingCell(getter, setter)
fun <T> Cell<T>.observeNow(
observer: (T) -> Unit,
): Disposable {
observer(value)
return observeChange { observer(it.value) }
}
fun <T1, T2> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
observer: (T1, T2) -> Unit,
): Disposable {
observer(c1.value, c2.value)
return CallbackObserver(c1, c2) { observer(c1.value, c2.value) }
}
fun <T1, T2, T3> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
observer: (T1, T2, T3) -> Unit,
): Disposable {
observer(c1.value, c2.value, c3.value)
return CallbackObserver(c1, c2, c3) { observer(c1.value, c2.value, c3.value) }
}
fun <T1, T2, T3, T4> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
c4: Cell<T4>,
observer: (T1, T2, T3, T4) -> Unit,
): Disposable {
observer(c1.value, c2.value, c3.value, c4.value)
return CallbackObserver(c1, c2, c3, c4) { observer(c1.value, c2.value, c3.value, c4.value) }
}
fun <T1, T2, T3, T4, T5> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
c4: Cell<T4>,
c5: Cell<T5>,
observer: (T1, T2, T3, T4, T5) -> Unit,
): Disposable {
observer(c1.value, c2.value, c3.value, c4.value, c5.value)
return CallbackObserver(c1, c2, c3, c4, c5) {
observer(c1.value, c2.value, c3.value, c4.value, c5.value)
}
}
/**
* Map a transformation function over this cell.
*

View File

@ -3,21 +3,12 @@ package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.nopDisposable
import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer
import world.phantasmal.observable.ChangeObserver
class ImmutableCell<T>(override val value: T) : AbstractDependency(), Cell<T> {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
if (callNow) {
observer(ChangeEvent(value))
}
return nopDisposable()
}
override fun observe(observer: Observer<T>): Disposable = nopDisposable()
override fun observeChange(observer: ChangeObserver<T>): Disposable = nopDisposable()
override fun emitDependencyChanged() {
error("StaticCell can't change.")
error("ImmutableCell can't change.")
}
}

View File

@ -2,9 +2,9 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.CallbackChangeObserver
import world.phantasmal.observable.ChangeObserver
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.AbstractDependentCell
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell
@ -55,28 +55,11 @@ abstract class AbstractDependentListCell<E> :
return unsafeAssertNotNull(_notEmpty)
}
final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
observeList(callNow, observer as ListObserver<E>)
final override fun observeChange(observer: ChangeObserver<List<E>>): Disposable =
observeListChange(observer)
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
val observingCell = CallbackObserver(this, observer)
if (callNow) {
observer(
ListChangeEvent(
value,
listOf(ListChange.Structural(
index = 0,
prevSize = 0,
removed = emptyList(),
inserted = value,
)),
)
)
}
return observingCell
}
override fun observeListChange(observer: ListChangeObserver<E>): Disposable =
CallbackChangeObserver(this, observer)
final override fun dependenciesChanged() {
val oldElements = value

View File

@ -2,8 +2,8 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.Observer
import world.phantasmal.observable.CallbackChangeObserver
import world.phantasmal.observable.ChangeObserver
import world.phantasmal.observable.cell.AbstractCell
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell
@ -19,7 +19,7 @@ abstract class AbstractListCell<E> : AbstractCell<List<E>>(), ListCell<E> {
* its old value whenever a change event was emitted.
*/
// TODO: Optimize this by using a weak reference to avoid copying when nothing references the
// wrapper.
// wrapper.
private var _elementsWrapper: DelegatingList<E>? = null
protected val elementsWrapper: DelegatingList<E>
get() {
@ -39,28 +39,11 @@ abstract class AbstractListCell<E> : AbstractCell<List<E>>(), ListCell<E> {
final override val notEmpty: Cell<Boolean> = !empty
final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
observeList(callNow, observer as ListObserver<E>)
final override fun observeChange(observer: ChangeObserver<List<E>>): Disposable =
observeListChange(observer)
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
val observingCell = CallbackObserver(this, observer)
if (callNow) {
observer(
ListChangeEvent(
value,
listOf(ListChange.Structural(
index = 0,
prevSize = 0,
removed = emptyList(),
inserted = value
)),
)
)
}
return observingCell
}
override fun observeListChange(observer: ListChangeObserver<E>): Disposable =
CallbackChangeObserver(this, observer)
protected fun copyAndResetWrapper() {
_elementsWrapper?.backingList = elements.toList()

View File

@ -3,8 +3,7 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.nopDisposable
import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer
import world.phantasmal.observable.ChangeObserver
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.cell
import world.phantasmal.observable.cell.falseCell
@ -20,31 +19,9 @@ class ImmutableListCell<E>(private val elements: List<E>) : AbstractDependency()
override fun get(index: Int): E =
elements[index]
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
if (callNow) {
observer(ChangeEvent(value))
}
override fun observeChange(observer: ChangeObserver<List<E>>): Disposable = nopDisposable()
return nopDisposable()
}
override fun observe(observer: Observer<List<E>>): Disposable = nopDisposable()
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
if (callNow) {
observer(ListChangeEvent(
value,
listOf(ListChange.Structural(
index = 0,
prevSize = 0,
removed = emptyList(),
inserted = value,
)),
))
}
return nopDisposable()
}
override fun observeListChange(observer: ListChangeObserver<E>): Disposable = nopDisposable()
override fun emitDependencyChanged() {
error("ImmutableListCell can't change.")

View File

@ -14,7 +14,11 @@ interface ListCell<out E> : Cell<List<E>> {
operator fun get(index: Int): E = value[index]
fun observeList(callNow: Boolean = false, observer: ListObserver<E>): Disposable
/**
* List variant of [Cell.observeChange].
*/
// Exists solely because function parameters are invariant.
fun observeListChange(observer: ListChangeObserver<E>): Disposable
operator fun contains(element: @UnsafeVariance E): Boolean = element in value
}

View File

@ -42,4 +42,4 @@ sealed class ListChange<out E> {
) : ListChange<E>()
}
typealias ListObserver<E> = (ListChangeEvent<E>) -> Unit
typealias ListChangeObserver<E> = (ListChangeEvent<E>) -> Unit

View File

@ -16,7 +16,7 @@ interface ObservableTests : DependencyTests {
var changes = 0
disposer.add(
p.observable.observe {
p.observable.observeChange {
changes++
}
)
@ -37,7 +37,7 @@ interface ObservableTests : DependencyTests {
val p = createProvider()
var changes = 0
val observer = p.observable.observe {
val observer = p.observable.observeChange {
changes++
}

View File

@ -1,6 +1,7 @@
package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.use
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.ObservableTests
import kotlin.test.*
@ -15,6 +16,7 @@ interface CellTests : ObservableTests {
fun value_is_accessible_without_observers() = test {
val p = createProvider()
// We literally just test that accessing the value property doesn't throw or return null.
assertNotNull(p.observable.value)
}
@ -22,106 +24,55 @@ interface CellTests : ObservableTests {
fun value_is_accessible_with_observers() = test {
val p = createProvider()
var observedValue: Any? = null
disposer.add(p.observable.observeChange {})
disposer.add(p.observable.observe(callNow = true) {
observedValue = it.value
})
assertNotNull(observedValue)
// We literally just test that accessing the value property doesn't throw or return null.
assertNotNull(p.observable.value)
}
@Test
fun propagates_changes_to_mapped_cell() = test {
fun emits_no_change_event_until_changed() = test {
val p = createProvider()
val mapped = p.observable.map { it.hashCode() }
val initialValue = mapped.value
var observedValue: Int? = null
var observedEvent: ChangeEvent<Any>? = null
disposer.add(mapped.observe {
assertNull(observedValue)
observedValue = it.value
disposer.add(p.observable.observeChange { changeEvent ->
observedEvent = changeEvent
})
assertNull(observedEvent)
p.emit()
assertNotEquals(initialValue, mapped.value)
assertEquals(mapped.value, observedValue)
}
@Test
fun propagates_changes_to_flat_mapped_cell() = test {
val p = createProvider()
val mapped = p.observable.flatMap { ImmutableCell(it.hashCode()) }
val initialValue = mapped.value
var observedValue: Int? = null
disposer.add(mapped.observe {
assertNull(observedValue)
observedValue = it.value
})
p.emit()
assertNotEquals(initialValue, mapped.value)
assertEquals(mapped.value, observedValue)
assertNotNull(observedEvent)
}
@Test
fun emits_correct_value_in_change_events() = test {
val p = createProvider()
var prevValue: Any?
var observedValue: Any? = null
disposer.add(p.observable.observe {
disposer.add(p.observable.observeChange { changeEvent ->
assertNull(observedValue)
observedValue = it.value
observedValue = changeEvent.value
})
repeat(3) {
prevValue = observedValue
observedValue = null
p.emit()
// We should have observed a value, it should be different from the previous value, and
// it should be equal to the cell's current value.
assertNotNull(observedValue)
assertNotEquals(prevValue, observedValue)
assertEquals(p.observable.value, observedValue)
}
}
/**
* When [Cell.observe] is called with callNow = true, it should call the observer immediately.
* Otherwise it should only call the observer when it changes.
*/
@Test
fun respects_call_now_argument() = test {
val p = createProvider()
var changes = 0
// Test callNow = false
p.observable.observe(callNow = false) {
changes++
}.use {
p.emit()
assertEquals(1, changes)
}
// Test callNow = true
changes = 0
p.observable.observe(callNow = true) {
changes++
}.use {
p.emit()
assertEquals(2, changes)
}
}
/**
* [Cell.value] should correctly reflect changes even when the [Cell] has no observers.
* Typically this means that the cell's value is not updated in real time, only when it is
@ -148,6 +99,63 @@ interface CellTests : ObservableTests {
}
}
//
// CellUtils Tests
//
@Test
fun propagates_changes_to_observeNow_observers() = test {
val p = createProvider()
var changes = 0
p.observable.observeNow {
changes++
}.use {
p.emit()
assertEquals(2, changes)
}
}
@Test
fun propagates_changes_to_mapped_cell() = test {
val p = createProvider()
val mapped = p.observable.map { it.hashCode() }
val initialValue = mapped.value
var observedValue: Int? = null
disposer.add(mapped.observeChange { changeEvent ->
assertNull(observedValue)
observedValue = changeEvent.value
})
p.emit()
assertNotEquals(initialValue, mapped.value)
assertEquals(mapped.value, observedValue)
}
@Test
fun propagates_changes_to_flat_mapped_cell() = test {
val p = createProvider()
val mapped = p.observable.flatMap { ImmutableCell(it.hashCode()) }
val initialValue = mapped.value
var observedValue: Int? = null
disposer.add(mapped.observeChange {
assertNull(observedValue)
observedValue = it.value
})
p.emit()
assertNotEquals(initialValue, mapped.value)
assertEquals(mapped.value, observedValue)
}
interface Provider : ObservableTests.Provider {
override val observable: Cell<Any>
}

View File

@ -19,7 +19,7 @@ interface CellWithDependenciesTests : CellTests {
var observedChanges = 0
disposer.add(leaf.observe { observedChanges++ })
disposer.add(leaf.observeChange { observedChanges++ })
// Change root, which results in both branches changing and thus two dependencies of leaf
// changing.
@ -47,7 +47,7 @@ interface CellWithDependenciesTests : CellTests {
assertTrue(dependency.publicDependents.isEmpty())
disposer.add(cell.observe { })
disposer.add(cell.observeChange { })
assertEquals(1, dependency.publicDependents.size)
}

View File

@ -13,7 +13,7 @@ class ChangeTests : ObservableTestSuite {
val dependent = dependency.map { 2 * it }
var dependentObservedValue: Int? = null
disposer.add(dependent.observe { dependentObservedValue = it.value })
disposer.add(dependent.observeChange { dependentObservedValue = it.value })
assertFails {
change {

View File

@ -7,24 +7,21 @@ import kotlin.test.assertEquals
class ImmutableCellTests : ObservableTestSuite {
@Test
fun observing_it_should_never_create_leaks() = test {
fun observing_it_never_creates_leaks() = test {
val cell = ImmutableCell("test value")
TrackedDisposable.checkNoLeaks {
// We never call dispose on the returned disposables.
cell.observe {}
cell.observe(callNow = false) {}
cell.observe(callNow = true) {}
// We never call dispose on the returned disposable.
cell.observeChange {}
}
}
@Test
fun observe_respects_callNow() = test {
fun observeNow_calls_the_observer_once() = test {
val cell = ImmutableCell("test value")
var calls = 0
cell.observe(callNow = false) { calls++ }
cell.observe(callNow = true) { calls++ }
cell.observeNow { calls++ }
assertEquals(1, calls)
}

View File

@ -14,7 +14,7 @@ interface MutableCellTests<T : Any> : CellTests {
var observedValue: Any? = null
disposer.add(p.observable.observe {
disposer.add(p.observable.observeChange {
assertNull(observedValue)
observedValue = it.value
})

View File

@ -46,10 +46,10 @@ class FilteredListCellTests : ListCellTests {
var changes = 0
var listChanges = 0
disposer.add(list.observe {
disposer.add(list.observeChange {
changes++
})
disposer.add(list.observeList {
disposer.add(list.observeListChange {
listChanges++
})
@ -74,7 +74,7 @@ class FilteredListCellTests : ListCellTests {
val list = FilteredListCell(dep, predicate = { it % 2 == 0 })
var event: ListChangeEvent<Int>? = null
disposer.add(list.observeList {
disposer.add(list.observeListChange {
assertNull(event)
event = it
})
@ -128,7 +128,7 @@ class FilteredListCellTests : ListCellTests {
val list = FilteredListCell(dep, predicate = { it.value % 2 == 0 })
var event: ListChangeEvent<SimpleCell<Int>>? = null
disposer.add(list.observeList {
disposer.add(list.observeListChange {
assertNull(event)
event = it
})

View File

@ -1,43 +1,29 @@
package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.observable.cell.observeNow
import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
class ImmutableListCellTests : ObservableTestSuite {
@Test
fun observing_it_should_never_create_leaks() = test {
fun observing_it_never_creates_leaks() = test {
val listCell = ImmutableListCell(listOf(1, 2, 3))
TrackedDisposable.checkNoLeaks {
// We never call dispose on the returned disposables.
listCell.observe {}
listCell.observe(callNow = false) {}
listCell.observe(callNow = true) {}
listCell.observeList(callNow = false) {}
listCell.observeList(callNow = true) {}
listCell.observeChange {}
listCell.observeListChange {}
}
}
@Test
fun observe_respects_callNow() = test {
fun observeNow_calls_the_observer_once() = test {
val listCell = ImmutableListCell(listOf(1, 2, 3))
var calls = 0
listCell.observe(callNow = false) { calls++ }
listCell.observe(callNow = true) { calls++ }
assertEquals(1, calls)
}
@Test
fun observeList_respects_callNow() = test {
val listCell = ImmutableListCell(listOf(1, 2, 3))
var calls = 0
listCell.observeList(callNow = false) { calls++ }
listCell.observeList(callNow = true) { calls++ }
listCell.observeNow { calls++ }
assertEquals(1, calls)
}

View File

@ -16,6 +16,8 @@ interface ListCellTests : CellTests {
fun list_value_is_accessible_without_observers() = test {
val p = createListProvider(empty = false)
// We literally just test that accessing the value property doesn't throw or return the
// wrong list.
assertTrue(p.observable.value.isNotEmpty())
}
@ -23,14 +25,28 @@ interface ListCellTests : CellTests {
fun list_value_is_accessible_with_observers() = test {
val p = createListProvider(empty = false)
var observedValue: List<*>? = null
disposer.add(p.observable.observeListChange {})
disposer.add(p.observable.observe(callNow = true) {
observedValue = it.value
// We literally just test that accessing the value property doesn't throw or return the
// wrong list.
assertTrue(p.observable.value.isNotEmpty())
}
@Test
fun emits_no_list_change_event_until_changed() = test {
val p = createListProvider(empty = false)
var observedEvent: ListChangeEvent<Any>? = null
disposer.add(p.observable.observeListChange { listChangeEvent ->
observedEvent = listChangeEvent
})
assertTrue(observedValue!!.isNotEmpty())
assertTrue(p.observable.value.isNotEmpty())
assertNull(observedEvent)
p.emit()
assertNotNull(observedEvent)
}
@Test
@ -40,7 +56,7 @@ interface ListCellTests : CellTests {
var event: ListChangeEvent<*>? = null
disposer.add(
p.observable.observeList {
p.observable.observeListChange {
assertNull(event)
event = it
}
@ -64,7 +80,7 @@ interface ListCellTests : CellTests {
var observedSize: Int? = null
disposer.add(
p.observable.size.observe {
p.observable.size.observeChange {
assertNull(observedSize)
observedSize = it.value
}
@ -102,7 +118,7 @@ interface ListCellTests : CellTests {
var observedValue: Int? = null
disposer.add(fold.observe {
disposer.add(fold.observeChange {
assertNull(observedValue)
observedValue = it.value
})
@ -127,7 +143,7 @@ interface ListCellTests : CellTests {
var observedValue: Int? = null
disposer.add(sum.observe {
disposer.add(sum.observeChange {
assertNull(observedValue)
observedValue = it.value
})
@ -152,7 +168,7 @@ interface ListCellTests : CellTests {
var event: ListChangeEvent<*>? = null
disposer.add(filtered.observeList {
disposer.add(filtered.observeListChange {
assertNull(event)
event = it
})
@ -177,7 +193,7 @@ interface ListCellTests : CellTests {
var observedValue: Any? = null
disposer.add(firstOrNull.observe {
disposer.add(firstOrNull.observeChange {
assertNull(observedValue)
observedValue = it.value
})

View File

@ -16,7 +16,7 @@ interface MutableListCellTests<T : Any> : ListCellTests, MutableCellTests<List<T
var changeEvent: ListChangeEvent<T>? = null
disposer.add(p.observable.observeList {
disposer.add(p.observable.observeListChange {
assertNull(changeEvent)
changeEvent = it
})

View File

@ -245,7 +245,7 @@ class SimpleListCellTests : MutableListCellTests<Int> {
var event: ListChangeEvent<SimpleCell<String>>? = null
disposer.add(list.observeList {
disposer.add(list.observeListChange {
assertNull(event)
event = it
})

View File

@ -1,7 +1,7 @@
package world.phantasmal.web.application.widgets
import org.w3c.dom.Node
import world.phantasmal.observable.Observable
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.nullCell
import world.phantasmal.observable.cell.trueCell
import world.phantasmal.web.core.PwToolType
@ -12,7 +12,7 @@ import world.phantasmal.webui.widgets.Control
class PwToolButton(
private val tool: PwToolType,
private val toggled: Observable<Boolean>,
private val toggled: Cell<Boolean>,
private val onMouseDown: () -> Unit,
) : Control(visible = trueCell(), enabled = trueCell(), tooltip = nullCell()) {
private val inputId = "pw-application-pw-tool-button-${tool.name.lowercase()}"
@ -25,7 +25,7 @@ class PwToolButton(
type = "radio"
id = inputId
name = "pw-application-pw-tool-button"
observe(toggled) { checked = it }
observeNow(toggled) { checked = it }
}
label {
htmlFor = inputId

View File

@ -15,7 +15,7 @@ open class PathAwareTabContainerController<T : PathAwareTab>(
tabs: List<T>,
) : TabContainerController<T>(tabs) {
init {
observe(uiStore.path) { path ->
observeNow(uiStore.path) { path ->
if (uiStore.currentTool.value == tool) {
tabs.find { path.startsWith(it.path) }?.let {
setActiveTab(it, replaceUrl = true)

View File

@ -82,7 +82,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
window.disposableListener("keydown", ::dispatchGlobalKeyDown),
)
observe(applicationUrl.url) { setDataFromUrl(it) }
observeNow(applicationUrl.url) { setDataFromUrl(it) }
}
fun setCurrentTool(tool: PwToolType) {
@ -126,12 +126,12 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
}
return Disposer(
value.observe {
value.observeChange {
if (it.value != param.value) {
setParameter(tool, path, param, it.value, replaceUrl = false)
}
},
param.observe { onChange(it.value) },
param.observeChange { onChange(it.value) },
)
}

View File

@ -64,7 +64,7 @@ class DockWidget(
style.width = ""
style.height = ""
addDisposable(size.observe { (size) ->
addDisposable(size.observeChange { (size) ->
goldenLayout.updateSize(size.width, size.height)
})
}

View File

@ -12,7 +12,7 @@ class RendererWidget(
div {
className = "pw-core-renderer"
observe(selfOrAncestorVisible) { visible ->
observeNow(selfOrAncestorVisible) { visible ->
if (visible) {
renderer.startRendering()
} else {
@ -20,7 +20,7 @@ class RendererWidget(
}
}
addDisposable(size.observe { (size) ->
addDisposable(size.observeChange { (size) ->
renderer.setSize(size.width.toInt(), size.height.toInt())
})

View File

@ -31,7 +31,7 @@ class HuntMethodStore(
/** Hunting methods supported by the current server. */
val methods: ListCell<HuntMethodModel> by lazy {
observe(uiStore.server) { _methodsStatus.load() }
observeNow(uiStore.server) { _methodsStatus.load() }
_methods
}

View File

@ -55,7 +55,7 @@ class HuntOptimizerStore(
private var wantedItemsPersistenceObserver: Disposable? = null
val huntableItems: ListCell<ItemType> by lazy {
observe(uiStore.server) { server ->
observeNow(uiStore.server) { server ->
_huntableItems.clear()
scope.launch {
@ -71,12 +71,12 @@ class HuntOptimizerStore(
}
val wantedItems: ListCell<WantedItemModel> by lazy {
observe(uiStore.server) { loadWantedItems(it) }
observeNow(uiStore.server) { loadWantedItems(it) }
_wantedItems
}
val optimizationResult: Cell<OptimizationResultModel> by lazy {
observe(wantedItems, huntMethodStore.methods) { wantedItems, huntMethods ->
observeNow(wantedItems, huntMethodStore.methods) { wantedItems, huntMethods ->
scope.launch(Dispatchers.Default) {
_optimizationResult.value = optimize(wantedItems, huntMethods)
}
@ -114,7 +114,7 @@ class HuntOptimizerStore(
_wantedItems.replaceAll(wantedItems)
// Wanted items are loaded, start observing them and persist whenever they change.
wantedItemsPersistenceObserver = _wantedItems.observe {
wantedItemsPersistenceObserver = _wantedItems.observeChange {
val items = it.value
scope.launch(Dispatchers.Main) {

View File

@ -11,8 +11,8 @@ class DestinationInstance(
) : Instance<QuestObjectModel>(entity, mesh, instanceIndex) {
init {
addDisposables(
entity.destinationPosition.observe { updateMatrix() },
entity.destinationRotationY.observe { updateMatrix() },
entity.destinationPosition.observeChange { updateMatrix() },
entity.destinationRotationY.observeChange { updateMatrix() },
)
}

View File

@ -13,14 +13,14 @@ class EntityInstance(
) : Instance<QuestEntityModel<*, *>>(entity, mesh, instanceIndex) {
init {
if (entity is QuestObjectModel) {
addDisposable(entity.model.observe(callNow = false) {
addDisposable(entity.model.observeChange {
modelChanged(this.instanceIndex)
})
}
addDisposables(
entity.worldPosition.observe { updateMatrix() },
entity.worldRotation.observe { updateMatrix() },
entity.worldPosition.observeChange { updateMatrix() },
entity.worldRotation.observeChange { updateMatrix() },
)
}

View File

@ -5,6 +5,7 @@ import mu.KotlinLogging
import org.khronos.webgl.Float32Array
import world.phantasmal.core.disposable.DisposableSupervisedScope
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.observable.cell.observeNow
import world.phantasmal.psolib.fileFormats.quest.EntityType
import world.phantasmal.web.core.rendering.disposeObject3DResources
import world.phantasmal.web.externals.three.*
@ -92,13 +93,13 @@ class EntityMeshManager(
}
init {
observe(questEditorStore.highlightedEntity) { entity ->
observeNow(questEditorStore.highlightedEntity) { entity ->
// getEntityInstance can return null at this point because the entity mesh might not be
// loaded yet.
markHighlighted(entity?.let(::getEntityInstance))
}
observe(questEditorStore.selectedEntity) { entity ->
observeNow(questEditorStore.selectedEntity) { entity ->
// getEntityInstance can return null at this point because the entity mesh might not be
// loaded yet.
markSelected(entity?.let(::getEntityInstance))
@ -135,7 +136,9 @@ class EntityMeshManager(
destinationInstanceContainer.addInstance(entity)
}
} catch (e: CancellationException) {
// Do nothing.
logger.trace(e) {
"Mesh loading for entity of type ${entity.type} cancelled."
}
} catch (e: Throwable) {
logger.error(e) {
"Couldn't load mesh for entity of type ${entity.type}."
@ -228,12 +231,12 @@ class EntityMeshManager(
newInstance.entity is QuestObjectModel &&
newInstance.entity.hasDestination
) {
warpLineDisposer.add(newInstance.entity.worldPosition.observe(callNow = true) {
warpLineBufferAttribute.setXYZ(0, it.value.x, it.value.y, it.value.z)
warpLineDisposer.add(newInstance.entity.worldPosition.observeNow {
warpLineBufferAttribute.setXYZ(0, it.x, it.y, it.z)
warpLineBufferAttribute.needsUpdate = true
})
warpLineDisposer.add(newInstance.entity.destinationPosition.observe(callNow = true) {
warpLineBufferAttribute.setXYZ(1, it.value.x, it.value.y, it.value.z)
warpLineDisposer.add(newInstance.entity.destinationPosition.observeNow {
warpLineBufferAttribute.setXYZ(1, it.x, it.y, it.z)
warpLineBufferAttribute.needsUpdate = true
})
warpLines.visible = true

View File

@ -14,14 +14,14 @@ class QuestEditorMeshManager(
renderContext: QuestRenderContext,
) : QuestMeshManager(areaAssetLoader, entityAssetLoader, questEditorStore, renderContext) {
init {
observe(
observeNow(
questEditorStore.currentQuest,
questEditorStore.currentAreaVariant,
) { quest, areaVariant ->
loadAreaMeshes(quest?.episode, areaVariant)
}
observe(
observeNow(
questEditorStore.currentQuest,
questEditorStore.currentArea,
questEditorStore.selectedEvent.flatMapNull { it?.wave },
@ -39,7 +39,7 @@ class QuestEditorMeshManager(
)
}
observe(
observeNow(
questEditorStore.currentQuest,
questEditorStore.currentArea,
) { quest, area ->
@ -54,7 +54,7 @@ class QuestEditorMeshManager(
)
}
observe(questEditorStore.showCollisionGeometry) {
observeNow(questEditorStore.showCollisionGeometry) {
renderContext.collisionGeometryVisible = it
renderContext.renderGeometryVisible = !it
}

View File

@ -5,10 +5,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.DisposableSupervisedScope
import world.phantasmal.psolib.Episode
import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.ListChange
import world.phantasmal.observable.cell.list.ListChangeEvent
import world.phantasmal.psolib.Episode
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.models.AreaVariantModel
@ -55,7 +55,9 @@ abstract class QuestMeshManager protected constructor(
npcObserver?.dispose()
npcMeshManager.removeAll()
npcObserver = npcs.observeList(callNow = true, ::npcsChanged)
npcs.value.forEach(npcMeshManager::add)
npcObserver = npcs.observeListChange(::npcsChanged)
}
}
@ -65,7 +67,9 @@ abstract class QuestMeshManager protected constructor(
objectObserver?.dispose()
objectMeshManager.removeAll()
objectObserver = objects.observeList(callNow = true, ::objectsChanged)
objects.value.forEach(objectMeshManager::add)
objectObserver = objects.observeListChange(::objectsChanged)
}
}

View File

@ -39,7 +39,7 @@ class QuestRenderer(
),
)
observe(questEditorStore.currentQuest) { inputManager.resetCamera() }
observe(questEditorStore.currentAreaVariant) { inputManager.resetCamera() }
observeNow(questEditorStore.currentQuest) { inputManager.resetCamera() }
observeNow(questEditorStore.currentAreaVariant) { inputManager.resetCamera() }
}
}

View File

@ -84,8 +84,8 @@ class QuestInputManager(
stateContext = StateContext(questEditorStore, renderContext, cameraInputManager)
state = IdleState(stateContext, entityManipulationEnabled)
observe(questEditorStore.selectedEntity) { returnToIdleState() }
observe(questEditorStore.questEditingEnabled) { entityManipulationEnabled = it }
observeNow(questEditorStore.selectedEntity) { returnToIdleState() }
observeNow(questEditorStore.questEditingEnabled) { entityManipulationEnabled = it }
pointerTrap.className = "pw-quest-editor-input-manager-pointer-trap"
pointerTrap.hidden = true

View File

@ -55,11 +55,11 @@ class AsmStore(
val problems: ListCell<AssemblyProblem> = asmAnalyser.problems
init {
observe(questEditorStore.currentQuest) { quest ->
observeNow(questEditorStore.currentQuest) { quest ->
setTextModel(quest, inlineStackArgs.value)
}
observe(inlineStackArgs) { inlineStackArgs ->
observeNow(inlineStackArgs) { inlineStackArgs ->
// Ensure we have the most up-to-date bytecode before we disassemble it again.
if (setBytecodeIrTimeout != null) {
setBytecodeIr()
@ -72,7 +72,7 @@ class AsmStore(
scope.launch { questEditorStore.setMapDesignations(it) }
}
observe(problems) { problems ->
observeNow(problems) { problems ->
textModel.value?.let { model ->
val markers = Array<IMarkerData>(problems.size) {
val problem = problems[it]

View File

@ -94,13 +94,13 @@ class QuestEditorStore(
},
)
observe(uiStore.currentTool) { tool ->
observeNow(uiStore.currentTool) { tool ->
if (tool == PwToolType.QuestEditor) {
makeMainUndoCurrent()
}
}
observe(currentQuest.flatMap { it?.npcs ?: emptyListCell() }) { npcs ->
observeNow(currentQuest.flatMap { it?.npcs ?: emptyListCell() }) { npcs ->
val selected = selectedEntity.value
if (selected is QuestNpcModel && selected !in npcs) {
@ -108,7 +108,7 @@ class QuestEditorStore(
}
}
observe(currentQuest.flatMap { it?.objects ?: emptyListCell() }) { objects ->
observeNow(currentQuest.flatMap { it?.objects ?: emptyListCell() }) { objects ->
val selected = selectedEntity.value
if (selected is QuestObjectModel && selected !in objects) {

View File

@ -53,7 +53,7 @@ class TextModelUndo(
init {
undoManager.addUndo(this)
modelObserver = model.observe(callNow = true) { onModelChange(it.value) }
modelObserver = model.observeNow(::onModelChange)
}
override fun dispose() {

View File

@ -32,11 +32,11 @@ class AsmEditorWidget(private val ctrl: AsmEditorController) : Widget() {
addDisposable(disposable { editor.dispose() })
observe(ctrl.textModel) { editor.setModel(it) }
observeNow(ctrl.textModel) { editor.setModel(it) }
observe(ctrl.readOnly) { editor.updateOptions(obj { readOnly = it }) }
observeNow(ctrl.readOnly) { editor.updateOptions(obj { readOnly = it }) }
addDisposable(size.observe { (size) ->
addDisposable(size.observeChange { (size) ->
if (size.width > .0 && size.height > .0) {
editor.layout(obj {
width = size.width
@ -65,7 +65,7 @@ class AsmEditorWidget(private val ctrl: AsmEditorController) : Widget() {
editor.trigger(
source = AsmEditorWidget::class.simpleName,
handlerId = "undo",
payload = undefined
payload = undefined,
)
}
@ -74,7 +74,7 @@ class AsmEditorWidget(private val ctrl: AsmEditorController) : Widget() {
editor.trigger(
source = AsmEditorWidget::class.simpleName,
handlerId = "redo",
payload = undefined
payload = undefined,
)
}

View File

@ -100,7 +100,7 @@ class EntityInfoWidget(private val ctrl: EntityInfoController) : Widget(enabled
val inputValue = mutableCell(value.value)
var timeout = -1
observe(value) {
observeNow(value) {
if (timeout == -1) {
timeout = window.setTimeout({
inputValue.value = value.value

View File

@ -36,7 +36,7 @@ class EventWidget(
}
}
observe(isSelected) {
observeNow(isSelected) {
if (it) {
scrollIntoView(obj<ScrollIntoViewOptions> {
behavior = ScrollBehavior.SMOOTH

View File

@ -56,14 +56,14 @@ class MeshRenderer(
)
init {
observe(viewerStore.currentNinjaGeometry) { rebuildMesh(resetCamera = true) }
observe(viewerStore.currentTextures) { rebuildMesh(resetCamera = true) }
observe(viewerStore.applyTextures) { rebuildMesh(resetCamera = false) }
observe(viewerStore.currentNinjaMotion, ::ninjaMotionChanged)
observe(viewerStore.showSkeleton) { skeletonHelper?.visible = it }
observe(viewerStore.animationPlaying, ::animationPlayingChanged)
observe(viewerStore.frameRate, ::frameRateChanged)
observe(viewerStore.frame, ::frameChanged)
observeNow(viewerStore.currentNinjaGeometry) { rebuildMesh(resetCamera = true) }
observeNow(viewerStore.currentTextures) { rebuildMesh(resetCamera = true) }
observeNow(viewerStore.applyTextures) { rebuildMesh(resetCamera = false) }
observeNow(viewerStore.currentNinjaMotion, ::ninjaMotionChanged)
observeNow(viewerStore.showSkeleton) { skeletonHelper?.visible = it }
observeNow(viewerStore.animationPlaying, ::animationPlayingChanged)
observeNow(viewerStore.frameRate, ::frameRateChanged)
observeNow(viewerStore.frame, ::frameChanged)
}
override fun dispose() {

View File

@ -46,7 +46,7 @@ class TextureRenderer(
))
init {
observe(store.currentTextures) {
observeNow(store.currentTextures) {
texturesChanged(it.filterNotNull())
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.observable.cell.observeNow
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.web.test.WebTestSuite
@ -115,9 +116,9 @@ class EventsControllerTests : WebTestSuite {
// We test the observed value instead of the cell's value property.
var canGoToEventValue: Boolean? = null
disposer.add(canGoToEvent.observe(callNow = true) {
disposer.add(canGoToEvent.observeNow {
assertNull(canGoToEventValue)
canGoToEventValue = it.value
canGoToEventValue = it
})
assertEquals(true, canGoToEventValue)

View File

@ -4,8 +4,9 @@ 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.cell.Cell
import world.phantasmal.observable.cell.observeNow
import world.phantasmal.observable.observe
abstract class DisposableContainer : TrackedDisposable() {
private val disposer = Disposer()
@ -29,85 +30,55 @@ abstract class DisposableContainer : TrackedDisposable() {
disposer.remove(disposable, dispose)
}
protected fun <V1> observe(observable: Observable<V1>, operation: (V1) -> Unit) {
addDisposable(
if (observable is Cell<V1>) {
observable.observe(callNow = true) { operation(it.value) }
} else {
observable.observe { operation(it.value) }
}
)
protected fun <T> observe(
o1: Observable<T>,
observer: (T) -> Unit,
) {
addDisposable(o1.observe(observer))
}
protected fun <V1, V2> observe(
v1: Cell<V1>,
v2: Cell<V2>,
operation: (V1, V2) -> Unit,
protected fun <T> observeNow(
c1: Cell<T>,
observer: (T) -> Unit,
) {
val observer: Observer<*> = {
operation(v1.value, v2.value)
}
addDisposables(
v1.observe(observer),
v2.observe(observer),
)
operation(v1.value, v2.value)
addDisposable(c1.observeNow(observer))
}
protected fun <V1, V2, V3> observe(
v1: Cell<V1>,
v2: Cell<V2>,
v3: Cell<V3>,
operation: (V1, V2, V3) -> Unit,
protected fun <T1, T2> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
observer: (T1, T2) -> 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)
addDisposable(world.phantasmal.observable.cell.observeNow(c1, c2, observer))
}
protected fun <V1, V2, V3, V4> observe(
v1: Cell<V1>,
v2: Cell<V2>,
v3: Cell<V3>,
v4: Cell<V4>,
operation: (V1, V2, V3, V4) -> Unit,
protected fun <T1, T2, T3> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
observer: (T1, T2, T3) -> 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)
addDisposable(world.phantasmal.observable.cell.observeNow(c1, c2, c3, observer))
}
protected fun <V1, V2, V3, V4, V5> observe(
v1: Cell<V1>,
v2: Cell<V2>,
v3: Cell<V3>,
v4: Cell<V4>,
v5: Cell<V5>,
operation: (V1, V2, V3, V4, V5) -> Unit,
protected fun <T1, T2, T3, T4> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
c4: Cell<T4>,
observer: (T1, T2, T3, T4) -> 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)
addDisposable(world.phantasmal.observable.cell.observeNow(c1, c2, c3, c4, observer))
}
protected fun <T1, T2, T3, T4, T5> observeNow(
c1: Cell<T1>,
c2: Cell<T2>,
c3: Cell<T3>,
c4: Cell<T4>,
c5: Cell<T5>,
observer: (T1, T2, T3, T4, T5) -> Unit,
) {
addDisposable(world.phantasmal.observable.cell.observeNow(c1, c2, c3, c4, c5, observer))
}
}

View File

@ -249,11 +249,8 @@ private fun <T> bindChildrenTo(
list: Cell<List<T>>,
createChild: Node.(T, index: Int) -> Node,
childrenRemoved: () -> Unit,
): Disposable =
list.observe(callNow = true) { (items) ->
parent.innerHTML = ""
childrenRemoved()
): Disposable {
inline fun createChildNodes(items: List<T>) {
val frag = document.createDocumentFragment()
items.forEachIndexed { i, item ->
@ -263,14 +260,33 @@ private fun <T> bindChildrenTo(
parent.appendChild(frag)
}
parent.innerHTML = ""
createChildNodes(list.value)
return list.observeChange { (items) ->
parent.innerHTML = ""
childrenRemoved()
createChildNodes(items)
}
}
private fun <T> bindChildrenTo(
parent: Element,
list: ListCell<T>,
createChild: Node.(T, index: Int) -> Node,
childrenRemoved: (index: Int, count: Int) -> Unit,
after: (ListChangeEvent<T>) -> Unit,
): Disposable =
list.observeList(callNow = true) { event: ListChangeEvent<T> ->
): Disposable {
parent.innerHTML = ""
val initialFrag = document.createDocumentFragment()
list.value.forEachIndexed { i, value ->
initialFrag.appendChild(initialFrag.createChild(value, i))
}
parent.appendChild(initialFrag)
return list.observeListChange { event: ListChangeEvent<T> ->
for (change in event.changes) {
if (change is ListChange.Structural) {
if (change.allRemoved) {
@ -299,3 +315,4 @@ private fun <T> bindChildrenTo(
after(event)
}
}

View File

@ -54,7 +54,7 @@ open class Button(
className = "pw-button-center"
if (textCell != null) {
observe(textCell) {
observeNow(textCell) {
textContent = it
hidden = it.isEmpty()
}

View File

@ -23,7 +23,7 @@ class Checkbox(
type = "checkbox"
if (this@Checkbox.checked != null) {
observe(this@Checkbox.checked) {
observeNow(this@Checkbox.checked) {
checked = it
}
}

View File

@ -42,8 +42,8 @@ class ComboBox<T : Any>(
id = labelId
placeholderText?.let { placeholder = it }
hidden(!visible)
observe(enabled) { disabled = !it }
observe(selected) { value = it?.let(itemToString) ?: "" }
observeNow(enabled) { disabled = !it }
observeNow(selected) { value = it?.let(itemToString) ?: "" }
onmousedown = ::onInputMouseDown
onkeydown = ::onInputKeyDown

View File

@ -73,7 +73,7 @@ open class Dialog(
}
init {
observe(visible) {
observeNow(visible) {
if (it) {
setPosition(
(window.innerWidth - WIDTH) / 2,

View File

@ -34,13 +34,13 @@ abstract class Input<T>(
id = labelId
classList.add("pw-input-inner")
observe(this@Input.enabled) { disabled = !it }
observeNow(this@Input.enabled) { disabled = !it }
onchange = { callOnChange(this) }
interceptInputElement(this)
observe(this@Input.value) {
observeNow(this@Input.value) {
setInputValue(this, it)
}
}

View File

@ -16,7 +16,7 @@ class LazyLoader(
div {
className = "pw-lazy-loader"
observe(this@LazyLoader.visible) { v ->
observeNow(this@LazyLoader.visible) { v ->
if (v && !initialized) {
initialized = true
addChild(createWidget())

View File

@ -53,7 +53,7 @@ class Menu<T : Any>(
}
}
observe(this@Menu.visible) {
observeNow(this@Menu.visible) {
if (it) {
onDocumentMouseDownListener =
document.disposableListener("mousedown", ::onDocumentMouseDown)
@ -66,13 +66,13 @@ class Menu<T : Any>(
}
}
observe(enabled) {
observeNow(enabled) {
if (!it) {
clearHighlightItem()
}
}
observe(items) {
observeNow(items) {
clearHighlightItem()
}

View File

@ -37,7 +37,7 @@ class Select<T : Any>(
this@Select.className?.let { classList.add(it) }
// Default to a single space so the inner text part won't be hidden.
observe(selected) { buttonText.value = it?.let(itemToString) ?: " " }
observeNow(selected) { buttonText.value = it?.let(itemToString) ?: " " }
addWidget(Button(
enabled = enabled,

View File

@ -28,7 +28,7 @@ class TabContainer<T : Tab>(
title = tab.title
textContent = tab.title
observe(ctrl.activeTab) {
observeNow(ctrl.activeTab) {
if (it == tab) {
classList.add(ACTIVE_CLASS)
} else {
@ -55,7 +55,7 @@ class TabContainer<T : Tab>(
}
init {
observe(selfOrAncestorVisible, ctrl::visibleChanged)
observeNow(selfOrAncestorVisible, ctrl::visibleChanged)
}
companion object {

View File

@ -4,6 +4,7 @@ import org.w3c.dom.*
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.observeNow
import world.phantasmal.observable.cell.trueCell
import world.phantasmal.webui.LoadingStatus
import world.phantasmal.webui.controllers.Column
@ -30,7 +31,7 @@ class Table<T>(
div {
className = "pw-table-notification"
observe(loadingStatus) { status ->
observeNow(loadingStatus) { status ->
when (status) {
LoadingStatus.Uninitialized,
LoadingStatus.InitialLoad,
@ -167,8 +168,8 @@ class Table<T>(
style.width = "${column.width}px"
column.textAlign?.let { style.textAlign = it }
disposer.add(column.footer.observe(callNow = true) { textContent = it.value ?: "" })
disposer.add(column.footerTooltip.observe(callNow = true) { title = it.value ?: "" })
disposer.add(column.footer.observeNow { textContent = it ?: "" })
disposer.add(column.footerTooltip.observeNow { title = it ?: "" })
}
return Pair(cell, disposer)

View File

@ -37,13 +37,13 @@ class TextArea(
id = labelId
className = "pw-text-area-inner"
observe(this@TextArea.enabled) { disabled = !it }
observeNow(this@TextArea.enabled) { disabled = !it }
if (onChange != null) {
onchange = { onChange.invoke(value) }
}
observe(this@TextArea.value) { value = it }
observeNow(this@TextArea.value) { value = it }
this@TextArea.maxLength?.let { maxLength = it }
fontFamily?.let { style.fontFamily = it }

View File

@ -12,7 +12,6 @@ import org.w3c.dom.pointerevents.PointerEvent
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.DisposableSupervisedScope
import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.Observable
import world.phantasmal.observable.cell.*
import world.phantasmal.webui.DisposableContainer
import world.phantasmal.webui.dom.*
@ -36,12 +35,12 @@ abstract class Widget(
private val elementDelegate = lazy {
val el = documentFragment().createElement()
observe(visible) { visible ->
observeNow(visible) { visible ->
el.hidden = !visible
children.forEach { setAncestorVisible(it, visible && ancestorVisible.value) }
}
observe(enabled) { enabled ->
observeNow(enabled) { enabled ->
if (enabled) {
el.removeAttribute("disabled")
el.classList.remove("pw-disabled")
@ -51,7 +50,7 @@ abstract class Widget(
}
}
observe(tooltip) { tooltip ->
observeNow(tooltip) { tooltip ->
if (tooltip == null) {
el.removeAttribute("title")
} else {
@ -111,16 +110,16 @@ abstract class Widget(
super.dispose()
}
protected fun Node.text(observable: Observable<String>) {
observe(observable) { textContent = it }
protected fun Node.text(cell: Cell<String>) {
observeNow(cell) { textContent = it }
}
protected fun HTMLElement.hidden(observable: Observable<Boolean>) {
observe(observable) { hidden = it }
protected fun HTMLElement.hidden(cell: Cell<Boolean>) {
observeNow(cell) { hidden = it }
}
protected fun HTMLElement.toggleClass(className: String, observable: Observable<Boolean>) {
observe(observable) {
protected fun HTMLElement.toggleClass(className: String, cell: Cell<Boolean>) {
observeNow(cell) {
if (it) classList.add(className)
else classList.remove(className)
}