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
|
||||
|
||||
/**
|
||||
* Asserts that receiver is of type T. No runtime check happens in KJS. Should only be used when
|
||||
* absolutely certain that receiver is indeed a T.
|
||||
* Asserts that [value] is of type T. No runtime check happens in KJS. Should only be used when it's
|
||||
* 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
|
||||
* certain that T is indeed not null.
|
||||
* Asserts that [value] is not null. No runtime check happens in KJS. Should only be used when it's
|
||||
* absolutely certain that [value] is indeed not null.
|
||||
*/
|
||||
@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
|
||||
|
||||
import kotlin.js.unsafeCast as kotlinUnsafeCast
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
|
||||
interface Observable<out T> {
|
||||
interface Observable<out T> : Dependency {
|
||||
fun observe(observer: Observer<T>): Disposable
|
||||
}
|
||||
|
@ -4,4 +4,4 @@ open class ChangeEvent<out T>(val value: T) {
|
||||
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
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
|
||||
class SimpleEmitter<T> : Emitter<T> {
|
||||
private val observers = mutableListOf<Observer<T>>()
|
||||
|
||||
override fun observe(observer: Observer<T>): Disposable {
|
||||
observers.add(observer)
|
||||
|
||||
return disposable {
|
||||
observers.remove(observer)
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleEmitter<T> : AbstractDependency(), Emitter<T> {
|
||||
override fun emit(event: ChangeEvent<T>) {
|
||||
observers.forEach { it(event) }
|
||||
for (dependent in dependents) {
|
||||
dependent.dependencyMightChange()
|
||||
}
|
||||
|
||||
for (dependent in dependents) {
|
||||
dependent.dependencyChanged(this, event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe(observer: Observer<T>): Disposable =
|
||||
CallbackObserver(this, observer)
|
||||
}
|
||||
|
@ -1,30 +1,42 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
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.Observer
|
||||
|
||||
abstract class AbstractCell<T> : Cell<T> {
|
||||
protected val observers: MutableList<Observer<T>> = mutableListOf()
|
||||
abstract class AbstractCell<T> : AbstractDependency(), Cell<T> {
|
||||
private var mightChangeEmitted = false
|
||||
|
||||
final override fun observe(observer: Observer<T>): Disposable =
|
||||
observe(callNow = false, observer)
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
observers.add(observer)
|
||||
val observingCell = CallbackObserver(this, observer)
|
||||
|
||||
if (callNow) {
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return disposable {
|
||||
observers.remove(observer)
|
||||
return observingCell
|
||||
}
|
||||
|
||||
protected fun emitMightChange() {
|
||||
if (!mightChangeEmitted) {
|
||||
mightChangeEmitted = true
|
||||
|
||||
for (dependent in dependents) {
|
||||
dependent.dependencyMightChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun emit() {
|
||||
val event = ChangeEvent(value)
|
||||
observers.forEach { it(event) }
|
||||
protected fun emitChanged(event: ChangeEvent<T>?) {
|
||||
mightChangeEmitted = false
|
||||
|
||||
for (dependent in dependents) {
|
||||
dependent.dependencyChanged(this, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +1,33 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
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.ChangeEvent
|
||||
import world.phantasmal.observable.Dependency
|
||||
import world.phantasmal.observable.Dependent
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this cell is registered. Stops
|
||||
* observing its dependencies when the last observer ov this cell is disposed. This way no extra
|
||||
* 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>()
|
||||
abstract class AbstractDependentCell<T> : AbstractCell<T>(), Dependent {
|
||||
private var changingDependencies = 0
|
||||
private var dependenciesActuallyChanged = false
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
protected var hasObservers = false
|
||||
|
||||
protected var _value: T? = null
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
_value = computeValue()
|
||||
override fun dependencyMightChange() {
|
||||
changingDependencies++
|
||||
emitMightChange()
|
||||
}
|
||||
|
||||
return _value.unsafeCast()
|
||||
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
|
||||
if (event != null) {
|
||||
dependenciesActuallyChanged = true
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (dependencyObservers.isEmpty()) {
|
||||
hasObservers = true
|
||||
if (--changingDependencies == 0) {
|
||||
if (dependenciesActuallyChanged) {
|
||||
dependenciesActuallyChanged = false
|
||||
|
||||
dependencies.forEach { dependency ->
|
||||
dependencyObservers.add(
|
||||
dependency.observe {
|
||||
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()
|
||||
dependenciesChanged()
|
||||
} else {
|
||||
emitChanged(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
||||
|
@ -59,3 +59,6 @@ fun Cell<String>.isBlank(): Cell<Boolean> =
|
||||
|
||||
fun Cell<String>.isNotBlank(): Cell<Boolean> =
|
||||
map { it.isNotBlank() }
|
||||
|
||||
fun <T> Cell<Cell<T>>.flatten(): Cell<T> =
|
||||
FlatteningDependentCell(this) { this.value }
|
||||
|
@ -1,5 +1,7 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
|
||||
class DelegatingCell<T>(
|
||||
private val getter: () -> T,
|
||||
private val setter: (T) -> Unit,
|
||||
@ -10,8 +12,11 @@ class DelegatingCell<T>(
|
||||
val oldValue = getter()
|
||||
|
||||
if (value != oldValue) {
|
||||
emitMightChange()
|
||||
|
||||
setter(value)
|
||||
emit()
|
||||
|
||||
emitChanged(ChangeEvent(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,58 @@
|
||||
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.
|
||||
*/
|
||||
class DependentCell<T>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private val compute: () -> T,
|
||||
) : AbstractDependentCell<T>(*dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
private vararg val dependencies: Dependency,
|
||||
private val compute: () -> T
|
||||
) : AbstractDependentCell<T>() {
|
||||
|
||||
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
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
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.
|
||||
*/
|
||||
class FlatteningDependentCell<T>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private val compute: () -> Cell<T>,
|
||||
) : AbstractDependentCell<T>(*dependencies) {
|
||||
private var computedCell: Cell<T>? = null
|
||||
private var computedCellObserver: Disposable? = null
|
||||
private vararg val dependencies: Dependency,
|
||||
private val compute: () -> Cell<T>
|
||||
) : AbstractDependentCell<T>() {
|
||||
|
||||
private var computedCell: Cell<T>? = null
|
||||
private var computedInDeps = false
|
||||
private var shouldRecompute = false
|
||||
|
||||
private var _value: T? = null
|
||||
override val value: T
|
||||
get() {
|
||||
return if (hasObservers) {
|
||||
computedCell.unsafeAssertNotNull().value
|
||||
} else {
|
||||
super.value
|
||||
if (dependents.isEmpty()) {
|
||||
_value = compute().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 {
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
super.addDependent(dependent)
|
||||
}
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
override fun removeDependent(dependent: Dependent) {
|
||||
super.removeDependent(dependent)
|
||||
|
||||
if (!hasObservers) {
|
||||
computedCellObserver?.dispose()
|
||||
computedCellObserver = null
|
||||
if (dependents.isEmpty()) {
|
||||
computedCell?.removeDependent(this)
|
||||
computedCell = null
|
||||
computedInDeps = false
|
||||
|
||||
for (dependency in dependencies) {
|
||||
dependency.removeDependent(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun computeValue(): T {
|
||||
val computedCell = compute()
|
||||
this.computedCell = computedCell
|
||||
|
||||
computedCellObserver?.dispose()
|
||||
|
||||
if (hasObservers) {
|
||||
computedCellObserver = computedCell.observe { (value) ->
|
||||
_value = value
|
||||
emit()
|
||||
}
|
||||
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
|
||||
if ((dependency !== computedCell || computedInDeps) && event != null) {
|
||||
shouldRecompute = true
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
|
||||
class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> {
|
||||
override var value: T = value
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
emitMightChange()
|
||||
|
||||
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.nopDisposable
|
||||
import world.phantasmal.observable.AbstractDependency
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
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 {
|
||||
if (callNow) {
|
||||
observer(ChangeEvent(value))
|
||||
|
@ -1,131 +1,66 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
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.cell.AbstractCell
|
||||
import world.phantasmal.observable.cell.AbstractDependentCell
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.DependentCell
|
||||
import world.phantasmal.observable.cell.not
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this cell is registered. Stops
|
||||
* observing its dependencies when the last observer on this cell is disposed. This way no extra
|
||||
* disposables need to be managed when e.g. [map] is used.
|
||||
*/
|
||||
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>()
|
||||
abstract class AbstractDependentListCell<E> :
|
||||
AbstractDependentCell<List<E>>(),
|
||||
ListCell<E>,
|
||||
Dependent {
|
||||
|
||||
protected abstract val elements: List<E>
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
protected var hasObservers = false
|
||||
|
||||
override val value: List<E>
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
if (dependents.isEmpty()) {
|
||||
computeElements()
|
||||
}
|
||||
|
||||
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 {
|
||||
initDependencyObservers()
|
||||
final override val empty: Cell<Boolean> = size.map { it == 0 }
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
final override val notEmpty: Cell<Boolean> = !empty
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
}
|
||||
}
|
||||
final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
|
||||
observeList(callNow, observer as ListObserver<E>)
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
initDependencyObservers()
|
||||
val observingCell = CallbackObserver(this, observer)
|
||||
|
||||
val superDisposable = super.observeList(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
if (callNow) {
|
||||
observer(
|
||||
ListChangeEvent(
|
||||
value,
|
||||
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)))
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
protected abstract fun computeElements()
|
||||
}
|
||||
|
@ -1,140 +1,38 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.CallbackObserver
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.DependentCell
|
||||
import world.phantasmal.observable.cell.not
|
||||
|
||||
abstract class AbstractListCell<E>(
|
||||
private val extractObservables: ObservablesExtractor<E>?,
|
||||
) : AbstractCell<List<E>>(), ListCell<E> {
|
||||
/**
|
||||
* 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>()
|
||||
abstract class AbstractListCell<E> : AbstractCell<List<E>>(), ListCell<E> {
|
||||
@Suppress("LeakingThis")
|
||||
final override val size: Cell<Int> = DependentCell(this) { value.size }
|
||||
|
||||
/**
|
||||
* External list observers which are observing this list.
|
||||
*/
|
||||
protected val listObservers = mutableListOf<ListObserver<E>>()
|
||||
final override val empty: Cell<Boolean> = size.map { it == 0 }
|
||||
|
||||
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 }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
final override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable =
|
||||
observeList(callNow, observer as ListObserver<E>)
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
if (elementObservers.isEmpty() && extractObservables != null) {
|
||||
replaceElementObservers(0, elementObservers.size, value)
|
||||
}
|
||||
|
||||
listObservers.add(observer)
|
||||
val observingCell = CallbackObserver(this, observer)
|
||||
|
||||
if (callNow) {
|
||||
observer(ListChangeEvent.Change(0, emptyList(), value))
|
||||
}
|
||||
|
||||
return disposable {
|
||||
listObservers.remove(observer)
|
||||
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)
|
||||
observer(
|
||||
ListChangeEvent(
|
||||
value,
|
||||
listOf(
|
||||
ListChange.Structural(index = 0, removed = emptyList(), inserted = value),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val shift = insertedElements.size - amountRemoved
|
||||
|
||||
while (index < elementObservers.size) {
|
||||
elementObservers[index++].index += shift
|
||||
}
|
||||
}
|
||||
|
||||
private fun disposeElementObserversIfNecessary() {
|
||||
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))
|
||||
}
|
||||
}
|
||||
return observingCell
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,42 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.Dependent
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
/**
|
||||
* ListCell of which the value depends on 0 or more other cells.
|
||||
*/
|
||||
class DependentListCell<E>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private vararg val dependencies: Cell<*>,
|
||||
private val computeElements: () -> List<E>,
|
||||
) : AbstractDependentListCell<E>(*dependencies) {
|
||||
private var _elements: List<E>? = null
|
||||
) : AbstractDependentListCell<E>() {
|
||||
|
||||
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() {
|
||||
_elements = computeElements.invoke()
|
||||
elements = computeElements.invoke()
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,16 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Dependency
|
||||
import world.phantasmal.observable.Dependent
|
||||
|
||||
// TODO: This class shares 95% of its code with AbstractDependentListCell.
|
||||
class FilteredListCell<E>(
|
||||
private val dependency: ListCell<E>,
|
||||
private val predicate: (E) -> Boolean,
|
||||
) : AbstractListCell<E>(extractObservables = null) {
|
||||
private val _size = SizeCell()
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
private var hasObservers = false
|
||||
|
||||
private var dependencyObserver: Disposable? = null
|
||||
|
||||
) : AbstractListCell<E>(), Dependent {
|
||||
/**
|
||||
* 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>()
|
||||
|
||||
@ -30,66 +18,52 @@ class FilteredListCell<E>(
|
||||
|
||||
override val value: List<E>
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
if (dependents.isEmpty()) {
|
||||
recompute()
|
||||
}
|
||||
|
||||
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 {
|
||||
initDependencyObservers()
|
||||
super.addDependent(dependent)
|
||||
}
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
override fun removeDependent(dependent: Dependent) {
|
||||
super.removeDependent(dependent)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
if (dependents.isEmpty()) {
|
||||
dependency.removeDependent(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
||||
val superDisposable = super.observeList(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
}
|
||||
override fun dependencyMightChange() {
|
||||
emitMightChange()
|
||||
}
|
||||
|
||||
private fun recompute() {
|
||||
elements = ListWrapper(mutableListOf())
|
||||
indexMap.clear()
|
||||
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
|
||||
if (event is ListChangeEvent<*>) {
|
||||
val filteredChanges = mutableListOf<ListChange<E>>()
|
||||
|
||||
dependency.value.forEach { element ->
|
||||
if (predicate(element)) {
|
||||
elements.mutate { add(element) }
|
||||
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 -> {
|
||||
for (change in event.changes) {
|
||||
when (change) {
|
||||
is ListChange.Structural -> {
|
||||
// Figure out which elements should be removed from this list, then simply
|
||||
// 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>()
|
||||
var eventIndex = -1
|
||||
|
||||
event.removed.forEachIndexed { i, element ->
|
||||
val index = indexMap[event.index + i]
|
||||
change.removed.forEachIndexed { i, element ->
|
||||
val index = indexMap[change.index + i]
|
||||
|
||||
if (index != -1) {
|
||||
removed.add(element)
|
||||
@ -104,8 +78,8 @@ class FilteredListCell<E>(
|
||||
|
||||
val inserted = mutableListOf<E>()
|
||||
|
||||
event.inserted.forEachIndexed { i, element ->
|
||||
val index = indexMap[event.index + i]
|
||||
change.inserted.forEachIndexed { i, element ->
|
||||
val index = indexMap[change.index + i]
|
||||
|
||||
if (index != -1) {
|
||||
inserted.add(element)
|
||||
@ -118,23 +92,31 @@ class FilteredListCell<E>(
|
||||
|
||||
if (removed.isNotEmpty() || inserted.isNotEmpty()) {
|
||||
check(eventIndex != -1)
|
||||
finalizeUpdate(ListChangeEvent.Change(eventIndex, removed, inserted))
|
||||
filteredChanges.add(
|
||||
ListChange.Structural(
|
||||
eventIndex,
|
||||
removed,
|
||||
inserted
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ListChangeEvent.ElementChange -> {
|
||||
// Emit a Change or ElementChange event based on whether the updated element
|
||||
is ListChange.Element -> {
|
||||
// Emit a structural or element change based on whether the updated element
|
||||
// passes the predicate test and whether it was already in the elements list
|
||||
// (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 the element now passed the test and previously didn't pass,
|
||||
// insert it and emit a Change event.
|
||||
var insertIndex = elements.size
|
||||
|
||||
for (depIdx in (event.index + 1)..indexMap.lastIndex) {
|
||||
for (depIdx in (change.index + 1)..indexMap.lastIndex) {
|
||||
val thisIdx = indexMap[depIdx]
|
||||
|
||||
if (thisIdx != -1) {
|
||||
@ -143,10 +125,10 @@ class FilteredListCell<E>(
|
||||
}
|
||||
}
|
||||
|
||||
elements = elements.mutate { add(insertIndex, event.updated) }
|
||||
indexMap[event.index] = insertIndex
|
||||
elements = elements.mutate { add(insertIndex, change.updated) }
|
||||
indexMap[change.index] = insertIndex
|
||||
|
||||
for (depIdx in (event.index + 1)..indexMap.lastIndex) {
|
||||
for (depIdx in (change.index + 1)..indexMap.lastIndex) {
|
||||
val thisIdx = indexMap[depIdx]
|
||||
|
||||
if (thisIdx != -1) {
|
||||
@ -154,25 +136,25 @@ class FilteredListCell<E>(
|
||||
}
|
||||
}
|
||||
|
||||
finalizeUpdate(ListChangeEvent.Change(
|
||||
filteredChanges.add(
|
||||
ListChange.Structural(
|
||||
insertIndex,
|
||||
emptyList(),
|
||||
listOf(event.updated),
|
||||
))
|
||||
} else {
|
||||
// Otherwise just propagate the ElementChange event.
|
||||
finalizeUpdate(
|
||||
ListChangeEvent.ElementChange(index, event.updated)
|
||||
removed = emptyList(),
|
||||
inserted = listOf(change.updated),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Otherwise just propagate the element change.
|
||||
filteredChanges.add(ListChange.Element(index, change.updated))
|
||||
}
|
||||
} else {
|
||||
if (index != -1) {
|
||||
// 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) }
|
||||
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]
|
||||
|
||||
if (thisIdx != -1) {
|
||||
@ -180,67 +162,45 @@ class FilteredListCell<E>(
|
||||
}
|
||||
}
|
||||
|
||||
finalizeUpdate(ListChangeEvent.Change(
|
||||
filteredChanges.add(
|
||||
ListChange.Structural(
|
||||
index,
|
||||
listOf(event.updated),
|
||||
emptyList(),
|
||||
))
|
||||
} else {
|
||||
// Otherwise just propagate the ElementChange event.
|
||||
finalizeUpdate(
|
||||
ListChangeEvent.ElementChange(index, event.updated)
|
||||
removed = listOf(change.updated),
|
||||
inserted = emptyList(),
|
||||
)
|
||||
)
|
||||
} 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))
|
||||
}
|
||||
} else {
|
||||
emitChanged(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disposeDependencyObservers() {
|
||||
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
|
||||
hasObservers = false
|
||||
dependencyObserver?.dispose()
|
||||
dependencyObserver = null
|
||||
private fun recompute() {
|
||||
val newElements = mutableListOf<E>()
|
||||
indexMap.clear()
|
||||
|
||||
dependency.value.forEach { element ->
|
||||
if (predicate(element)) {
|
||||
newElements.add(element)
|
||||
indexMap.add(newElements.lastIndex)
|
||||
} else {
|
||||
indexMap.add(-1)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
elements = ListWrapper(newElements)
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,75 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
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].
|
||||
*/
|
||||
class FlatteningDependentListCell<E>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private vararg val dependencies: Dependency,
|
||||
private val computeElements: () -> ListCell<E>,
|
||||
) : AbstractDependentListCell<E>(*dependencies) {
|
||||
private var computedCell: ListCell<E>? = null
|
||||
private var computedCellObserver: Disposable? = null
|
||||
) : AbstractDependentListCell<E>() {
|
||||
|
||||
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() {
|
||||
computedCell = computeElements.invoke()
|
||||
if (shouldRecompute || dependents.isEmpty()) {
|
||||
computedCell?.removeDependent(this)
|
||||
|
||||
computedCellObserver?.dispose()
|
||||
|
||||
computedCellObserver =
|
||||
if (hasObservers) {
|
||||
computedCell.unsafeAssertNotNull().observeList(observer = ::finalizeUpdate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
computedCell = computeElements.invoke().also { computedCell ->
|
||||
computedCell.addDependent(this)
|
||||
computedInDeps = dependencies.any { it === computedCell }
|
||||
}
|
||||
|
||||
override fun lastObserverRemoved() {
|
||||
super.lastObserverRemoved()
|
||||
shouldRecompute = false
|
||||
}
|
||||
|
||||
computedCellObserver?.dispose()
|
||||
computedCellObserver = null
|
||||
elements = unsafeAssertNotNull(computedCell).value.toList()
|
||||
}
|
||||
}
|
||||
|
@ -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.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.DependentCell
|
||||
|
||||
interface ListCell<out E> : Cell<List<E>> {
|
||||
/**
|
||||
@ -16,23 +17,24 @@ interface ListCell<out E> : Cell<List<E>> {
|
||||
|
||||
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 <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> =
|
||||
fold(true) { acc, el -> acc && predicate(el) }
|
||||
DependentCell(this) { value.all(predicate) }
|
||||
|
||||
fun sumBy(selector: (E) -> Int): Cell<Int> =
|
||||
fold(0) { acc, el -> acc + selector(el) }
|
||||
fun sumOf(selector: (E) -> Int): Cell<Int> =
|
||||
DependentCell(this) { value.sumOf(selector) }
|
||||
|
||||
fun filtered(predicate: (E) -> Boolean): ListCell<E> =
|
||||
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
|
||||
}
|
||||
|
@ -10,8 +10,9 @@ fun <E> emptyListCell(): ListCell<E> = EMPTY_LIST_CELL
|
||||
|
||||
fun <E> mutableListCell(
|
||||
vararg elements: E,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
): MutableListCell<E> = SimpleListCell(mutableListOf(*elements), extractObservables)
|
||||
extractDependencies: DependenciesExtractor<E>? = null,
|
||||
): MutableListCell<E> =
|
||||
SimpleListCell(mutableListOf(*elements), extractDependencies)
|
||||
|
||||
fun <T1, T2, R> flatMapToList(
|
||||
c1: Cell<T1>,
|
||||
|
@ -1,37 +1,42 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
sealed class ListChangeEvent<out E> {
|
||||
abstract val index: Int
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
|
||||
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>(
|
||||
override val index: Int,
|
||||
class Structural<out E>(
|
||||
val index: Int,
|
||||
/**
|
||||
* 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
|
||||
* mutated when the originating [ListCell] is mutated.
|
||||
* Do not keep long-lived references to a [ChangeEvent]'s [removed] list, it may or may not
|
||||
* be mutated when the originating [ListCell] is mutated.
|
||||
*/
|
||||
val removed: List<E>,
|
||||
/**
|
||||
* 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
|
||||
* mutated when the originating [ListCell] is mutated.
|
||||
* Do not keep long-lived references to a [ChangeEvent]'s [inserted] list, it may or may not
|
||||
* be mutated when the originating [ListCell] is mutated.
|
||||
*/
|
||||
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
|
||||
* to do so.
|
||||
* Represents a change to an element in a list cell. Will only be emitted if the list is
|
||||
* configured to do so.
|
||||
*/
|
||||
class ElementChange<E>(
|
||||
override val index: Int,
|
||||
class Element<E>(
|
||||
val index: Int,
|
||||
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 splice(from: Int, removeCount: Int, newElement: E)
|
||||
fun splice(fromIndex: Int, removeCount: Int, newElement: E)
|
||||
|
||||
fun clear()
|
||||
|
||||
|
@ -1,23 +1,33 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
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 extractObservables Extractor function called on each element in this list, changes to the
|
||||
* returned observables will be propagated via ElementChange events
|
||||
* @param elements The backing list for this [ListCell].
|
||||
* @param extractDependencies Extractor function called on each element in this list, changes to
|
||||
* the returned dependencies will be propagated via [ListChange.Element]s in a [ListChangeEvent]
|
||||
* event.
|
||||
*/
|
||||
class SimpleListCell<E>(
|
||||
elements: MutableList<E>,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
) : AbstractListCell<E>(extractObservables), MutableListCell<E> {
|
||||
private val extractDependencies: DependenciesExtractor<E>? = null,
|
||||
) : AbstractListCell<E>(), MutableListCell<E> {
|
||||
|
||||
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>
|
||||
get() = elements
|
||||
@ -25,27 +35,47 @@ class SimpleListCell<E>(
|
||||
replaceAll(value)
|
||||
}
|
||||
|
||||
override val size: Cell<Int> = _size
|
||||
|
||||
override operator fun get(index: Int): E =
|
||||
elements[index]
|
||||
|
||||
override operator fun set(index: Int, element: E): E {
|
||||
checkIndex(index, elements.lastIndex)
|
||||
emitMightChange()
|
||||
|
||||
val removed: E
|
||||
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
|
||||
}
|
||||
|
||||
override fun add(element: E) {
|
||||
emitMightChange()
|
||||
|
||||
val index = elements.size
|
||||
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) {
|
||||
checkIndex(index, elements.size)
|
||||
emitMightChange()
|
||||
|
||||
elements = elements.mutate { add(index, element) }
|
||||
finalizeUpdate(ListChangeEvent.Change(index, emptyList(), listOf(element)))
|
||||
|
||||
finalizeStructuralChange(index, emptyList(), listOf(element))
|
||||
}
|
||||
|
||||
override fun remove(element: E): Boolean {
|
||||
@ -60,46 +90,180 @@ class SimpleListCell<E>(
|
||||
}
|
||||
|
||||
override fun removeAt(index: Int): E {
|
||||
checkIndex(index, elements.lastIndex)
|
||||
emitMightChange()
|
||||
|
||||
val removed: E
|
||||
elements = elements.mutate { removed = removeAt(index) }
|
||||
finalizeUpdate(ListChangeEvent.Change(index, listOf(removed), emptyList()))
|
||||
|
||||
finalizeStructuralChange(index, listOf(removed), emptyList())
|
||||
return removed
|
||||
}
|
||||
|
||||
override fun replaceAll(elements: Iterable<E>) {
|
||||
emitMightChange()
|
||||
|
||||
val removed = this.elements
|
||||
this.elements = ListWrapper(elements.toMutableList())
|
||||
finalizeUpdate(ListChangeEvent.Change(0, removed, this.elements))
|
||||
|
||||
finalizeStructuralChange(0, removed, this.elements)
|
||||
}
|
||||
|
||||
override fun replaceAll(elements: Sequence<E>) {
|
||||
emitMightChange()
|
||||
|
||||
val removed = this.elements
|
||||
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) {
|
||||
val removed = ArrayList(elements.subList(from, from + removeCount))
|
||||
override fun splice(fromIndex: Int, removeCount: Int, newElement: E) {
|
||||
val removed = ArrayList(elements.subList(fromIndex, fromIndex + removeCount))
|
||||
|
||||
emitMightChange()
|
||||
|
||||
elements = elements.mutate {
|
||||
repeat(removeCount) { removeAt(from) }
|
||||
add(from, newElement)
|
||||
repeat(removeCount) { removeAt(fromIndex) }
|
||||
add(fromIndex, newElement)
|
||||
}
|
||||
finalizeUpdate(ListChangeEvent.Change(from, removed, listOf(newElement)))
|
||||
|
||||
finalizeStructuralChange(fromIndex, removed, listOf(newElement))
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
emitMightChange()
|
||||
|
||||
val removed = elements
|
||||
elements = ListWrapper(mutableListOf())
|
||||
finalizeUpdate(ListChangeEvent.Change(0, removed, emptyList()))
|
||||
|
||||
finalizeStructuralChange(0, removed, emptyList())
|
||||
}
|
||||
|
||||
override fun sortWith(comparator: Comparator<E>) {
|
||||
emitMightChange()
|
||||
|
||||
var throwable: Throwable? = null
|
||||
|
||||
try {
|
||||
elements = elements.mutate { sortWith(comparator) }
|
||||
finalizeUpdate(ListChangeEvent.Change(0, elements, elements))
|
||||
} catch (e: Throwable) {
|
||||
throwable = e
|
||||
}
|
||||
|
||||
override fun finalizeUpdate(event: ListChangeEvent<E>) {
|
||||
_size.value = elements.size
|
||||
super.finalizeUpdate(event)
|
||||
finalizeStructuralChange(0, elements, elements)
|
||||
|
||||
if (throwable != null) {
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
override fun addDependent(dependent: Dependent) {
|
||||
if (dependents.isEmpty() && extractDependencies != null) {
|
||||
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.nopDisposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.AbstractDependency
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.cell.*
|
||||
|
||||
class StaticListCell<E>(private val elements: List<E>) : ListCell<E> {
|
||||
private val firstOrNull = StaticCell(elements.firstOrNull())
|
||||
class StaticListCell<E>(private val elements: List<E>) : AbstractDependency(), ListCell<E> {
|
||||
private var firstOrNull: Cell<E?>? = null
|
||||
|
||||
override val size: Cell<Int> = cell(elements.size)
|
||||
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 {
|
||||
if (callNow) {
|
||||
observer(ListChangeEvent.Change(0, emptyList(), value))
|
||||
observer(ListChangeEvent(value, listOf(ListChange.Structural(0, emptyList(), value))))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@Test
|
||||
fun observable_calls_observers_when_events_are_emitted() = test {
|
||||
fun calls_observers_when_events_are_emitted() = test {
|
||||
val p = createProvider()
|
||||
var changes = 0
|
||||
|
||||
@ -34,7 +34,7 @@ interface ObservableTests : ObservableTestSuite {
|
||||
}
|
||||
|
||||
@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()
|
||||
var changes = 0
|
||||
|
||||
|
@ -11,6 +11,27 @@ import kotlin.test.*
|
||||
interface CellTests : ObservableTests {
|
||||
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
|
||||
fun propagates_changes_to_mapped_cell() = test {
|
||||
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> {
|
||||
override fun createProvider() = object : MutableCellTests.Provider<Int> {
|
||||
private var v = 0
|
||||
private var v = 17
|
||||
|
||||
override val observable = DelegatingCell({ v }, { v = it })
|
||||
|
||||
|
@ -1,18 +1,23 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class DependentCellTests : RegularCellTests {
|
||||
override fun createProvider() = object : CellTests.Provider {
|
||||
val dependency = SimpleCell(0)
|
||||
class DependentCellTests : RegularCellTests, CellWithDependenciesTests {
|
||||
override fun createProvider() = Provider()
|
||||
|
||||
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 fun emit() {
|
||||
dependency.value += 2
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): DependentCell<T> {
|
||||
val dependency = SimpleCell(value)
|
||||
return DependentCell(dependency) { dependency.value }
|
||||
override fun createWithDependencies(vararg dependencies: Cell<Int>) =
|
||||
DependentCell(*dependencies) { dependencies.sumOf { it.value } }
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,23 @@ package world.phantasmal.observable.cell
|
||||
/**
|
||||
* In these tests the dependency of the [FlatteningDependentCell]'s direct dependency changes.
|
||||
*/
|
||||
class FlatteningDependentCellTransitiveDependencyEmitsTests : RegularCellTests {
|
||||
override fun createProvider() = object : CellTests.Provider {
|
||||
class FlatteningDependentCellTransitiveDependencyEmitsTests :
|
||||
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.
|
||||
val transitiveDependency = SimpleCell(5)
|
||||
private val transitiveDependency = SimpleCell(5)
|
||||
|
||||
// The direct dependency of the cell under test can't change.
|
||||
val directDependency = StaticCell(transitiveDependency)
|
||||
private val directDependency = StaticCell(transitiveDependency)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentCell(directDependency) { directDependency.value }
|
||||
@ -18,10 +28,8 @@ class FlatteningDependentCellTransitiveDependencyEmitsTests : RegularCellTests {
|
||||
// Update the transitive dependency.
|
||||
transitiveDependency.value += 5
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): FlatteningDependentCell<T> {
|
||||
val dependency = StaticCell(StaticCell(value))
|
||||
return FlatteningDependentCell(dependency) { dependency.value }
|
||||
override fun createWithDependencies(vararg dependencies: Cell<Int>): Cell<Any> =
|
||||
FlatteningDependentCell(*dependencies) { StaticCell(dependencies.sumOf { it.value }) }
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,6 @@ interface RegularCellTests : CellTests {
|
||||
|
||||
// Value should not change when emit hasn't been called since the last access.
|
||||
assertEquals(new, p.observable.value)
|
||||
|
||||
old = new
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,23 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
class DependentListCellTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
private val dependency = SimpleListCell<Int>(mutableListOf())
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.CellWithDependenciesTests
|
||||
|
||||
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 fun addElement() {
|
||||
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
|
||||
|
||||
import world.phantasmal.observable.cell.SimpleCell
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.*
|
||||
|
||||
class FilteredListCellTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
private val dependency = SimpleListCell<Int>(mutableListOf())
|
||||
override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider {
|
||||
private val dependency =
|
||||
SimpleListCell(if (empty) mutableListOf(5) else mutableListOf(5, 10))
|
||||
|
||||
override val observable = FilteredListCell(dependency, predicate = { it % 2 == 0 })
|
||||
|
||||
@ -82,37 +81,49 @@ class FilteredListCellTests : ListCellTests {
|
||||
|
||||
dep.replaceAll(listOf(1, 2, 3, 4, 5))
|
||||
|
||||
(event as ListChangeEvent.Change).let { e ->
|
||||
assertEquals(0, e.index)
|
||||
assertEquals(0, e.removed.size)
|
||||
assertEquals(2, e.inserted.size)
|
||||
assertEquals(2, e.inserted[0])
|
||||
assertEquals(4, e.inserted[1])
|
||||
run {
|
||||
val e = event
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
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
|
||||
|
||||
dep.splice(2, 2, 10)
|
||||
|
||||
(event as ListChangeEvent.Change).let { e ->
|
||||
assertEquals(1, e.index)
|
||||
assertEquals(1, e.removed.size)
|
||||
assertEquals(4, e.removed[0])
|
||||
assertEquals(1, e.inserted.size)
|
||||
assertEquals(10, e.inserted[0])
|
||||
run {
|
||||
val e = event
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
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
|
||||
* [FilteredListCell] should emit either Change events or ElementChange events, depending on
|
||||
* whether the predicate result has changed.
|
||||
* When the dependency of a [FilteredListCell] emits [ListChange.Element] changes, the
|
||||
* [FilteredListCell] should emit either [ListChange.Structural] or [ListChange.Element]
|
||||
* changes, depending on whether the predicate result has changed.
|
||||
*/
|
||||
@Test
|
||||
fun emits_correct_events_when_dependency_emits_ElementChange_events() = test {
|
||||
fun emits_correct_events_when_dependency_emits_element_changes() = test {
|
||||
val dep = SimpleListCell(
|
||||
mutableListOf(SimpleCell(1), SimpleCell(2), SimpleCell(3), SimpleCell(4)),
|
||||
extractObservables = { arrayOf(it) },
|
||||
extractDependencies = { arrayOf(it) },
|
||||
)
|
||||
val list = FilteredListCell(dep, predicate = { it.value % 2 == 0 })
|
||||
var event: ListChangeEvent<SimpleCell<Int>>? = null
|
||||
@ -125,20 +136,25 @@ class FilteredListCellTests : ListCellTests {
|
||||
for (i in 0 until dep.size.value) {
|
||||
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
|
||||
dep[i].value = newValue
|
||||
|
||||
(event as ListChangeEvent.Change).let { e ->
|
||||
val e = event
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
val c = e.changes.first()
|
||||
assertTrue(c is ListChange.Structural)
|
||||
|
||||
if (newValue % 2 == 0) {
|
||||
assertEquals(0, e.removed.size)
|
||||
assertEquals(1, e.inserted.size)
|
||||
assertEquals(newValue, e.inserted[0].value)
|
||||
assertEquals(0, c.removed.size)
|
||||
assertEquals(1, c.inserted.size)
|
||||
assertEquals(newValue, c.inserted[0].value)
|
||||
} else {
|
||||
assertEquals(1, e.removed.size)
|
||||
assertEquals(0, e.inserted.size)
|
||||
assertEquals(newValue, e.removed[0].value)
|
||||
}
|
||||
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
|
||||
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.
|
||||
*/
|
||||
class FlatteningDependentListCellDirectDependencyEmitsTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
override fun createListProvider(empty: Boolean) = object : ListCellTests.Provider {
|
||||
// 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.
|
||||
private val dependency = SimpleCell<ListCell<Int>>(transitiveDependency)
|
||||
|
@ -1,14 +1,24 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.CellWithDependenciesTests
|
||||
import world.phantasmal.observable.cell.StaticCell
|
||||
|
||||
/**
|
||||
* In these tests the dependency of the [FlatteningDependentListCell]'s direct dependency changes.
|
||||
*/
|
||||
class FlatteningDependentListCellTransitiveDependencyEmitsTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
class FlatteningDependentListCellTransitiveDependencyEmitsTests :
|
||||
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.
|
||||
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.
|
||||
private val dependency = StaticCell<ListCell<Int>>(transitiveDependency)
|
||||
@ -20,5 +30,10 @@ class FlatteningDependentListCellTransitiveDependencyEmitsTests : ListCellTests
|
||||
// Update the transitive dependency.
|
||||
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.
|
||||
*/
|
||||
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
|
||||
fun calls_list_observers_when_changed() = test {
|
||||
@ -28,7 +51,7 @@ interface ListCellTests : CellTests {
|
||||
|
||||
p.addElement()
|
||||
|
||||
assertTrue(event is ListChangeEvent.Change<*>)
|
||||
assertNotNull(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +123,7 @@ interface ListCellTests : CellTests {
|
||||
fun sumBy() = test {
|
||||
val p = createProvider()
|
||||
|
||||
val sum = p.observable.sumBy { 1 }
|
||||
val sum = p.observable.sumOf { 1 }
|
||||
|
||||
var observedValue: Int? = null
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.cell.MutableCellTests
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.*
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val p = createProvider()
|
||||
|
||||
var change: ListChangeEvent<T>? = null
|
||||
var changeEvent: ListChangeEvent<T>? = null
|
||||
|
||||
disposer.add(p.observable.observeList {
|
||||
assertNull(change)
|
||||
change = it
|
||||
assertNull(changeEvent)
|
||||
changeEvent = it
|
||||
})
|
||||
|
||||
// Insert once.
|
||||
val v1 = p.createElement()
|
||||
p.observable.add(v1)
|
||||
|
||||
run {
|
||||
assertEquals(1, p.observable.size.value)
|
||||
assertEquals(v1, p.observable[0])
|
||||
val c1 = change
|
||||
assertTrue(c1 is ListChangeEvent.Change<T>)
|
||||
assertEquals(0, c1.index)
|
||||
assertTrue(c1.removed.isEmpty())
|
||||
assertEquals(1, c1.inserted.size)
|
||||
assertEquals(v1, c1.inserted[0])
|
||||
|
||||
val e = changeEvent
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
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.
|
||||
change = null
|
||||
changeEvent = null
|
||||
|
||||
val v2 = p.createElement()
|
||||
p.observable.add(v2)
|
||||
|
||||
run {
|
||||
assertEquals(2, p.observable.size.value)
|
||||
assertEquals(v1, p.observable[0])
|
||||
assertEquals(v2, p.observable[1])
|
||||
val c2 = change
|
||||
assertTrue(c2 is ListChangeEvent.Change<T>)
|
||||
assertEquals(1, c2.index)
|
||||
assertTrue(c2.removed.isEmpty())
|
||||
assertEquals(1, c2.inserted.size)
|
||||
assertEquals(v2, c2.inserted[0])
|
||||
|
||||
val e = changeEvent
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
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.
|
||||
change = null
|
||||
changeEvent = null
|
||||
|
||||
val v3 = p.createElement()
|
||||
p.observable.add(1, v3)
|
||||
|
||||
run {
|
||||
assertEquals(3, p.observable.size.value)
|
||||
assertEquals(v1, p.observable[0])
|
||||
assertEquals(v3, p.observable[1])
|
||||
assertEquals(v2, p.observable[2])
|
||||
val c3 = change
|
||||
assertTrue(c3 is ListChangeEvent.Change<T>)
|
||||
assertEquals(1, c3.index)
|
||||
assertTrue(c3.removed.isEmpty())
|
||||
assertEquals(1, c3.inserted.size)
|
||||
assertEquals(v3, c3.inserted[0])
|
||||
|
||||
val e = changeEvent
|
||||
assertNotNull(e)
|
||||
assertEquals(1, e.changes.size)
|
||||
|
||||
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>> {
|
||||
|
@ -1,13 +1,16 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import world.phantasmal.observable.cell.SimpleCell
|
||||
import world.phantasmal.testUtils.TestContext
|
||||
import kotlin.test.*
|
||||
|
||||
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
|
||||
|
||||
override val observable = SimpleListCell(mutableListOf<Int>())
|
||||
override val observable = SimpleListCell(if (empty) mutableListOf() else mutableListOf(-13))
|
||||
|
||||
override fun addElement() {
|
||||
observable.add(createElement())
|
||||
@ -28,4 +31,147 @@ class SimpleListCellTests : MutableListCellTests<Int> {
|
||||
assertEquals(2, list[1])
|
||||
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
|
||||
|
||||
class UndoManager {
|
||||
private val undos = mutableListCell<Undo>(NopUndo) { arrayOf(it.atSavePoint) }
|
||||
private val undos = mutableListCell<Undo>(NopUndo)
|
||||
private val _current = mutableCell<Undo>(NopUndo)
|
||||
|
||||
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
|
||||
* 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) {
|
||||
undos.add(undo)
|
||||
|
@ -100,7 +100,7 @@ class HuntOptimizerStore(
|
||||
val wantedItems = wantedItemPersister.loadWantedItems(server)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_wantedItems.value = wantedItems
|
||||
_wantedItems.replaceAll(wantedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,21 +45,26 @@ class QuestEditor(
|
||||
|
||||
// Stores
|
||||
val areaStore = addDisposable(AreaStore(areaAssetLoader))
|
||||
val questEditorStore = addDisposable(QuestEditorStore(
|
||||
val questEditorStore = addDisposable(
|
||||
QuestEditorStore(
|
||||
questLoader,
|
||||
uiStore,
|
||||
areaStore,
|
||||
undoManager,
|
||||
))
|
||||
initializeNewQuest = true,
|
||||
)
|
||||
)
|
||||
val asmStore = addDisposable(AsmStore(questEditorStore, undoManager))
|
||||
|
||||
// Controllers
|
||||
val questEditorController = addDisposable(QuestEditorController(questEditorUiPersister))
|
||||
val toolbarController = addDisposable(QuestEditorToolbarController(
|
||||
val toolbarController = addDisposable(
|
||||
QuestEditorToolbarController(
|
||||
uiStore,
|
||||
areaStore,
|
||||
questEditorStore,
|
||||
))
|
||||
)
|
||||
)
|
||||
val questInfoController = addDisposable(QuestInfoController(questEditorStore))
|
||||
val npcCountsController = addDisposable(NpcCountsController(questEditorStore))
|
||||
val entityInfoController = addDisposable(EntityInfoController(areaStore, questEditorStore))
|
||||
@ -70,12 +75,14 @@ class QuestEditor(
|
||||
val eventsController = addDisposable(EventsController(questEditorStore))
|
||||
|
||||
// Rendering
|
||||
val renderer = addDisposable(QuestRenderer(
|
||||
val renderer = addDisposable(
|
||||
QuestRenderer(
|
||||
areaAssetLoader,
|
||||
entityAssetLoader,
|
||||
questEditorStore,
|
||||
createThreeRenderer,
|
||||
))
|
||||
)
|
||||
)
|
||||
val entityImageRenderer =
|
||||
addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer))
|
||||
|
||||
|
@ -53,8 +53,6 @@ class AreaAssetLoader(private val assetLoader: AssetLoader) : DisposableContaine
|
||||
|
||||
addSectionsToCollisionGeometry(collisionObj3d, renderObj3d)
|
||||
|
||||
// cullRenderGeometry(collisionObj3d, renderObj3d)
|
||||
|
||||
Geom(sections, renderObj3d, collisionObj3d)
|
||||
},
|
||||
{ geom ->
|
||||
|
@ -7,6 +7,7 @@ import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.ListChange
|
||||
import world.phantasmal.observable.cell.list.ListChangeEvent
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
@ -68,17 +69,21 @@ abstract class QuestMeshManager protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun npcsChanged(change: ListChangeEvent<QuestNpcModel>) {
|
||||
if (change is ListChangeEvent.Change) {
|
||||
private fun npcsChanged(event: ListChangeEvent<QuestNpcModel>) {
|
||||
for (change in event.changes) {
|
||||
if (change is ListChange.Structural) {
|
||||
change.removed.forEach(npcMeshManager::remove)
|
||||
change.inserted.forEach(npcMeshManager::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun objectsChanged(change: ListChangeEvent<QuestObjectModel>) {
|
||||
if (change is ListChangeEvent.Change) {
|
||||
private fun objectsChanged(event: ListChangeEvent<QuestObjectModel>) {
|
||||
for (change in event.changes) {
|
||||
if (change is ListChange.Structural) {
|
||||
change.removed.forEach(objectMeshManager::remove)
|
||||
change.inserted.forEach(objectMeshManager::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ class QuestEditorStore(
|
||||
uiStore: UiStore,
|
||||
private val areaStore: AreaStore,
|
||||
private val undoManager: UndoManager,
|
||||
initializeNewQuest: Boolean,
|
||||
) : Store() {
|
||||
private val _devMode = mutableCell(false)
|
||||
private val _currentQuest = mutableCell<QuestModel?>(null)
|
||||
@ -102,8 +103,10 @@ class QuestEditorStore(
|
||||
}
|
||||
}
|
||||
|
||||
if (initializeNewQuest) {
|
||||
scope.launch { setCurrentQuest(getDefaultQuest(Episode.I)) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
runner.stop()
|
||||
|
@ -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 questEditorStore: QuestEditorStore by default {
|
||||
QuestEditorStore(questLoader, uiStore, areaStore, undoManager)
|
||||
QuestEditorStore(questLoader, uiStore, areaStore, undoManager, initializeNewQuest = false)
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
@ -4,18 +4,21 @@ import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.asm.BytecodeIr
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
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.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
||||
|
||||
fun createQuestModel(
|
||||
fun WebTestContext.createQuestModel(
|
||||
id: Int = 1,
|
||||
name: String = "Test",
|
||||
shortDescription: String = name,
|
||||
longDescription: String = name,
|
||||
episode: Episode = Episode.I,
|
||||
mapDesignations: Map<Int, Int> = emptyMap(),
|
||||
npcs: List<QuestNpcModel> = emptyList(),
|
||||
objects: List<QuestObjectModel> = emptyList(),
|
||||
events: List<QuestEventModel> = emptyList(),
|
||||
bytecodeIr: BytecodeIr = BytecodeIr(emptyList()),
|
||||
): QuestModel =
|
||||
QuestModel(
|
||||
@ -25,14 +28,15 @@ fun createQuestModel(
|
||||
shortDescription,
|
||||
longDescription,
|
||||
episode,
|
||||
emptyMap(),
|
||||
mapDesignations,
|
||||
npcs.toMutableList(),
|
||||
objects.toMutableList(),
|
||||
events = mutableListOf(),
|
||||
events.toMutableList(),
|
||||
datUnknowns = emptyList(),
|
||||
bytecodeIr,
|
||||
UIntArray(0),
|
||||
) { _, _, _ -> null }
|
||||
components.areaStore::getVariant,
|
||||
)
|
||||
|
||||
fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel =
|
||||
QuestNpcModel(
|
||||
|
@ -12,6 +12,7 @@ import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.ListChange
|
||||
import world.phantasmal.observable.cell.list.ListChangeEvent
|
||||
|
||||
fun <E : Event> EventTarget.disposableListener(
|
||||
@ -255,8 +256,9 @@ private fun <T> bindChildrenTo(
|
||||
childrenRemoved: (index: Int, count: Int) -> Unit,
|
||||
after: (ListChangeEvent<T>) -> Unit,
|
||||
): Disposable =
|
||||
list.observeList(callNow = true) { change: ListChangeEvent<T> ->
|
||||
if (change is ListChangeEvent.Change) {
|
||||
list.observeList(callNow = true) { event: ListChangeEvent<T> ->
|
||||
for (change in event.changes) {
|
||||
if (change is ListChange.Structural) {
|
||||
repeat(change.removed.size) {
|
||||
parent.removeChild(parent.childNodes[change.index].unsafeCast<Node>())
|
||||
}
|
||||
@ -275,6 +277,7 @@ private fun <T> bindChildrenTo(
|
||||
parent.insertBefore(frag, parent.childNodes[change.index])
|
||||
}
|
||||
}
|
||||
|
||||
after(change)
|
||||
}
|
||||
|
||||
after(event)
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
package world.phantasmal.webui.dom
|
||||
|
||||
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.observable.Observer
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Dependent
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
|
||||
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>() {
|
||||
private var resizeObserver: dynamic = null
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
*/
|
||||
private var hasObservers = false
|
||||
|
||||
private var _value: Size? = null
|
||||
|
||||
var element: HTMLElement? = null
|
||||
var element: HTMLElement? = element
|
||||
set(element) {
|
||||
if (resizeObserver != null) {
|
||||
if (field != null) {
|
||||
@ -34,24 +28,17 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
|
||||
field = element
|
||||
}
|
||||
|
||||
init {
|
||||
// Ensure we call the setter with element.
|
||||
this.element = element
|
||||
}
|
||||
|
||||
override val value: Size
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
if (dependents.isEmpty()) {
|
||||
_value = getSize()
|
||||
}
|
||||
|
||||
return _value.unsafeAssertNotNull()
|
||||
return unsafeAssertNotNull(_value)
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<Size>): Disposable {
|
||||
if (!hasObservers) {
|
||||
hasObservers = true
|
||||
|
||||
override fun addDependent(dependent: Dependent) {
|
||||
if (dependents.isEmpty()) {
|
||||
if (resizeObserver == null) {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val resize = ::resizeCallback
|
||||
@ -65,15 +52,14 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
|
||||
_value = getSize()
|
||||
}
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
|
||||
if (observers.isEmpty()) {
|
||||
hasObservers = false
|
||||
resizeObserver.disconnect()
|
||||
super.addDependent(dependent)
|
||||
}
|
||||
|
||||
override fun removeDependent(dependent: Dependent) {
|
||||
super.removeDependent(dependent)
|
||||
|
||||
if (dependents.isEmpty()) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,12 +69,16 @@ class HTMLElementSizeCell(element: HTMLElement? = null) : AbstractCell<Size>() {
|
||||
?: Size(0.0, 0.0)
|
||||
|
||||
private fun resizeCallback(entries: Array<dynamic>) {
|
||||
entries.forEach { entry ->
|
||||
_value = Size(
|
||||
val entry = entries.first()
|
||||
val newValue = Size(
|
||||
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