Observables will now always see a consistent view of their dependencies when they change.

This commit is contained in:
Daan Vanden Bosch 2021-05-27 15:00:19 +02:00
parent dceb80afec
commit 327dfe79bb
57 changed files with 1339 additions and 806 deletions

View File

@ -1,14 +1,14 @@
package world.phantasmal.core.unsafe package world.phantasmal.core.unsafe
/** /**
* Asserts that receiver is of type T. No runtime check happens in KJS. Should only be used when * Asserts that [value] is of type T. No runtime check happens in KJS. Should only be used when it's
* absolutely certain that receiver is indeed a T. * absolutely certain that [value] is indeed a T.
*/ */
expect inline fun <T> Any?.unsafeCast(): T expect inline fun <T> unsafeCast(value: Any?): T
/** /**
* Asserts that T is not null. No runtime check happens in KJS. Should only be used when absolutely * Asserts that [value] is not null. No runtime check happens in KJS. Should only be used when it's
* certain that T is indeed not null. * absolutely certain that [value] is indeed not null.
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun <T> T?.unsafeAssertNotNull(): T = unsafeCast() inline fun <T> unsafeAssertNotNull(value: T?): T = unsafeCast(value)

View File

@ -1,6 +1,4 @@
package world.phantasmal.core.unsafe package world.phantasmal.core.unsafe
import kotlin.js.unsafeCast as kotlinUnsafeCast
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
actual inline fun <T> Any?.unsafeCast(): T = kotlinUnsafeCast<T>() actual inline fun <T> unsafeCast(value: Any?): T = value.unsafeCast<T>()

View File

@ -3,4 +3,4 @@
package world.phantasmal.core.unsafe package world.phantasmal.core.unsafe
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
actual inline fun <T> Any?.unsafeCast(): T = this as T actual inline fun <T> unsafeCast(value: Any?): T = value as T

View File

@ -138,7 +138,7 @@ class Instruction(
} }
} }
return paramToArgs.unsafeAssertNotNull()[paramIndex] return unsafeAssertNotNull(paramToArgs)[paramIndex]
} }
/** /**

View File

@ -0,0 +1,13 @@
package world.phantasmal.observable
abstract class AbstractDependency : Dependency {
protected val dependents: MutableList<Dependent> = mutableListOf()
override fun addDependent(dependent: Dependent) {
dependents.add(dependent)
}
override fun removeDependent(dependent: Dependent) {
dependents.remove(dependent)
}
}

View File

@ -0,0 +1,28 @@
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,
) : 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

@ -0,0 +1,14 @@
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].
*/
fun addDependent(dependent: Dependent)
/**
* This method is not meant to be called from typical application code.
*/
fun removeDependent(dependent: Dependent)
}

View File

@ -0,0 +1,21 @@
package world.phantasmal.observable
interface Dependent {
/**
* This method is not meant to be called from typical application code.
*
* Called whenever a dependency of this dependent might change. Sometimes a dependency doesn't
* know that it will actually change, just that it might change. Always call [dependencyChanged]
* after calling this method.
*
* E.g. C depends on B and B depends on A. A is about to change, so it calls [dependencyMightChange] on B. At this point B doesn't know whether it will actually change since the new value of A doesn't necessarily result in a new value for B (e.g. B = A % 2 and A changes from 0 to 2). So B then calls [dependencyMightChange] on C.
*/
fun dependencyMightChange()
/**
* This method is not meant to be called from typical application code.
*
* Always call [dependencyMightChange] before calling this method.
*/
fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?)
}

View File

@ -2,6 +2,6 @@ package world.phantasmal.observable
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
interface Observable<out T> { interface Observable<out T> : Dependency {
fun observe(observer: Observer<T>): Disposable fun observe(observer: Observer<T>): Disposable
} }

View File

@ -4,4 +4,4 @@ open class ChangeEvent<out T>(val value: T) {
operator fun component1() = value operator fun component1() = value
} }
typealias Observer<T> = (event: ChangeEvent<T>) -> Unit typealias Observer<T> = (ChangeEvent<T>) -> Unit

View File

@ -1,20 +1,18 @@
package world.phantasmal.observable package world.phantasmal.observable
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
class SimpleEmitter<T> : Emitter<T> { class SimpleEmitter<T> : AbstractDependency(), Emitter<T> {
private val observers = mutableListOf<Observer<T>>() override fun emit(event: ChangeEvent<T>) {
for (dependent in dependents) {
dependent.dependencyMightChange()
}
override fun observe(observer: Observer<T>): Disposable { for (dependent in dependents) {
observers.add(observer) dependent.dependencyChanged(this, event)
return disposable {
observers.remove(observer)
} }
} }
override fun emit(event: ChangeEvent<T>) { override fun observe(observer: Observer<T>): Disposable =
observers.forEach { it(event) } CallbackObserver(this, observer)
}
} }

View File

@ -1,30 +1,42 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
abstract class AbstractCell<T> : Cell<T> { abstract class AbstractCell<T> : AbstractDependency(), Cell<T> {
protected val observers: MutableList<Observer<T>> = mutableListOf() private var mightChangeEmitted = false
final override fun observe(observer: Observer<T>): Disposable = final override fun observe(observer: Observer<T>): Disposable =
observe(callNow = false, observer) observe(callNow = false, observer)
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable { override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
observers.add(observer) val observingCell = CallbackObserver(this, observer)
if (callNow) { if (callNow) {
observer(ChangeEvent(value)) observer(ChangeEvent(value))
} }
return disposable { return observingCell
observers.remove(observer) }
protected fun emitMightChange() {
if (!mightChangeEmitted) {
mightChangeEmitted = true
for (dependent in dependents) {
dependent.dependencyMightChange()
}
} }
} }
protected fun emit() { protected fun emitChanged(event: ChangeEvent<T>?) {
val event = ChangeEvent(value) mightChangeEmitted = false
observers.forEach { it(event) }
for (dependent in dependents) {
dependent.dependencyChanged(this, event)
}
} }
} }

View File

@ -1,71 +1,33 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.ChangeEvent
import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.Dependency
import world.phantasmal.core.unsafe.unsafeCast import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observer
/** abstract class AbstractDependentCell<T> : AbstractCell<T>(), Dependent {
* Starts observing its dependencies when the first observer on this cell is registered. Stops private var changingDependencies = 0
* observing its dependencies when the last observer ov this cell is disposed. This way no extra private var dependenciesActuallyChanged = false
* disposables need to be managed when e.g. [map] is used.
*/
abstract class AbstractDependentCell<T>(
private vararg val dependencies: Cell<*>,
) : AbstractCell<T>() {
/**
* Is either empty or has a disposable per dependency.
*/
private val dependencyObservers = mutableListOf<Disposable>()
/** override fun dependencyMightChange() {
* Set to true right before actual observers are added. changingDependencies++
*/ emitMightChange()
protected var hasObservers = false }
protected var _value: T? = null override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if (event != null) {
override val value: T dependenciesActuallyChanged = true
get() {
if (!hasObservers) {
_value = computeValue()
}
return _value.unsafeCast()
} }
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable { if (--changingDependencies == 0) {
if (dependencyObservers.isEmpty()) { if (dependenciesActuallyChanged) {
hasObservers = true dependenciesActuallyChanged = false
dependencies.forEach { dependency -> dependenciesChanged()
dependencyObservers.add( } else {
dependency.observe { emitChanged(null)
val oldValue = _value
_value = computeValue()
if (_value != oldValue) {
emit()
}
}
)
}
_value = computeValue()
}
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
if (observers.isEmpty()) {
hasObservers = false
dependencyObservers.forEach { it.dispose() }
dependencyObservers.clear()
} }
} }
} }
protected abstract fun computeValue(): T abstract fun dependenciesChanged()
} }

View File

@ -16,7 +16,7 @@ interface Cell<out T> : Observable<T> {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
/** /**
* @param callNow Call [observer] immediately with the current [mutableCell]. * @param callNow Call [observer] immediately with the current [value].
*/ */
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable

View File

@ -59,3 +59,6 @@ fun Cell<String>.isBlank(): Cell<Boolean> =
fun Cell<String>.isNotBlank(): Cell<Boolean> = fun Cell<String>.isNotBlank(): Cell<Boolean> =
map { it.isNotBlank() } map { it.isNotBlank() }
fun <T> Cell<Cell<T>>.flatten(): Cell<T> =
FlatteningDependentCell(this) { this.value }

View File

@ -1,5 +1,7 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.observable.ChangeEvent
class DelegatingCell<T>( class DelegatingCell<T>(
private val getter: () -> T, private val getter: () -> T,
private val setter: (T) -> Unit, private val setter: (T) -> Unit,
@ -10,8 +12,11 @@ class DelegatingCell<T>(
val oldValue = getter() val oldValue = getter()
if (value != oldValue) { if (value != oldValue) {
emitMightChange()
setter(value) setter(value)
emit()
emitChanged(ChangeEvent(value))
} }
} }
} }

View File

@ -1,11 +1,58 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.core.unsafe.unsafeCast
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
/** /**
* Cell of which the value depends on 0 or more other cells. * Cell of which the value depends on 0 or more other cells.
*/ */
class DependentCell<T>( class DependentCell<T>(
vararg dependencies: Cell<*>, private vararg val dependencies: Dependency,
private val compute: () -> T, private val compute: () -> T
) : AbstractDependentCell<T>(*dependencies) { ) : AbstractDependentCell<T>() {
override fun computeValue(): T = compute()
private var _value: T? = null
override val value: T
get() {
if (dependents.isEmpty()) {
_value = compute()
}
return unsafeCast(_value)
}
override fun addDependent(dependent: Dependent) {
if (dependents.isEmpty()) {
_value = compute()
for (dependency in dependencies) {
dependency.addDependent(this)
}
}
super.addDependent(dependent)
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun dependenciesChanged() {
val newValue = compute()
if (newValue != _value) {
_value = newValue
emitChanged(ChangeEvent(newValue))
} else {
emitChanged(null)
}
}
} }

View File

@ -1,56 +1,90 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.Observer import world.phantasmal.core.unsafe.unsafeCast
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
/** /**
* Similar to [DependentCell], except that this cell's [compute] returns a cell. * Similar to [DependentCell], except that this cell's [compute] returns a cell.
*/ */
class FlatteningDependentCell<T>( class FlatteningDependentCell<T>(
vararg dependencies: Cell<*>, private vararg val dependencies: Dependency,
private val compute: () -> Cell<T>, private val compute: () -> Cell<T>
) : AbstractDependentCell<T>(*dependencies) { ) : AbstractDependentCell<T>() {
private var computedCell: Cell<T>? = null
private var computedCellObserver: Disposable? = null
private var computedCell: Cell<T>? = null
private var computedInDeps = false
private var shouldRecompute = false
private var _value: T? = null
override val value: T override val value: T
get() { get() {
return if (hasObservers) { if (dependents.isEmpty()) {
computedCell.unsafeAssertNotNull().value _value = compute().value
} else { }
super.value
return unsafeCast(_value)
}
override fun addDependent(dependent: Dependent) {
if (dependents.isEmpty()) {
for (dependency in dependencies) {
dependency.addDependent(this)
}
computedCell = compute().also { computedCell ->
computedCell.addDependent(this)
computedInDeps = dependencies.any { it === computedCell }
_value = computedCell.value
} }
} }
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable { super.addDependent(dependent)
val superDisposable = super.observe(callNow, observer) }
return disposable { override fun removeDependent(dependent: Dependent) {
superDisposable.dispose() super.removeDependent(dependent)
if (!hasObservers) { if (dependents.isEmpty()) {
computedCellObserver?.dispose() computedCell?.removeDependent(this)
computedCellObserver = null computedCell = null
computedCell = null computedInDeps = false
for (dependency in dependencies) {
dependency.removeDependent(this)
} }
} }
} }
override fun computeValue(): T { override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
val computedCell = compute() if ((dependency !== computedCell || computedInDeps) && event != null) {
this.computedCell = computedCell shouldRecompute = true
computedCellObserver?.dispose()
if (hasObservers) {
computedCellObserver = computedCell.observe { (value) ->
_value = value
emit()
}
} }
return computedCell.value super.dependencyChanged(dependency, event)
}
override fun dependenciesChanged() {
if (shouldRecompute) {
computedCell?.removeDependent(this)
computedCell = compute().also { computedCell ->
computedCell.addDependent(this)
computedInDeps = dependencies.any { it === computedCell }
}
shouldRecompute = false
}
val newValue = unsafeAssertNotNull(computedCell).value
if (newValue != _value) {
_value = newValue
emitChanged(ChangeEvent(newValue))
} else {
emitChanged(null)
}
} }
} }

View File

@ -1,11 +1,16 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
import world.phantasmal.observable.ChangeEvent
class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> { class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> {
override var value: T = value override var value: T = value
set(value) { set(value) {
if (value != field) { if (value != field) {
emitMightChange()
field = value field = value
emit()
emitChanged(ChangeEvent(value))
} }
} }
} }

View File

@ -2,10 +2,11 @@ package world.phantasmal.observable.cell
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.nopDisposable import world.phantasmal.core.disposable.nopDisposable
import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
class StaticCell<T>(override val value: T) : Cell<T> { class StaticCell<T>(override val value: T) : AbstractDependency(), Cell<T> {
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable { override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
if (callNow) { if (callNow) {
observer(ChangeEvent(value)) observer(ChangeEvent(value))

View File

@ -1,131 +1,66 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.CallbackObserver
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.AbstractCell import world.phantasmal.observable.cell.AbstractDependentCell
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell
import world.phantasmal.observable.cell.not
/** abstract class AbstractDependentListCell<E> :
* Starts observing its dependencies when the first observer on this cell is registered. Stops AbstractDependentCell<List<E>>(),
* observing its dependencies when the last observer on this cell is disposed. This way no extra ListCell<E>,
* disposables need to be managed when e.g. [map] is used. Dependent {
*/
abstract class AbstractDependentListCell<E>(
private vararg val dependencies: Cell<*>,
) : AbstractListCell<E>(extractObservables = null) {
private val _size = SizeCell()
/**
* Is either empty or has a disposable per dependency.
*/
private val dependencyObservers = mutableListOf<Disposable>()
protected abstract val elements: List<E> protected abstract val elements: List<E>
/**
* Set to true right before actual observers are added.
*/
protected var hasObservers = false
override val value: List<E> override val value: List<E>
get() { get() {
if (!hasObservers) { if (dependents.isEmpty()) {
computeElements() computeElements()
} }
return elements return elements
} }
override val size: Cell<Int> = _size @Suppress("LeakingThis")
final override val size: Cell<Int> = DependentCell(this) { elements.size }
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable { final override val empty: Cell<Boolean> = size.map { it == 0 }
initDependencyObservers()
val superDisposable = super.observe(callNow, observer) final override val notEmpty: Cell<Boolean> = !empty
return disposable { final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
superDisposable.dispose() observeList(callNow, observer as ListObserver<E>)
disposeDependencyObservers()
}
}
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable { override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
initDependencyObservers() val observingCell = CallbackObserver(this, observer)
val superDisposable = super.observeList(callNow, observer) if (callNow) {
observer(
return disposable { ListChangeEvent(
superDisposable.dispose() value,
disposeDependencyObservers() listOf(
ListChange.Structural(index = 0, removed = emptyList(), inserted = value),
),
)
)
} }
return observingCell
}
final override fun dependenciesChanged() {
val oldElements = value
computeElements()
emitChanged(
ListChangeEvent(elements, listOf(ListChange.Structural(0, oldElements, elements)))
)
} }
protected abstract fun computeElements() protected abstract fun computeElements()
protected open fun lastObserverRemoved() {
dependencyObservers.forEach { it.dispose() }
dependencyObservers.clear()
}
private fun initDependencyObservers() {
if (dependencyObservers.isEmpty()) {
hasObservers = true
dependencies.forEach { dependency ->
dependencyObservers.add(
dependency.observe {
val removed = elements
computeElements()
finalizeUpdate(ListChangeEvent.Change(0, removed, elements))
}
)
}
computeElements()
}
}
private fun disposeDependencyObservers() {
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
hasObservers = false
lastObserverRemoved()
}
}
override fun finalizeUpdate(event: ListChangeEvent<E>) {
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
_size.publicEmit()
}
super.finalizeUpdate(event)
}
private inner class SizeCell : AbstractCell<Int>() {
override val value: Int
get() {
if (!hasObservers) {
computeElements()
}
return elements.size
}
val publicObservers = super.observers
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable {
initDependencyObservers()
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
}
fun publicEmit() {
super.emit()
}
}
} }

View File

@ -1,140 +1,38 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.CallbackObserver
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.AbstractCell import world.phantasmal.observable.cell.AbstractCell
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell import world.phantasmal.observable.cell.DependentCell
import world.phantasmal.observable.cell.not import world.phantasmal.observable.cell.not
abstract class AbstractListCell<E>( abstract class AbstractListCell<E> : AbstractCell<List<E>>(), ListCell<E> {
private val extractObservables: ObservablesExtractor<E>?, @Suppress("LeakingThis")
) : AbstractCell<List<E>>(), ListCell<E> { final override val size: Cell<Int> = DependentCell(this) { value.size }
/**
* Internal observers which observe observables related to this list's elements so that their
* changes can be propagated via ElementChange events.
*/
private val elementObservers = mutableListOf<ElementObserver>()
/** final override val empty: Cell<Boolean> = size.map { it == 0 }
* External list observers which are observing this list.
*/
protected val listObservers = mutableListOf<ListObserver<E>>()
override val empty: Cell<Boolean> by lazy { size.map { it == 0 } } final override val notEmpty: Cell<Boolean> = !empty
override val notEmpty: Cell<Boolean> by lazy { !empty } final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
observeList(callNow, observer as ListObserver<E>)
override fun get(index: Int): E =
value[index]
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) {
replaceElementObservers(0, elementObservers.size, value)
}
observers.add(observer)
if (callNow) {
observer(ChangeEvent(value))
}
return disposable {
observers.remove(observer)
disposeElementObserversIfNecessary()
}
}
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable { override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
if (elementObservers.isEmpty() && extractObservables != null) { val observingCell = CallbackObserver(this, observer)
replaceElementObservers(0, elementObservers.size, value)
}
listObservers.add(observer)
if (callNow) { if (callNow) {
observer(ListChangeEvent.Change(0, emptyList(), value)) observer(
} ListChangeEvent(
value,
return disposable { listOf(
listObservers.remove(observer) ListChange.Structural(index = 0, removed = emptyList(), inserted = value),
disposeElementObserversIfNecessary() ),
}
}
override fun firstOrNull(): Cell<E?> =
DependentCell(this) { value.firstOrNull() }
/**
* Does the following in the given order:
* - Updates element observers
* - Emits ListChangeEvent
* - Emits ChangeEvent
*/
protected open fun finalizeUpdate(event: ListChangeEvent<E>) {
if (
(listObservers.isNotEmpty() || observers.isNotEmpty()) &&
extractObservables != null &&
event is ListChangeEvent.Change
) {
replaceElementObservers(event.index, event.removed.size, event.inserted)
}
listObservers.forEach { observer: ListObserver<E> ->
observer(event)
}
emit()
}
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
repeat(amountRemoved) {
elementObservers.removeAt(from).observers.forEach { it.dispose() }
}
var index = from
elementObservers.addAll(
from,
insertedElements.map { element ->
ElementObserver(
index++,
element,
extractObservables.unsafeAssertNotNull()(element)
) )
} )
)
val shift = insertedElements.size - amountRemoved
while (index < elementObservers.size) {
elementObservers[index++].index += shift
} }
}
private fun disposeElementObserversIfNecessary() { return observingCell
if (listObservers.isEmpty() && observers.isEmpty()) {
elementObservers.forEach { elementObserver: ElementObserver ->
elementObserver.observers.forEach { it.dispose() }
}
elementObservers.clear()
}
}
private inner class ElementObserver(
var index: Int,
element: E,
observables: Array<Observable<*>>,
) {
val observers: Array<Disposable> = Array(observables.size) {
observables[it].observe {
finalizeUpdate(ListChangeEvent.ElementChange(index, element))
}
}
} }
} }

View File

@ -1,20 +1,42 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.observable.Dependent
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
/** /**
* ListCell of which the value depends on 0 or more other cells. * ListCell of which the value depends on 0 or more other cells.
*/ */
class DependentListCell<E>( class DependentListCell<E>(
vararg dependencies: Cell<*>, private vararg val dependencies: Cell<*>,
private val computeElements: () -> List<E>, private val computeElements: () -> List<E>,
) : AbstractDependentListCell<E>(*dependencies) { ) : AbstractDependentListCell<E>() {
private var _elements: List<E>? = null
override val elements: List<E> get() = _elements.unsafeAssertNotNull() override var elements: List<E> = emptyList()
private set
override fun addDependent(dependent: Dependent) {
if (dependents.isEmpty()) {
computeElements()
for (dependency in dependencies) {
dependency.addDependent(this)
}
}
super.addDependent(dependent)
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun computeElements() { override fun computeElements() {
_elements = computeElements.invoke() elements = computeElements.invoke()
} }
} }

View File

@ -1,28 +1,16 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.ChangeEvent
import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Observer import world.phantasmal.observable.Dependent
import world.phantasmal.observable.cell.AbstractCell
import world.phantasmal.observable.cell.Cell
// TODO: This class shares 95% of its code with AbstractDependentListCell.
class FilteredListCell<E>( class FilteredListCell<E>(
private val dependency: ListCell<E>, private val dependency: ListCell<E>,
private val predicate: (E) -> Boolean, private val predicate: (E) -> Boolean,
) : AbstractListCell<E>(extractObservables = null) { ) : AbstractListCell<E>(), Dependent {
private val _size = SizeCell()
/**
* Set to true right before actual observers are added.
*/
private var hasObservers = false
private var dependencyObserver: Disposable? = null
/** /**
* Maps the dependency's indices to this list's indices. When an element of the dependency list * Maps the dependency's indices to this list's indices. When an element of the dependency list
* doesn't pass the predicate, it's index in this mapping is set to -1. * doesn't pass the predicate, its index in this mapping is set to -1.
*/ */
private val indexMap = mutableListOf<Int>() private val indexMap = mutableListOf<Int>()
@ -30,66 +18,52 @@ class FilteredListCell<E>(
override val value: List<E> override val value: List<E>
get() { get() {
if (!hasObservers) { if (dependents.isEmpty()) {
recompute() recompute()
} }
return elements return elements
} }
override val size: Cell<Int> = _size override fun addDependent(dependent: Dependent) {
if (dependents.isEmpty()) {
dependency.addDependent(this)
recompute()
}
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable { super.addDependent(dependent)
initDependencyObservers() }
val superDisposable = super.observe(callNow, observer) override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
return disposable { if (dependents.isEmpty()) {
superDisposable.dispose() dependency.removeDependent(this)
disposeDependencyObservers()
} }
} }
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable { override fun dependencyMightChange() {
initDependencyObservers() emitMightChange()
val superDisposable = super.observeList(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
} }
private fun recompute() { override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
elements = ListWrapper(mutableListOf()) if (event is ListChangeEvent<*>) {
indexMap.clear() val filteredChanges = mutableListOf<ListChange<E>>()
dependency.value.forEach { element -> for (change in event.changes) {
if (predicate(element)) { when (change) {
elements.mutate { add(element) } is ListChange.Structural -> {
indexMap.add(elements.lastIndex)
} else {
indexMap.add(-1)
}
}
}
private fun initDependencyObservers() {
if (dependencyObserver == null) {
hasObservers = true
dependencyObserver = dependency.observeList { event ->
when (event) {
is ListChangeEvent.Change -> {
// Figure out which elements should be removed from this list, then simply // Figure out which elements should be removed from this list, then simply
// recompute the entire filtered list and finally figure out which elements // recompute the entire filtered list and finally figure out which elements
// have been added. Emit a Change event if something actually changed. // have been added. Emit a change event if something actually changed.
@Suppress("UNCHECKED_CAST")
change as ListChange.Structural<E>
val removed = mutableListOf<E>() val removed = mutableListOf<E>()
var eventIndex = -1 var eventIndex = -1
event.removed.forEachIndexed { i, element -> change.removed.forEachIndexed { i, element ->
val index = indexMap[event.index + i] val index = indexMap[change.index + i]
if (index != -1) { if (index != -1) {
removed.add(element) removed.add(element)
@ -104,8 +78,8 @@ class FilteredListCell<E>(
val inserted = mutableListOf<E>() val inserted = mutableListOf<E>()
event.inserted.forEachIndexed { i, element -> change.inserted.forEachIndexed { i, element ->
val index = indexMap[event.index + i] val index = indexMap[change.index + i]
if (index != -1) { if (index != -1) {
inserted.add(element) inserted.add(element)
@ -118,23 +92,31 @@ class FilteredListCell<E>(
if (removed.isNotEmpty() || inserted.isNotEmpty()) { if (removed.isNotEmpty() || inserted.isNotEmpty()) {
check(eventIndex != -1) check(eventIndex != -1)
finalizeUpdate(ListChangeEvent.Change(eventIndex, removed, inserted)) filteredChanges.add(
ListChange.Structural(
eventIndex,
removed,
inserted
)
)
} }
} }
is ListChange.Element -> {
is ListChangeEvent.ElementChange -> { // Emit a structural or element change based on whether the updated element
// Emit a Change or ElementChange event based on whether the updated element
// passes the predicate test and whether it was already in the elements list // passes the predicate test and whether it was already in the elements list
// (i.e. whether it passed the predicate test before the update). // (i.e. whether it passed the predicate test before the update).
val index = indexMap[event.index] @Suppress("UNCHECKED_CAST")
change as ListChange.Element<E>
if (predicate(event.updated)) { val index = indexMap[change.index]
if (predicate(change.updated)) {
if (index == -1) { if (index == -1) {
// If the element now passed the test and previously didn't pass, // If the element now passed the test and previously didn't pass,
// insert it and emit a Change event. // insert it and emit a Change event.
var insertIndex = elements.size var insertIndex = elements.size
for (depIdx in (event.index + 1)..indexMap.lastIndex) { for (depIdx in (change.index + 1)..indexMap.lastIndex) {
val thisIdx = indexMap[depIdx] val thisIdx = indexMap[depIdx]
if (thisIdx != -1) { if (thisIdx != -1) {
@ -143,10 +125,10 @@ class FilteredListCell<E>(
} }
} }
elements = elements.mutate { add(insertIndex, event.updated) } elements = elements.mutate { add(insertIndex, change.updated) }
indexMap[event.index] = insertIndex indexMap[change.index] = insertIndex
for (depIdx in (event.index + 1)..indexMap.lastIndex) { for (depIdx in (change.index + 1)..indexMap.lastIndex) {
val thisIdx = indexMap[depIdx] val thisIdx = indexMap[depIdx]
if (thisIdx != -1) { if (thisIdx != -1) {
@ -154,25 +136,25 @@ class FilteredListCell<E>(
} }
} }
finalizeUpdate(ListChangeEvent.Change( filteredChanges.add(
insertIndex, ListChange.Structural(
emptyList(), insertIndex,
listOf(event.updated), removed = emptyList(),
)) inserted = listOf(change.updated),
} else { )
// Otherwise just propagate the ElementChange event.
finalizeUpdate(
ListChangeEvent.ElementChange(index, event.updated)
) )
} else {
// Otherwise just propagate the element change.
filteredChanges.add(ListChange.Element(index, change.updated))
} }
} else { } else {
if (index != -1) { if (index != -1) {
// If the element now doesn't pass the test and it previously did // If the element now doesn't pass the test and it previously did
// pass, remove it and emit a Change event. // pass, remove it and emit a structural change.
elements = elements.mutate { removeAt(index) } elements = elements.mutate { removeAt(index) }
indexMap[event.index] = -1 indexMap[change.index] = -1
for (depIdx in (event.index + 1)..indexMap.lastIndex) { for (depIdx in (change.index + 1)..indexMap.lastIndex) {
val thisIdx = indexMap[depIdx] val thisIdx = indexMap[depIdx]
if (thisIdx != -1) { if (thisIdx != -1) {
@ -180,67 +162,45 @@ class FilteredListCell<E>(
} }
} }
finalizeUpdate(ListChangeEvent.Change( filteredChanges.add(
index, ListChange.Structural(
listOf(event.updated), index,
emptyList(), removed = listOf(change.updated),
)) inserted = emptyList(),
} else { )
// Otherwise just propagate the ElementChange event.
finalizeUpdate(
ListChangeEvent.ElementChange(index, event.updated)
) )
} else {
// Otherwise just propagate the element change.
filteredChanges.add(ListChange.Element(index, change.updated))
} }
} }
} }
} }
} }
recompute() if (filteredChanges.isEmpty()) {
} emitChanged(null)
} } else {
emitChanged(ListChangeEvent(elements, filteredChanges))
private fun disposeDependencyObservers() {
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
hasObservers = false
dependencyObserver?.dispose()
dependencyObserver = null
}
}
override fun finalizeUpdate(event: ListChangeEvent<E>) {
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
_size.publicEmit()
}
super.finalizeUpdate(event)
}
private inner class SizeCell : AbstractCell<Int>() {
override val value: Int
get() {
if (!hasObservers) {
recompute()
}
return elements.size
} }
} else {
emitChanged(null)
}
}
val publicObservers = super.observers private fun recompute() {
val newElements = mutableListOf<E>()
indexMap.clear()
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable { dependency.value.forEach { element ->
initDependencyObservers() if (predicate(element)) {
newElements.add(element)
val superDisposable = super.observe(callNow, observer) indexMap.add(newElements.lastIndex)
} else {
return disposable { indexMap.add(-1)
superDisposable.dispose()
disposeDependencyObservers()
} }
} }
fun publicEmit() { elements = ListWrapper(newElements)
super.emit()
}
} }
} }

View File

@ -1,38 +1,75 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
/** /**
* Similar to [DependentListCell], except that this cell's [computeElements] returns a [ListCell]. * Similar to [DependentListCell], except that this cell's [computeElements] returns a [ListCell].
*/ */
class FlatteningDependentListCell<E>( class FlatteningDependentListCell<E>(
vararg dependencies: Cell<*>, private vararg val dependencies: Dependency,
private val computeElements: () -> ListCell<E>, private val computeElements: () -> ListCell<E>,
) : AbstractDependentListCell<E>(*dependencies) { ) : AbstractDependentListCell<E>() {
private var computedCell: ListCell<E>? = null
private var computedCellObserver: Disposable? = null
override val elements: List<E> get() = computedCell.unsafeAssertNotNull().value private var computedCell: ListCell<E>? = null
private var computedInDeps = false
private var shouldRecompute = false
override var elements: List<E> = emptyList()
private set
override fun addDependent(dependent: Dependent) {
if (dependents.isEmpty()) {
for (dependency in dependencies) {
dependency.addDependent(this)
}
computedCell = computeElements.invoke().also { computedCell ->
computedCell.addDependent(this)
computedInDeps = dependencies.any { it === computedCell }
elements = computedCell.value.toList()
}
}
super.addDependent(dependent)
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
computedCell?.removeDependent(this)
computedCell = null
computedInDeps = false
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if ((dependency !== computedCell || computedInDeps) && event != null) {
shouldRecompute = true
}
super.dependencyChanged(dependency, event)
}
override fun computeElements() { override fun computeElements() {
computedCell = computeElements.invoke() if (shouldRecompute || dependents.isEmpty()) {
computedCell?.removeDependent(this)
computedCellObserver?.dispose() computedCell = computeElements.invoke().also { computedCell ->
computedCell.addDependent(this)
computedCellObserver = computedInDeps = dependencies.any { it === computedCell }
if (hasObservers) {
computedCell.unsafeAssertNotNull().observeList(observer = ::finalizeUpdate)
} else {
null
} }
}
override fun lastObserverRemoved() { shouldRecompute = false
super.lastObserverRemoved() }
computedCellObserver?.dispose() elements = unsafeAssertNotNull(computedCell).value.toList()
computedCellObserver = null
} }
} }

View File

@ -1,49 +0,0 @@
package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.unsafe.unsafeCast
import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.AbstractCell
class FoldedCell<T, R>(
private val dependency: ListCell<T>,
private val initial: R,
private val operation: (R, T) -> R,
) : AbstractCell<R>() {
private var dependencyDisposable: Disposable? = null
private var _value: R? = null
override val value: R
get() {
return if (dependencyDisposable == null) {
computeValue()
} else {
_value.unsafeCast()
}
}
override fun observe(callNow: Boolean, observer: Observer<R>): Disposable {
val superDisposable = super.observe(callNow, observer)
if (dependencyDisposable == null) {
_value = computeValue()
dependencyDisposable = dependency.observe {
_value = computeValue()
emit()
}
}
return disposable {
superDisposable.dispose()
if (observers.isEmpty()) {
dependencyDisposable?.dispose()
dependencyDisposable = null
}
}
}
private fun computeValue(): R = dependency.value.fold(initial, operation)
}

View File

@ -2,6 +2,7 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.DependentCell
interface ListCell<out E> : Cell<List<E>> { interface ListCell<out E> : Cell<List<E>> {
/** /**
@ -16,23 +17,24 @@ interface ListCell<out E> : Cell<List<E>> {
val notEmpty: Cell<Boolean> val notEmpty: Cell<Boolean>
operator fun get(index: Int): E operator fun get(index: Int): E = value[index]
fun observeList(callNow: Boolean = false, observer: ListObserver<E>): Disposable fun observeList(callNow: Boolean = false, observer: ListObserver<E>): Disposable
fun <R> fold(initialValue: R, operation: (R, E) -> R): Cell<R> = fun <R> fold(initialValue: R, operation: (R, E) -> R): Cell<R> =
FoldedCell(this, initialValue, operation) DependentCell(this) { value.fold(initialValue, operation) }
fun all(predicate: (E) -> Boolean): Cell<Boolean> = fun all(predicate: (E) -> Boolean): Cell<Boolean> =
fold(true) { acc, el -> acc && predicate(el) } DependentCell(this) { value.all(predicate) }
fun sumBy(selector: (E) -> Int): Cell<Int> = fun sumOf(selector: (E) -> Int): Cell<Int> =
fold(0) { acc, el -> acc + selector(el) } DependentCell(this) { value.sumOf(selector) }
fun filtered(predicate: (E) -> Boolean): ListCell<E> = fun filtered(predicate: (E) -> Boolean): ListCell<E> =
FilteredListCell(this, predicate) FilteredListCell(this, predicate)
fun firstOrNull(): Cell<E?> fun firstOrNull(): Cell<E?> =
DependentCell(this) { value.firstOrNull() }
operator fun contains(element: @UnsafeVariance E): Boolean = element in value operator fun contains(element: @UnsafeVariance E): Boolean = element in value
} }

View File

@ -10,8 +10,9 @@ fun <E> emptyListCell(): ListCell<E> = EMPTY_LIST_CELL
fun <E> mutableListCell( fun <E> mutableListCell(
vararg elements: E, vararg elements: E,
extractObservables: ObservablesExtractor<E>? = null, extractDependencies: DependenciesExtractor<E>? = null,
): MutableListCell<E> = SimpleListCell(mutableListOf(*elements), extractObservables) ): MutableListCell<E> =
SimpleListCell(mutableListOf(*elements), extractDependencies)
fun <T1, T2, R> flatMapToList( fun <T1, T2, R> flatMapToList(
c1: Cell<T1>, c1: Cell<T1>,

View File

@ -1,37 +1,42 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
sealed class ListChangeEvent<out E> { import world.phantasmal.observable.ChangeEvent
abstract val index: Int
class ListChangeEvent<out E>(
value: List<E>,
val changes: List<ListChange<E>>,
) : ChangeEvent<List<E>>(value)
sealed class ListChange<out E> {
/** /**
* Represents a structural change to the list. E.g. an element is inserted or removed. * Represents a structural change to a list cell. E.g. an element is inserted or removed.
*/ */
class Change<E>( class Structural<out E>(
override val index: Int, val index: Int,
/** /**
* The elements that were removed from the list at [index]. * The elements that were removed from the list at [index].
* *
* Do not keep long-lived references to a [Change]'s [removed] list, it may or may not be * Do not keep long-lived references to a [ChangeEvent]'s [removed] list, it may or may not
* mutated when the originating [ListCell] is mutated. * be mutated when the originating [ListCell] is mutated.
*/ */
val removed: List<E>, val removed: List<E>,
/** /**
* The elements that were inserted into the list at [index]. * The elements that were inserted into the list at [index].
* *
* Do not keep long-lived references to a [Change]'s [inserted] list, it may or may not be * Do not keep long-lived references to a [ChangeEvent]'s [inserted] list, it may or may not
* mutated when the originating [ListCell] is mutated. * be mutated when the originating [ListCell] is mutated.
*/ */
val inserted: List<E>, val inserted: List<E>,
) : ListChangeEvent<E>() ) : ListChange<E>()
/** /**
* Represents a change to an element in the list. Will only be emitted if the list is configured * Represents a change to an element in a list cell. Will only be emitted if the list is
* to do so. * configured to do so.
*/ */
class ElementChange<E>( class Element<E>(
override val index: Int, val index: Int,
val updated: E, val updated: E,
) : ListChangeEvent<E>() ) : ListChange<E>()
} }
typealias ListObserver<E> = (change: ListChangeEvent<E>) -> Unit typealias ListObserver<E> = (ListChangeEvent<E>) -> Unit

View File

@ -17,7 +17,7 @@ interface MutableListCell<E> : ListCell<E>, MutableCell<List<E>> {
fun replaceAll(elements: Sequence<E>) fun replaceAll(elements: Sequence<E>)
fun splice(from: Int, removeCount: Int, newElement: E) fun splice(fromIndex: Int, removeCount: Int, newElement: E)
fun clear() fun clear()

View File

@ -1,23 +1,33 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.observable.Observable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.observable.cell.Cell import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.cell.MutableCell import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.cell.mutableCell import world.phantasmal.observable.Dependency
import world.phantasmal.observable.Dependent
typealias ObservablesExtractor<E> = (element: E) -> Array<Observable<*>> typealias DependenciesExtractor<E> = (element: E) -> Array<Dependency>
/** /**
* @param elements The backing list for this [ListCell] * @param elements The backing list for this [ListCell].
* @param extractObservables Extractor function called on each element in this list, changes to the * @param extractDependencies Extractor function called on each element in this list, changes to
* returned observables will be propagated via ElementChange events * the returned dependencies will be propagated via [ListChange.Element]s in a [ListChangeEvent]
* event.
*/ */
class SimpleListCell<E>( class SimpleListCell<E>(
elements: MutableList<E>, elements: MutableList<E>,
extractObservables: ObservablesExtractor<E>? = null, private val extractDependencies: DependenciesExtractor<E>? = null,
) : AbstractListCell<E>(extractObservables), MutableListCell<E> { ) : AbstractListCell<E>(), MutableListCell<E> {
private var elements = ListWrapper(elements) private var elements = ListWrapper(elements)
private val _size: MutableCell<Int> = mutableCell(elements.size)
/**
* Dependents of dependencies related to this list's elements. Allows us to propagate changes to
* elements via [ListChangeEvent]s.
*/
private val elementDependents = mutableListOf<ElementDependent>()
private var changingElements = 0
private var elementListChanges = mutableListOf<ListChange.Element<E>>()
override var value: List<E> override var value: List<E>
get() = elements get() = elements
@ -25,27 +35,47 @@ class SimpleListCell<E>(
replaceAll(value) replaceAll(value)
} }
override val size: Cell<Int> = _size
override operator fun get(index: Int): E = override operator fun get(index: Int): E =
elements[index] elements[index]
override operator fun set(index: Int, element: E): E { override operator fun set(index: Int, element: E): E {
checkIndex(index, elements.lastIndex)
emitMightChange()
val removed: E val removed: E
elements = elements.mutate { removed = set(index, element) } elements = elements.mutate { removed = set(index, element) }
finalizeUpdate(ListChangeEvent.Change(index, listOf(removed), listOf(element)))
if (extractDependencies != null) {
elementDependents[index].dispose()
elementDependents[index] = ElementDependent(index, element)
}
emitChanged(
ListChangeEvent(
elements,
listOf(ListChange.Structural(index, listOf(removed), listOf(element))),
),
)
return removed return removed
} }
override fun add(element: E) { override fun add(element: E) {
emitMightChange()
val index = elements.size val index = elements.size
elements = elements.mutate { add(index, element) } elements = elements.mutate { add(index, element) }
finalizeUpdate(ListChangeEvent.Change(index, emptyList(), listOf(element)))
finalizeStructuralChange(index, emptyList(), listOf(element))
} }
override fun add(index: Int, element: E) { override fun add(index: Int, element: E) {
checkIndex(index, elements.size)
emitMightChange()
elements = elements.mutate { add(index, element) } elements = elements.mutate { add(index, element) }
finalizeUpdate(ListChangeEvent.Change(index, emptyList(), listOf(element)))
finalizeStructuralChange(index, emptyList(), listOf(element))
} }
override fun remove(element: E): Boolean { override fun remove(element: E): Boolean {
@ -60,46 +90,180 @@ class SimpleListCell<E>(
} }
override fun removeAt(index: Int): E { override fun removeAt(index: Int): E {
checkIndex(index, elements.lastIndex)
emitMightChange()
val removed: E val removed: E
elements = elements.mutate { removed = removeAt(index) } elements = elements.mutate { removed = removeAt(index) }
finalizeUpdate(ListChangeEvent.Change(index, listOf(removed), emptyList()))
finalizeStructuralChange(index, listOf(removed), emptyList())
return removed return removed
} }
override fun replaceAll(elements: Iterable<E>) { override fun replaceAll(elements: Iterable<E>) {
emitMightChange()
val removed = this.elements val removed = this.elements
this.elements = ListWrapper(elements.toMutableList()) this.elements = ListWrapper(elements.toMutableList())
finalizeUpdate(ListChangeEvent.Change(0, removed, this.elements))
finalizeStructuralChange(0, removed, this.elements)
} }
override fun replaceAll(elements: Sequence<E>) { override fun replaceAll(elements: Sequence<E>) {
emitMightChange()
val removed = this.elements val removed = this.elements
this.elements = ListWrapper(elements.toMutableList()) this.elements = ListWrapper(elements.toMutableList())
finalizeUpdate(ListChangeEvent.Change(0, removed, this.elements))
finalizeStructuralChange(0, removed, this.elements)
} }
override fun splice(from: Int, removeCount: Int, newElement: E) { override fun splice(fromIndex: Int, removeCount: Int, newElement: E) {
val removed = ArrayList(elements.subList(from, from + removeCount)) val removed = ArrayList(elements.subList(fromIndex, fromIndex + removeCount))
emitMightChange()
elements = elements.mutate { elements = elements.mutate {
repeat(removeCount) { removeAt(from) } repeat(removeCount) { removeAt(fromIndex) }
add(from, newElement) add(fromIndex, newElement)
} }
finalizeUpdate(ListChangeEvent.Change(from, removed, listOf(newElement)))
finalizeStructuralChange(fromIndex, removed, listOf(newElement))
} }
override fun clear() { override fun clear() {
emitMightChange()
val removed = elements val removed = elements
elements = ListWrapper(mutableListOf()) elements = ListWrapper(mutableListOf())
finalizeUpdate(ListChangeEvent.Change(0, removed, emptyList()))
finalizeStructuralChange(0, removed, emptyList())
} }
override fun sortWith(comparator: Comparator<E>) { override fun sortWith(comparator: Comparator<E>) {
elements = elements.mutate { sortWith(comparator) } emitMightChange()
finalizeUpdate(ListChangeEvent.Change(0, elements, elements))
var throwable: Throwable? = null
try {
elements = elements.mutate { sortWith(comparator) }
} catch (e: Throwable) {
throwable = e
}
finalizeStructuralChange(0, elements, elements)
if (throwable != null) {
throw throwable
}
} }
override fun finalizeUpdate(event: ListChangeEvent<E>) { override fun addDependent(dependent: Dependent) {
_size.value = elements.size if (dependents.isEmpty() && extractDependencies != null) {
super.finalizeUpdate(event) for ((index, element) in elements.withIndex()) {
elementDependents.add(ElementDependent(index, element))
}
}
super.addDependent(dependent)
}
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (dependents.isEmpty()) {
for (elementDependent in elementDependents) {
elementDependent.dispose()
}
elementDependents.clear()
}
}
private fun checkIndex(index: Int, maxIndex: Int) {
if (index !in 0..maxIndex) {
throw IndexOutOfBoundsException(
"Index $index out of bounds for length ${elements.size}",
)
}
}
private fun finalizeStructuralChange(index: Int, removed: List<E>, inserted: List<E>) {
if (extractDependencies != null) {
repeat(removed.size) {
elementDependents.removeAt(index).dispose()
}
for ((i, element) in inserted.withIndex()) {
val elementIdx = index + i
elementDependents.add(elementIdx, ElementDependent(elementIdx, element))
}
val shift = inserted.size - removed.size
for (i in (index + inserted.size)..elementDependents.lastIndex) {
elementDependents[i].index += shift
}
}
emitChanged(
ListChangeEvent(
elements,
listOf(ListChange.Structural(index, removed, inserted)),
),
)
}
private inner class ElementDependent(
var index: Int,
private val element: E,
) : Dependent, Disposable {
private val dependencies = unsafeAssertNotNull(extractDependencies)(element)
private var changingDependencies = 0
private var dependenciesActuallyChanged = false
init {
for (dependency in dependencies) {
dependency.addDependent(this)
}
}
override fun dispose() {
for (dependency in dependencies) {
dependency.removeDependent(this)
}
}
override fun dependencyMightChange() {
if (changingDependencies++ == 0) {
changingElements++
emitMightChange()
}
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if (event != null) {
dependenciesActuallyChanged = true
}
if (--changingDependencies == 0) {
if (dependenciesActuallyChanged) {
dependenciesActuallyChanged = false
elementListChanges.add(ListChange.Element(index, element))
}
if (--changingElements == 0) {
try {
if (elementListChanges.isNotEmpty()) {
emitChanged(ListChangeEvent(value, elementListChanges))
} else {
emitChanged(null)
}
} finally {
elementListChanges = mutableListOf()
}
}
}
}
} }
} }

View File

@ -2,12 +2,14 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.nopDisposable import world.phantasmal.core.disposable.nopDisposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.AbstractDependency
import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observer import world.phantasmal.observable.Observer
import world.phantasmal.observable.cell.* import world.phantasmal.observable.cell.*
class StaticListCell<E>(private val elements: List<E>) : ListCell<E> { class StaticListCell<E>(private val elements: List<E>) : AbstractDependency(), ListCell<E> {
private val firstOrNull = StaticCell(elements.firstOrNull()) private var firstOrNull: Cell<E?>? = null
override val size: Cell<Int> = cell(elements.size) override val size: Cell<Int> = cell(elements.size)
override val empty: Cell<Boolean> = if (elements.isEmpty()) trueCell() else falseCell() override val empty: Cell<Boolean> = if (elements.isEmpty()) trueCell() else falseCell()
@ -30,11 +32,17 @@ class StaticListCell<E>(private val elements: List<E>) : ListCell<E> {
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable { override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
if (callNow) { if (callNow) {
observer(ListChangeEvent.Change(0, emptyList(), value)) observer(ListChangeEvent(value, listOf(ListChange.Structural(0, emptyList(), value))))
} }
return nopDisposable() return nopDisposable()
} }
override fun firstOrNull(): Cell<E?> = firstOrNull override fun firstOrNull(): Cell<E?> {
if (firstOrNull == null) {
firstOrNull = StaticCell(elements.firstOrNull())
}
return unsafeAssertNotNull(firstOrNull)
}
} }

View File

@ -12,7 +12,7 @@ interface ObservableTests : ObservableTestSuite {
fun createProvider(): Provider fun createProvider(): Provider
@Test @Test
fun observable_calls_observers_when_events_are_emitted() = test { fun calls_observers_when_events_are_emitted() = test {
val p = createProvider() val p = createProvider()
var changes = 0 var changes = 0
@ -34,7 +34,7 @@ interface ObservableTests : ObservableTestSuite {
} }
@Test @Test
fun observable_does_not_call_observers_after_they_are_disposed() = test { fun does_not_call_observers_after_they_are_disposed() = test {
val p = createProvider() val p = createProvider()
var changes = 0 var changes = 0

View File

@ -11,6 +11,27 @@ import kotlin.test.*
interface CellTests : ObservableTests { interface CellTests : ObservableTests {
override fun createProvider(): Provider override fun createProvider(): Provider
@Test
fun value_is_accessible_without_observers() = test {
val p = createProvider()
assertNotNull(p.observable.value)
}
@Test
fun value_is_accessible_with_observers() = test {
val p = createProvider()
var observedValue: Any? = null
disposer.add(p.observable.observe(callNow = true) {
observedValue = it.value
})
assertNotNull(observedValue)
assertNotNull(p.observable.value)
}
@Test @Test
fun propagates_changes_to_mapped_cell() = test { fun propagates_changes_to_mapped_cell() = test {
val p = createProvider() val p = createProvider()

View File

@ -0,0 +1,53 @@
package world.phantasmal.observable.cell
import world.phantasmal.observable.Dependent
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
interface CellWithDependenciesTests : CellTests {
override fun createProvider(): Provider
@Test
fun is_recomputed_once_even_when_many_dependencies_change() = test {
val p = createProvider()
val root = SimpleCell(5)
val branch1 = root.map { it * 2 }
val branch2 = root.map { it * 4 }
val leaf = p.createWithDependencies(branch1, branch2)
var observedChanges = 0
disposer.add(leaf.observe { observedChanges++ })
// Change root, which results in both branches changing and thus two dependencies of leaf
// changing.
root.value = 7
assertEquals(1, observedChanges)
}
@Test
fun doesnt_register_as_dependent_of_its_dependencies_until_it_has_dependents_itself() = test {
val p = createProvider()
val dependency = object : AbstractCell<Int>() {
val publicDependents: List<Dependent> = dependents
override val value: Int = 5
}
val cell = p.createWithDependencies(dependency)
assertTrue(dependency.publicDependents.isEmpty())
disposer.add(cell.observe { })
assertEquals(1, dependency.publicDependents.size)
}
interface Provider : CellTests.Provider {
fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any>
}
}

View File

@ -2,7 +2,7 @@ package world.phantasmal.observable.cell
class DelegatingCellTests : RegularCellTests, MutableCellTests<Int> { class DelegatingCellTests : RegularCellTests, MutableCellTests<Int> {
override fun createProvider() = object : MutableCellTests.Provider<Int> { override fun createProvider() = object : MutableCellTests.Provider<Int> {
private var v = 0 private var v = 17
override val observable = DelegatingCell({ v }, { v = it }) override val observable = DelegatingCell({ v }, { v = it })

View File

@ -1,18 +1,23 @@
package world.phantasmal.observable.cell package world.phantasmal.observable.cell
class DependentCellTests : RegularCellTests { class DependentCellTests : RegularCellTests, CellWithDependenciesTests {
override fun createProvider() = object : CellTests.Provider { override fun createProvider() = Provider()
val dependency = SimpleCell(0)
override fun <T> createWithValue(value: T): DependentCell<T> {
val dependency = SimpleCell(value)
return DependentCell(dependency) { dependency.value }
}
class Provider : CellTests.Provider, CellWithDependenciesTests.Provider {
private val dependency = SimpleCell(1)
override val observable = DependentCell(dependency) { 2 * dependency.value } override val observable = DependentCell(dependency) { 2 * dependency.value }
override fun emit() { override fun emit() {
dependency.value += 2 dependency.value += 2
} }
}
override fun <T> createWithValue(value: T): DependentCell<T> { override fun createWithDependencies(vararg dependencies: Cell<Int>) =
val dependency = SimpleCell(value) DependentCell(*dependencies) { dependencies.sumOf { it.value } }
return DependentCell(dependency) { dependency.value }
} }
} }

View File

@ -3,13 +3,23 @@ package world.phantasmal.observable.cell
/** /**
* In these tests the dependency of the [FlatteningDependentCell]'s direct dependency changes. * In these tests the dependency of the [FlatteningDependentCell]'s direct dependency changes.
*/ */
class FlatteningDependentCellTransitiveDependencyEmitsTests : RegularCellTests { class FlatteningDependentCellTransitiveDependencyEmitsTests :
override fun createProvider() = object : CellTests.Provider { RegularCellTests,
CellWithDependenciesTests {
override fun createProvider() = Provider()
override fun <T> createWithValue(value: T): FlatteningDependentCell<T> {
val dependency = StaticCell(StaticCell(value))
return FlatteningDependentCell(dependency) { dependency.value }
}
class Provider : CellTests.Provider, CellWithDependenciesTests.Provider {
// The transitive dependency can change. // The transitive dependency can change.
val transitiveDependency = SimpleCell(5) private val transitiveDependency = SimpleCell(5)
// The direct dependency of the cell under test can't change. // The direct dependency of the cell under test can't change.
val directDependency = StaticCell(transitiveDependency) private val directDependency = StaticCell(transitiveDependency)
override val observable = override val observable =
FlatteningDependentCell(directDependency) { directDependency.value } FlatteningDependentCell(directDependency) { directDependency.value }
@ -18,10 +28,8 @@ class FlatteningDependentCellTransitiveDependencyEmitsTests : RegularCellTests {
// Update the transitive dependency. // Update the transitive dependency.
transitiveDependency.value += 5 transitiveDependency.value += 5
} }
}
override fun <T> createWithValue(value: T): FlatteningDependentCell<T> { override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> =
val dependency = StaticCell(StaticCell(value)) FlatteningDependentCell(*dependencies) { StaticCell(dependencies.sumOf { it.value }) }
return FlatteningDependentCell(dependency) { dependency.value }
} }
} }

View File

@ -32,8 +32,6 @@ interface RegularCellTests : CellTests {
// Value should not change when emit hasn't been called since the last access. // Value should not change when emit hasn't been called since the last access.
assertEquals(new, p.observable.value) assertEquals(new, p.observable.value)
old = new
} }
} }

View File

@ -1,13 +1,23 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
class DependentListCellTests : ListCellTests { import world.phantasmal.observable.cell.Cell
override fun createProvider() = object : ListCellTests.Provider { import world.phantasmal.observable.cell.CellWithDependenciesTests
private val dependency = SimpleListCell<Int>(mutableListOf())
class DependentListCellTests : ListCellTests, CellWithDependenciesTests {
override fun createProvider() = createListProvider(empty = true)
override fun createListProvider(empty: Boolean) = Provider(empty)
class Provider(empty: Boolean) : ListCellTests.Provider, CellWithDependenciesTests.Provider {
private val dependency = SimpleListCell(if (empty) mutableListOf() else mutableListOf(5))
override val observable = DependentListCell(dependency) { dependency.value.map { 2 * it } } override val observable = DependentListCell(dependency) { dependency.value.map { 2 * it } }
override fun addElement() { override fun addElement() {
dependency.add(4) dependency.add(4)
} }
override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> =
DependentListCell(*dependencies) { dependencies.map { it.value } }
} }
} }

View File

@ -1,13 +1,12 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.observable.cell.SimpleCell import world.phantasmal.observable.cell.SimpleCell
import kotlin.test.Test import kotlin.test.*
import kotlin.test.assertEquals
import kotlin.test.assertNull
class FilteredListCellTests : ListCellTests { class FilteredListCellTests : ListCellTests {
override fun createProvider() = object : ListCellTests.Provider { override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider {
private val dependency = SimpleListCell<Int>(mutableListOf()) private val dependency =
SimpleListCell(if (empty) mutableListOf(5) else mutableListOf(5, 10))
override val observable = FilteredListCell(dependency, predicate = { it % 2 == 0 }) override val observable = FilteredListCell(dependency, predicate = { it % 2 == 0 })
@ -82,37 +81,49 @@ class FilteredListCellTests : ListCellTests {
dep.replaceAll(listOf(1, 2, 3, 4, 5)) dep.replaceAll(listOf(1, 2, 3, 4, 5))
(event as ListChangeEvent.Change).let { e -> run {
assertEquals(0, e.index) val e = event
assertEquals(0, e.removed.size) assertNotNull(e)
assertEquals(2, e.inserted.size) assertEquals(1, e.changes.size)
assertEquals(2, e.inserted[0])
assertEquals(4, e.inserted[1]) val c = e.changes.first()
assertTrue(c is ListChange.Structural)
assertEquals(0, c.index)
assertEquals(0, c.removed.size)
assertEquals(2, c.inserted.size)
assertEquals(2, c.inserted[0])
assertEquals(4, c.inserted[1])
} }
event = null event = null
dep.splice(2, 2, 10) dep.splice(2, 2, 10)
(event as ListChangeEvent.Change).let { e -> run {
assertEquals(1, e.index) val e = event
assertEquals(1, e.removed.size) assertNotNull(e)
assertEquals(4, e.removed[0]) assertEquals(1, e.changes.size)
assertEquals(1, e.inserted.size)
assertEquals(10, e.inserted[0]) val c = e.changes.first()
assertTrue(c is ListChange.Structural)
assertEquals(1, c.index)
assertEquals(1, c.removed.size)
assertEquals(4, c.removed[0])
assertEquals(1, c.inserted.size)
assertEquals(10, c.inserted[0])
} }
} }
/** /**
* When the dependency of a [FilteredListCell] emits ElementChange events, the * When the dependency of a [FilteredListCell] emits [ListChange.Element] changes, the
* [FilteredListCell] should emit either Change events or ElementChange events, depending on * [FilteredListCell] should emit either [ListChange.Structural] or [ListChange.Element]
* whether the predicate result has changed. * changes, depending on whether the predicate result has changed.
*/ */
@Test @Test
fun emits_correct_events_when_dependency_emits_ElementChange_events() = test { fun emits_correct_events_when_dependency_emits_element_changes() = test {
val dep = SimpleListCell( val dep = SimpleListCell(
mutableListOf(SimpleCell(1), SimpleCell(2), SimpleCell(3), SimpleCell(4)), mutableListOf(SimpleCell(1), SimpleCell(2), SimpleCell(3), SimpleCell(4)),
extractObservables = { arrayOf(it) }, extractDependencies = { arrayOf(it) },
) )
val list = FilteredListCell(dep, predicate = { it.value % 2 == 0 }) val list = FilteredListCell(dep, predicate = { it.value % 2 == 0 })
var event: ListChangeEvent<SimpleCell<Int>>? = null var event: ListChangeEvent<SimpleCell<Int>>? = null
@ -125,20 +136,25 @@ class FilteredListCellTests : ListCellTests {
for (i in 0 until dep.size.value) { for (i in 0 until dep.size.value) {
event = null event = null
// Make an even number odd or an odd number even. List should emit a Change event. // Make an even number odd or an odd number even. List should emit a structural change.
val newValue = dep[i].value + 1 val newValue = dep[i].value + 1
dep[i].value = newValue dep[i].value = newValue
(event as ListChangeEvent.Change).let { e -> val e = event
if (newValue % 2 == 0) { assertNotNull(e)
assertEquals(0, e.removed.size) assertEquals(1, e.changes.size)
assertEquals(1, e.inserted.size)
assertEquals(newValue, e.inserted[0].value) val c = e.changes.first()
} else { assertTrue(c is ListChange.Structural)
assertEquals(1, e.removed.size)
assertEquals(0, e.inserted.size) if (newValue % 2 == 0) {
assertEquals(newValue, e.removed[0].value) assertEquals(0, c.removed.size)
} assertEquals(1, c.inserted.size)
assertEquals(newValue, c.inserted[0].value)
} else {
assertEquals(1, c.removed.size)
assertEquals(0, c.inserted.size)
assertEquals(newValue, c.removed[0].value)
} }
} }
@ -150,7 +166,14 @@ class FilteredListCellTests : ListCellTests {
val newValue = dep[i].value + 2 val newValue = dep[i].value + 2
dep[i].value = newValue dep[i].value = newValue
assertEquals(newValue, (event as ListChangeEvent.ElementChange).updated.value) val e = event
assertNotNull(e)
assertEquals(1, e.changes.size)
val c = e.changes.first()
assertTrue(c is ListChange.Element)
assertEquals(newValue, c.updated.value)
} }
} }
} }

View File

@ -6,9 +6,9 @@ import world.phantasmal.observable.cell.SimpleCell
* In these tests the direct dependency of the [FlatteningDependentListCell] changes. * In these tests the direct dependency of the [FlatteningDependentListCell] changes.
*/ */
class FlatteningDependentListCellDirectDependencyEmitsTests : ListCellTests { class FlatteningDependentListCellDirectDependencyEmitsTests : ListCellTests {
override fun createProvider() = object : ListCellTests.Provider { override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider {
// The transitive dependency can't change. // The transitive dependency can't change.
private val transitiveDependency = StaticListCell<Int>(emptyList()) private val transitiveDependency = StaticListCell(if (empty) emptyList() else listOf(7))
// The direct dependency of the list under test can change. // The direct dependency of the list under test can change.
private val dependency = SimpleCell<ListCell<Int>>(transitiveDependency) private val dependency = SimpleCell<ListCell<Int>>(transitiveDependency)

View File

@ -1,14 +1,24 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.CellWithDependenciesTests
import world.phantasmal.observable.cell.StaticCell import world.phantasmal.observable.cell.StaticCell
/** /**
* In these tests the dependency of the [FlatteningDependentListCell]'s direct dependency changes. * In these tests the dependency of the [FlatteningDependentListCell]'s direct dependency changes.
*/ */
class FlatteningDependentListCellTransitiveDependencyEmitsTests : ListCellTests { class FlatteningDependentListCellTransitiveDependencyEmitsTests :
override fun createProvider() = object : ListCellTests.Provider { ListCellTests,
CellWithDependenciesTests {
override fun createProvider() = createListProvider(empty = true)
override fun createListProvider(empty: Boolean) = Provider(empty)
class Provider(empty: Boolean) : ListCellTests.Provider, CellWithDependenciesTests.Provider {
// The transitive dependency can change. // The transitive dependency can change.
private val transitiveDependency = SimpleListCell(mutableListOf<Int>()) private val transitiveDependency =
SimpleListCell(if (empty) mutableListOf() else mutableListOf(7))
// The direct dependency of the list under test can't change. // The direct dependency of the list under test can't change.
private val dependency = StaticCell<ListCell<Int>>(transitiveDependency) private val dependency = StaticCell<ListCell<Int>>(transitiveDependency)
@ -20,5 +30,10 @@ class FlatteningDependentListCellTransitiveDependencyEmitsTests : ListCellTests
// Update the transitive dependency. // Update the transitive dependency.
transitiveDependency.add(4) transitiveDependency.add(4)
} }
override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> =
FlatteningDependentListCell(*dependencies) {
StaticListCell(dependencies.map { it.value })
}
} }
} }

View File

@ -8,7 +8,30 @@ import kotlin.test.*
* [ListCell] implementation. * [ListCell] implementation.
*/ */
interface ListCellTests : CellTests { interface ListCellTests : CellTests {
override fun createProvider(): Provider override fun createProvider(): Provider = createListProvider(empty = true)
fun createListProvider(empty: Boolean): Provider
@Test
fun list_value_is_accessible_without_observers() = test {
val p = createListProvider(empty = false)
assertTrue(p.observable.value.isNotEmpty())
}
@Test
fun list_value_is_accessible_with_observers() = test {
val p = createListProvider(empty = false)
var observedValue: List<*>? = null
disposer.add(p.observable.observe(callNow = true) {
observedValue = it.value
})
assertTrue(observedValue!!.isNotEmpty())
assertTrue(p.observable.value.isNotEmpty())
}
@Test @Test
fun calls_list_observers_when_changed() = test { fun calls_list_observers_when_changed() = test {
@ -28,7 +51,7 @@ interface ListCellTests : CellTests {
p.addElement() p.addElement()
assertTrue(event is ListChangeEvent.Change<*>) assertNotNull(event)
} }
} }
@ -100,7 +123,7 @@ interface ListCellTests : CellTests {
fun sumBy() = test { fun sumBy() = test {
val p = createProvider() val p = createProvider()
val sum = p.observable.sumBy { 1 } val sum = p.observable.sumOf { 1 }
var observedValue: Int? = null var observedValue: Int? = null

View File

@ -1,10 +1,7 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import world.phantasmal.observable.cell.MutableCellTests import world.phantasmal.observable.cell.MutableCellTests
import kotlin.test.Test import kotlin.test.*
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
/** /**
* Test suite for all [MutableListCell] implementations. There is a subclass of this suite for every * Test suite for all [MutableListCell] implementations. There is a subclass of this suite for every
@ -17,58 +14,79 @@ interface MutableListCellTests<T : Any> : ListCellTests, MutableCellTests<List<T
fun add() = test { fun add() = test {
val p = createProvider() val p = createProvider()
var change: ListChangeEvent<T>? = null var changeEvent: ListChangeEvent<T>? = null
disposer.add(p.observable.observeList { disposer.add(p.observable.observeList {
assertNull(change) assertNull(changeEvent)
change = it changeEvent = it
}) })
// Insert once. // Insert once.
val v1 = p.createElement() val v1 = p.createElement()
p.observable.add(v1) p.observable.add(v1)
assertEquals(1, p.observable.size.value) run {
assertEquals(v1, p.observable[0]) assertEquals(1, p.observable.size.value)
val c1 = change assertEquals(v1, p.observable[0])
assertTrue(c1 is ListChangeEvent.Change<T>)
assertEquals(0, c1.index) val e = changeEvent
assertTrue(c1.removed.isEmpty()) assertNotNull(e)
assertEquals(1, c1.inserted.size) assertEquals(1, e.changes.size)
assertEquals(v1, c1.inserted[0])
val c0 = e.changes[0]
assertTrue(c0 is ListChange.Structural)
assertEquals(0, c0.index)
assertTrue(c0.removed.isEmpty())
assertEquals(1, c0.inserted.size)
assertEquals(v1, c0.inserted[0])
}
// Insert a second time. // Insert a second time.
change = null changeEvent = null
val v2 = p.createElement() val v2 = p.createElement()
p.observable.add(v2) p.observable.add(v2)
assertEquals(2, p.observable.size.value) run {
assertEquals(v1, p.observable[0]) assertEquals(2, p.observable.size.value)
assertEquals(v2, p.observable[1]) assertEquals(v1, p.observable[0])
val c2 = change assertEquals(v2, p.observable[1])
assertTrue(c2 is ListChangeEvent.Change<T>)
assertEquals(1, c2.index) val e = changeEvent
assertTrue(c2.removed.isEmpty()) assertNotNull(e)
assertEquals(1, c2.inserted.size) assertEquals(1, e.changes.size)
assertEquals(v2, c2.inserted[0])
val c0 = e.changes[0]
assertTrue(c0 is ListChange.Structural)
assertEquals(1, c0.index)
assertTrue(c0.removed.isEmpty())
assertEquals(1, c0.inserted.size)
assertEquals(v2, c0.inserted[0])
}
// Insert at index. // Insert at index.
change = null changeEvent = null
val v3 = p.createElement() val v3 = p.createElement()
p.observable.add(1, v3) p.observable.add(1, v3)
assertEquals(3, p.observable.size.value) run {
assertEquals(v1, p.observable[0]) assertEquals(3, p.observable.size.value)
assertEquals(v3, p.observable[1]) assertEquals(v1, p.observable[0])
assertEquals(v2, p.observable[2]) assertEquals(v3, p.observable[1])
val c3 = change assertEquals(v2, p.observable[2])
assertTrue(c3 is ListChangeEvent.Change<T>)
assertEquals(1, c3.index) val e = changeEvent
assertTrue(c3.removed.isEmpty()) assertNotNull(e)
assertEquals(1, c3.inserted.size) assertEquals(1, e.changes.size)
assertEquals(v3, c3.inserted[0])
val c0 = e.changes[0]
assertTrue(c0 is ListChange.Structural)
assertEquals(1, c0.index)
assertTrue(c0.removed.isEmpty())
assertEquals(1, c0.inserted.size)
assertEquals(v3, c0.inserted[0])
}
} }
interface Provider<T : Any> : ListCellTests.Provider, MutableCellTests.Provider<List<T>> { interface Provider<T : Any> : ListCellTests.Provider, MutableCellTests.Provider<List<T>> {

View File

@ -1,13 +1,16 @@
package world.phantasmal.observable.cell.list package world.phantasmal.observable.cell.list
import kotlin.test.Test import world.phantasmal.observable.cell.SimpleCell
import kotlin.test.assertEquals import world.phantasmal.testUtils.TestContext
import kotlin.test.*
class SimpleListCellTests : MutableListCellTests<Int> { class SimpleListCellTests : MutableListCellTests<Int> {
override fun createProvider() = object : MutableListCellTests.Provider<Int> { override fun createProvider() = createListProvider(empty = true)
override fun createListProvider(empty: Boolean) = object : MutableListCellTests.Provider<Int> {
private var nextElement = 0 private var nextElement = 0
override val observable = SimpleListCell(mutableListOf<Int>()) override val observable = SimpleListCell(if (empty) mutableListOf() else mutableListOf(-13))
override fun addElement() { override fun addElement() {
observable.add(createElement()) observable.add(createElement())
@ -28,4 +31,147 @@ class SimpleListCellTests : MutableListCellTests<Int> {
assertEquals(2, list[1]) assertEquals(2, list[1])
assertEquals(3, list[2]) assertEquals(3, list[2])
} }
@Test
fun add_with_index() = test {
val list = SimpleListCell(mutableListOf<String>())
list.add(0, "b")
list.add(1, "c")
list.add(0, "a")
assertEquals(3, list.size.value)
assertEquals("a", list[0])
assertEquals("b", list[1])
assertEquals("c", list[2])
}
@Test
fun element_changes_propagate_correctly_after_set() = test {
testElementChangePropagation {
val old = it[1]
it[1] = SimpleCell("new")
listOf(old)
}
}
@Test
fun element_changes_propagate_correctly_after_add() = test {
testElementChangePropagation {
it.add(SimpleCell("new"))
emptyList()
}
}
@Test
fun element_changes_propagate_correctly_after_add_with_index() = test {
testElementChangePropagation {
it.add(1, SimpleCell("new"))
emptyList()
}
}
@Test
fun element_changes_propagate_correctly_after_remove() = test {
testElementChangePropagation {
val removed = it[1]
it.remove(removed)
listOf(removed)
}
}
@Test
fun element_changes_propagate_correctly_after_removeAt() = test {
testElementChangePropagation {
listOf(it.removeAt(2))
}
}
@Test
fun element_changes_propagate_correctly_after_replaceAll() = test {
testElementChangePropagation {
val removed = it.value.toList()
it.replaceAll(listOf(SimpleCell("new a"), SimpleCell("new b")))
removed
}
}
@Test
fun element_changes_propagate_correctly_after_replaceAll_with_sequence() = test {
testElementChangePropagation {
val removed = it.value.toList()
it.replaceAll(sequenceOf(SimpleCell("new a"), SimpleCell("new b")))
removed
}
}
@Test
fun element_changes_propagate_correctly_after_splice() = test {
testElementChangePropagation {
val removed = it.value.toList().drop(1)
it.splice(1, 2, SimpleCell("new"))
removed
}
}
@Test
fun element_changes_propagate_correctly_after_clear() = test {
testElementChangePropagation {
val removed = it.value.toList()
it.clear()
removed
}
}
/**
* Creates a list with 3 SimpleCells as elements, calls [updateList] with this list and then
* checks that changes to old elements don't affect the list and changes to new elements do
* affect the list.
*
* @param updateList Function that changes the list and returns the removed elements if any.
*/
private fun TestContext.testElementChangePropagation(
updateList: (SimpleListCell<SimpleCell<String>>) -> List<SimpleCell<String>>
) {
val list = SimpleListCell(
mutableListOf(
SimpleCell("a"),
SimpleCell("b"),
SimpleCell("c")
)
) { arrayOf(it) }
var event: ListChangeEvent<SimpleCell<String>>? = null
disposer.add(list.observeList {
assertNull(event)
event = it
})
val removed = updateList(list)
event = null
// The list should not emit events when an old element is changed.
for (element in removed) {
element.value += "-1"
assertNull(event)
}
// The list should emit events when any of the current elements are changed.
for ((index, element) in list.value.withIndex()) {
event = null
element.value += "-2"
val e = event
assertNotNull(e)
assertEquals(1, e.changes.size)
val c = e.changes.first()
assertTrue(c is ListChange.Element)
assertEquals(index, c.index)
assertEquals(element, c.updated)
}
}
} }

View File

@ -5,7 +5,7 @@ import world.phantasmal.observable.cell.list.mutableListCell
import world.phantasmal.web.core.actions.Action import world.phantasmal.web.core.actions.Action
class UndoManager { class UndoManager {
private val undos = mutableListCell<Undo>(NopUndo) { arrayOf(it.atSavePoint) } private val undos = mutableListCell<Undo>(NopUndo)
private val _current = mutableCell<Undo>(NopUndo) private val _current = mutableCell<Undo>(NopUndo)
val current: Cell<Undo> = _current val current: Cell<Undo> = _current
@ -19,7 +19,9 @@ class UndoManager {
* True if all undos are at the most recent save point. I.e., true if there are no changes to * True if all undos are at the most recent save point. I.e., true if there are no changes to
* save. * save.
*/ */
val allAtSavePoint: Cell<Boolean> = undos.all { it.atSavePoint.value } // TODO: Optimize this once ListCell supports more performant method for this use-case.
val allAtSavePoint: Cell<Boolean> =
undos.fold(trueCell()) { acc, undo -> acc and undo.atSavePoint }.flatten()
fun addUndo(undo: Undo) { fun addUndo(undo: Undo) {
undos.add(undo) undos.add(undo)

View File

@ -100,7 +100,7 @@ class HuntOptimizerStore(
val wantedItems = wantedItemPersister.loadWantedItems(server) val wantedItems = wantedItemPersister.loadWantedItems(server)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_wantedItems.value = wantedItems _wantedItems.replaceAll(wantedItems)
} }
} }
} }

View File

@ -45,21 +45,26 @@ class QuestEditor(
// Stores // Stores
val areaStore = addDisposable(AreaStore(areaAssetLoader)) val areaStore = addDisposable(AreaStore(areaAssetLoader))
val questEditorStore = addDisposable(QuestEditorStore( val questEditorStore = addDisposable(
questLoader, QuestEditorStore(
uiStore, questLoader,
areaStore, uiStore,
undoManager, areaStore,
)) undoManager,
initializeNewQuest = true,
)
)
val asmStore = addDisposable(AsmStore(questEditorStore, undoManager)) val asmStore = addDisposable(AsmStore(questEditorStore, undoManager))
// Controllers // Controllers
val questEditorController = addDisposable(QuestEditorController(questEditorUiPersister)) val questEditorController = addDisposable(QuestEditorController(questEditorUiPersister))
val toolbarController = addDisposable(QuestEditorToolbarController( val toolbarController = addDisposable(
uiStore, QuestEditorToolbarController(
areaStore, uiStore,
questEditorStore, areaStore,
)) questEditorStore,
)
)
val questInfoController = addDisposable(QuestInfoController(questEditorStore)) val questInfoController = addDisposable(QuestInfoController(questEditorStore))
val npcCountsController = addDisposable(NpcCountsController(questEditorStore)) val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
val entityInfoController = addDisposable(EntityInfoController(areaStore, questEditorStore)) val entityInfoController = addDisposable(EntityInfoController(areaStore, questEditorStore))
@ -70,12 +75,14 @@ class QuestEditor(
val eventsController = addDisposable(EventsController(questEditorStore)) val eventsController = addDisposable(EventsController(questEditorStore))
// Rendering // Rendering
val renderer = addDisposable(QuestRenderer( val renderer = addDisposable(
areaAssetLoader, QuestRenderer(
entityAssetLoader, areaAssetLoader,
questEditorStore, entityAssetLoader,
createThreeRenderer, questEditorStore,
)) createThreeRenderer,
)
)
val entityImageRenderer = val entityImageRenderer =
addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer)) addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer))

View File

@ -53,8 +53,6 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
addSectionsToCollisionGeometry(collisionObj3d, renderObj3d) addSectionsToCollisionGeometry(collisionObj3d, renderObj3d)
// cullRenderGeometry(collisionObj3d, renderObj3d)
Geom(sections, renderObj3d, collisionObj3d) Geom(sections, renderObj3d, collisionObj3d)
}, },
{ geom -> { geom ->

View File

@ -7,6 +7,7 @@ import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.DisposableSupervisedScope import world.phantasmal.core.disposable.DisposableSupervisedScope
import world.phantasmal.lib.Episode import world.phantasmal.lib.Episode
import world.phantasmal.observable.cell.list.ListCell import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.ListChange
import world.phantasmal.observable.cell.list.ListChangeEvent import world.phantasmal.observable.cell.list.ListChangeEvent
import world.phantasmal.web.questEditor.loading.AreaAssetLoader import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader
@ -68,17 +69,21 @@ abstract class QuestMeshManager protected constructor(
} }
} }
private fun npcsChanged(change: ListChangeEvent<QuestNpcModel>) { private fun npcsChanged(event: ListChangeEvent<QuestNpcModel>) {
if (change is ListChangeEvent.Change) { for (change in event.changes) {
change.removed.forEach(npcMeshManager::remove) if (change is ListChange.Structural) {
change.inserted.forEach(npcMeshManager::add) change.removed.forEach(npcMeshManager::remove)
change.inserted.forEach(npcMeshManager::add)
}
} }
} }
private fun objectsChanged(change: ListChangeEvent<QuestObjectModel>) { private fun objectsChanged(event: ListChangeEvent<QuestObjectModel>) {
if (change is ListChangeEvent.Change) { for (change in event.changes) {
change.removed.forEach(objectMeshManager::remove) if (change is ListChange.Structural) {
change.inserted.forEach(objectMeshManager::add) change.removed.forEach(objectMeshManager::remove)
change.inserted.forEach(objectMeshManager::add)
}
} }
} }
} }

View File

@ -22,6 +22,7 @@ class QuestEditorStore(
uiStore: UiStore, uiStore: UiStore,
private val areaStore: AreaStore, private val areaStore: AreaStore,
private val undoManager: UndoManager, private val undoManager: UndoManager,
initializeNewQuest: Boolean,
) : Store() { ) : Store() {
private val _devMode = mutableCell(false) private val _devMode = mutableCell(false)
private val _currentQuest = mutableCell<QuestModel?>(null) private val _currentQuest = mutableCell<QuestModel?>(null)
@ -102,7 +103,9 @@ class QuestEditorStore(
} }
} }
scope.launch { setCurrentQuest(getDefaultQuest(Episode.I)) } if (initializeNewQuest) {
scope.launch { setCurrentQuest(getDefaultQuest(Episode.I)) }
}
} }
override fun dispose() { override fun dispose() {

View File

@ -0,0 +1,77 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.web.questEditor.models.QuestEventActionModel
import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class EventsControllerTests : WebTestSuite {
@Test
fun addEvent() = testAsync {
// Setup.
val store = components.questEditorStore
val quest = createQuestModel(mapDesignations = mapOf(1 to 0))
store.setCurrentQuest(quest)
store.setCurrentArea(quest.areaVariants.value.first().area)
store.makeMainUndoCurrent()
val ctrl = disposer.add(EventsController(store))
// Add an event.
ctrl.addEvent()
assertEquals(1, quest.events.value.size)
// Undo.
assertTrue(store.canUndo.value)
assertFalse(store.canRedo.value)
store.undo()
assertTrue(quest.events.value.isEmpty())
// Redo.
assertFalse(store.canUndo.value)
assertTrue(store.canRedo.value)
store.redo()
assertEquals(1, quest.events.value.size)
}
@Test
fun addAction() = testAsync {
// Setup.
val store = components.questEditorStore
val quest = createQuestModel(mapDesignations = mapOf(1 to 0))
store.setCurrentQuest(quest)
store.setCurrentArea(quest.areaVariants.value.first().area)
store.makeMainUndoCurrent()
val ctrl = disposer.add(EventsController(store))
// Add an event and an action.
ctrl.addEvent()
val event = ctrl.events.value.first()
ctrl.addAction(event, QuestEventActionModel.Door.Unlock.SHORT_NAME)
// Undo.
assertTrue(store.canUndo.value)
assertFalse(store.canRedo.value)
store.undo()
assertTrue(event.actions.value.isEmpty())
// Redo.
assertTrue(store.canUndo.value) // Can still undo event creation at this point.
assertTrue(store.canRedo.value)
store.redo()
assertEquals(1, event.actions.value.size)
}
}

View File

@ -63,7 +63,7 @@ class TestComponents(private val ctx: TestContext) {
var areaStore: AreaStore by default { AreaStore(areaAssetLoader) } var areaStore: AreaStore by default { AreaStore(areaAssetLoader) }
var questEditorStore: QuestEditorStore by default { var questEditorStore: QuestEditorStore by default {
QuestEditorStore(questLoader, uiStore, areaStore, undoManager) QuestEditorStore(questLoader, uiStore, areaStore, undoManager, initializeNewQuest = false)
} }
// Rendering // Rendering

View File

@ -4,18 +4,21 @@ import world.phantasmal.lib.Episode
import world.phantasmal.lib.asm.BytecodeIr import world.phantasmal.lib.asm.BytecodeIr
import world.phantasmal.lib.fileFormats.quest.NpcType import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.lib.fileFormats.quest.QuestNpc import world.phantasmal.lib.fileFormats.quest.QuestNpc
import world.phantasmal.web.questEditor.models.QuestEventModel
import world.phantasmal.web.questEditor.models.QuestModel import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.QuestNpcModel import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel import world.phantasmal.web.questEditor.models.QuestObjectModel
fun createQuestModel( fun WebTestContext.createQuestModel(
id: Int = 1, id: Int = 1,
name: String = "Test", name: String = "Test",
shortDescription: String = name, shortDescription: String = name,
longDescription: String = name, longDescription: String = name,
episode: Episode = Episode.I, episode: Episode = Episode.I,
mapDesignations: Map<Int, Int> = emptyMap(),
npcs: List<QuestNpcModel> = emptyList(), npcs: List<QuestNpcModel> = emptyList(),
objects: List<QuestObjectModel> = emptyList(), objects: List<QuestObjectModel> = emptyList(),
events: List<QuestEventModel> = emptyList(),
bytecodeIr: BytecodeIr = BytecodeIr(emptyList()), bytecodeIr: BytecodeIr = BytecodeIr(emptyList()),
): QuestModel = ): QuestModel =
QuestModel( QuestModel(
@ -25,14 +28,15 @@ fun createQuestModel(
shortDescription, shortDescription,
longDescription, longDescription,
episode, episode,
emptyMap(), mapDesignations,
npcs.toMutableList(), npcs.toMutableList(),
objects.toMutableList(), objects.toMutableList(),
events = mutableListOf(), events.toMutableList(),
datUnknowns = emptyList(), datUnknowns = emptyList(),
bytecodeIr, bytecodeIr,
UIntArray(0), UIntArray(0),
) { _, _, _ -> null } components.areaStore::getVariant,
)
fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel = fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel =
QuestNpcModel( QuestNpcModel(

View File

@ -12,6 +12,7 @@ import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.disposable import world.phantasmal.core.disposable.disposable
import world.phantasmal.observable.cell.Cell import world.phantasmal.observable.cell.Cell
import world.phantasmal.observable.cell.list.ListCell import world.phantasmal.observable.cell.list.ListCell
import world.phantasmal.observable.cell.list.ListChange
import world.phantasmal.observable.cell.list.ListChangeEvent import world.phantasmal.observable.cell.list.ListChangeEvent
fun <E : Event> EventTarget.disposableListener( fun <E : Event> EventTarget.disposableListener(
@ -255,26 +256,28 @@ private fun <T> bindChildrenTo(
childrenRemoved: (index: Int, count: Int) -> Unit, childrenRemoved: (index: Int, count: Int) -> Unit,
after: (ListChangeEvent<T>) -> Unit, after: (ListChangeEvent<T>) -> Unit,
): Disposable = ): Disposable =
list.observeList(callNow = true) { change: ListChangeEvent<T> -> list.observeList(callNow = true) { event: ListChangeEvent<T> ->
if (change is ListChangeEvent.Change) { for (change in event.changes) {
repeat(change.removed.size) { if (change is ListChange.Structural) {
parent.removeChild(parent.childNodes[change.index].unsafeCast<Node>()) repeat(change.removed.size) {
} parent.removeChild(parent.childNodes[change.index].unsafeCast<Node>())
}
childrenRemoved(change.index, change.removed.size) childrenRemoved(change.index, change.removed.size)
val frag = document.createDocumentFragment() val frag = document.createDocumentFragment()
change.inserted.forEachIndexed { i, value -> change.inserted.forEachIndexed { i, value ->
frag.appendChild(frag.createChild(value, change.index + i)) frag.appendChild(frag.createChild(value, change.index + i))
} }
if (change.index >= parent.childNodes.length) { if (change.index >= parent.childNodes.length) {
parent.appendChild(frag) parent.appendChild(frag)
} else { } else {
parent.insertBefore(frag, parent.childNodes[change.index]) parent.insertBefore(frag, parent.childNodes[change.index])
}
} }
} }
after(change) after(event)
} }

View File

@ -1,10 +1,9 @@
package world.phantasmal.webui.dom package world.phantasmal.webui.dom
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.unsafe.unsafeAssertNotNull import world.phantasmal.core.unsafe.unsafeAssertNotNull
import world.phantasmal.observable.Observer import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Dependent
import world.phantasmal.observable.cell.AbstractCell import world.phantasmal.observable.cell.AbstractCell
data class Size(val width: Double, val height: Double) data class Size(val width: Double, val height: Double)
@ -12,14 +11,9 @@ data class Size(val width: Double, val height: Double)
class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() { class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
private var resizeObserver: dynamic = null private var resizeObserver: dynamic = null
/**
* Set to true right before actual observers are added.
*/
private var hasObservers = false
private var _value: Size? = null private var _value: Size? = null
var element: HTMLElement? = null var element: HTMLElement? = element
set(element) { set(element) {
if (resizeObserver != null) { if (resizeObserver != null) {
if (field != null) { if (field != null) {
@ -34,24 +28,17 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
field = element field = element
} }
init {
// Ensure we call the setter with element.
this.element = element
}
override val value: Size override val value: Size
get() { get() {
if (!hasObservers) { if (dependents.isEmpty()) {
_value = getSize() _value = getSize()
} }
return _value.unsafeAssertNotNull() return unsafeAssertNotNull(_value)
} }
override fun observe(callNow: Boolean, observer: Observer<Size>): Disposable { override fun addDependent(dependent: Dependent) {
if (!hasObservers) { if (dependents.isEmpty()) {
hasObservers = true
if (resizeObserver == null) { if (resizeObserver == null) {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val resize = ::resizeCallback val resize = ::resizeCallback
@ -65,15 +52,14 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
_value = getSize() _value = getSize()
} }
val superDisposable = super.observe(callNow, observer) super.addDependent(dependent)
}
return disposable { override fun removeDependent(dependent: Dependent) {
superDisposable.dispose() super.removeDependent(dependent)
if (observers.isEmpty()) { if (dependents.isEmpty()) {
hasObservers = false resizeObserver.disconnect()
resizeObserver.disconnect()
}
} }
} }
@ -83,12 +69,16 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
?: Size(0.0, 0.0) ?: Size(0.0, 0.0)
private fun resizeCallback(entries: Array<dynamic>) { private fun resizeCallback(entries: Array<dynamic>) {
entries.forEach { entry -> val entry = entries.first()
_value = Size( val newValue = Size(
entry.contentRect.width.unsafeCast<Double>(), entry.contentRect.width.unsafeCast<Double>(),
entry.contentRect.height.unsafeCast<Double>() entry.contentRect.height.unsafeCast<Double>(),
) )
emit()
if (newValue != _value) {
emitMightChange()
_value = newValue
emitChanged(ChangeEvent(newValue))
} }
} }
} }