mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Observables will now always see a consistent view of their dependencies when they change.
This commit is contained in:
parent
dceb80afec
commit
327dfe79bb
@ -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)
|
||||||
|
@ -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>()
|
||||||
|
@ -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
|
||||||
|
@ -138,7 +138,7 @@ class Instruction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paramToArgs.unsafeAssertNotNull()[paramIndex]
|
return unsafeAssertNotNull(paramToArgs)[paramIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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<*>?)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
@ -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 })
|
||||||
|
|
||||||
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>> {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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 ->
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user