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

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

View File

@ -1,14 +1,14 @@
package world.phantasmal.core.unsafe
/**
* 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)

View File

@ -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>()

View File

@ -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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
package world.phantasmal.observable
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.core.unsafe.unsafeCast
class CallbackObserver<T, E : ChangeEvent<T>>(
private val dependency: Dependency,
private val callback: (E) -> Unit,
) : TrackedDisposable(), Dependent {
init {
dependency.addDependent(this)
}
override fun dispose() {
dependency.removeDependent(this)
super.dispose()
}
override fun dependencyMightChange() {
// Do nothing.
}
override fun dependencyChanged(dependency: Dependency, event: ChangeEvent<*>?) {
if (event != null) {
callback(unsafeCast(event))
}
}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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>>()
class SimpleEmitter<T> : AbstractDependency(), Emitter<T> {
override fun emit(event: ChangeEvent<T>) {
for (dependent in dependents) {
dependent.dependencyMightChange()
}
override fun observe(observer: Observer<T>): Disposable {
observers.add(observer)
return disposable {
observers.remove(observer)
for (dependent in dependents) {
dependent.dependencyChanged(this, event)
}
}
override fun emit(event: ChangeEvent<T>) {
observers.forEach { it(event) }
}
override fun observe(observer: Observer<T>): Disposable =
CallbackObserver(this, observer)
}

View File

@ -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)
}
}
}

View File

@ -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
override fun dependencyMightChange() {
changingDependencies++
emitMightChange()
}
protected var _value: T? = null
override val value: T
get() {
if (!hasObservers) {
_value = computeValue()
}
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()
}

View File

@ -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

View File

@ -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 }

View File

@ -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))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
computedCell = 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)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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))

View File

@ -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()
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)))
)
}
protected abstract fun computeElements()
protected open fun lastObserverRemoved() {
dependencyObservers.forEach { it.dispose() }
dependencyObservers.clear()
}
private fun initDependencyObservers() {
if (dependencyObservers.isEmpty()) {
hasObservers = true
dependencies.forEach { dependency ->
dependencyObservers.add(
dependency.observe {
val removed = elements
computeElements()
finalizeUpdate(ListChangeEvent.Change(0, removed, elements))
}
)
}
computeElements()
}
}
private fun disposeDependencyObservers() {
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
hasObservers = false
lastObserverRemoved()
}
}
override fun finalizeUpdate(event: ListChangeEvent<E>) {
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
_size.publicEmit()
}
super.finalizeUpdate(event)
}
private inner class SizeCell : AbstractCell<Int>() {
override val value: Int
get() {
if (!hasObservers) {
computeElements()
}
return elements.size
}
val publicObservers = super.observers
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable {
initDependencyObservers()
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
}
}
fun publicEmit() {
super.emit()
}
}
}

View File

@ -1,140 +1,38 @@
package world.phantasmal.observable.cell.list
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
}
}

View File

@ -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()
}
}

View File

@ -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(
insertIndex,
emptyList(),
listOf(event.updated),
))
} else {
// Otherwise just propagate the ElementChange event.
finalizeUpdate(
ListChangeEvent.ElementChange(index, event.updated)
filteredChanges.add(
ListChange.Structural(
insertIndex,
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(
index,
listOf(event.updated),
emptyList(),
))
} else {
// Otherwise just propagate the ElementChange event.
finalizeUpdate(
ListChangeEvent.ElementChange(index, event.updated)
filteredChanges.add(
ListChange.Structural(
index,
removed = listOf(change.updated),
inserted = emptyList(),
)
)
} else {
// Otherwise just propagate the element change.
filteredChanges.add(ListChange.Element(index, change.updated))
}
}
}
}
}
recompute()
}
}
private fun disposeDependencyObservers() {
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
hasObservers = false
dependencyObserver?.dispose()
dependencyObserver = null
}
}
override fun finalizeUpdate(event: ListChangeEvent<E>) {
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
_size.publicEmit()
}
super.finalizeUpdate(event)
}
private inner class SizeCell : AbstractCell<Int>() {
override val value: Int
get() {
if (!hasObservers) {
recompute()
}
return elements.size
if (filteredChanges.isEmpty()) {
emitChanged(null)
} else {
emitChanged(ListChangeEvent(elements, filteredChanges))
}
} else {
emitChanged(null)
}
}
val publicObservers = super.observers
private fun recompute() {
val newElements = mutableListOf<E>()
indexMap.clear()
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable {
initDependencyObservers()
val superDisposable = super.observe(callNow, observer)
return disposable {
superDisposable.dispose()
disposeDependencyObservers()
dependency.value.forEach { element ->
if (predicate(element)) {
newElements.add(element)
indexMap.add(newElements.lastIndex)
} else {
indexMap.add(-1)
}
}
fun publicEmit() {
super.emit()
}
elements = ListWrapper(newElements)
}
}

View File

@ -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()
}
}

View File

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

View File

@ -2,6 +2,7 @@ package world.phantasmal.observable.cell.list
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.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
}

View File

@ -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>,

View File

@ -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

View File

@ -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()

View File

@ -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>) {
elements = elements.mutate { sortWith(comparator) }
finalizeUpdate(ListChangeEvent.Change(0, elements, elements))
emitMightChange()
var throwable: Throwable? = null
try {
elements = elements.mutate { sortWith(comparator) }
} catch (e: Throwable) {
throwable = e
}
finalizeStructuralChange(0, elements, elements)
if (throwable != null) {
throw throwable
}
}
override fun finalizeUpdate(event: ListChangeEvent<E>) {
_size.value = elements.size
super.finalizeUpdate(event)
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()
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()

View File

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

View File

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

View File

@ -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 } }
}
}

View File

@ -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 }) }
}
}

View File

@ -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
}
}

View File

@ -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 } }
}
}

View File

@ -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 ->
if (newValue % 2 == 0) {
assertEquals(0, e.removed.size)
assertEquals(1, e.inserted.size)
assertEquals(newValue, e.inserted[0].value)
} else {
assertEquals(1, e.removed.size)
assertEquals(0, e.inserted.size)
assertEquals(newValue, e.removed[0].value)
}
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, c.removed.size)
assertEquals(1, c.inserted.size)
assertEquals(newValue, c.inserted[0].value)
} else {
assertEquals(1, c.removed.size)
assertEquals(0, c.inserted.size)
assertEquals(newValue, c.removed[0].value)
}
}
@ -150,7 +166,14 @@ class FilteredListCellTests : ListCellTests {
val newValue = dep[i].value + 2
dep[i].value = newValue
assertEquals(newValue, (event as ListChangeEvent.ElementChange).updated.value)
val e = event
assertNotNull(e)
assertEquals(1, e.changes.size)
val c = e.changes.first()
assertTrue(c is ListChange.Element)
assertEquals(newValue, c.updated.value)
}
}
}

View File

@ -6,9 +6,9 @@ import world.phantasmal.observable.cell.SimpleCell
* In these tests the direct dependency of the [FlatteningDependentListCell] changes.
*/
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)

View File

@ -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 })
}
}
}

View File

@ -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

View File

@ -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)
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])
run {
assertEquals(1, p.observable.size.value)
assertEquals(v1, p.observable[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)
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])
run {
assertEquals(2, p.observable.size.value)
assertEquals(v1, p.observable[0])
assertEquals(v2, p.observable[1])
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)
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])
run {
assertEquals(3, p.observable.size.value)
assertEquals(v1, p.observable[0])
assertEquals(v3, p.observable[1])
assertEquals(v2, p.observable[2])
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>> {

View File

@ -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)
}
}
}

View File

@ -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)

View File

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

View File

@ -45,21 +45,26 @@ class QuestEditor(
// Stores
val areaStore = addDisposable(AreaStore(areaAssetLoader))
val questEditorStore = addDisposable(QuestEditorStore(
questLoader,
uiStore,
areaStore,
undoManager,
))
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(
uiStore,
areaStore,
questEditorStore,
))
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(
areaAssetLoader,
entityAssetLoader,
questEditorStore,
createThreeRenderer,
))
val renderer = addDisposable(
QuestRenderer(
areaAssetLoader,
entityAssetLoader,
questEditorStore,
createThreeRenderer,
)
)
val entityImageRenderer =
addDisposable(EntityImageRenderer(entityAssetLoader, createThreeRenderer))

View File

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

View File

@ -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) {
change.removed.forEach(npcMeshManager::remove)
change.inserted.forEach(npcMeshManager::add)
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) {
change.removed.forEach(objectMeshManager::remove)
change.inserted.forEach(objectMeshManager::add)
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)
}
}
}
}

View File

@ -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,7 +103,9 @@ class QuestEditorStore(
}
}
scope.launch { setCurrentQuest(getDefaultQuest(Episode.I)) }
if (initializeNewQuest) {
scope.launch { setCurrentQuest(getDefaultQuest(Episode.I)) }
}
}
override fun dispose() {

View File

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

View File

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

View File

@ -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(

View File

@ -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,26 +256,28 @@ 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) {
repeat(change.removed.size) {
parent.removeChild(parent.childNodes[change.index].unsafeCast<Node>())
}
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>())
}
childrenRemoved(change.index, change.removed.size)
childrenRemoved(change.index, change.removed.size)
val frag = document.createDocumentFragment()
val frag = document.createDocumentFragment()
change.inserted.forEachIndexed { i, value ->
frag.appendChild(frag.createChild(value, change.index + i))
}
change.inserted.forEachIndexed { i, value ->
frag.appendChild(frag.createChild(value, change.index + i))
}
if (change.index >= parent.childNodes.length) {
parent.appendChild(frag)
} else {
parent.insertBefore(frag, parent.childNodes[change.index])
if (change.index >= parent.childNodes.length) {
parent.appendChild(frag)
} else {
parent.insertBefore(frag, parent.childNodes[change.index])
}
}
}
after(change)
after(event)
}

View File

@ -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)
super.addDependent(dependent)
}
return disposable {
superDisposable.dispose()
override fun removeDependent(dependent: Dependent) {
super.removeDependent(dependent)
if (observers.isEmpty()) {
hasObservers = false
resizeObserver.disconnect()
}
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(
entry.contentRect.width.unsafeCast<Double>(),
entry.contentRect.height.unsafeCast<Double>()
)
emit()
val entry = entries.first()
val newValue = Size(
entry.contentRect.width.unsafeCast<Double>(),
entry.contentRect.height.unsafeCast<Double>(),
)
if (newValue != _value) {
emitMightChange()
_value = newValue
emitChanged(ChangeEvent(newValue))
}
}
}