mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Renamed Val to Cell.
This commit is contained in:
parent
4c37e8f741
commit
3af3f65c43
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.core.disposable
|
||||
|
||||
private object StubDisposable : Disposable {
|
||||
private object NopDisposable : Disposable {
|
||||
override fun dispose() {
|
||||
// Do nothing.
|
||||
}
|
||||
@ -8,4 +8,7 @@ private object StubDisposable : Disposable {
|
||||
|
||||
fun disposable(dispose: () -> Unit): Disposable = SimpleDisposable(dispose)
|
||||
|
||||
fun stubDisposable(): Disposable = StubDisposable
|
||||
/**
|
||||
* Returns a disposable that does nothing when disposed.
|
||||
*/
|
||||
fun nopDisposable(): Disposable = NopDisposable
|
||||
|
@ -1,7 +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.
|
||||
*/
|
||||
expect inline fun <T> Any?.unsafeCast(): 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.
|
||||
*/
|
||||
expect inline fun <T> T?.unsafeAssertNotNull(): T
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> T?.unsafeAssertNotNull(): T = unsafeCast()
|
||||
|
@ -1,4 +1,6 @@
|
||||
package world.phantasmal.core.unsafe
|
||||
|
||||
import kotlin.js.unsafeCast as kotlinUnsafeCast
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
actual inline fun <T> T?.unsafeAssertNotNull(): T = unsafeCast<T>()
|
||||
actual inline fun <T> Any?.unsafeCast(): T = kotlinUnsafeCast<T>()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package world.phantasmal.core.unsafe
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
|
||||
actual inline fun <T> T?.unsafeAssertNotNull(): T = this as T
|
||||
actual inline fun <T> Any?.unsafeCast(): T = this as T
|
||||
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
abstract class AbstractVal<T> : Val<T> {
|
||||
abstract class AbstractCell<T> : Cell<T> {
|
||||
protected val observers: MutableList<Observer<T>> = mutableListOf()
|
||||
|
||||
final override fun observe(observer: Observer<T>): Disposable =
|
@ -1,18 +1,18 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.core.unsafe.unsafeCast
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this val is registered. Stops
|
||||
* observing its dependencies when the last observer on this val is disposed. This way no extra
|
||||
* 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 AbstractDependentVal<T>(
|
||||
private vararg val dependencies: Val<*>,
|
||||
) : AbstractVal<T>() {
|
||||
abstract class AbstractDependentCell<T>(
|
||||
private vararg val dependencies: Cell<*>,
|
||||
) : AbstractCell<T>() {
|
||||
/**
|
||||
* Is either empty or has a disposable per dependency.
|
||||
*/
|
||||
@ -31,7 +31,7 @@ abstract class AbstractDependentVal<T>(
|
||||
_value = computeValue()
|
||||
}
|
||||
|
||||
return _value.unsafeAssertNotNull()
|
||||
return _value.unsafeCast()
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
@ -0,0 +1,51 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.cell.list.DependentListCell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* An observable with the notion of a current [value].
|
||||
*/
|
||||
interface Cell<out T> : Observable<T> {
|
||||
val value: T
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
|
||||
/**
|
||||
* @param callNow Call [observer] immediately with the current [mutableCell].
|
||||
*/
|
||||
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable
|
||||
|
||||
/**
|
||||
* Map a transformation function over this cell.
|
||||
*
|
||||
* @param transform called whenever this cell changes
|
||||
*/
|
||||
fun <R> map(transform: (T) -> R): Cell<R> =
|
||||
DependentCell(this) { transform(value) }
|
||||
|
||||
fun <R> mapToList(transform: (T) -> List<R>): ListCell<R> =
|
||||
DependentListCell(this) { transform(value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a cell over this cell. The resulting cell will
|
||||
* change when this cell changes and when the cell returned by [transform] changes.
|
||||
*
|
||||
* @param transform called whenever this cell changes
|
||||
*/
|
||||
fun <R> flatMap(transform: (T) -> Cell<R>): Cell<R> =
|
||||
FlatteningDependentCell(this) { transform(value) }
|
||||
|
||||
fun <R> flatMapNull(transform: (T) -> Cell<R>?): Cell<R?> =
|
||||
FlatteningDependentCell(this) { transform(value) ?: nullCell() }
|
||||
|
||||
fun isNull(): Cell<Boolean> =
|
||||
map { it == null }
|
||||
|
||||
fun isNotNull(): Cell<Boolean> =
|
||||
map { it != null }
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
private val TRUE_CELL: Cell<Boolean> = StaticCell(true)
|
||||
private val FALSE_CELL: Cell<Boolean> = StaticCell(false)
|
||||
private val NULL_CELL: Cell<Nothing?> = StaticCell(null)
|
||||
private val ZERO_INT_CELL: Cell<Int> = StaticCell(0)
|
||||
private val EMPTY_STRING_CELL: Cell<String> = StaticCell("")
|
||||
|
||||
fun <T> cell(value: T): Cell<T> = StaticCell(value)
|
||||
|
||||
fun trueCell(): Cell<Boolean> = TRUE_CELL
|
||||
|
||||
fun falseCell(): Cell<Boolean> = FALSE_CELL
|
||||
|
||||
fun nullCell(): Cell<Nothing?> = NULL_CELL
|
||||
|
||||
fun zeroIntCell(): Cell<Int> = ZERO_INT_CELL
|
||||
|
||||
fun emptyStringCell(): Cell<String> = EMPTY_STRING_CELL
|
||||
|
||||
/**
|
||||
* Creates a [MutableCell] with initial value [value].
|
||||
*/
|
||||
fun <T> mutableCell(value: T): MutableCell<T> = SimpleCell(value)
|
||||
|
||||
/**
|
||||
* Creates a [MutableCell] which calls [getter] or [setter] when its value is being read or written
|
||||
* to, respectively.
|
||||
*/
|
||||
fun <T> mutableCell(getter: () -> T, setter: (T) -> Unit): MutableCell<T> =
|
||||
DelegatingCell(getter, setter)
|
||||
|
||||
/**
|
||||
* Map a transformation function over 2 cells.
|
||||
*
|
||||
* @param transform called whenever [c1] or [c2] changes
|
||||
*/
|
||||
fun <T1, T2, R> map(
|
||||
c1: Cell<T1>,
|
||||
c2: Cell<T2>,
|
||||
transform: (T1, T2) -> R,
|
||||
): Cell<R> =
|
||||
DependentCell(c1, c2) { transform(c1.value, c2.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function over 3 cells.
|
||||
*
|
||||
* @param transform called whenever [c1], [c2] or [c3] changes
|
||||
*/
|
||||
fun <T1, T2, T3, R> map(
|
||||
c1: Cell<T1>,
|
||||
c2: Cell<T2>,
|
||||
c3: Cell<T3>,
|
||||
transform: (T1, T2, T3) -> R,
|
||||
): Cell<R> =
|
||||
DependentCell(c1, c2, c3) { transform(c1.value, c2.value, c3.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a cell over 2 cells. The resulting cell will change
|
||||
* when either cell changes and also when the cell returned by [transform] changes.
|
||||
*
|
||||
* @param transform called whenever this cell changes
|
||||
*/
|
||||
fun <T1, T2, R> flatMap(
|
||||
c1: Cell<T1>,
|
||||
c2: Cell<T2>,
|
||||
transform: (T1, T2) -> Cell<R>,
|
||||
): Cell<R> =
|
||||
FlatteningDependentCell(c1, c2) { transform(c1.value, c2.value) }
|
||||
|
||||
fun and(vararg cells: Cell<Boolean>): Cell<Boolean> =
|
||||
DependentCell(*cells) { cells.all { it.value } }
|
@ -0,0 +1,61 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
infix fun <T> Cell<T>.eq(value: T): Cell<Boolean> =
|
||||
map { it == value }
|
||||
|
||||
infix fun <T> Cell<T>.eq(other: Cell<T>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a == b }
|
||||
|
||||
infix fun <T> Cell<T>.ne(value: T): Cell<Boolean> =
|
||||
map { it != value }
|
||||
|
||||
infix fun <T> Cell<T>.ne(other: Cell<T>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a != b }
|
||||
|
||||
fun <T> Cell<T?>.orElse(defaultValue: () -> T): Cell<T> =
|
||||
map { it ?: defaultValue() }
|
||||
|
||||
infix fun <T : Comparable<T>> Cell<T>.gt(value: T): Cell<Boolean> =
|
||||
map { it > value }
|
||||
|
||||
infix fun <T : Comparable<T>> Cell<T>.gt(other: Cell<T>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a > b }
|
||||
|
||||
infix fun <T : Comparable<T>> Cell<T>.lt(value: T): Cell<Boolean> =
|
||||
map { it < value }
|
||||
|
||||
infix fun <T : Comparable<T>> Cell<T>.lt(other: Cell<T>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a < b }
|
||||
|
||||
infix fun Cell<Boolean>.and(other: Cell<Boolean>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a && b }
|
||||
|
||||
infix fun Cell<Boolean>.and(other: Boolean): Cell<Boolean> =
|
||||
if (other) this else falseCell()
|
||||
|
||||
infix fun Cell<Boolean>.or(other: Cell<Boolean>): Cell<Boolean> =
|
||||
map(this, other) { a, b -> a || b }
|
||||
|
||||
infix fun Cell<Boolean>.xor(other: Cell<Boolean>): Cell<Boolean> =
|
||||
// Use != because of https://youtrack.jetbrains.com/issue/KT-31277.
|
||||
map(this, other) { a, b -> a != b }
|
||||
|
||||
operator fun Cell<Boolean>.not(): Cell<Boolean> = map { !it }
|
||||
|
||||
operator fun Cell<Int>.plus(value: Int): Cell<Int> =
|
||||
map { it + value }
|
||||
|
||||
operator fun Cell<Int>.minus(value: Int): Cell<Int> =
|
||||
map { it - value }
|
||||
|
||||
fun Cell<String>.isEmpty(): Cell<Boolean> =
|
||||
map { it.isEmpty() }
|
||||
|
||||
fun Cell<String>.isNotEmpty(): Cell<Boolean> =
|
||||
map { it.isNotEmpty() }
|
||||
|
||||
fun Cell<String>.isBlank(): Cell<Boolean> =
|
||||
map { it.isBlank() }
|
||||
|
||||
fun Cell<String>.isNotBlank(): Cell<Boolean> =
|
||||
map { it.isNotBlank() }
|
@ -1,9 +1,9 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class DelegatingVal<T>(
|
||||
class DelegatingCell<T>(
|
||||
private val getter: () -> T,
|
||||
private val setter: (T) -> Unit,
|
||||
) : AbstractVal<T>(), MutableVal<T> {
|
||||
) : AbstractCell<T>(), MutableCell<T> {
|
||||
override var value: T
|
||||
get() = getter()
|
||||
set(value) {
|
@ -0,0 +1,11 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
@ -6,19 +6,19 @@ import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
/**
|
||||
* Similar to [DependentVal], except that this val's [compute] returns a val.
|
||||
* Similar to [DependentCell], except that this cell's [compute] returns a cell.
|
||||
*/
|
||||
class FlatteningDependentVal<T>(
|
||||
vararg dependencies: Val<*>,
|
||||
private val compute: () -> Val<T>,
|
||||
) : AbstractDependentVal<T>(*dependencies) {
|
||||
private var computedVal: Val<T>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
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
|
||||
|
||||
override val value: T
|
||||
get() {
|
||||
return if (hasObservers) {
|
||||
computedVal.unsafeAssertNotNull().value
|
||||
computedCell.unsafeAssertNotNull().value
|
||||
} else {
|
||||
super.value
|
||||
}
|
||||
@ -31,26 +31,26 @@ class FlatteningDependentVal<T>(
|
||||
superDisposable.dispose()
|
||||
|
||||
if (!hasObservers) {
|
||||
computedValObserver?.dispose()
|
||||
computedValObserver = null
|
||||
computedVal = null
|
||||
computedCellObserver?.dispose()
|
||||
computedCellObserver = null
|
||||
computedCell = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun computeValue(): T {
|
||||
val computedVal = compute()
|
||||
this.computedVal = computedVal
|
||||
val computedCell = compute()
|
||||
this.computedCell = computedCell
|
||||
|
||||
computedValObserver?.dispose()
|
||||
computedCellObserver?.dispose()
|
||||
|
||||
if (hasObservers) {
|
||||
computedValObserver = computedVal.observe { (value) ->
|
||||
computedCellObserver = computedCell.observe { (value) ->
|
||||
_value = value
|
||||
emit()
|
||||
}
|
||||
}
|
||||
|
||||
return computedVal.value
|
||||
return computedCell.value
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface MutableVal<T> : Val<T> {
|
||||
interface MutableCell<T> : Cell<T> {
|
||||
override var value: T
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class SimpleVal<T>(value: T) : AbstractVal<T>(), MutableVal<T> {
|
||||
class SimpleCell<T>(value: T) : AbstractCell<T>(), MutableCell<T> {
|
||||
override var value: T = value
|
||||
set(value) {
|
||||
if (value != field) {
|
@ -1,18 +1,18 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.stubDisposable
|
||||
import world.phantasmal.core.disposable.nopDisposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
class StaticVal<T>(override val value: T) : Val<T> {
|
||||
class StaticCell<T>(override val value: T) : Cell<T> {
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
return nopDisposable()
|
||||
}
|
||||
|
||||
override fun observe(observer: Observer<T>): Disposable = stubDisposable()
|
||||
override fun observe(observer: Observer<T>): Disposable = nopDisposable()
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
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.value.AbstractVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
/**
|
||||
* Starts observing its dependencies when the first observer on this property is registered.
|
||||
* Stops observing its dependencies when the last observer on this property is disposed.
|
||||
* This way no extra disposables need to be managed when e.g. [map] is used.
|
||||
* 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 AbstractDependentListVal<E>(
|
||||
private vararg val dependencies: Val<*>,
|
||||
) : AbstractListVal<E>(extractObservables = null) {
|
||||
private val _sizeVal = SizeVal()
|
||||
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.
|
||||
@ -37,7 +37,7 @@ abstract class AbstractDependentListVal<E>(
|
||||
return elements
|
||||
}
|
||||
|
||||
override val size: Val<Int> = _sizeVal
|
||||
override val size: Cell<Int> = _size
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
initDependencyObservers()
|
||||
@ -50,7 +50,7 @@ abstract class AbstractDependentListVal<E>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
||||
val superDisposable = super.observeList(callNow, observer)
|
||||
@ -87,7 +87,7 @@ abstract class AbstractDependentListVal<E>(
|
||||
}
|
||||
|
||||
private fun disposeDependencyObservers() {
|
||||
if (observers.isEmpty() && listObservers.isEmpty() && _sizeVal.publicObservers.isEmpty()) {
|
||||
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
|
||||
hasObservers = false
|
||||
lastObserverRemoved()
|
||||
}
|
||||
@ -95,13 +95,13 @@ abstract class AbstractDependentListVal<E>(
|
||||
|
||||
override fun finalizeUpdate(event: ListChangeEvent<E>) {
|
||||
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
|
||||
_sizeVal.publicEmit()
|
||||
_size.publicEmit()
|
||||
}
|
||||
|
||||
super.finalizeUpdate(event)
|
||||
}
|
||||
|
||||
private inner class SizeVal : AbstractVal<Int>() {
|
||||
private inner class SizeCell : AbstractCell<Int>() {
|
||||
override val value: Int
|
||||
get() {
|
||||
if (!hasObservers) {
|
@ -1,4 +1,4 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
@ -6,14 +6,14 @@ import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.AbstractVal
|
||||
import world.phantasmal.observable.value.DependentVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.not
|
||||
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 AbstractListVal<E>(
|
||||
abstract class AbstractListCell<E>(
|
||||
private val extractObservables: ObservablesExtractor<E>?,
|
||||
) : AbstractVal<List<E>>(), ListVal<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.
|
||||
@ -23,11 +23,11 @@ abstract class AbstractListVal<E>(
|
||||
/**
|
||||
* External list observers which are observing this list.
|
||||
*/
|
||||
protected val listObservers = mutableListOf<ListValObserver<E>>()
|
||||
protected val listObservers = mutableListOf<ListObserver<E>>()
|
||||
|
||||
override val empty: Val<Boolean> by lazy { size.map { it == 0 } }
|
||||
override val empty: Cell<Boolean> by lazy { size.map { it == 0 } }
|
||||
|
||||
override val notEmpty: Val<Boolean> by lazy { !empty }
|
||||
override val notEmpty: Cell<Boolean> by lazy { !empty }
|
||||
|
||||
override fun get(index: Int): E =
|
||||
value[index]
|
||||
@ -49,7 +49,7 @@ abstract class AbstractListVal<E>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
if (elementObservers.isEmpty() && extractObservables != null) {
|
||||
replaceElementObservers(0, elementObservers.size, value)
|
||||
}
|
||||
@ -66,14 +66,14 @@ abstract class AbstractListVal<E>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Val<E?> =
|
||||
DependentVal(this) { value.firstOrNull() }
|
||||
override fun firstOrNull(): Cell<E?> =
|
||||
DependentCell(this) { value.firstOrNull() }
|
||||
|
||||
/**
|
||||
* Does the following in the given order:
|
||||
* - Updates element observers
|
||||
* - Emits ListValChangeEvent
|
||||
* - Emits ValChangeEvent
|
||||
* - Emits ListChangeEvent
|
||||
* - Emits ChangeEvent
|
||||
*/
|
||||
protected open fun finalizeUpdate(event: ListChangeEvent<E>) {
|
||||
if (
|
||||
@ -84,7 +84,7 @@ abstract class AbstractListVal<E>(
|
||||
replaceElementObservers(event.index, event.removed.size, event.inserted)
|
||||
}
|
||||
|
||||
listObservers.forEach { observer: ListValObserver<E> ->
|
||||
listObservers.forEach { observer: ListObserver<E> ->
|
||||
observer(event)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
/**
|
||||
* ListVal of which the value depends on 0 or more other vals.
|
||||
* ListCell of which the value depends on 0 or more other cells.
|
||||
*/
|
||||
class DependentListVal<E>(
|
||||
vararg dependencies: Val<*>,
|
||||
class DependentListCell<E>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private val computeElements: () -> List<E>,
|
||||
) : AbstractDependentListVal<E>(*dependencies) {
|
||||
) : AbstractDependentListCell<E>(*dependencies) {
|
||||
private var _elements: List<E>? = null
|
||||
|
||||
override val elements: List<E> get() = _elements.unsafeAssertNotNull()
|
@ -1,17 +1,17 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
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.value.AbstractVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
// TODO: This class shares 95% of its code with AbstractDependentListVal.
|
||||
class FilteredListVal<E>(
|
||||
private val dependency: ListVal<E>,
|
||||
// TODO: This class shares 95% of its code with AbstractDependentListCell.
|
||||
class FilteredListCell<E>(
|
||||
private val dependency: ListCell<E>,
|
||||
private val predicate: (E) -> Boolean,
|
||||
) : AbstractListVal<E>(extractObservables = null) {
|
||||
private val _sizeVal = SizeVal()
|
||||
) : AbstractListCell<E>(extractObservables = null) {
|
||||
private val _size = SizeCell()
|
||||
|
||||
/**
|
||||
* Set to true right before actual observers are added.
|
||||
@ -37,7 +37,7 @@ class FilteredListVal<E>(
|
||||
return elements
|
||||
}
|
||||
|
||||
override val size: Val<Int> = _sizeVal
|
||||
override val size: Cell<Int> = _size
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
initDependencyObservers()
|
||||
@ -50,7 +50,7 @@ class FilteredListVal<E>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
||||
val superDisposable = super.observeList(callNow, observer)
|
||||
@ -201,7 +201,7 @@ class FilteredListVal<E>(
|
||||
}
|
||||
|
||||
private fun disposeDependencyObservers() {
|
||||
if (observers.isEmpty() && listObservers.isEmpty() && _sizeVal.publicObservers.isEmpty()) {
|
||||
if (observers.isEmpty() && listObservers.isEmpty() && _size.publicObservers.isEmpty()) {
|
||||
hasObservers = false
|
||||
dependencyObserver?.dispose()
|
||||
dependencyObserver = null
|
||||
@ -210,13 +210,13 @@ class FilteredListVal<E>(
|
||||
|
||||
override fun finalizeUpdate(event: ListChangeEvent<E>) {
|
||||
if (event is ListChangeEvent.Change && event.removed.size != event.inserted.size) {
|
||||
_sizeVal.publicEmit()
|
||||
_size.publicEmit()
|
||||
}
|
||||
|
||||
super.finalizeUpdate(event)
|
||||
}
|
||||
|
||||
private inner class SizeVal : AbstractVal<Int>() {
|
||||
private inner class SizeCell : AbstractCell<Int>() {
|
||||
override val value: Int
|
||||
get() {
|
||||
if (!hasObservers) {
|
@ -0,0 +1,38 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
/**
|
||||
* Similar to [DependentListCell], except that this cell's [computeElements] returns a [ListCell].
|
||||
*/
|
||||
class FlatteningDependentListCell<E>(
|
||||
vararg dependencies: Cell<*>,
|
||||
private val computeElements: () -> ListCell<E>,
|
||||
) : AbstractDependentListCell<E>(*dependencies) {
|
||||
private var computedCell: ListCell<E>? = null
|
||||
private var computedCellObserver: Disposable? = null
|
||||
|
||||
override val elements: List<E> get() = computedCell.unsafeAssertNotNull().value
|
||||
|
||||
override fun computeElements() {
|
||||
computedCell = computeElements.invoke()
|
||||
|
||||
computedCellObserver?.dispose()
|
||||
|
||||
computedCellObserver =
|
||||
if (hasObservers) {
|
||||
computedCell.unsafeAssertNotNull().observeList(observer = ::finalizeUpdate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun lastObserverRemoved() {
|
||||
super.lastObserverRemoved()
|
||||
|
||||
computedCellObserver?.dispose()
|
||||
computedCellObserver = null
|
||||
}
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
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.core.unsafe.unsafeCast
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.AbstractVal
|
||||
import world.phantasmal.observable.cell.AbstractCell
|
||||
|
||||
class FoldedVal<T, R>(
|
||||
private val dependency: ListVal<T>,
|
||||
class FoldedCell<T, R>(
|
||||
private val dependency: ListCell<T>,
|
||||
private val initial: R,
|
||||
private val operation: (R, T) -> R,
|
||||
) : AbstractVal<R>() {
|
||||
) : AbstractCell<R>() {
|
||||
private var dependencyDisposable: Disposable? = null
|
||||
private var _value: R? = null
|
||||
|
||||
@ -19,7 +19,7 @@ class FoldedVal<T, R>(
|
||||
return if (dependencyDisposable == null) {
|
||||
computeValue()
|
||||
} else {
|
||||
_value.unsafeAssertNotNull()
|
||||
_value.unsafeCast()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
interface ListCell<out E> : Cell<List<E>> {
|
||||
/**
|
||||
* Do not keep long-lived references to a [ListCell]'s [value], it may or may not be mutated
|
||||
* when the [ListCell] is mutated.
|
||||
*/
|
||||
override val value: List<E>
|
||||
|
||||
val size: Cell<Int>
|
||||
|
||||
val empty: Cell<Boolean>
|
||||
|
||||
val notEmpty: Cell<Boolean>
|
||||
|
||||
operator fun get(index: Int): E
|
||||
|
||||
fun observeList(callNow: Boolean = false, observer: ListObserver<E>): Disposable
|
||||
|
||||
fun <R> fold(initialValue: R, operation: (R, E) -> R): Cell<R> =
|
||||
FoldedCell(this, initialValue, operation)
|
||||
|
||||
fun all(predicate: (E) -> Boolean): Cell<Boolean> =
|
||||
fold(true) { acc, el -> acc && predicate(el) }
|
||||
|
||||
fun sumBy(selector: (E) -> Int): Cell<Int> =
|
||||
fold(0) { acc, el -> acc + selector(el) }
|
||||
|
||||
fun filtered(predicate: (E) -> Boolean): ListCell<E> =
|
||||
FilteredListCell(this, predicate)
|
||||
|
||||
fun firstOrNull(): Cell<E?>
|
||||
|
||||
operator fun contains(element: @UnsafeVariance E): Boolean = element in value
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
|
||||
private val EMPTY_LIST_CELL = StaticListCell<Nothing>(emptyList())
|
||||
|
||||
fun <E> listCell(vararg elements: E): ListCell<E> = StaticListCell(elements.toList())
|
||||
|
||||
fun <E> emptyListCell(): ListCell<E> = EMPTY_LIST_CELL
|
||||
|
||||
fun <E> mutableListCell(
|
||||
vararg elements: E,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
): MutableListCell<E> = SimpleListCell(mutableListOf(*elements), extractObservables)
|
||||
|
||||
fun <T1, T2, R> flatMapToList(
|
||||
c1: Cell<T1>,
|
||||
c2: Cell<T2>,
|
||||
transform: (T1, T2) -> ListCell<R>,
|
||||
): ListCell<R> =
|
||||
FlatteningDependentListCell(c1, c2) { transform(c1.value, c2.value) }
|
@ -1,4 +1,4 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
sealed class ListChangeEvent<out E> {
|
||||
abstract val index: Int
|
||||
@ -12,14 +12,14 @@ sealed class ListChangeEvent<out E> {
|
||||
* 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 [ListVal] is mutated.
|
||||
* 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 [ListVal] is mutated.
|
||||
* mutated when the originating [ListCell] is mutated.
|
||||
*/
|
||||
val inserted: List<E>,
|
||||
) : ListChangeEvent<E>()
|
||||
@ -34,4 +34,4 @@ sealed class ListChangeEvent<out E> {
|
||||
) : ListChangeEvent<E>()
|
||||
}
|
||||
|
||||
typealias ListValObserver<E> = (change: ListChangeEvent<E>) -> Unit
|
||||
typealias ListObserver<E> = (change: ListChangeEvent<E>) -> Unit
|
@ -1,14 +1,14 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Wrapper is used to ensure that ListVal.value of some implementations references a new object
|
||||
* after every change to the ListVal. This is done to honor the contract that emission of a
|
||||
* ChangeEvent implies that Val.value is no longer equal to the previous value.
|
||||
* When a change is made to the ListVal, the underlying list of Wrapper is usually mutated and then
|
||||
* a new Wrapper is created that points to the same underlying list.
|
||||
* ListWrapper is used to ensure that ListCell.value of some implementations references a new object
|
||||
* after every change to the ListCell. This is done to honor the contract that emission of a
|
||||
* ChangeEvent implies that Cell.value is no longer equal to the previous value.
|
||||
* When a change is made to the ListCell, the underlying list of ListWrapper is usually mutated and
|
||||
* then a new wrapper is created that points to the same underlying list.
|
||||
*/
|
||||
internal class ListWrapper<E>(private val mut: MutableList<E>) : List<E> by mut {
|
||||
inline fun mutate(mutator: MutableList<E>.() -> Unit): ListWrapper<E> {
|
||||
@ -19,6 +19,7 @@ internal class ListWrapper<E>(private val mut: MutableList<E>) : List<E> by mut
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
// If other is also a ListWrapper but it's not the exact same object then it's not equal.
|
||||
if (other == null || this::class == other::class || other !is List<*>) return false
|
||||
// If other is a list but not a ListWrapper, call its equals method for a structured
|
||||
// comparison.
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
|
||||
interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>> {
|
||||
interface MutableListCell<E> : ListCell<E>, MutableCell<List<E>> {
|
||||
operator fun set(index: Int, element: E): E
|
||||
|
||||
fun add(element: E)
|
@ -1,23 +1,23 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
typealias ObservablesExtractor<E> = (element: E) -> Array<Observable<*>>
|
||||
|
||||
/**
|
||||
* @param elements The backing list for this ListVal
|
||||
* @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
|
||||
*/
|
||||
class SimpleListVal<E>(
|
||||
class SimpleListCell<E>(
|
||||
elements: MutableList<E>,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
) : AbstractListVal<E>(extractObservables), MutableListVal<E> {
|
||||
) : AbstractListCell<E>(extractObservables), MutableListCell<E> {
|
||||
private var elements = ListWrapper(elements)
|
||||
private val _sizeVal: MutableVal<Int> = mutableVal(elements.size)
|
||||
private val _size: MutableCell<Int> = mutableCell(elements.size)
|
||||
|
||||
override var value: List<E>
|
||||
get() = elements
|
||||
@ -25,7 +25,7 @@ class SimpleListVal<E>(
|
||||
replaceAll(value)
|
||||
}
|
||||
|
||||
override val size: Val<Int> = _sizeVal
|
||||
override val size: Cell<Int> = _size
|
||||
|
||||
override operator fun get(index: Int): E =
|
||||
elements[index]
|
||||
@ -99,7 +99,7 @@ class SimpleListVal<E>(
|
||||
}
|
||||
|
||||
override fun finalizeUpdate(event: ListChangeEvent<E>) {
|
||||
_sizeVal.value = elements.size
|
||||
_size.value = elements.size
|
||||
super.finalizeUpdate(event)
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.nopDisposable
|
||||
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())
|
||||
|
||||
override val size: Cell<Int> = cell(elements.size)
|
||||
override val empty: Cell<Boolean> = if (elements.isEmpty()) trueCell() else falseCell()
|
||||
override val notEmpty: Cell<Boolean> = if (elements.isNotEmpty()) trueCell() else falseCell()
|
||||
|
||||
override val value: List<E> = elements
|
||||
|
||||
override fun get(index: Int): E =
|
||||
elements[index]
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return nopDisposable()
|
||||
}
|
||||
|
||||
override fun observe(observer: Observer<List<E>>): Disposable = nopDisposable()
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListObserver<E>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ListChangeEvent.Change(0, emptyList(), value))
|
||||
}
|
||||
|
||||
return nopDisposable()
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Cell<E?> = firstOrNull
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
/**
|
||||
* Val of which the value depends on 0 or more other vals.
|
||||
*/
|
||||
class DependentVal<T>(
|
||||
vararg dependencies: Val<*>,
|
||||
private val compute: () -> T,
|
||||
) : AbstractDependentVal<T>(*dependencies) {
|
||||
override fun computeValue(): T = compute()
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.list.DependentListVal
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* An observable with the notion of a current [value].
|
||||
*/
|
||||
interface Val<out T> : Observable<T> {
|
||||
val value: T
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
|
||||
/**
|
||||
* @param callNow Call [observer] immediately with the current [mutableVal].
|
||||
*/
|
||||
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable
|
||||
|
||||
/**
|
||||
* Map a transformation function over this val.
|
||||
*
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
fun <R> map(transform: (T) -> R): Val<R> =
|
||||
DependentVal(this) { transform(value) }
|
||||
|
||||
fun <R> mapToListVal(transform: (T) -> List<R>): ListVal<R> =
|
||||
DependentListVal(this) { transform(value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a val over this val. The resulting val will change
|
||||
* when this val changes and when the val returned by [transform] changes.
|
||||
*
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
fun <R> flatMap(transform: (T) -> Val<R>): Val<R> =
|
||||
FlatteningDependentVal(this) { transform(value) }
|
||||
|
||||
fun <R> flatMapNull(transform: (T) -> Val<R>?): Val<R?> =
|
||||
FlatteningDependentVal(this) { transform(value) ?: nullVal() }
|
||||
|
||||
fun isNull(): Val<Boolean> =
|
||||
map { it == null }
|
||||
|
||||
fun isNotNull(): Val<Boolean> =
|
||||
map { it != null }
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
private val TRUE_VAL: Val<Boolean> = StaticVal(true)
|
||||
private val FALSE_VAL: Val<Boolean> = StaticVal(false)
|
||||
private val NULL_VAL: Val<Nothing?> = StaticVal(null)
|
||||
private val ZERO_INT_VAL: Val<Int> = StaticVal(0)
|
||||
private val EMPTY_STRING_VAL: Val<String> = StaticVal("")
|
||||
|
||||
fun <T> value(value: T): Val<T> = StaticVal(value)
|
||||
|
||||
fun trueVal(): Val<Boolean> = TRUE_VAL
|
||||
|
||||
fun falseVal(): Val<Boolean> = FALSE_VAL
|
||||
|
||||
fun nullVal(): Val<Nothing?> = NULL_VAL
|
||||
|
||||
fun zeroIntVal(): Val<Int> = ZERO_INT_VAL
|
||||
|
||||
fun emptyStringVal(): Val<String> = EMPTY_STRING_VAL
|
||||
|
||||
/**
|
||||
* Creates a [MutableVal] with initial value [value].
|
||||
*/
|
||||
fun <T> mutableVal(value: T): MutableVal<T> = SimpleVal(value)
|
||||
|
||||
/**
|
||||
* Creates a [MutableVal] which calls [getter] or [setter] when its value is being read or written
|
||||
* to, respectively.
|
||||
*/
|
||||
fun <T> mutableVal(getter: () -> T, setter: (T) -> Unit): MutableVal<T> =
|
||||
DelegatingVal(getter, setter)
|
||||
|
||||
/**
|
||||
* Map a transformation function over 2 vals.
|
||||
*
|
||||
* @param transform called whenever [v1] or [v2] changes
|
||||
*/
|
||||
fun <T1, T2, R> map(
|
||||
v1: Val<T1>,
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> R,
|
||||
): Val<R> =
|
||||
DependentVal(v1, v2) { transform(v1.value, v2.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function over 3 vals.
|
||||
*
|
||||
* @param transform called whenever [v1], [v2] or [v3] changes
|
||||
*/
|
||||
fun <T1, T2, T3, R> map(
|
||||
v1: Val<T1>,
|
||||
v2: Val<T2>,
|
||||
v3: Val<T3>,
|
||||
transform: (T1, T2, T3) -> R,
|
||||
): Val<R> =
|
||||
DependentVal(v1, v2, v3) { transform(v1.value, v2.value, v3.value) }
|
||||
|
||||
/**
|
||||
* Map a transformation function that returns a val over 2 vals. The resulting val will change when
|
||||
* either val changes and also when the val returned by [transform] changes.
|
||||
*
|
||||
* @param transform called whenever this val changes
|
||||
*/
|
||||
fun <T1, T2, R> flatMap(
|
||||
v1: Val<T1>,
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> Val<R>,
|
||||
): Val<R> =
|
||||
FlatteningDependentVal(v1, v2) { transform(v1.value, v2.value) }
|
||||
|
||||
fun and(vararg vals: Val<Boolean>): Val<Boolean> =
|
||||
DependentVal(*vals) { vals.all { it.value } }
|
@ -1,61 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
infix fun <T> Val<T>.eq(value: T): Val<Boolean> =
|
||||
map { it == value }
|
||||
|
||||
infix fun <T> Val<T>.eq(value: Val<T>): Val<Boolean> =
|
||||
map(this, value) { a, b -> a == b }
|
||||
|
||||
infix fun <T> Val<T>.ne(value: T): Val<Boolean> =
|
||||
map { it != value }
|
||||
|
||||
infix fun <T> Val<T>.ne(value: Val<T>): Val<Boolean> =
|
||||
map(this, value) { a, b -> a != b }
|
||||
|
||||
fun <T> Val<T?>.orElse(defaultValue: () -> T): Val<T> =
|
||||
map { it ?: defaultValue() }
|
||||
|
||||
infix fun <T : Comparable<T>> Val<T>.gt(value: T): Val<Boolean> =
|
||||
map { it > value }
|
||||
|
||||
infix fun <T : Comparable<T>> Val<T>.gt(value: Val<T>): Val<Boolean> =
|
||||
map(this, value) { a, b -> a > b }
|
||||
|
||||
infix fun <T : Comparable<T>> Val<T>.lt(value: T): Val<Boolean> =
|
||||
map { it < value }
|
||||
|
||||
infix fun <T : Comparable<T>> Val<T>.lt(value: Val<T>): Val<Boolean> =
|
||||
map(this, value) { a, b -> a < b }
|
||||
|
||||
infix fun Val<Boolean>.and(other: Val<Boolean>): Val<Boolean> =
|
||||
map(this, other) { a, b -> a && b }
|
||||
|
||||
infix fun Val<Boolean>.and(other: Boolean): Val<Boolean> =
|
||||
if (other) this else falseVal()
|
||||
|
||||
infix fun Val<Boolean>.or(other: Val<Boolean>): Val<Boolean> =
|
||||
map(this, other) { a, b -> a || b }
|
||||
|
||||
infix fun Val<Boolean>.xor(other: Val<Boolean>): Val<Boolean> =
|
||||
// Use != because of https://youtrack.jetbrains.com/issue/KT-31277.
|
||||
map(this, other) { a, b -> a != b }
|
||||
|
||||
operator fun Val<Boolean>.not(): Val<Boolean> = map { !it }
|
||||
|
||||
operator fun Val<Int>.plus(other: Int): Val<Int> =
|
||||
map { it + other }
|
||||
|
||||
operator fun Val<Int>.minus(other: Int): Val<Int> =
|
||||
map { it - other }
|
||||
|
||||
fun Val<String>.isEmpty(): Val<Boolean> =
|
||||
map { it.isEmpty() }
|
||||
|
||||
fun Val<String>.isNotEmpty(): Val<Boolean> =
|
||||
map { it.isNotEmpty() }
|
||||
|
||||
fun Val<String>.isBlank(): Val<Boolean> =
|
||||
map { it.isBlank() }
|
||||
|
||||
fun Val<String>.isNotBlank(): Val<Boolean> =
|
||||
map { it.isNotBlank() }
|
@ -1,38 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.unsafe.unsafeAssertNotNull
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
/**
|
||||
* Similar to [DependentListVal], except that this val's [computeElements] returns a ListVal.
|
||||
*/
|
||||
class FlatteningDependentListVal<E>(
|
||||
vararg dependencies: Val<*>,
|
||||
private val computeElements: () -> ListVal<E>,
|
||||
) : AbstractDependentListVal<E>(*dependencies) {
|
||||
private var computedVal: ListVal<E>? = null
|
||||
private var computedValObserver: Disposable? = null
|
||||
|
||||
override val elements: List<E> get() = computedVal.unsafeAssertNotNull().value
|
||||
|
||||
override fun computeElements() {
|
||||
computedVal = computeElements.invoke()
|
||||
|
||||
computedValObserver?.dispose()
|
||||
|
||||
computedValObserver =
|
||||
if (hasObservers) {
|
||||
computedVal.unsafeAssertNotNull().observeList(observer = ::finalizeUpdate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun lastObserverRemoved() {
|
||||
super.lastObserverRemoved()
|
||||
|
||||
computedValObserver?.dispose()
|
||||
computedValObserver = null
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
interface ListVal<out E> : Val<List<E>> {
|
||||
/**
|
||||
* Do not keep long-lived references to a [ListVal]'s [value], it may or may not be mutated
|
||||
* when the [ListVal] is mutated.
|
||||
*/
|
||||
override val value: List<E>
|
||||
|
||||
val size: Val<Int>
|
||||
|
||||
val empty: Val<Boolean>
|
||||
|
||||
val notEmpty: Val<Boolean>
|
||||
|
||||
operator fun get(index: Int): E
|
||||
|
||||
fun observeList(callNow: Boolean = false, observer: ListValObserver<E>): Disposable
|
||||
|
||||
fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> =
|
||||
FoldedVal(this, initialValue, operation)
|
||||
|
||||
fun all(predicate: (E) -> Boolean): Val<Boolean> =
|
||||
fold(true) { acc, el -> acc && predicate(el) }
|
||||
|
||||
fun sumBy(selector: (E) -> Int): Val<Int> =
|
||||
fold(0) { acc, el -> acc + selector(el) }
|
||||
|
||||
fun filtered(predicate: (E) -> Boolean): ListVal<E> =
|
||||
FilteredListVal(this, predicate)
|
||||
|
||||
fun firstOrNull(): Val<E?>
|
||||
|
||||
operator fun contains(element: @UnsafeVariance E): Boolean = element in value
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
|
||||
private val EMPTY_LIST_VAL = StaticListVal<Nothing>(emptyList())
|
||||
|
||||
fun <E> listVal(vararg elements: E): ListVal<E> = StaticListVal(elements.toList())
|
||||
|
||||
fun <E> emptyListVal(): ListVal<E> = EMPTY_LIST_VAL
|
||||
|
||||
fun <E> mutableListVal(
|
||||
vararg elements: E,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
): MutableListVal<E> = SimpleListVal(mutableListOf(*elements), extractObservables)
|
||||
|
||||
fun <T1, T2, R> flatMapToList(
|
||||
v1: Val<T1>,
|
||||
v2: Val<T2>,
|
||||
transform: (T1, T2) -> ListVal<R>,
|
||||
): ListVal<R> =
|
||||
FlatteningDependentListVal(v1, v2) { transform(v1.value, v2.value) }
|
@ -1,40 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.stubDisposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
import world.phantasmal.observable.value.*
|
||||
|
||||
class StaticListVal<E>(private val elements: List<E>) : ListVal<E> {
|
||||
private val firstOrNull = StaticVal(elements.firstOrNull())
|
||||
|
||||
override val size: Val<Int> = value(elements.size)
|
||||
override val empty: Val<Boolean> = if (elements.isEmpty()) trueVal() else falseVal()
|
||||
override val notEmpty: Val<Boolean> = if (elements.isNotEmpty()) trueVal() else falseVal()
|
||||
|
||||
override val value: List<E> = elements
|
||||
|
||||
override fun get(index: Int): E =
|
||||
elements[index]
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
}
|
||||
|
||||
override fun observe(observer: Observer<List<E>>): Disposable = stubDisposable()
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ListChangeEvent.Change(0, emptyList(), value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
}
|
||||
|
||||
override fun firstOrNull(): Val<E?> = firstOrNull
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.*
|
||||
|
||||
class CellCreationTests : ObservableTestSuite {
|
||||
@Test
|
||||
fun test_cell() = test {
|
||||
assertEquals(7, cell(7).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_trueCell() = test {
|
||||
assertTrue(trueCell().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_falseCell() = test {
|
||||
assertFalse(falseCell().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_nullCell() = test {
|
||||
assertNull(nullCell().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_mutableCell_with_initial_value() = test {
|
||||
val cell = mutableCell(17)
|
||||
|
||||
assertEquals(17, cell.value)
|
||||
|
||||
cell.value = 201
|
||||
|
||||
assertEquals(201, cell.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_mutableCell_with_getter_and_setter() = test {
|
||||
var x = 17
|
||||
val cell = mutableCell({ x }, { x = it })
|
||||
|
||||
assertEquals(17, cell.value)
|
||||
|
||||
cell.value = 201
|
||||
|
||||
assertEquals(201, cell.value)
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.core.disposable.use
|
||||
import world.phantasmal.observable.ObservableTests
|
||||
import kotlin.test.*
|
||||
|
||||
/**
|
||||
* Test suite for all [Val] implementations. There is a subclass of this suite for every [Val]
|
||||
* Test suite for all [Cell] implementations. There is a subclass of this suite for every [Cell]
|
||||
* implementation.
|
||||
*/
|
||||
interface ValTests : ObservableTests {
|
||||
interface CellTests : ObservableTests {
|
||||
override fun createProvider(): Provider
|
||||
|
||||
@Test
|
||||
fun propagates_changes_to_mapped_val() = test {
|
||||
fun propagates_changes_to_mapped_cell() = test {
|
||||
val p = createProvider()
|
||||
val mapped = p.observable.map { it.hashCode() }
|
||||
val initialValue = mapped.value
|
||||
@ -31,10 +31,10 @@ interface ValTests : ObservableTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propagates_changes_to_flat_mapped_val() = test {
|
||||
fun propagates_changes_to_flat_mapped_cell() = test {
|
||||
val p = createProvider()
|
||||
|
||||
val mapped = p.observable.flatMap { StaticVal(it.hashCode()) }
|
||||
val mapped = p.observable.flatMap { StaticCell(it.hashCode()) }
|
||||
val initialValue = mapped.value
|
||||
|
||||
var observedValue: Int? = null
|
||||
@ -72,7 +72,7 @@ interface ValTests : ObservableTests {
|
||||
}
|
||||
|
||||
/**
|
||||
* When [Val.observe] is called with callNow = true, it should call the observer immediately.
|
||||
* When [Cell.observe] is called with callNow = true, it should call the observer immediately.
|
||||
* Otherwise it should only call the observer when it changes.
|
||||
*/
|
||||
@Test
|
||||
@ -102,6 +102,6 @@ interface ValTests : ObservableTests {
|
||||
}
|
||||
|
||||
interface Provider : ObservableTests.Provider {
|
||||
override val observable: Val<Any>
|
||||
override val observable: Cell<Any>
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class DelegatingCellTests : RegularCellTests, MutableCellTests<Int> {
|
||||
override fun createProvider() = object : MutableCellTests.Provider<Int> {
|
||||
private var v = 0
|
||||
|
||||
override val observable = DelegatingCell({ v }, { v = it })
|
||||
|
||||
override fun emit() {
|
||||
observable.value += 2
|
||||
}
|
||||
|
||||
override fun createValue(): Int = v + 1
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): DelegatingCell<T> {
|
||||
var v = value
|
||||
return DelegatingCell({ v }, { v = it })
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class DependentCellTests : RegularCellTests {
|
||||
override fun createProvider() = object : CellTests.Provider {
|
||||
val dependency = SimpleCell(0)
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
/**
|
||||
* In these tests the direct dependency of the [FlatteningDependentCell] changes.
|
||||
*/
|
||||
class FlatteningDependentCellDirectDependencyEmitsTests : RegularCellTests {
|
||||
override fun createProvider() = object : CellTests.Provider {
|
||||
// The transitive dependency can't change.
|
||||
val transitiveDependency = StaticCell(5)
|
||||
|
||||
// The direct dependency of the cell under test can change.
|
||||
val directDependency = SimpleCell(transitiveDependency)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentCell(directDependency) { directDependency.value }
|
||||
|
||||
override fun emit() {
|
||||
// Update the direct dependency.
|
||||
val oldTransitiveDependency = directDependency.value
|
||||
directDependency.value = StaticCell(oldTransitiveDependency.value + 5)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): FlatteningDependentCell<T> {
|
||||
val v = StaticCell(StaticCell(value))
|
||||
return FlatteningDependentCell(v) { v.value }
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
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 {
|
||||
// The transitive dependency can change.
|
||||
val transitiveDependency = SimpleCell(5)
|
||||
|
||||
// The direct dependency of the cell under test can't change.
|
||||
val directDependency = StaticCell(transitiveDependency)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentCell(directDependency) { directDependency.value }
|
||||
|
||||
override fun emit() {
|
||||
// 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 }
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
interface MutableValTests<T : Any> : ValTests {
|
||||
interface MutableCellTests<T : Any> : CellTests {
|
||||
override fun createProvider(): Provider<T>
|
||||
|
||||
@Test
|
||||
@ -25,8 +25,8 @@ interface MutableValTests<T : Any> : ValTests {
|
||||
assertEquals(newValue, observedValue)
|
||||
}
|
||||
|
||||
interface Provider<T : Any> : ValTests.Provider {
|
||||
override val observable: MutableVal<T>
|
||||
interface Provider<T : Any> : CellTests.Provider {
|
||||
override val observable: MutableCell<T>
|
||||
|
||||
/**
|
||||
* Returns a value that can be assigned to [observable] and that's different from
|
@ -0,0 +1,161 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
/**
|
||||
* Test suite for all [Cell] implementations that aren't ListCells. There is a subclass of this
|
||||
* suite for every non-ListCell [Cell] implementation.
|
||||
*/
|
||||
interface RegularCellTests : CellTests {
|
||||
fun <T> createWithValue(value: T): Cell<T>
|
||||
|
||||
/**
|
||||
* [Cell.value] should correctly reflect changes even when the [Cell] has no observers.
|
||||
* Typically this means that the cell's value is not updated in real time, only when it is
|
||||
* queried.
|
||||
*/
|
||||
@Test
|
||||
fun reflects_changes_without_observers() = test {
|
||||
val p = createProvider()
|
||||
|
||||
var old: Any?
|
||||
|
||||
repeat(5) {
|
||||
// Value should change after emit.
|
||||
old = p.observable.value
|
||||
|
||||
p.emit()
|
||||
|
||||
val new = p.observable.value
|
||||
|
||||
assertNotEquals(old, new)
|
||||
|
||||
// Value should not change when emit hasn't been called since the last access.
|
||||
assertEquals(new, p.observable.value)
|
||||
|
||||
old = new
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun convenience_methods() = test {
|
||||
listOf(Any(), null).forEach { any ->
|
||||
val anyCell = createWithValue(any)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(any, anyCell.value)
|
||||
|
||||
// Test `isNull`.
|
||||
assertEquals(any == null, anyCell.isNull().value)
|
||||
|
||||
// Test `isNotNull`.
|
||||
assertEquals(any != null, anyCell.isNotNull().value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generic_extensions() = test {
|
||||
listOf(Any(), null).forEach { any ->
|
||||
val anyCell = createWithValue(any)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(any, anyCell.value)
|
||||
|
||||
// Test `orElse`.
|
||||
assertEquals(any ?: "default", anyCell.orElse { "default" }.value)
|
||||
}
|
||||
|
||||
fun <T> testEqNe(a: T, b: T) {
|
||||
val aCell = createWithValue(a)
|
||||
val bCell = createWithValue(b)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(a, aCell.value)
|
||||
assertEquals(b, bCell.value)
|
||||
|
||||
// Test `eq`.
|
||||
assertEquals(a == b, (aCell eq b).value)
|
||||
assertEquals(a == b, (aCell eq bCell).value)
|
||||
|
||||
// Test `ne`.
|
||||
assertEquals(a != b, (aCell ne b).value)
|
||||
assertEquals(a != b, (aCell ne bCell).value)
|
||||
}
|
||||
|
||||
testEqNe(10, 10)
|
||||
testEqNe(5, 99)
|
||||
testEqNe("a", "a")
|
||||
testEqNe("x", "y")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun comparable_extensions() = test {
|
||||
fun <T : Comparable<T>> comparable_tests(a: T, b: T) {
|
||||
val aCell = createWithValue(a)
|
||||
val bCell = createWithValue(b)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(a, aCell.value)
|
||||
assertEquals(b, bCell.value)
|
||||
|
||||
// Test `gt`.
|
||||
assertEquals(a > b, (aCell gt b).value)
|
||||
assertEquals(a > b, (aCell gt bCell).value)
|
||||
|
||||
// Test `lt`.
|
||||
assertEquals(a < b, (aCell lt b).value)
|
||||
assertEquals(a < b, (aCell lt bCell).value)
|
||||
}
|
||||
|
||||
comparable_tests(10, 10)
|
||||
comparable_tests(7.0, 5.0)
|
||||
comparable_tests((5000).toShort(), (7000).toShort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun boolean_extensions() = test {
|
||||
listOf(true, false).forEach { bool ->
|
||||
val boolCell = createWithValue(bool)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(bool, boolCell.value)
|
||||
|
||||
// Test `and`.
|
||||
assertEquals(bool, (boolCell and trueCell()).value)
|
||||
assertFalse((boolCell and falseCell()).value)
|
||||
|
||||
// Test `or`.
|
||||
assertTrue((boolCell or trueCell()).value)
|
||||
assertEquals(bool, (boolCell or falseCell()).value)
|
||||
|
||||
// Test `xor`.
|
||||
assertEquals(!bool, (boolCell xor trueCell()).value)
|
||||
assertEquals(bool, (boolCell xor falseCell()).value)
|
||||
|
||||
// Test `!` (unary not).
|
||||
assertEquals(!bool, (!boolCell).value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun string_extensions() = test {
|
||||
listOf("", " ", "\t\t", "non-empty-non-blank").forEach { string ->
|
||||
val stringCell = createWithValue(string)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(string, stringCell.value)
|
||||
|
||||
// Test `isEmpty`.
|
||||
assertEquals(string.isEmpty(), stringCell.isEmpty().value)
|
||||
|
||||
// Test `isNotEmpty`.
|
||||
assertEquals(string.isNotEmpty(), stringCell.isNotEmpty().value)
|
||||
|
||||
// Test `isBlank`.
|
||||
assertEquals(string.isBlank(), stringCell.isBlank().value)
|
||||
|
||||
// Test `isNotBlank`.
|
||||
assertEquals(string.isNotBlank(), stringCell.isNotBlank().value)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
class SimpleCellTests : RegularCellTests, MutableCellTests<Int> {
|
||||
override fun createProvider() = object : MutableCellTests.Provider<Int> {
|
||||
override val observable = SimpleCell(1)
|
||||
|
||||
override fun emit() {
|
||||
observable.value += 2
|
||||
}
|
||||
|
||||
override fun createValue(): Int = observable.value + 1
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T) = SimpleCell(value)
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package world.phantasmal.observable.value
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StaticValTests : ObservableTestSuite {
|
||||
class StaticCellTests : ObservableTestSuite {
|
||||
@Test
|
||||
fun observing_StaticVal_should_never_create_leaks() = test {
|
||||
val static = StaticVal("test value")
|
||||
fun observing_StaticCell_should_never_create_leaks() = test {
|
||||
val static = StaticCell("test value")
|
||||
|
||||
// We never call dispose on the returned disposables.
|
||||
static.observe {}
|
||||
static.observe(callNow = false) {}
|
||||
static.observe(callNow = true) {}
|
||||
@ -16,7 +17,7 @@ class StaticValTests : ObservableTestSuite {
|
||||
|
||||
@Test
|
||||
fun observe_respects_callNow() = test {
|
||||
val static = StaticVal("test value")
|
||||
val static = StaticCell("test value")
|
||||
var calls = 0
|
||||
|
||||
static.observe(callNow = false) { calls++ }
|
@ -0,0 +1,13 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
class DependentListCellTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
private val dependency = SimpleListCell<Int>(mutableListOf())
|
||||
|
||||
override val observable = DependentListCell(dependency) { dependency.value.map { 2 * it } }
|
||||
|
||||
override fun addElement() {
|
||||
dependency.add(4)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.value.SimpleVal
|
||||
import world.phantasmal.observable.cell.SimpleCell
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class FilteredListValTests : ListValTests {
|
||||
override fun createProvider() = object : ListValTests.Provider {
|
||||
private val dependency = SimpleListVal<Int>(mutableListOf())
|
||||
class FilteredListCellTests : ListCellTests {
|
||||
override fun createProvider() = object : ListCellTests.Provider {
|
||||
private val dependency = SimpleListCell<Int>(mutableListOf())
|
||||
|
||||
override val observable = FilteredListVal(dependency, predicate = { it % 2 == 0 })
|
||||
override val observable = FilteredListCell(dependency, predicate = { it % 2 == 0 })
|
||||
|
||||
override fun addElement() {
|
||||
dependency.add(4)
|
||||
@ -18,8 +18,8 @@ class FilteredListValTests : ListValTests {
|
||||
|
||||
@Test
|
||||
fun contains_only_values_that_match_the_predicate() = test {
|
||||
val dep = SimpleListVal(mutableListOf("a", "b"))
|
||||
val list = FilteredListVal(dep, predicate = { 'a' in it })
|
||||
val dep = SimpleListCell(mutableListOf("a", "b"))
|
||||
val list = FilteredListCell(dep, predicate = { 'a' in it })
|
||||
|
||||
assertEquals(1, list.value.size)
|
||||
assertEquals("a", list.value[0])
|
||||
@ -42,8 +42,8 @@ class FilteredListValTests : ListValTests {
|
||||
|
||||
@Test
|
||||
fun only_emits_when_necessary() = test {
|
||||
val dep = SimpleListVal<Int>(mutableListOf())
|
||||
val list = FilteredListVal(dep, predicate = { it % 2 == 0 })
|
||||
val dep = SimpleListCell<Int>(mutableListOf())
|
||||
val list = FilteredListCell(dep, predicate = { it % 2 == 0 })
|
||||
var changes = 0
|
||||
var listChanges = 0
|
||||
|
||||
@ -71,8 +71,8 @@ class FilteredListValTests : ListValTests {
|
||||
|
||||
@Test
|
||||
fun emits_correct_change_events() = test {
|
||||
val dep = SimpleListVal<Int>(mutableListOf())
|
||||
val list = FilteredListVal(dep, predicate = { it % 2 == 0 })
|
||||
val dep = SimpleListCell<Int>(mutableListOf())
|
||||
val list = FilteredListCell(dep, predicate = { it % 2 == 0 })
|
||||
var event: ListChangeEvent<Int>? = null
|
||||
|
||||
disposer.add(list.observeList {
|
||||
@ -104,18 +104,18 @@ class FilteredListValTests : ListValTests {
|
||||
}
|
||||
|
||||
/**
|
||||
* When the dependency list of a FilteredListVal emits ElementChange events, the FilteredListVal
|
||||
* should emit either Change events or ElementChange events, depending on whether the predicate
|
||||
* result has changed.
|
||||
* 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.
|
||||
*/
|
||||
@Test
|
||||
fun emits_correct_events_when_dependency_emits_ElementChange_events() = test {
|
||||
val dep = SimpleListVal(
|
||||
mutableListOf(SimpleVal(1), SimpleVal(2), SimpleVal(3), SimpleVal(4)),
|
||||
val dep = SimpleListCell(
|
||||
mutableListOf(SimpleCell(1), SimpleCell(2), SimpleCell(3), SimpleCell(4)),
|
||||
extractObservables = { arrayOf(it) },
|
||||
)
|
||||
val list = FilteredListVal(dep, predicate = { it.value % 2 == 0 })
|
||||
var event: ListChangeEvent<SimpleVal<Int>>? = null
|
||||
val list = FilteredListCell(dep, predicate = { it.value % 2 == 0 })
|
||||
var event: ListChangeEvent<SimpleCell<Int>>? = null
|
||||
|
||||
disposer.add(list.observeList {
|
||||
assertNull(event)
|
@ -0,0 +1,25 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
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 {
|
||||
// The transitive dependency can't change.
|
||||
private val transitiveDependency = StaticListCell<Int>(emptyList())
|
||||
|
||||
// The direct dependency of the list under test can change.
|
||||
private val dependency = SimpleCell<ListCell<Int>>(transitiveDependency)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListCell(dependency) { dependency.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the direct dependency.
|
||||
val oldTransitiveDependency: ListCell<Int> = dependency.value
|
||||
dependency.value = StaticListCell(oldTransitiveDependency.value + 4)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
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 {
|
||||
// The transitive dependency can change.
|
||||
private val transitiveDependency = SimpleListCell(mutableListOf<Int>())
|
||||
|
||||
// The direct dependency of the list under test can't change.
|
||||
private val dependency = StaticCell<ListCell<Int>>(transitiveDependency)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListCell(dependency) { dependency.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the transitive dependency.
|
||||
transitiveDependency.add(4)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.value.ValTests
|
||||
import world.phantasmal.observable.cell.CellTests
|
||||
import kotlin.test.*
|
||||
|
||||
/**
|
||||
* Test suite for all [ListVal] implementations. There is a subclass of this suite for every
|
||||
* [ListVal] implementation.
|
||||
* Test suite for all [ListCell] implementations. There is a subclass of this suite for every
|
||||
* [ListCell] implementation.
|
||||
*/
|
||||
interface ListValTests : ValTests {
|
||||
interface ListCellTests : CellTests {
|
||||
override fun createProvider(): Provider
|
||||
|
||||
@Test
|
||||
@ -177,11 +177,11 @@ interface ListValTests : ValTests {
|
||||
}
|
||||
}
|
||||
|
||||
interface Provider : ValTests.Provider {
|
||||
override val observable: ListVal<Any>
|
||||
interface Provider : CellTests.Provider {
|
||||
override val observable: ListCell<Any>
|
||||
|
||||
/**
|
||||
* Adds an element to the ListVal under test.
|
||||
* Adds an element to the [ListCell] under test.
|
||||
*/
|
||||
fun addElement()
|
||||
|
@ -1,16 +1,16 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.value.MutableValTests
|
||||
import world.phantasmal.observable.cell.MutableCellTests
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Test suite for all [MutableListVal] implementations. There is a subclass of this suite for every
|
||||
* [MutableListVal] implementation.
|
||||
* Test suite for all [MutableListCell] implementations. There is a subclass of this suite for every
|
||||
* [MutableListCell] implementation.
|
||||
*/
|
||||
interface MutableListValTests<T : Any> : ListValTests, MutableValTests<List<T>> {
|
||||
interface MutableListCellTests<T : Any> : ListCellTests, MutableCellTests<List<T>> {
|
||||
override fun createProvider(): Provider<T>
|
||||
|
||||
@Test
|
||||
@ -71,8 +71,8 @@ interface MutableListValTests<T : Any> : ListValTests, MutableValTests<List<T>>
|
||||
assertEquals(v3, c3.inserted[0])
|
||||
}
|
||||
|
||||
interface Provider<T : Any> : ListValTests.Provider, MutableValTests.Provider<List<T>> {
|
||||
override val observable: MutableListVal<T>
|
||||
interface Provider<T : Any> : ListCellTests.Provider, MutableCellTests.Provider<List<T>> {
|
||||
override val observable: MutableListCell<T>
|
||||
|
||||
fun createElement(): T
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SimpleListValTests : MutableListValTests<Int> {
|
||||
override fun createProvider() = object : MutableListValTests.Provider<Int> {
|
||||
class SimpleListCellTests : MutableListCellTests<Int> {
|
||||
override fun createProvider() = object : MutableListCellTests.Provider<Int> {
|
||||
private var nextElement = 0
|
||||
|
||||
override val observable = SimpleListVal(mutableListOf<Int>())
|
||||
override val observable = SimpleListCell(mutableListOf<Int>())
|
||||
|
||||
override fun addElement() {
|
||||
observable.add(createElement())
|
||||
@ -20,7 +20,7 @@ class SimpleListValTests : MutableListValTests<Int> {
|
||||
|
||||
@Test
|
||||
fun instantiates_correctly() = test {
|
||||
val list = SimpleListVal(mutableListOf(1, 2, 3))
|
||||
val list = SimpleListCell(mutableListOf(1, 2, 3))
|
||||
|
||||
assertEquals(3, list.size.value)
|
||||
assertEquals(3, list.value.size)
|
@ -1,14 +1,15 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
package world.phantasmal.observable.cell.list
|
||||
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StaticListValTests : ObservableTestSuite {
|
||||
class StaticListCellTests : ObservableTestSuite {
|
||||
@Test
|
||||
fun observing_StaticListVal_should_never_create_leaks() = test {
|
||||
val static = StaticListVal(listOf(1, 2, 3))
|
||||
fun observing_StaticListCell_should_never_create_leaks() = test {
|
||||
val static = StaticListCell(listOf(1, 2, 3))
|
||||
|
||||
// We never call dispose on the returned disposables.
|
||||
static.observe {}
|
||||
static.observe(callNow = false) {}
|
||||
static.observe(callNow = true) {}
|
||||
@ -18,7 +19,7 @@ class StaticListValTests : ObservableTestSuite {
|
||||
|
||||
@Test
|
||||
fun observe_respects_callNow() = test {
|
||||
val static = StaticListVal(listOf(1, 2, 3))
|
||||
val static = StaticListCell(listOf(1, 2, 3))
|
||||
var calls = 0
|
||||
|
||||
static.observe(callNow = false) { calls++ }
|
||||
@ -29,7 +30,7 @@ class StaticListValTests : ObservableTestSuite {
|
||||
|
||||
@Test
|
||||
fun observeList_respects_callNow() = test {
|
||||
val static = StaticListVal(listOf(1, 2, 3))
|
||||
val static = StaticListCell(listOf(1, 2, 3))
|
||||
var calls = 0
|
||||
|
||||
static.observeList(callNow = false) { calls++ }
|
@ -1,20 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class DelegatingValTests : RegularValTests, MutableValTests<Int> {
|
||||
override fun createProvider() = object : MutableValTests.Provider<Int> {
|
||||
private var v = 0
|
||||
|
||||
override val observable = DelegatingVal({ v }, { v = it })
|
||||
|
||||
override fun emit() {
|
||||
observable.value += 2
|
||||
}
|
||||
|
||||
override fun createValue(): Int = v + 1
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): DelegatingVal<T> {
|
||||
var v = value
|
||||
return DelegatingVal({ v }, { v = it })
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class DependentValTests : RegularValTests {
|
||||
override fun createProvider() = object : ValTests.Provider {
|
||||
val v = SimpleVal(0)
|
||||
|
||||
override val observable = DependentVal(v) { 2 * v.value }
|
||||
|
||||
override fun emit() {
|
||||
v.value += 2
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): DependentVal<T> {
|
||||
val v = SimpleVal(value)
|
||||
return DependentVal(v) { v.value }
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* In these tests the direct dependency of the [FlatteningDependentVal] changes.
|
||||
*/
|
||||
class FlatteningDependentValDependentValEmitsTests : RegularValTests {
|
||||
override fun createProvider() = object : ValTests.Provider {
|
||||
val v = SimpleVal(StaticVal(5))
|
||||
|
||||
override val observable = FlatteningDependentVal(v) { v.value }
|
||||
|
||||
override fun emit() {
|
||||
v.value = StaticVal(v.value.value + 5)
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): FlatteningDependentVal<T> {
|
||||
val v = StaticVal(StaticVal(value))
|
||||
return FlatteningDependentVal(v) { v.value }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a regression test, it's important that this exact sequence of statements stays the
|
||||
* same.
|
||||
*/
|
||||
@Test
|
||||
fun emits_a_change_when_its_direct_val_dependency_changes() = test {
|
||||
val v = SimpleVal(SimpleVal(7))
|
||||
val fv = FlatteningDependentVal(v) { v.value }
|
||||
var observedValue: Int? = null
|
||||
|
||||
disposer.add(
|
||||
fv.observe { observedValue = it.value }
|
||||
)
|
||||
|
||||
assertNull(observedValue)
|
||||
|
||||
v.value.value = 99
|
||||
|
||||
assertEquals(99, observedValue)
|
||||
|
||||
v.value = SimpleVal(7)
|
||||
|
||||
assertEquals(7, observedValue)
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
/**
|
||||
* In these tests the dependency of the [FlatteningDependentVal]'s direct dependency changes.
|
||||
*/
|
||||
class FlatteningDependentValNestedValEmitsTests : RegularValTests {
|
||||
override fun createProvider() = object : ValTests.Provider {
|
||||
val v = StaticVal(SimpleVal(5))
|
||||
|
||||
override val observable = FlatteningDependentVal(v) { v.value }
|
||||
|
||||
override fun emit() {
|
||||
v.value.value += 5
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T): FlatteningDependentVal<T> {
|
||||
val v = StaticVal(StaticVal(value))
|
||||
return FlatteningDependentVal(v) { v.value }
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
/**
|
||||
* Test suite for all [Val] implementations that aren't ListVals. There is a subclass of this suite
|
||||
* for every non-ListVal [Val] implementation.
|
||||
*/
|
||||
interface RegularValTests : ValTests {
|
||||
fun <T> createWithValue(value: T): Val<T>
|
||||
|
||||
/**
|
||||
* [Val.value] should correctly reflect changes even when the [Val] has no observers. Typically
|
||||
* this means that the val's value is not updated in real time, only when it is queried.
|
||||
*/
|
||||
@Test
|
||||
fun reflects_changes_without_observers() = test {
|
||||
val p = createProvider()
|
||||
|
||||
var old: Any?
|
||||
|
||||
repeat(5) {
|
||||
// Value should change after emit.
|
||||
old = p.observable.value
|
||||
|
||||
p.emit()
|
||||
|
||||
val new = p.observable.value
|
||||
|
||||
assertNotEquals(old, new)
|
||||
|
||||
// Value should not change when emit hasn't been called since the last access.
|
||||
assertEquals(new, p.observable.value)
|
||||
|
||||
old = new
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun val_convenience_methods() = test {
|
||||
listOf(Any(), null).forEach { any ->
|
||||
val anyVal = createWithValue(any)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(any, anyVal.value)
|
||||
|
||||
// Test `isNull`.
|
||||
assertEquals(any == null, anyVal.isNull().value)
|
||||
|
||||
// Test `isNotNull`.
|
||||
assertEquals(any != null, anyVal.isNotNull().value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun val_generic_extensions() = test {
|
||||
listOf(Any(), null).forEach { any ->
|
||||
val anyVal = createWithValue(any)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(any, anyVal.value)
|
||||
|
||||
// Test `orElse`.
|
||||
assertEquals(any ?: "default", anyVal.orElse { "default" }.value)
|
||||
}
|
||||
|
||||
fun <T> testEqNe(a: T, b: T) {
|
||||
val aVal = createWithValue(a)
|
||||
val bVal = createWithValue(b)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(a, aVal.value)
|
||||
assertEquals(b, bVal.value)
|
||||
|
||||
// Test `eq`.
|
||||
assertEquals(a == b, (aVal eq b).value)
|
||||
assertEquals(a == b, (aVal eq bVal).value)
|
||||
|
||||
// Test `ne`.
|
||||
assertEquals(a != b, (aVal ne b).value)
|
||||
assertEquals(a != b, (aVal ne bVal).value)
|
||||
}
|
||||
|
||||
testEqNe(10, 10)
|
||||
testEqNe(5, 99)
|
||||
testEqNe("a", "a")
|
||||
testEqNe("x", "y")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun val_comparable_extensions() = test {
|
||||
fun <T : Comparable<T>> comparable_tests(a: T, b: T) {
|
||||
val aVal = createWithValue(a)
|
||||
val bVal = createWithValue(b)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(a, aVal.value)
|
||||
assertEquals(b, bVal.value)
|
||||
|
||||
// Test `gt`.
|
||||
assertEquals(a > b, (aVal gt b).value)
|
||||
assertEquals(a > b, (aVal gt bVal).value)
|
||||
|
||||
// Test `lt`.
|
||||
assertEquals(a < b, (aVal lt b).value)
|
||||
assertEquals(a < b, (aVal lt bVal).value)
|
||||
}
|
||||
|
||||
comparable_tests(10, 10)
|
||||
comparable_tests(7.0, 5.0)
|
||||
comparable_tests((5000).toShort(), (7000).toShort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun val_boolean_extensions() = test {
|
||||
listOf(true, false).forEach { bool ->
|
||||
val boolVal = createWithValue(bool)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(bool, boolVal.value)
|
||||
|
||||
// Test `and`.
|
||||
assertEquals(bool, (boolVal and trueVal()).value)
|
||||
assertFalse((boolVal and falseVal()).value)
|
||||
|
||||
// Test `or`.
|
||||
assertTrue((boolVal or trueVal()).value)
|
||||
assertEquals(bool, (boolVal or falseVal()).value)
|
||||
|
||||
// Test `xor`.
|
||||
assertEquals(!bool, (boolVal xor trueVal()).value)
|
||||
assertEquals(bool, (boolVal xor falseVal()).value)
|
||||
|
||||
// Test `!` (unary not).
|
||||
assertEquals(!bool, (!boolVal).value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun val_string_extensions() = test {
|
||||
listOf("", " ", "\t\t", "non-empty-non-blank").forEach { string ->
|
||||
val stringVal = createWithValue(string)
|
||||
|
||||
// Test the test setup first.
|
||||
assertEquals(string, stringVal.value)
|
||||
|
||||
// Test `isEmpty`.
|
||||
assertEquals(string.isEmpty(), stringVal.isEmpty().value)
|
||||
|
||||
// Test `isNotEmpty`.
|
||||
assertEquals(string.isNotEmpty(), stringVal.isNotEmpty().value)
|
||||
|
||||
// Test `isBlank`.
|
||||
assertEquals(string.isBlank(), stringVal.isBlank().value)
|
||||
|
||||
// Test `isNotBlank`.
|
||||
assertEquals(string.isNotBlank(), stringVal.isNotBlank().value)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
class SimpleValTests : RegularValTests, MutableValTests<Int> {
|
||||
override fun createProvider() = object : MutableValTests.Provider<Int> {
|
||||
override val observable = SimpleVal(1)
|
||||
|
||||
override fun emit() {
|
||||
observable.value += 2
|
||||
}
|
||||
|
||||
override fun createValue(): Int = observable.value + 1
|
||||
}
|
||||
|
||||
override fun <T> createWithValue(value: T) = SimpleVal(value)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.*
|
||||
|
||||
class ValCreationTests : ObservableTestSuite {
|
||||
@Test
|
||||
fun test_value() = test {
|
||||
assertEquals(7, value(7).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_trueVal() = test {
|
||||
assertTrue(trueVal().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_falseVal() = test {
|
||||
assertFalse(falseVal().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_nullVal() = test {
|
||||
assertNull(nullVal().value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_mutableVal_with_initial_value() = test {
|
||||
val v = mutableVal(17)
|
||||
|
||||
assertEquals(17, v.value)
|
||||
|
||||
v.value = 201
|
||||
|
||||
assertEquals(201, v.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_mutableVal_with_getter_and_setter() = test {
|
||||
var x = 17
|
||||
val v = mutableVal({ x }, { x = it })
|
||||
|
||||
assertEquals(17, v.value)
|
||||
|
||||
v.value = 201
|
||||
|
||||
assertEquals(201, v.value)
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
class DependentListValTests : ListValTests {
|
||||
override fun createProvider() = object : ListValTests.Provider {
|
||||
private val l = SimpleListVal<Int>(mutableListOf())
|
||||
|
||||
override val observable = DependentListVal(l) { l.value.map { 2 * it } }
|
||||
|
||||
override fun addElement() {
|
||||
l.add(4)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.observable.value.SimpleVal
|
||||
|
||||
/**
|
||||
* In these tests the direct dependency of the [FlatteningDependentListVal] changes.
|
||||
*/
|
||||
class FlatteningDependentListValDependentValEmitsTests : ListValTests {
|
||||
override fun createProvider() = object : ListValTests.Provider {
|
||||
// The nested val can't change.
|
||||
private val nestedVal = StaticListVal<Int>(emptyList())
|
||||
|
||||
// The direct dependency of the list under test can change.
|
||||
private val dependencyVal = SimpleVal<ListVal<Int>>(nestedVal)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListVal(dependencyVal) { dependencyVal.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the direct dependency.
|
||||
val oldNestedVal: ListVal<Int> = dependencyVal.value
|
||||
dependencyVal.value = StaticListVal(oldNestedVal.value + 4)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
import world.phantasmal.observable.value.StaticVal
|
||||
|
||||
/**
|
||||
* In these tests the dependency of the [FlatteningDependentListVal]'s direct dependency changes.
|
||||
*/
|
||||
class FlatteningDependentListValNestedValEmitsTests : ListValTests {
|
||||
override fun createProvider() = object : ListValTests.Provider {
|
||||
// The nested val can change.
|
||||
private val nestedVal = SimpleListVal(mutableListOf<Int>())
|
||||
|
||||
// The direct dependency of the list under test can't change.
|
||||
private val dependentVal = StaticVal<ListVal<Int>>(nestedVal)
|
||||
|
||||
override val observable =
|
||||
FlatteningDependentListVal(dependentVal) { dependentVal.value }
|
||||
|
||||
override fun addElement() {
|
||||
// Update the nested dependency.
|
||||
nestedVal.add(4)
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.application.Application
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
|
||||
@ -90,7 +90,7 @@ private fun createThreeRenderer(canvas: HTMLCanvasElement): DisposableThreeRende
|
||||
private class HistoryApplicationUrl : TrackedDisposable(), ApplicationUrl {
|
||||
private val path: String get() = window.location.pathname
|
||||
|
||||
override val url = mutableVal(window.location.hash.substring(1))
|
||||
override val url = mutableCell(window.location.hash.substring(1))
|
||||
|
||||
private val popStateListener = window.disposableListener<PopStateEvent>("popstate", {
|
||||
url.value = window.location.hash.substring(1)
|
||||
|
@ -1,10 +1,10 @@
|
||||
package world.phantasmal.web.application.controllers
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class MainContentController(uiStore: UiStore) : Controller() {
|
||||
val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
|
||||
val tools: Map<PwToolType, Cell<Boolean>> = uiStore.toolToActive
|
||||
}
|
||||
|
@ -4,19 +4,19 @@ import kotlinx.browser.window
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
import kotlin.math.floor
|
||||
|
||||
class NavigationController(private val uiStore: UiStore, private val clock: Clock) : Controller() {
|
||||
private val _internetTime = mutableVal("@")
|
||||
private val _internetTime = mutableCell("@")
|
||||
private var internetTimeInterval: Int
|
||||
|
||||
val tools: Map<PwToolType, Val<Boolean>> = uiStore.toolToActive
|
||||
val internetTime: Val<String> = _internetTime
|
||||
val tools: Map<PwToolType, Cell<Boolean>> = uiStore.toolToActive
|
||||
val internetTime: Cell<String> = _internetTime
|
||||
|
||||
init {
|
||||
internetTimeInterval = window.setInterval(::updateInternetTime, 1000)
|
||||
|
@ -1,11 +1,12 @@
|
||||
package world.phantasmal.web.application.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.observable.cell.cell
|
||||
import world.phantasmal.observable.cell.falseCell
|
||||
import world.phantasmal.observable.cell.list.listCell
|
||||
import world.phantasmal.web.application.controllers.NavigationController
|
||||
import world.phantasmal.web.core.dom.externalLink
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.webui.dom.Icon
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.dom.icon
|
||||
@ -29,11 +30,11 @@ class NavigationWidget(private val ctrl: NavigationController) : Widget() {
|
||||
className = "pw-application-navigation-right"
|
||||
|
||||
val serverSelect = Select(
|
||||
enabled = falseVal(),
|
||||
enabled = falseCell(),
|
||||
label = "Server:",
|
||||
items = listVal("Ephinea"),
|
||||
selected = value("Ephinea"),
|
||||
tooltip = value("Only Ephinea is supported at the moment"),
|
||||
items = listCell(Server.Ephinea.uiName),
|
||||
selected = cell(Server.Ephinea.uiName),
|
||||
tooltip = cell("Only ${Server.Ephinea.uiName} is supported at the moment"),
|
||||
)
|
||||
addChild(serverSelect.label!!)
|
||||
addChild(serverSelect)
|
||||
|
@ -2,8 +2,8 @@ package world.phantasmal.web.application.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.nullVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.observable.cell.nullCell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.webui.dom.input
|
||||
import world.phantasmal.webui.dom.label
|
||||
@ -14,7 +14,7 @@ class PwToolButton(
|
||||
private val tool: PwToolType,
|
||||
private val toggled: Observable<Boolean>,
|
||||
private val onMouseDown: () -> Unit,
|
||||
) : Control(visible = trueVal(), enabled = trueVal(), tooltip = nullVal()) {
|
||||
) : Control(visible = trueCell(), enabled = trueCell(), tooltip = nullCell()) {
|
||||
private val inputId = "pw-application-pw-tool-button-${tool.name.toLowerCase()}"
|
||||
|
||||
override fun Node.createElement() =
|
||||
|
@ -6,17 +6,17 @@ import org.w3c.dom.events.KeyboardEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.Disposer
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.eq
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
import world.phantasmal.observable.cell.eq
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
import world.phantasmal.webui.stores.Store
|
||||
|
||||
interface ApplicationUrl {
|
||||
val url: Val<String>
|
||||
val url: Cell<String>
|
||||
|
||||
fun pushUrl(url: String)
|
||||
|
||||
@ -24,16 +24,16 @@ interface ApplicationUrl {
|
||||
}
|
||||
|
||||
class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
private val _currentTool: MutableVal<PwToolType>
|
||||
private val _currentTool: MutableCell<PwToolType>
|
||||
|
||||
private val _path = mutableVal("")
|
||||
private val _server = mutableVal(Server.Ephinea)
|
||||
private val _path = mutableCell("")
|
||||
private val _server = mutableCell(Server.Ephinea)
|
||||
|
||||
/**
|
||||
* Maps full paths to maps of parameters and their values. In other words we keep track of
|
||||
* parameter values per [applicationUrl].
|
||||
*/
|
||||
private val parameters: MutableMap<String, MutableMap<String, MutableVal<String?>>> =
|
||||
private val parameters: MutableMap<String, MutableMap<String, MutableCell<String?>>> =
|
||||
mutableMapOf()
|
||||
private val globalKeyDownHandlers: MutableMap<String, suspend (e: KeyboardEvent) -> Unit> =
|
||||
mutableMapOf()
|
||||
@ -55,32 +55,28 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
/**
|
||||
* The tool that is currently visible.
|
||||
*/
|
||||
val currentTool: Val<PwToolType>
|
||||
val currentTool: Cell<PwToolType>
|
||||
|
||||
/**
|
||||
* Map of tools to a boolean Val that says whether they are the current tool or not.
|
||||
* Map of tools to a boolean cell that says whether they are the current tool or not.
|
||||
*/
|
||||
val toolToActive: Map<PwToolType, Val<Boolean>>
|
||||
val toolToActive: Map<PwToolType, Cell<Boolean>>
|
||||
|
||||
/**
|
||||
* Application URL without the tool path prefix.
|
||||
*/
|
||||
val path: Val<String> = _path
|
||||
val path: Cell<String> = _path
|
||||
|
||||
/**
|
||||
* The private server we're currently showing data and tools for.
|
||||
*/
|
||||
val server: Val<Server> = _server
|
||||
val server: Cell<Server> = _server
|
||||
|
||||
init {
|
||||
_currentTool = mutableVal(defaultTool)
|
||||
_currentTool = mutableCell(defaultTool)
|
||||
currentTool = _currentTool
|
||||
|
||||
toolToActive = tools
|
||||
.map { tool ->
|
||||
tool to (currentTool eq tool)
|
||||
}
|
||||
.toMap()
|
||||
toolToActive = tools.associateWith { tool -> currentTool eq tool }
|
||||
|
||||
addDisposables(
|
||||
window.disposableListener("keydown", ::dispatchGlobalKeyDown),
|
||||
@ -111,7 +107,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
path: String,
|
||||
parameter: String,
|
||||
setInitialValue: (String?) -> Unit,
|
||||
value: Val<String?>,
|
||||
value: Cell<String?>,
|
||||
onChange: (String?) -> Unit,
|
||||
): Disposable {
|
||||
require(parameter !== FEATURES_PARAM) {
|
||||
@ -119,7 +115,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
}
|
||||
|
||||
val pathParams = parameters.getOrPut("/${tool.slug}$path", ::mutableMapOf)
|
||||
val param = pathParams.getOrPut(parameter) { mutableVal(null) }
|
||||
val param = pathParams.getOrPut(parameter) { mutableCell(null) }
|
||||
|
||||
setInitialValue(param.value)
|
||||
|
||||
@ -142,7 +138,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
private fun setParameter(
|
||||
tool: PwToolType,
|
||||
path: String,
|
||||
parameter: MutableVal<String?>,
|
||||
parameter: MutableCell<String?>,
|
||||
value: String?,
|
||||
replaceUrl: Boolean,
|
||||
) {
|
||||
@ -194,7 +190,7 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
features.add(feature)
|
||||
}
|
||||
} else {
|
||||
params.getOrPut(param) { mutableVal(value) }.value = value
|
||||
params.getOrPut(param) { mutableCell(value) }.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -262,6 +258,6 @@ class UiStore(private val applicationUrl: ApplicationUrl) : Store() {
|
||||
companion object {
|
||||
private const val FEATURES_PARAM = "features"
|
||||
private val SLUG_TO_PW_TOOL: Map<String, PwToolType> =
|
||||
PwToolType.values().map { it.slug to it }.toMap()
|
||||
PwToolType.values().associateBy { it.slug }
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,28 @@
|
||||
package world.phantasmal.web.core.undo
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
|
||||
interface Undo {
|
||||
val canUndo: Val<Boolean>
|
||||
val canRedo: Val<Boolean>
|
||||
val canUndo: Cell<Boolean>
|
||||
val canRedo: Cell<Boolean>
|
||||
|
||||
/**
|
||||
* The first action that will be undone when calling undo().
|
||||
*/
|
||||
val firstUndo: Val<Action?>
|
||||
val firstUndo: Cell<Action?>
|
||||
|
||||
/**
|
||||
* The first action that will be redone when calling redo().
|
||||
*/
|
||||
val firstRedo: Val<Action?>
|
||||
val firstRedo: Cell<Action?>
|
||||
|
||||
/**
|
||||
* True if this undo is at the point in time where the last save happened. See [savePoint].
|
||||
* If false, it should be safe to leave the application because no changes have happened since
|
||||
* the last save point (either because there were no changes or all changes have been undone).
|
||||
*/
|
||||
val atSavePoint: Val<Boolean>
|
||||
val atSavePoint: Cell<Boolean>
|
||||
|
||||
fun undo(): Boolean
|
||||
fun redo(): Boolean
|
||||
|
@ -1,25 +1,25 @@
|
||||
package world.phantasmal.web.core.undo
|
||||
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.cell.*
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
|
||||
class UndoManager {
|
||||
private val undos = mutableListVal<Undo>(NopUndo) { arrayOf(it.atSavePoint) }
|
||||
private val _current = mutableVal<Undo>(NopUndo)
|
||||
private val undos = mutableListCell<Undo>(NopUndo) { arrayOf(it.atSavePoint) }
|
||||
private val _current = mutableCell<Undo>(NopUndo)
|
||||
|
||||
val current: Val<Undo> = _current
|
||||
val current: Cell<Undo> = _current
|
||||
|
||||
val canUndo: Val<Boolean> = current.flatMap { it.canUndo }
|
||||
val canRedo: Val<Boolean> = current.flatMap { it.canRedo }
|
||||
val firstUndo: Val<Action?> = current.flatMap { it.firstUndo }
|
||||
val firstRedo: Val<Action?> = current.flatMap { it.firstRedo }
|
||||
val canUndo: Cell<Boolean> = current.flatMap { it.canUndo }
|
||||
val canRedo: Cell<Boolean> = current.flatMap { it.canRedo }
|
||||
val firstUndo: Cell<Action?> = current.flatMap { it.firstUndo }
|
||||
val firstRedo: Cell<Action?> = current.flatMap { it.firstRedo }
|
||||
|
||||
/**
|
||||
* True if all undos are at the most recent save point. I.e., true if there are no changes to
|
||||
* save.
|
||||
*/
|
||||
val allAtSavePoint: Val<Boolean> = undos.all { it.atSavePoint.value }
|
||||
val allAtSavePoint: Cell<Boolean> = undos.all { it.atSavePoint.value }
|
||||
|
||||
fun addUndo(undo: Undo) {
|
||||
undos.add(undo)
|
||||
@ -56,11 +56,11 @@ class UndoManager {
|
||||
}
|
||||
|
||||
private object NopUndo : Undo {
|
||||
override val canUndo = falseVal()
|
||||
override val canRedo = falseVal()
|
||||
override val firstUndo = nullVal()
|
||||
override val firstRedo = nullVal()
|
||||
override val atSavePoint = trueVal()
|
||||
override val canUndo = falseCell()
|
||||
override val canRedo = falseCell()
|
||||
override val firstUndo = nullCell()
|
||||
override val firstRedo = nullCell()
|
||||
override val atSavePoint = trueCell()
|
||||
|
||||
override fun undo(): Boolean = false
|
||||
|
||||
|
@ -1,32 +1,32 @@
|
||||
package world.phantasmal.web.core.undo
|
||||
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.cell.*
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.web.core.actions.Action
|
||||
|
||||
/**
|
||||
* Full-fledged linear undo/redo implementation.
|
||||
*/
|
||||
class UndoStack(manager: UndoManager) : Undo {
|
||||
private val stack = mutableListVal<Action>()
|
||||
private val stack = mutableListCell<Action>()
|
||||
|
||||
/**
|
||||
* The index where new actions are inserted. If not equal to the [stack]'s size, points to the
|
||||
* action that will be redone when calling [redo].
|
||||
*/
|
||||
private val index = mutableVal(0)
|
||||
private val savePointIndex = mutableVal(0)
|
||||
private val index = mutableCell(0)
|
||||
private val savePointIndex = mutableCell(0)
|
||||
private var undoingOrRedoing = false
|
||||
|
||||
override val canUndo: Val<Boolean> = index gt 0
|
||||
override val canUndo: Cell<Boolean> = index gt 0
|
||||
|
||||
override val canRedo: Val<Boolean> = map(stack, index) { stack, index -> index < stack.size }
|
||||
override val canRedo: Cell<Boolean> = map(stack, index) { stack, index -> index < stack.size }
|
||||
|
||||
override val firstUndo: Val<Action?> = index.map { stack.value.getOrNull(it - 1) }
|
||||
override val firstUndo: Cell<Action?> = index.map { stack.value.getOrNull(it - 1) }
|
||||
|
||||
override val firstRedo: Val<Action?> = index.map { stack.value.getOrNull(it) }
|
||||
override val firstRedo: Cell<Action?> = index.map { stack.value.getOrNull(it) }
|
||||
|
||||
override val atSavePoint: Val<Boolean> = index eq savePointIndex
|
||||
override val atSavePoint: Cell<Boolean> = index eq savePointIndex
|
||||
|
||||
init {
|
||||
manager.addUndo(this)
|
||||
|
@ -3,8 +3,8 @@ package world.phantasmal.web.core.widgets
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.web.core.controllers.*
|
||||
import world.phantasmal.web.externals.goldenLayout.GoldenLayout
|
||||
import world.phantasmal.webui.dom.div
|
||||
@ -14,7 +14,7 @@ import world.phantasmal.webui.widgets.Widget
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class DockWidget(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
visible: Cell<Boolean> = trueCell(),
|
||||
private val ctrl: DockController,
|
||||
private val createWidget: (id: String) -> Widget,
|
||||
) : Widget(visible) {
|
||||
|
@ -1,22 +1,22 @@
|
||||
package world.phantasmal.web.core.widgets
|
||||
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.trueVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.falseCell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.widgets.Label
|
||||
import world.phantasmal.webui.widgets.Widget
|
||||
|
||||
class UnavailableWidget(
|
||||
visible: Val<Boolean> = trueVal(),
|
||||
visible: Cell<Boolean> = trueCell(),
|
||||
private val message: String,
|
||||
) : Widget(visible) {
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
className = "pw-core-unavailable"
|
||||
|
||||
addWidget(Label(enabled = falseVal(), text = message))
|
||||
addWidget(Label(enabled = falseCell(), text = message))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -2,9 +2,9 @@ package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.listCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.web.huntOptimizer.models.HuntMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntMethodStore
|
||||
import world.phantasmal.webui.controllers.Column
|
||||
@ -17,14 +17,14 @@ class MethodsForEpisodeController(
|
||||
private val huntMethodStore: HuntMethodStore,
|
||||
episode: Episode,
|
||||
) : TableController<HuntMethodModel>() {
|
||||
private val methods = mutableListVal<HuntMethodModel>()
|
||||
private val methods = mutableListCell<HuntMethodModel>()
|
||||
private val enemies: List<NpcType> = NpcType.VALUES.filter { it.enemy && it.episode == episode }
|
||||
|
||||
override val fixedColumns = 2
|
||||
|
||||
override val values: ListVal<HuntMethodModel> = methods
|
||||
override val values: ListCell<HuntMethodModel> = methods
|
||||
|
||||
override val columns: ListVal<Column<HuntMethodModel>> = listVal(
|
||||
override val columns: ListCell<Column<HuntMethodModel>> = listCell(
|
||||
Column(
|
||||
key = METHOD_COL_KEY,
|
||||
title = "Method",
|
||||
|
@ -1,7 +1,7 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.observable.cell.cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.web.huntOptimizer.models.OptimalMethodModel
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntOptimizerStore
|
||||
import world.phantasmal.webui.controllers.Column
|
||||
@ -15,11 +15,11 @@ class OptimizationResultController(
|
||||
override val fixedColumns = 4
|
||||
override val hasFooter = true
|
||||
|
||||
override val values: ListVal<OptimalMethodModel> =
|
||||
huntOptimizerStore.optimizationResult.mapToListVal { it.optimalMethods }
|
||||
override val values: ListCell<OptimalMethodModel> =
|
||||
huntOptimizerStore.optimizationResult.mapToList { it.optimalMethods }
|
||||
|
||||
override val columns: ListVal<Column<OptimalMethodModel>> =
|
||||
huntOptimizerStore.optimizationResult.mapToListVal { result ->
|
||||
override val columns: ListCell<Column<OptimalMethodModel>> =
|
||||
huntOptimizerStore.optimizationResult.mapToList { result ->
|
||||
var totalRuns = .0
|
||||
var totalTime = Duration.ZERO
|
||||
|
||||
@ -33,7 +33,7 @@ class OptimizationResultController(
|
||||
key = DIFF_COL,
|
||||
title = "Difficulty",
|
||||
width = 80,
|
||||
footer = value("Totals:"),
|
||||
footer = cell("Totals:"),
|
||||
),
|
||||
Column(
|
||||
key = METHOD_COL,
|
||||
@ -62,8 +62,8 @@ class OptimizationResultController(
|
||||
width = 60,
|
||||
textAlign = "right",
|
||||
tooltip = { it.runs.toString() },
|
||||
footer = value(totalRuns.toRoundedString(1)),
|
||||
footerTooltip = value(totalRuns.toString()),
|
||||
footer = cell(totalRuns.toRoundedString(1)),
|
||||
footerTooltip = cell(totalRuns.toString()),
|
||||
),
|
||||
Column(
|
||||
key = TOTAL_TIME_COL,
|
||||
@ -71,8 +71,8 @@ class OptimizationResultController(
|
||||
width = 60,
|
||||
textAlign = "right",
|
||||
tooltip = { it.totalTime.inHours.toString() },
|
||||
footer = value(totalTime.inHours.toRoundedString(1)),
|
||||
footerTooltip = value(totalTime.inHours.toString()),
|
||||
footer = cell(totalTime.inHours.toRoundedString(1)),
|
||||
footerTooltip = cell(totalTime.inHours.toString()),
|
||||
),
|
||||
*Array(result.wantedItems.size) { index ->
|
||||
val wanted = result.wantedItems[index]
|
||||
@ -86,8 +86,8 @@ class OptimizationResultController(
|
||||
width = 80,
|
||||
textAlign = "right",
|
||||
tooltip = { it.itemTypeIdToCount[wanted.id]?.toString() },
|
||||
footer = value(totalCount.toRoundedString(2)),
|
||||
footerTooltip = value(totalCount.toString()),
|
||||
footer = cell(totalCount.toRoundedString(2)),
|
||||
footerTooltip = cell(totalCount.toString()),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -1,9 +1,9 @@
|
||||
package world.phantasmal.web.huntOptimizer.controllers
|
||||
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.huntOptimizer.models.WantedItemModel
|
||||
import world.phantasmal.web.huntOptimizer.stores.HuntOptimizerStore
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
@ -12,14 +12,14 @@ import world.phantasmal.webui.controllers.Controller
|
||||
class WantedItemsController(
|
||||
private val huntOptimizerStore: HuntOptimizerStore,
|
||||
) : Controller() {
|
||||
private val selectableItemsFilter: MutableVal<(ItemType) -> Boolean> = mutableVal { true }
|
||||
private val selectableItemsFilter: MutableCell<(ItemType) -> Boolean> = mutableCell { true }
|
||||
|
||||
// TODO: Use ListVal.filtered with a Val when this is supported.
|
||||
val selectableItems: Val<List<ItemType>> = selectableItemsFilter.flatMap { filter ->
|
||||
// TODO: Use ListCell.filtered with a Cell when this is supported.
|
||||
val selectableItems: Cell<List<ItemType>> = selectableItemsFilter.flatMap { filter ->
|
||||
huntOptimizerStore.huntableItems.filtered(filter)
|
||||
}
|
||||
|
||||
val wantedItems: ListVal<WantedItemModel> = huntOptimizerStore.wantedItems
|
||||
val wantedItems: ListCell<WantedItemModel> = huntOptimizerStore.wantedItems
|
||||
|
||||
fun filterSelectableItems(text: String) {
|
||||
val sanitized = text.trim()
|
||||
|
@ -2,9 +2,9 @@ package world.phantasmal.web.huntOptimizer.models
|
||||
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.value.orElse
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.cell.orElse
|
||||
import kotlin.time.Duration
|
||||
|
||||
class HuntMethodModel(
|
||||
@ -16,7 +16,7 @@ class HuntMethodModel(
|
||||
*/
|
||||
val defaultTime: Duration,
|
||||
) {
|
||||
private val _userTime = mutableVal<Duration?>(null)
|
||||
private val _userTime = mutableCell<Duration?>(null)
|
||||
|
||||
val episode: Episode = quest.episode
|
||||
|
||||
@ -25,9 +25,9 @@ class HuntMethodModel(
|
||||
/**
|
||||
* The time it takes to complete the quest in hours as specified by the user.
|
||||
*/
|
||||
val userTime: Val<Duration?> = _userTime
|
||||
val userTime: Cell<Duration?> = _userTime
|
||||
|
||||
val time: Val<Duration> = userTime.orElse { defaultTime }
|
||||
val time: Cell<Duration> = userTime.orElse { defaultTime }
|
||||
|
||||
fun setUserTime(userTime: Duration?) {
|
||||
_userTime.value = userTime
|
||||
|
@ -1,13 +1,13 @@
|
||||
package world.phantasmal.web.huntOptimizer.models
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.shared.dto.ItemType
|
||||
|
||||
class WantedItemModel(val itemType: ItemType, amount: Int) {
|
||||
private val _amount = mutableVal(0)
|
||||
private val _amount = mutableCell(0)
|
||||
|
||||
val amount: Val<Int> = _amount
|
||||
val amount: Cell<Int> = _amount
|
||||
|
||||
init {
|
||||
setAmount(amount)
|
||||
|
@ -5,8 +5,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.web.core.loading.AssetLoader
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
@ -26,9 +26,9 @@ class HuntMethodStore(
|
||||
private val assetLoader: AssetLoader,
|
||||
private val huntMethodPersister: HuntMethodPersister,
|
||||
) : Store() {
|
||||
private val _methods = mutableListVal<HuntMethodModel> { arrayOf(it.time) }
|
||||
private val _methods = mutableListCell<HuntMethodModel> { arrayOf(it.time) }
|
||||
|
||||
val methods: ListVal<HuntMethodModel> by lazy {
|
||||
val methods: ListCell<HuntMethodModel> by lazy {
|
||||
observe(uiStore.server) { loadMethods(it) }
|
||||
_methods
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import mu.KotlinLogging
|
||||
import world.phantasmal.core.*
|
||||
import world.phantasmal.core.unsafe.UnsafeMap
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.core.models.Server
|
||||
import world.phantasmal.web.core.stores.EnemyDropTable
|
||||
import world.phantasmal.web.core.stores.ItemDropStore
|
||||
@ -44,11 +44,11 @@ class HuntOptimizerStore(
|
||||
private val itemTypeStore: ItemTypeStore,
|
||||
private val itemDropStore: ItemDropStore,
|
||||
) : Store() {
|
||||
private val _huntableItems = mutableListVal<ItemType>()
|
||||
private val _wantedItems = mutableListVal<WantedItemModel> { arrayOf(it.amount) }
|
||||
private val _optimizationResult = mutableVal(OptimizationResultModel(emptyList(), emptyList()))
|
||||
private val _huntableItems = mutableListCell<ItemType>()
|
||||
private val _wantedItems = mutableListCell<WantedItemModel> { arrayOf(it.amount) }
|
||||
private val _optimizationResult = mutableCell(OptimizationResultModel(emptyList(), emptyList()))
|
||||
|
||||
val huntableItems: ListVal<ItemType> by lazy {
|
||||
val huntableItems: ListCell<ItemType> by lazy {
|
||||
observe(uiStore.server) { server ->
|
||||
_huntableItems.clear()
|
||||
|
||||
@ -64,12 +64,12 @@ class HuntOptimizerStore(
|
||||
_huntableItems
|
||||
}
|
||||
|
||||
val wantedItems: ListVal<WantedItemModel> by lazy {
|
||||
val wantedItems: ListCell<WantedItemModel> by lazy {
|
||||
observe(uiStore.server) { loadWantedItems(it) }
|
||||
_wantedItems
|
||||
}
|
||||
|
||||
val optimizationResult: Val<OptimizationResultModel> = _optimizationResult
|
||||
val optimizationResult: Cell<OptimizationResultModel> = _optimizationResult
|
||||
|
||||
init {
|
||||
observe(wantedItems) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
package world.phantasmal.web.questEditor
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.falseCell
|
||||
|
||||
/**
|
||||
* Orchestrates everything related to emulating a quest run. Drives a VirtualMachine and
|
||||
* delegates to Debugger.
|
||||
*/
|
||||
class QuestRunner {
|
||||
val running: Val<Boolean> = falseVal()
|
||||
val running: Cell<Boolean> = falseCell()
|
||||
|
||||
fun stop() {
|
||||
// TODO
|
||||
|
@ -11,9 +11,9 @@ import mu.KotlinLogging
|
||||
import org.w3c.dom.Worker
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.observable.emitter
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.web.shared.JSON_FORMAT
|
||||
import world.phantasmal.web.shared.messages.*
|
||||
import kotlin.coroutines.Continuation
|
||||
@ -24,7 +24,7 @@ private val logger = KotlinLogging.logger {}
|
||||
class AsmAnalyser {
|
||||
private var inlineStackArgs: Boolean = true
|
||||
private var _mapDesignations = emitter<Map<Int, Int>>()
|
||||
private val _problems = mutableListVal<AssemblyProblem>()
|
||||
private val _problems = mutableListCell<AssemblyProblem>()
|
||||
|
||||
private val worker = Worker("/assembly-worker.js")
|
||||
private var nextRequestId = atomic(0)
|
||||
@ -35,7 +35,7 @@ class AsmAnalyser {
|
||||
private val inFlightRequests = mutableMapOf<Int, CancellableContinuation<*>>()
|
||||
|
||||
val mapDesignations: Observable<Map<Int, Int>> = _mapDesignations
|
||||
val problems: ListVal<AssemblyProblem> = _problems
|
||||
val problems: ListCell<AssemblyProblem> = _problems
|
||||
|
||||
init {
|
||||
worker.onmessage = { e ->
|
||||
|
@ -1,27 +1,27 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.not
|
||||
import world.phantasmal.observable.value.or
|
||||
import world.phantasmal.observable.value.orElse
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.not
|
||||
import world.phantasmal.observable.cell.or
|
||||
import world.phantasmal.observable.cell.orElse
|
||||
import world.phantasmal.web.externals.monacoEditor.ITextModel
|
||||
import world.phantasmal.web.externals.monacoEditor.createModel
|
||||
import world.phantasmal.web.questEditor.stores.AsmStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class AsmEditorController(private val store: AsmStore) : Controller() {
|
||||
val enabled: Val<Boolean> = store.editingEnabled
|
||||
val readOnly: Val<Boolean> = !enabled or store.textModel.isNull()
|
||||
val enabled: Cell<Boolean> = store.editingEnabled
|
||||
val readOnly: Cell<Boolean> = !enabled or store.textModel.isNull()
|
||||
|
||||
val textModel: Val<ITextModel> = store.textModel.orElse { EMPTY_MODEL }
|
||||
val textModel: Cell<ITextModel> = store.textModel.orElse { EMPTY_MODEL }
|
||||
|
||||
val didUndo: Observable<Unit> = store.didUndo
|
||||
val didRedo: Observable<Unit> = store.didRedo
|
||||
|
||||
val inlineStackArgs: Val<Boolean> = store.inlineStackArgs
|
||||
val inlineStackArgsEnabled: Val<Boolean> = store.problems.map { it.isEmpty() }
|
||||
val inlineStackArgsTooltip: Val<String> =
|
||||
val inlineStackArgs: Cell<Boolean> = store.inlineStackArgs
|
||||
val inlineStackArgsEnabled: Cell<Boolean> = store.problems.map { it.isEmpty() }
|
||||
val inlineStackArgsTooltip: Cell<String> =
|
||||
inlineStackArgsEnabled.map { enabled ->
|
||||
buildString {
|
||||
append("Transform arg_push* opcodes to be inline with the opcode the arguments are given to.")
|
||||
|
@ -2,10 +2,10 @@ package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.core.math.degToRad
|
||||
import world.phantasmal.core.math.radToDeg
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.observable.value.zeroIntVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.cell
|
||||
import world.phantasmal.observable.cell.list.emptyListCell
|
||||
import world.phantasmal.observable.cell.zeroIntCell
|
||||
import world.phantasmal.web.core.euler
|
||||
import world.phantasmal.web.externals.three.Euler
|
||||
import world.phantasmal.web.externals.three.Vector3
|
||||
@ -21,43 +21,43 @@ class EntityInfoController(
|
||||
private val areaStore: AreaStore,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
) : Controller() {
|
||||
val unavailable: Val<Boolean> = questEditorStore.selectedEntity.isNull()
|
||||
val enabled: Val<Boolean> = questEditorStore.questEditingEnabled
|
||||
val unavailable: Cell<Boolean> = questEditorStore.selectedEntity.isNull()
|
||||
val enabled: Cell<Boolean> = questEditorStore.questEditingEnabled
|
||||
|
||||
val type: Val<String> = questEditorStore.selectedEntity.map {
|
||||
val type: Cell<String> = questEditorStore.selectedEntity.map {
|
||||
it?.let { if (it is QuestNpcModel) "NPC" else "Object" } ?: ""
|
||||
}
|
||||
|
||||
val name: Val<String> = questEditorStore.selectedEntity.map { it?.type?.simpleName ?: "" }
|
||||
val name: Cell<String> = questEditorStore.selectedEntity.map { it?.type?.simpleName ?: "" }
|
||||
|
||||
val sectionId: Val<Int> = questEditorStore.selectedEntity
|
||||
.flatMap { it?.sectionId ?: zeroIntVal() }
|
||||
val sectionId: Cell<Int> = questEditorStore.selectedEntity
|
||||
.flatMap { it?.sectionId ?: zeroIntCell() }
|
||||
|
||||
val waveId: Val<Int> = questEditorStore.selectedEntity
|
||||
val waveId: Cell<Int> = questEditorStore.selectedEntity
|
||||
.flatMap { entity ->
|
||||
if (entity is QuestNpcModel) {
|
||||
entity.wave.map { it.id }
|
||||
} else {
|
||||
zeroIntVal()
|
||||
zeroIntCell()
|
||||
}
|
||||
}
|
||||
|
||||
val waveHidden: Val<Boolean> = questEditorStore.selectedEntity.map { it !is QuestNpcModel }
|
||||
val waveHidden: Cell<Boolean> = questEditorStore.selectedEntity.map { it !is QuestNpcModel }
|
||||
|
||||
private val pos: Val<Vector3> =
|
||||
private val pos: Cell<Vector3> =
|
||||
questEditorStore.selectedEntity.flatMap { it?.position ?: DEFAULT_POSITION }
|
||||
val posX: Val<Double> = pos.map { it.x }
|
||||
val posY: Val<Double> = pos.map { it.y }
|
||||
val posZ: Val<Double> = pos.map { it.z }
|
||||
val posX: Cell<Double> = pos.map { it.x }
|
||||
val posY: Cell<Double> = pos.map { it.y }
|
||||
val posZ: Cell<Double> = pos.map { it.z }
|
||||
|
||||
private val rot: Val<Euler> =
|
||||
private val rot: Cell<Euler> =
|
||||
questEditorStore.selectedEntity.flatMap { it?.rotation ?: DEFAULT_ROTATION }
|
||||
val rotX: Val<Double> = rot.map { radToDeg(it.x) }
|
||||
val rotY: Val<Double> = rot.map { radToDeg(it.y) }
|
||||
val rotZ: Val<Double> = rot.map { radToDeg(it.z) }
|
||||
val rotX: Cell<Double> = rot.map { radToDeg(it.x) }
|
||||
val rotY: Cell<Double> = rot.map { radToDeg(it.y) }
|
||||
val rotZ: Cell<Double> = rot.map { radToDeg(it.z) }
|
||||
|
||||
val props: Val<List<QuestEntityPropModel>> =
|
||||
questEditorStore.selectedEntity.flatMap { it?.properties ?: emptyListVal() }
|
||||
val props: Cell<List<QuestEntityPropModel>> =
|
||||
questEditorStore.selectedEntity.flatMap { it?.properties ?: emptyListCell() }
|
||||
|
||||
fun focused() {
|
||||
questEditorStore.makeMainUndoCurrent()
|
||||
@ -178,7 +178,7 @@ class EntityInfoController(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_POSITION = value(Vector3(0.0, 0.0, 0.0))
|
||||
private val DEFAULT_ROTATION = value(euler(0.0, 0.0, 0.0))
|
||||
private val DEFAULT_POSITION = cell(Vector3(0.0, 0.0, 0.0))
|
||||
private val DEFAULT_ROTATION = cell(euler(0.0, 0.0, 0.0))
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.lib.fileFormats.quest.ObjectType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
@ -13,9 +13,9 @@ class EntityListController(store: QuestEditorStore, private val npcs: Boolean) :
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val entityTypes = (if (npcs) NpcType.VALUES else ObjectType.VALUES) as Array<EntityType>
|
||||
|
||||
val enabled: Val<Boolean> = store.questEditingEnabled
|
||||
val enabled: Cell<Boolean> = store.questEditingEnabled
|
||||
|
||||
val entities: Val<List<EntityType>> =
|
||||
val entities: Cell<List<EntityType>> =
|
||||
map(store.currentQuest, store.currentArea) { quest, area ->
|
||||
val episode = quest?.episode ?: Episode.I
|
||||
val areaId = area?.id ?: 0
|
||||
|
@ -1,12 +1,12 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.and
|
||||
import world.phantasmal.observable.value.eq
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
import world.phantasmal.observable.value.list.flatMapToList
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.and
|
||||
import world.phantasmal.observable.cell.eq
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.emptyListCell
|
||||
import world.phantasmal.observable.cell.list.flatMapToList
|
||||
import world.phantasmal.observable.cell.list.listCell
|
||||
import world.phantasmal.web.questEditor.actions.*
|
||||
import world.phantasmal.web.questEditor.models.QuestEventActionModel
|
||||
import world.phantasmal.web.questEditor.models.QuestEventModel
|
||||
@ -14,20 +14,20 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class EventsController(private val store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.isNull()
|
||||
val enabled: Val<Boolean> = store.questEditingEnabled
|
||||
val removeEventEnabled: Val<Boolean> = enabled and store.selectedEvent.isNotNull()
|
||||
val unavailable: Cell<Boolean> = store.currentQuest.isNull()
|
||||
val enabled: Cell<Boolean> = store.questEditingEnabled
|
||||
val removeEventEnabled: Cell<Boolean> = enabled and store.selectedEvent.isNotNull()
|
||||
|
||||
val events: ListVal<QuestEventModel> =
|
||||
val events: ListCell<QuestEventModel> =
|
||||
flatMapToList(store.currentQuest, store.currentArea) { quest, area ->
|
||||
if (quest != null && area != null) {
|
||||
quest.events.filtered { it.areaId == area.id }
|
||||
} else {
|
||||
emptyListVal()
|
||||
emptyListCell()
|
||||
}
|
||||
}
|
||||
|
||||
val eventActionTypes: ListVal<String> = listVal(
|
||||
val eventActionTypes: ListCell<String> = listCell(
|
||||
QuestEventActionModel.SpawnNpcs.SHORT_NAME,
|
||||
QuestEventActionModel.Door.Unlock.SHORT_NAME,
|
||||
QuestEventActionModel.Door.Lock.SHORT_NAME,
|
||||
@ -42,7 +42,7 @@ class EventsController(private val store: QuestEditorStore) : Controller() {
|
||||
store.makeMainUndoCurrent()
|
||||
}
|
||||
|
||||
fun isSelected(event: QuestEventModel): Val<Boolean> =
|
||||
fun isSelected(event: QuestEventModel): Cell<Boolean> =
|
||||
store.selectedEvent eq event
|
||||
|
||||
fun selectEvent(event: QuestEventModel?) {
|
||||
|
@ -1,17 +1,17 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.emptyListCell
|
||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class NpcCountsController(private val store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.isNull()
|
||||
val unavailable: Cell<Boolean> = store.currentQuest.isNull()
|
||||
|
||||
val npcCounts: Val<List<NameWithCount>> = store.currentQuest
|
||||
.flatMap { it?.npcs ?: emptyListVal() }
|
||||
val npcCounts: Cell<List<NameWithCount>> = store.currentQuest
|
||||
.flatMap { it?.npcs ?: emptyListCell() }
|
||||
.map(::countNpcs)
|
||||
|
||||
fun focused() {
|
||||
|
@ -6,7 +6,7 @@ import world.phantasmal.core.*
|
||||
import world.phantasmal.lib.Endianness
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.fileFormats.quest.*
|
||||
import world.phantasmal.observable.value.*
|
||||
import world.phantasmal.observable.cell.*
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.files.cursor
|
||||
import world.phantasmal.web.core.files.writeBuffer
|
||||
@ -30,22 +30,22 @@ class QuestEditorToolbarController(
|
||||
private val areaStore: AreaStore,
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
) : Controller() {
|
||||
private val _resultDialogVisible = mutableVal(false)
|
||||
private val _result = mutableVal<PwResult<*>?>(null)
|
||||
private val saving = mutableVal(false)
|
||||
private val _resultDialogVisible = mutableCell(false)
|
||||
private val _result = mutableCell<PwResult<*>?>(null)
|
||||
private val saving = mutableCell(false)
|
||||
|
||||
// We mainly disable saving while a save is underway for visual feedback that a save is
|
||||
// happening/has happened.
|
||||
private val savingEnabled = questEditorStore.currentQuest.isNotNull() and !saving
|
||||
private val _saveAsDialogVisible = mutableVal(false)
|
||||
private val fileHolder = mutableVal<FileHolder?>(null)
|
||||
private val _filename = mutableVal("")
|
||||
private val _version = mutableVal(Version.BB)
|
||||
private val _saveAsDialogVisible = mutableCell(false)
|
||||
private val fileHolder = mutableCell<FileHolder?>(null)
|
||||
private val _filename = mutableCell("")
|
||||
private val _version = mutableCell(Version.BB)
|
||||
|
||||
// Result
|
||||
|
||||
val resultDialogVisible: Val<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
val resultDialogVisible: Cell<Boolean> = _resultDialogVisible
|
||||
val result: Cell<PwResult<*>?> = _result
|
||||
|
||||
val supportedFileTypes = listOf(
|
||||
FileType(
|
||||
@ -56,43 +56,43 @@ class QuestEditorToolbarController(
|
||||
|
||||
// Saving
|
||||
|
||||
val saveEnabled: Val<Boolean> =
|
||||
val saveEnabled: Cell<Boolean> =
|
||||
savingEnabled and questEditorStore.canSaveChanges and UserAgentFeatures.fileSystemApi
|
||||
val saveTooltip: Val<String> =
|
||||
val saveTooltip: Cell<String> =
|
||||
if (UserAgentFeatures.fileSystemApi) {
|
||||
questEditorStore.canSaveChanges.map {
|
||||
(if (it) "Save changes" else "No changes to save") + " (Ctrl-S)"
|
||||
}
|
||||
} else {
|
||||
value("This browser doesn't support saving changes to existing files")
|
||||
cell("This browser doesn't support saving changes to existing files")
|
||||
}
|
||||
val saveAsEnabled: Val<Boolean> = savingEnabled
|
||||
val saveAsDialogVisible: Val<Boolean> = _saveAsDialogVisible
|
||||
val saveAsEnabled: Cell<Boolean> = savingEnabled
|
||||
val saveAsDialogVisible: Cell<Boolean> = _saveAsDialogVisible
|
||||
val showSaveAsDialogNameField: Boolean = !UserAgentFeatures.fileSystemApi
|
||||
val filename: Val<String> = _filename
|
||||
val version: Val<Version> = _version
|
||||
val filename: Cell<String> = _filename
|
||||
val version: Cell<Version> = _version
|
||||
|
||||
// Undo
|
||||
|
||||
val undoTooltip: Val<String> = questEditorStore.firstUndo.map { action ->
|
||||
val undoTooltip: Cell<String> = questEditorStore.firstUndo.map { action ->
|
||||
(action?.let { "Undo \"${action.description}\"" } ?: "Nothing to undo") + " (Ctrl-Z)"
|
||||
}
|
||||
|
||||
val undoEnabled: Val<Boolean> = questEditorStore.canUndo
|
||||
val undoEnabled: Cell<Boolean> = questEditorStore.canUndo
|
||||
|
||||
// Redo
|
||||
|
||||
val redoTooltip: Val<String> = questEditorStore.firstRedo.map { action ->
|
||||
val redoTooltip: Cell<String> = questEditorStore.firstRedo.map { action ->
|
||||
(action?.let { "Redo \"${action.description}\"" } ?: "Nothing to redo") + " (Ctrl-Shift-Z)"
|
||||
}
|
||||
|
||||
val redoEnabled: Val<Boolean> = questEditorStore.canRedo
|
||||
val redoEnabled: Cell<Boolean> = questEditorStore.canRedo
|
||||
|
||||
// Areas
|
||||
|
||||
// Ensure the areas list is updated when entities are added or removed (the count in the label
|
||||
// should update).
|
||||
val areas: Val<List<AreaAndLabel>> = questEditorStore.currentQuest.flatMap { quest ->
|
||||
val areas: Cell<List<AreaAndLabel>> = questEditorStore.currentQuest.flatMap { quest ->
|
||||
quest?.let {
|
||||
map(quest.entitiesPerArea, quest.areaVariants) { entitiesPerArea, variants ->
|
||||
areaStore.getAreasForEpisode(quest.episode).map { area ->
|
||||
@ -101,18 +101,18 @@ class QuestEditorToolbarController(
|
||||
AreaAndLabel(area, name + (entityCount?.let { " ($it)" } ?: ""))
|
||||
}
|
||||
}
|
||||
} ?: value(emptyList())
|
||||
} ?: cell(emptyList())
|
||||
}
|
||||
|
||||
val currentArea: Val<AreaAndLabel?> = map(areas, questEditorStore.currentArea) { areas, area ->
|
||||
val currentArea: Cell<AreaAndLabel?> = map(areas, questEditorStore.currentArea) { areas, area ->
|
||||
areas.find { it.area == area }
|
||||
}
|
||||
|
||||
val areaSelectEnabled: Val<Boolean> = questEditorStore.currentQuest.isNotNull()
|
||||
val areaSelectEnabled: Cell<Boolean> = questEditorStore.currentQuest.isNotNull()
|
||||
|
||||
// Settings
|
||||
|
||||
val showCollisionGeometry: Val<Boolean> = questEditorStore.showCollisionGeometry
|
||||
val showCollisionGeometry: Cell<Boolean> = questEditorStore.showCollisionGeometry
|
||||
|
||||
init {
|
||||
addDisposables(
|
||||
|
@ -1,23 +1,23 @@
|
||||
package world.phantasmal.web.questEditor.controllers
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.emptyStringVal
|
||||
import world.phantasmal.observable.value.value
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.cell
|
||||
import world.phantasmal.observable.cell.emptyStringCell
|
||||
import world.phantasmal.web.questEditor.actions.EditPropertyAction
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.controllers.Controller
|
||||
|
||||
class QuestInfoController(private val store: QuestEditorStore) : Controller() {
|
||||
val unavailable: Val<Boolean> = store.currentQuest.isNull()
|
||||
val enabled: Val<Boolean> = store.questEditingEnabled
|
||||
val unavailable: Cell<Boolean> = store.currentQuest.isNull()
|
||||
val enabled: Cell<Boolean> = store.questEditingEnabled
|
||||
|
||||
val episode: Val<String> = store.currentQuest.map { it?.episode?.name ?: "" }
|
||||
val id: Val<Int> = store.currentQuest.flatMap { it?.id ?: value(0) }
|
||||
val name: Val<String> = store.currentQuest.flatMap { it?.name ?: emptyStringVal() }
|
||||
val shortDescription: Val<String> =
|
||||
store.currentQuest.flatMap { it?.shortDescription ?: emptyStringVal() }
|
||||
val longDescription: Val<String> =
|
||||
store.currentQuest.flatMap { it?.longDescription ?: emptyStringVal() }
|
||||
val episode: Cell<String> = store.currentQuest.map { it?.episode?.name ?: "" }
|
||||
val id: Cell<Int> = store.currentQuest.flatMap { it?.id ?: cell(0) }
|
||||
val name: Cell<String> = store.currentQuest.flatMap { it?.name ?: emptyStringCell() }
|
||||
val shortDescription: Cell<String> =
|
||||
store.currentQuest.flatMap { it?.shortDescription ?: emptyStringCell() }
|
||||
val longDescription: Cell<String> =
|
||||
store.currentQuest.flatMap { it?.longDescription ?: emptyStringCell() }
|
||||
|
||||
fun focused() {
|
||||
store.makeMainUndoCurrent()
|
||||
|
@ -1,17 +1,17 @@
|
||||
package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.core.requireNonNegative
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.mutableListVal
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
|
||||
class AreaVariantModel(val id: Int, val area: AreaModel) {
|
||||
private val _sections = mutableListVal<SectionModel>()
|
||||
private val _sections = mutableListCell<SectionModel>()
|
||||
|
||||
// Exception for Seaside Area at Night, variant 1.
|
||||
// Phantasmal World 4 and Lost heart breaker use this to have two tower maps.
|
||||
val name: String = if (area.id == 16 && id == 1) "West Tower" else area.name
|
||||
|
||||
val sections: ListVal<SectionModel> = _sections
|
||||
val sections: ListCell<SectionModel> = _sections
|
||||
|
||||
init {
|
||||
requireNonNegative(id, "id")
|
||||
|
@ -3,10 +3,10 @@ package world.phantasmal.web.questEditor.models
|
||||
import world.phantasmal.core.math.floorMod
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityType
|
||||
import world.phantasmal.lib.fileFormats.quest.QuestEntity
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.listCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToEuler
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToThree
|
||||
@ -24,38 +24,38 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
*/
|
||||
val entity: Entity,
|
||||
) {
|
||||
private val _sectionId = mutableVal(entity.sectionId.toInt())
|
||||
private val _section = mutableVal<SectionModel?>(null)
|
||||
private val _sectionInitialized = mutableVal(false)
|
||||
private val _position = mutableVal(vec3ToThree(entity.position))
|
||||
private val _worldPosition = mutableVal(_position.value)
|
||||
private val _rotation = mutableVal(vec3ToEuler(entity.rotation))
|
||||
private val _worldRotation = mutableVal(_rotation.value)
|
||||
private val _sectionId = mutableCell(entity.sectionId.toInt())
|
||||
private val _section = mutableCell<SectionModel?>(null)
|
||||
private val _sectionInitialized = mutableCell(false)
|
||||
private val _position = mutableCell(vec3ToThree(entity.position))
|
||||
private val _worldPosition = mutableCell(_position.value)
|
||||
private val _rotation = mutableCell(vec3ToEuler(entity.rotation))
|
||||
private val _worldRotation = mutableCell(_rotation.value)
|
||||
|
||||
val type: Type get() = entity.type
|
||||
|
||||
val areaId: Int get() = entity.areaId
|
||||
|
||||
val sectionId: Val<Int> = _sectionId
|
||||
val sectionId: Cell<Int> = _sectionId
|
||||
|
||||
val section: Val<SectionModel?> = _section
|
||||
val sectionInitialized: Val<Boolean> = _sectionInitialized
|
||||
val section: Cell<SectionModel?> = _section
|
||||
val sectionInitialized: Cell<Boolean> = _sectionInitialized
|
||||
|
||||
/**
|
||||
* Section-relative position
|
||||
*/
|
||||
val position: Val<Vector3> = _position
|
||||
val position: Cell<Vector3> = _position
|
||||
|
||||
val worldPosition: Val<Vector3> = _worldPosition
|
||||
val worldPosition: Cell<Vector3> = _worldPosition
|
||||
|
||||
/**
|
||||
* Section-relative rotation
|
||||
*/
|
||||
val rotation: Val<Euler> = _rotation
|
||||
val rotation: Cell<Euler> = _rotation
|
||||
|
||||
val worldRotation: Val<Euler> = _worldRotation
|
||||
val worldRotation: Cell<Euler> = _worldRotation
|
||||
|
||||
val properties: ListVal<QuestEntityPropModel> = listVal(*Array(type.properties.size) {
|
||||
val properties: ListCell<QuestEntityPropModel> = listCell(*Array(type.properties.size) {
|
||||
QuestEntityPropModel(this, type.properties[it])
|
||||
})
|
||||
|
||||
|
@ -5,12 +5,12 @@ import world.phantasmal.lib.fileFormats.ninja.radToAngle
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityProp
|
||||
import world.phantasmal.lib.fileFormats.quest.EntityPropType
|
||||
import world.phantasmal.lib.fileFormats.quest.ObjectType
|
||||
import world.phantasmal.observable.value.MutableVal
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.MutableCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
class QuestEntityPropModel(private val entity: QuestEntityModel<*, *>, prop: EntityProp) {
|
||||
private val _value: MutableVal<Any> = mutableVal(when (prop.type) {
|
||||
private val _value: MutableCell<Any> = mutableCell(when (prop.type) {
|
||||
EntityPropType.I32 -> entity.entity.data.getInt(prop.offset)
|
||||
EntityPropType.F32 -> entity.entity.data.getFloat(prop.offset)
|
||||
EntityPropType.Angle -> angleToRad(entity.entity.data.getInt(prop.offset))
|
||||
@ -48,7 +48,7 @@ class QuestEntityPropModel(private val entity: QuestEntityModel<*, *>, prop: Ent
|
||||
val name: String = prop.name
|
||||
val offset = prop.offset
|
||||
val type: EntityPropType = prop.type
|
||||
val value: Val<Any> = _value
|
||||
val value: Cell<Any> = _value
|
||||
|
||||
fun setValue(value: Any, propagateToEntity: Boolean = true) {
|
||||
when (type) {
|
||||
|
@ -1,18 +1,18 @@
|
||||
package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
sealed class QuestEventActionModel {
|
||||
abstract val shortName: String
|
||||
|
||||
class SpawnNpcs(sectionId: Int, appearFlag: Int) : QuestEventActionModel() {
|
||||
private val _sectionId = mutableVal(sectionId)
|
||||
private val _appearFlag = mutableVal(appearFlag)
|
||||
private val _sectionId = mutableCell(sectionId)
|
||||
private val _appearFlag = mutableCell(appearFlag)
|
||||
|
||||
override val shortName = SHORT_NAME
|
||||
val sectionId: Val<Int> = _sectionId
|
||||
val appearFlag: Val<Int> = _appearFlag
|
||||
val sectionId: Cell<Int> = _sectionId
|
||||
val appearFlag: Cell<Int> = _appearFlag
|
||||
|
||||
fun setSectionId(sectionId: Int) {
|
||||
_sectionId.value = sectionId
|
||||
@ -28,9 +28,9 @@ sealed class QuestEventActionModel {
|
||||
}
|
||||
|
||||
sealed class Door(doorId: Int) : QuestEventActionModel() {
|
||||
private val _doorId = mutableVal(doorId)
|
||||
private val _doorId = mutableCell(doorId)
|
||||
|
||||
val doorId: Val<Int> = _doorId
|
||||
val doorId: Cell<Int> = _doorId
|
||||
|
||||
fun setDoorId(doorId: Int) {
|
||||
_doorId.value = doorId
|
||||
@ -54,10 +54,10 @@ sealed class QuestEventActionModel {
|
||||
}
|
||||
|
||||
class TriggerEvent(eventId: Int) : QuestEventActionModel() {
|
||||
private val _eventId = mutableVal(eventId)
|
||||
private val _eventId = mutableCell(eventId)
|
||||
|
||||
override val shortName = SHORT_NAME
|
||||
val eventId: Val<Int> = _eventId
|
||||
val eventId: Cell<Int> = _eventId
|
||||
|
||||
fun setEventId(eventId: Int) {
|
||||
_eventId.value = eventId
|
||||
|
@ -1,10 +1,10 @@
|
||||
package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.SimpleListVal
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.SimpleListCell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
class QuestEventModel(
|
||||
id: Int,
|
||||
@ -15,19 +15,19 @@ class QuestEventModel(
|
||||
val unknown: Int,
|
||||
actions: MutableList<QuestEventActionModel>,
|
||||
) {
|
||||
private val _id = mutableVal(id)
|
||||
private val _sectionId = mutableVal(sectionId)
|
||||
private val _waveId = mutableVal(waveId)
|
||||
private val _delay = mutableVal(delay)
|
||||
private val _actions = SimpleListVal(actions)
|
||||
private val _id = mutableCell(id)
|
||||
private val _sectionId = mutableCell(sectionId)
|
||||
private val _waveId = mutableCell(waveId)
|
||||
private val _delay = mutableCell(delay)
|
||||
private val _actions = SimpleListCell(actions)
|
||||
|
||||
val id: Val<Int> = _id
|
||||
val sectionId: Val<Int> = _sectionId
|
||||
val wave: Val<WaveModel> = map(_waveId, _sectionId) { id, sectionId ->
|
||||
val id: Cell<Int> = _id
|
||||
val sectionId: Cell<Int> = _sectionId
|
||||
val wave: Cell<WaveModel> = map(_waveId, _sectionId) { id, sectionId ->
|
||||
WaveModel(id, areaId, sectionId)
|
||||
}
|
||||
val delay: Val<Int> = _delay
|
||||
val actions: ListVal<QuestEventActionModel> = _actions
|
||||
val delay: Cell<Int> = _delay
|
||||
val actions: ListCell<QuestEventActionModel> = _actions
|
||||
|
||||
fun setId(id: Int) {
|
||||
_id.value = id
|
||||
|
@ -3,13 +3,13 @@ package world.phantasmal.web.questEditor.models
|
||||
import world.phantasmal.lib.Episode
|
||||
import world.phantasmal.lib.asm.BytecodeIr
|
||||
import world.phantasmal.lib.fileFormats.quest.DatUnknown
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.list.ListVal
|
||||
import world.phantasmal.observable.value.list.SimpleListVal
|
||||
import world.phantasmal.observable.value.list.flatMapToList
|
||||
import world.phantasmal.observable.value.list.listVal
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.SimpleListCell
|
||||
import world.phantasmal.observable.cell.list.flatMapToList
|
||||
import world.phantasmal.observable.cell.list.listCell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
class QuestModel(
|
||||
id: Int,
|
||||
@ -30,41 +30,41 @@ class QuestModel(
|
||||
val shopItems: UIntArray,
|
||||
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
|
||||
) {
|
||||
private val _id = mutableVal(0)
|
||||
private val _language = mutableVal(0)
|
||||
private val _name = mutableVal("")
|
||||
private val _shortDescription = mutableVal("")
|
||||
private val _longDescription = mutableVal("")
|
||||
private val _mapDesignations = mutableVal(mapDesignations)
|
||||
private val _npcs = SimpleListVal(npcs) { arrayOf(it.sectionInitialized, it.wave) }
|
||||
private val _objects = SimpleListVal(objects) { arrayOf(it.sectionInitialized) }
|
||||
private val _events = SimpleListVal(events)
|
||||
private val _id = mutableCell(0)
|
||||
private val _language = mutableCell(0)
|
||||
private val _name = mutableCell("")
|
||||
private val _shortDescription = mutableCell("")
|
||||
private val _longDescription = mutableCell("")
|
||||
private val _mapDesignations = mutableCell(mapDesignations)
|
||||
private val _npcs = SimpleListCell(npcs) { arrayOf(it.sectionInitialized, it.wave) }
|
||||
private val _objects = SimpleListCell(objects) { arrayOf(it.sectionInitialized) }
|
||||
private val _events = SimpleListCell(events)
|
||||
|
||||
val id: Val<Int> = _id
|
||||
val language: Val<Int> = _language
|
||||
val name: Val<String> = _name
|
||||
val shortDescription: Val<String> = _shortDescription
|
||||
val longDescription: Val<String> = _longDescription
|
||||
val id: Cell<Int> = _id
|
||||
val language: Cell<Int> = _language
|
||||
val name: Cell<String> = _name
|
||||
val shortDescription: Cell<String> = _shortDescription
|
||||
val longDescription: Cell<String> = _longDescription
|
||||
|
||||
/**
|
||||
* Map of area IDs to area variant IDs. One designation per area.
|
||||
*/
|
||||
val mapDesignations: Val<Map<Int, Int>> = _mapDesignations
|
||||
val mapDesignations: Cell<Map<Int, Int>> = _mapDesignations
|
||||
|
||||
/**
|
||||
* Map of area IDs to entity counts.
|
||||
*/
|
||||
val entitiesPerArea: Val<Map<Int, Int>>
|
||||
val entitiesPerArea: Cell<Map<Int, Int>>
|
||||
|
||||
/**
|
||||
* One variant per area.
|
||||
*/
|
||||
val areaVariants: ListVal<AreaVariantModel>
|
||||
val areaVariants: ListCell<AreaVariantModel>
|
||||
|
||||
val npcs: ListVal<QuestNpcModel> = _npcs
|
||||
val objects: ListVal<QuestObjectModel> = _objects
|
||||
val npcs: ListCell<QuestNpcModel> = _npcs
|
||||
val objects: ListCell<QuestObjectModel> = _objects
|
||||
|
||||
val events: ListVal<QuestEventModel> = _events
|
||||
val events: ListCell<QuestEventModel> = _events
|
||||
|
||||
var bytecodeIr: BytecodeIr = bytecodeIr
|
||||
private set
|
||||
@ -106,7 +106,7 @@ class QuestModel(
|
||||
}
|
||||
}
|
||||
|
||||
listVal(*variants.values.toTypedArray())
|
||||
listCell(*variants.values.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,14 @@ package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.NpcType
|
||||
import world.phantasmal.lib.fileFormats.quest.QuestNpc
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.map
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
class QuestNpcModel(npc: QuestNpc, waveId: Int) : QuestEntityModel<NpcType, QuestNpc>(npc) {
|
||||
private val _waveId = mutableVal(waveId)
|
||||
private val _waveId = mutableCell(waveId)
|
||||
|
||||
val wave: Val<WaveModel> = map(_waveId, sectionId) { id, sectionId ->
|
||||
val wave: Cell<WaveModel> = map(_waveId, sectionId) { id, sectionId ->
|
||||
WaveModel(id, areaId, sectionId)
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,13 @@ package world.phantasmal.web.questEditor.models
|
||||
|
||||
import world.phantasmal.lib.fileFormats.quest.ObjectType
|
||||
import world.phantasmal.lib.fileFormats.quest.QuestObject
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.mutableVal
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
|
||||
class QuestObjectModel(obj: QuestObject) : QuestEntityModel<ObjectType, QuestObject>(obj) {
|
||||
private val _model = mutableVal(obj.model)
|
||||
private val _model = mutableCell(obj.model)
|
||||
|
||||
val model: Val<Int?> = _model
|
||||
val model: Cell<Int?> = _model
|
||||
|
||||
fun setModel(model: Int, propagateToProps: Boolean = true) {
|
||||
_model.value = model
|
||||
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import world.phantasmal.observable.value.list.emptyListVal
|
||||
import world.phantasmal.observable.cell.list.emptyListCell
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
@ -32,7 +32,7 @@ class QuestEditorMeshManager(
|
||||
(wave == null || it.wave.value == wave)
|
||||
}
|
||||
} else {
|
||||
emptyListVal()
|
||||
emptyListCell()
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -47,7 +47,7 @@ class QuestEditorMeshManager(
|
||||
it.sectionInitialized.value && it.areaId == area.id
|
||||
}
|
||||
} else {
|
||||
emptyListVal()
|
||||
emptyListCell()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user