mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-03 13:58:28 +08:00
Added mutateDeferred and made some other changes to avoid having to use setTimeout to change observables in response to observables changing. This fixes some unit tests.
This commit is contained in:
parent
9aa963fd3b
commit
9cc6c51b9c
@ -9,12 +9,12 @@ class TrackedDisposableTests {
|
||||
@Test
|
||||
fun is_correctly_tracked() {
|
||||
assertFails {
|
||||
checkNoDisposableLeaks {
|
||||
DisposableTracking.checkNoLeaks {
|
||||
object : TrackedDisposable() {}
|
||||
}
|
||||
}
|
||||
|
||||
checkNoDisposableLeaks {
|
||||
DisposableTracking.checkNoLeaks {
|
||||
val disposable = object : TrackedDisposable() {}
|
||||
disposable.dispose()
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ abstract class AbstractDependency<T> : Dependency<T> {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
ChangeManager.changeDependency {
|
||||
MutationManager.changeDependency {
|
||||
emitDependencyInvalidated()
|
||||
block()
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class CallbackChangeObserver<T, E : ChangeEvent<T>>(
|
||||
}
|
||||
|
||||
override fun dependencyInvalidated(dependency: Dependency<*>) {
|
||||
ChangeManager.invalidated(this)
|
||||
MutationManager.invalidated(this)
|
||||
}
|
||||
|
||||
override fun pull() {
|
||||
|
@ -25,7 +25,7 @@ class CallbackObserver(
|
||||
}
|
||||
|
||||
override fun dependencyInvalidated(dependency: Dependency<*>) {
|
||||
ChangeManager.invalidated(this)
|
||||
MutationManager.invalidated(this)
|
||||
}
|
||||
|
||||
override fun pull() {
|
||||
|
@ -1,10 +0,0 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
/**
|
||||
* Defer propagation of changes to observables until the end of a code block. All changes to
|
||||
* observables in a single change set won't be propagated to their dependencies until the change set
|
||||
* is completed.
|
||||
*/
|
||||
fun change(block: () -> Unit) {
|
||||
ChangeManager.inChangeSet(block)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
/**
|
||||
* Defer propagation of changes to observables until the end of a code block. All changes to
|
||||
* observables in a single mutation won't be propagated to their dependencies until the mutation is
|
||||
* completed.
|
||||
*/
|
||||
inline fun mutate(block: () -> Unit) {
|
||||
MutationManager.mutate(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a mutation to run right after the current mutation finishes. You can use this to change
|
||||
* observables in an observer callback. This is usually a bad idea, but sometimes the situation
|
||||
* where you have to change observables in response to observables changing is very hard to avoid.
|
||||
*/
|
||||
fun mutateDeferred(block: () -> Unit) {
|
||||
MutationManager.mutateDeferred(block)
|
||||
}
|
@ -8,17 +8,32 @@ import kotlin.contracts.contract
|
||||
// Dependencies will need to partially apply ListChangeEvents etc. and remember which part of
|
||||
// the event they've already applied (i.e. an index into the changes list).
|
||||
// TODO: Think about nested change sets. Initially don't allow nesting?
|
||||
object ChangeManager {
|
||||
object MutationManager {
|
||||
private val invalidatedLeaves = HashSet<LeafDependent>()
|
||||
|
||||
/** Whether a dependency's value is changing at the moment. */
|
||||
private var dependencyChanging = false
|
||||
|
||||
fun inChangeSet(block: () -> Unit) {
|
||||
// TODO: Implement inChangeSet correctly.
|
||||
private val deferredMutations: MutableList<() -> Unit> = mutableListOf()
|
||||
private var applyingDeferredMutations = false
|
||||
|
||||
inline fun mutate(block: () -> Unit) {
|
||||
contract {
|
||||
callsInPlace(block, EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
// TODO: Implement mutate correctly.
|
||||
block()
|
||||
}
|
||||
|
||||
fun mutateDeferred(block: () -> Unit) {
|
||||
if (dependencyChanging) {
|
||||
deferredMutations.add(block)
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidated(dependent: LeafDependent) {
|
||||
invalidatedLeaves.add(dependent)
|
||||
}
|
||||
@ -51,6 +66,21 @@ object ChangeManager {
|
||||
} finally {
|
||||
dependencyChanging = false
|
||||
invalidatedLeaves.clear()
|
||||
|
||||
if (!applyingDeferredMutations) {
|
||||
try {
|
||||
applyingDeferredMutations = true
|
||||
var i = 0
|
||||
|
||||
while (i < deferredMutations.size) {
|
||||
deferredMutations[i]()
|
||||
i++
|
||||
}
|
||||
} finally {
|
||||
applyingDeferredMutations = false
|
||||
deferredMutations.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package world.phantasmal.observable
|
||||
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MutationTests : ObservableTestSuite {
|
||||
@Test
|
||||
fun can_change_observed_cell_with_mutateDeferred() = test {
|
||||
val cell = mutableCell(0)
|
||||
var observerCalls = 0
|
||||
|
||||
disposer.add(cell.observe {
|
||||
observerCalls++
|
||||
|
||||
if (it < 10) {
|
||||
mutateDeferred {
|
||||
cell.value++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cell.value = 1
|
||||
|
||||
assertEquals(10, observerCalls)
|
||||
assertEquals(10, cell.value)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package world.phantasmal.observable.cell
|
||||
|
||||
import world.phantasmal.observable.change
|
||||
import world.phantasmal.observable.mutate
|
||||
import world.phantasmal.observable.test.ObservableTestSuite
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -16,7 +16,7 @@ class ChangeTests : ObservableTestSuite {
|
||||
disposer.add(dependent.observeChange { dependentObservedValue = it.value })
|
||||
|
||||
assertFails {
|
||||
change {
|
||||
mutate {
|
||||
dependency.value = 11
|
||||
throw Exception()
|
||||
}
|
||||
@ -27,7 +27,7 @@ class ChangeTests : ObservableTestSuite {
|
||||
assertEquals(22, dependent.value)
|
||||
|
||||
// The machinery behind change is still in a valid state.
|
||||
change {
|
||||
mutate {
|
||||
dependency.value = 13
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.core.controllers
|
||||
|
||||
import kotlinx.browser.window
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.mutateDeferred
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.stores.UiStore
|
||||
import world.phantasmal.webui.controllers.Tab
|
||||
@ -38,11 +38,11 @@ open class PathAwareTabContainerController<T : PathAwareTab>(
|
||||
super.visibleChanged(visible)
|
||||
|
||||
if (visible) {
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
setPathPrefix(activeTab.value, replace = true)
|
||||
}, 0)
|
||||
mutateDeferred {
|
||||
if (!disposed) {
|
||||
setPathPrefix(activeTab.value, replace = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,19 +20,21 @@ abstract class Renderer : DisposableContainer() {
|
||||
private var animationFrameHandle: Int = 0
|
||||
|
||||
fun startRendering() {
|
||||
logger.trace { "${this::class.simpleName} - start rendering." }
|
||||
|
||||
if (!rendering) {
|
||||
logger.trace { "${this::class.simpleName} - start rendering." }
|
||||
|
||||
rendering = true
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRendering() {
|
||||
logger.trace { "${this::class.simpleName} - stop rendering." }
|
||||
if (rendering) {
|
||||
logger.trace { "${this::class.simpleName} - stop rendering." }
|
||||
|
||||
rendering = false
|
||||
window.cancelAnimationFrame(animationFrameHandle)
|
||||
rendering = false
|
||||
window.cancelAnimationFrame(animationFrameHandle)
|
||||
}
|
||||
}
|
||||
|
||||
open fun setSize(width: Int, height: Int) {
|
||||
|
@ -12,14 +12,6 @@ class RendererWidget(
|
||||
div {
|
||||
className = "pw-core-renderer"
|
||||
|
||||
observeNow(selfOrAncestorVisible) { visible ->
|
||||
if (visible) {
|
||||
renderer.startRendering()
|
||||
} else {
|
||||
renderer.stopRendering()
|
||||
}
|
||||
}
|
||||
|
||||
addDisposable(size.observeChange { (size) ->
|
||||
renderer.setSize(size.width.toInt(), size.height.toInt())
|
||||
})
|
||||
@ -27,6 +19,14 @@ class RendererWidget(
|
||||
append(renderer.canvas)
|
||||
}
|
||||
|
||||
override fun selfAndAncestorsVisibleChanged(visible: Boolean) {
|
||||
if (visible) {
|
||||
renderer.startRendering()
|
||||
} else {
|
||||
renderer.stopRendering()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@Suppress("CssUnusedSymbol")
|
||||
|
@ -6,7 +6,7 @@ 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.observable.change
|
||||
import world.phantasmal.observable.mutate
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToEuler
|
||||
import world.phantasmal.web.core.rendering.conversion.vec3ToThree
|
||||
@ -60,7 +60,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
})
|
||||
|
||||
open fun setSectionId(sectionId: Int) {
|
||||
change {
|
||||
mutate {
|
||||
entity.sectionId = sectionId.toShort()
|
||||
_sectionId.value = sectionId
|
||||
|
||||
@ -83,7 +83,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
"Quest entities can't be moved across areas."
|
||||
}
|
||||
|
||||
change {
|
||||
mutate {
|
||||
entity.sectionId = section.id.toShort()
|
||||
_sectionId.value = section.id
|
||||
|
||||
@ -106,7 +106,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
fun setPosition(pos: Vector3) {
|
||||
change {
|
||||
mutate {
|
||||
entity.setPosition(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
|
||||
|
||||
_position.value = pos
|
||||
@ -120,7 +120,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
fun setWorldPosition(pos: Vector3) {
|
||||
change {
|
||||
mutate {
|
||||
val section = section.value
|
||||
|
||||
val relPos =
|
||||
@ -135,7 +135,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
fun setRotation(rot: Euler) {
|
||||
change {
|
||||
mutate {
|
||||
floorModEuler(rot)
|
||||
|
||||
entity.setRotation(rot.x.toFloat(), rot.y.toFloat(), rot.z.toFloat())
|
||||
@ -155,7 +155,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
}
|
||||
|
||||
fun setWorldRotation(rot: Euler) {
|
||||
change {
|
||||
mutate {
|
||||
floorModEuler(rot)
|
||||
|
||||
val section = section.value
|
||||
|
@ -9,6 +9,7 @@ import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.mutateDeferred
|
||||
import world.phantasmal.psolib.asm.assemble
|
||||
import world.phantasmal.psolib.asm.disassemble
|
||||
import world.phantasmal.web.core.undo.UndoManager
|
||||
@ -111,8 +112,7 @@ class AsmStore(
|
||||
}
|
||||
|
||||
private fun setTextModel(quest: QuestModel?, inlineStackArgs: Boolean) {
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
mutateDeferred {
|
||||
setBytecodeIrTimeout?.let { it ->
|
||||
window.clearTimeout(it)
|
||||
setBytecodeIrTimeout = null
|
||||
@ -120,7 +120,7 @@ class AsmStore(
|
||||
|
||||
modelDisposer.disposeAll()
|
||||
|
||||
quest ?: return@setTimeout
|
||||
quest ?: return@mutateDeferred
|
||||
|
||||
val asm = disassemble(quest.bytecodeIr, inlineStackArgs)
|
||||
asmAnalyser.setAsm(asm, inlineStackArgs)
|
||||
@ -147,7 +147,7 @@ class AsmStore(
|
||||
// TODO: Update breakpoints.
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBytecodeIr() {
|
||||
|
@ -7,7 +7,7 @@ import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.emptyListCell
|
||||
import world.phantasmal.observable.cell.list.filtered
|
||||
import world.phantasmal.observable.cell.list.flatMapToList
|
||||
import world.phantasmal.observable.change
|
||||
import world.phantasmal.observable.mutate
|
||||
import world.phantasmal.psolib.Episode
|
||||
import world.phantasmal.web.core.PwToolType
|
||||
import world.phantasmal.web.core.commands.Command
|
||||
@ -174,14 +174,14 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun addEvent(quest: QuestModel, index: Int, event: QuestEventModel) {
|
||||
change {
|
||||
mutate {
|
||||
quest.addEvent(index, event)
|
||||
setSelectedEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEvent(quest: QuestModel, event: QuestEventModel) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(null)
|
||||
quest.removeEvent(event)
|
||||
}
|
||||
@ -218,28 +218,28 @@ class QuestEditorStore(
|
||||
setter: (QuestEventModel, T) -> Unit,
|
||||
value: T,
|
||||
) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(event)
|
||||
setter(event, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun addEventAction(event: QuestEventModel, action: QuestEventActionModel) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(event)
|
||||
event.addAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun addEventAction(event: QuestEventModel, index: Int, action: QuestEventActionModel) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(event)
|
||||
event.addAction(index, action)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEventAction(event: QuestEventModel, action: QuestEventActionModel) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(event)
|
||||
event.removeAction(action)
|
||||
}
|
||||
@ -251,7 +251,7 @@ class QuestEditorStore(
|
||||
setter: (Action, T) -> Unit,
|
||||
value: T,
|
||||
) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEvent(event)
|
||||
setter(action, value)
|
||||
}
|
||||
@ -272,14 +272,14 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun addEntity(quest: QuestModel, entity: QuestEntityModel<*, *>) {
|
||||
change {
|
||||
mutate {
|
||||
quest.addEntity(entity)
|
||||
setSelectedEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntity(quest: QuestModel, entity: QuestEntityModel<*, *>) {
|
||||
change {
|
||||
mutate {
|
||||
if (entity == _selectedEntity.value) {
|
||||
_selectedEntity.value = null
|
||||
}
|
||||
@ -289,7 +289,7 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun setEntityPosition(entity: QuestEntityModel<*, *>, sectionId: Int?, position: Vector3) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
sectionId?.let { setEntitySection(entity, it) }
|
||||
entity.setPosition(position)
|
||||
@ -297,14 +297,14 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun setEntityRotation(entity: QuestEntityModel<*, *>, rotation: Euler) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
entity.setRotation(rotation)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEntityWorldRotation(entity: QuestEntityModel<*, *>, rotation: Euler) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
entity.setWorldRotation(rotation)
|
||||
}
|
||||
@ -315,14 +315,14 @@ class QuestEditorStore(
|
||||
setter: (Entity, T) -> Unit,
|
||||
value: T,
|
||||
) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
setter(entity, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEntityProp(entity: QuestEntityModel<*, *>, prop: QuestEntityPropModel, value: Any) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
prop.setValue(value)
|
||||
}
|
||||
@ -336,14 +336,14 @@ class QuestEditorStore(
|
||||
}
|
||||
|
||||
fun setEntitySectionId(entity: QuestEntityModel<*, *>, sectionId: Int) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
entity.setSectionId(sectionId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEntitySection(entity: QuestEntityModel<*, *>, section: SectionModel) {
|
||||
change {
|
||||
mutate {
|
||||
setSelectedEntity(entity)
|
||||
entity.setSection(section)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package world.phantasmal.web.questEditor.undo
|
||||
|
||||
import kotlinx.browser.window
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.TrackedDisposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observable
|
||||
import world.phantasmal.observable.cell.*
|
||||
import world.phantasmal.observable.emitter
|
||||
import world.phantasmal.observable.mutateDeferred
|
||||
import world.phantasmal.web.core.commands.Command
|
||||
import world.phantasmal.web.core.undo.Undo
|
||||
import world.phantasmal.web.core.undo.UndoManager
|
||||
@ -64,15 +64,14 @@ class TextModelUndo(
|
||||
}
|
||||
|
||||
private fun onModelChange(model: ITextModel?) {
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
mutateDeferred {
|
||||
if (disposed) return@mutateDeferred
|
||||
|
||||
modelChangeObserver?.dispose()
|
||||
|
||||
if (model == null) {
|
||||
reset()
|
||||
return@setTimeout
|
||||
return@mutateDeferred
|
||||
}
|
||||
|
||||
_canUndo.value = false
|
||||
@ -113,7 +112,7 @@ class TextModelUndo(
|
||||
|
||||
currentVersionId.value = versionId
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun undo(): Boolean =
|
||||
|
@ -1,8 +1,8 @@
|
||||
package world.phantasmal.web.questEditor.widgets
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.mutateDeferred
|
||||
import world.phantasmal.web.externals.monacoEditor.*
|
||||
import world.phantasmal.web.questEditor.asm.monaco.EditorHistory
|
||||
import world.phantasmal.web.questEditor.controllers.AsmEditorController
|
||||
@ -64,31 +64,29 @@ class AsmEditorWidget(private val ctrl: AsmEditorController) : Widget() {
|
||||
observe(ctrl.didUndo) {
|
||||
editor.focus()
|
||||
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "undo",
|
||||
payload = undefined,
|
||||
)
|
||||
}, 0)
|
||||
mutateDeferred {
|
||||
if (!disposed) {
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "undo",
|
||||
payload = undefined,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(ctrl.didRedo) {
|
||||
editor.focus()
|
||||
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "redo",
|
||||
payload = undefined,
|
||||
)
|
||||
}, 0)
|
||||
mutateDeferred {
|
||||
if (!disposed) {
|
||||
editor.trigger(
|
||||
source = AsmEditorWidget::class.simpleName,
|
||||
handlerId = "redo",
|
||||
payload = undefined,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.onDidFocusEditorWidget(ctrl::makeUndoCurrent)
|
||||
|
@ -17,7 +17,7 @@ import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.isNotNull
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.change
|
||||
import world.phantasmal.observable.mutate
|
||||
import world.phantasmal.web.core.files.cursor
|
||||
import world.phantasmal.web.viewer.stores.NinjaGeometry
|
||||
import world.phantasmal.web.viewer.stores.ViewerStore
|
||||
@ -166,7 +166,7 @@ class ViewerToolbarController(private val store: ViewerStore) : Controller() {
|
||||
result.addProblem(Severity.Error, "Couldn't parse files.", cause = e)
|
||||
}
|
||||
|
||||
change {
|
||||
mutate {
|
||||
ninjaGeometry?.let(store::setCurrentNinjaGeometry)
|
||||
textures?.let(store::setCurrentTextures)
|
||||
ninjaMotion?.let(store::setCurrentNinjaMotion)
|
||||
|
@ -10,7 +10,7 @@ import world.phantasmal.observable.cell.list.ListCell
|
||||
import world.phantasmal.observable.cell.list.mutableListCell
|
||||
import world.phantasmal.observable.cell.map
|
||||
import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.change
|
||||
import world.phantasmal.observable.mutate
|
||||
import world.phantasmal.psolib.fileFormats.AreaGeometry
|
||||
import world.phantasmal.psolib.fileFormats.CollisionGeometry
|
||||
import world.phantasmal.psolib.fileFormats.ninja.NinjaObject
|
||||
@ -176,7 +176,7 @@ class ViewerStore(
|
||||
}
|
||||
|
||||
fun setCurrentNinjaGeometry(geometry: NinjaGeometry?) {
|
||||
change {
|
||||
mutate {
|
||||
if (_currentCharacterClass.value != null) {
|
||||
setCurrentCharacterClassValue(null)
|
||||
_currentTextures.clear()
|
||||
@ -215,7 +215,7 @@ class ViewerStore(
|
||||
}
|
||||
|
||||
fun setCurrentNinjaMotion(njm: NjMotion) {
|
||||
change {
|
||||
mutate {
|
||||
_currentNinjaMotion.value = njm
|
||||
_animationPlaying.value = true
|
||||
}
|
||||
@ -267,7 +267,7 @@ class ViewerStore(
|
||||
val ninjaObject = characterClassAssetLoader.loadNinjaObject(char)
|
||||
val textures = characterClassAssetLoader.loadXvrTextures(char, sectionId, body)
|
||||
|
||||
change {
|
||||
mutate {
|
||||
if (clearAnimation) {
|
||||
_currentAnimation.value = null
|
||||
_currentNinjaMotion.value = null
|
||||
@ -279,7 +279,7 @@ class ViewerStore(
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Couldn't load Ninja model for $char." }
|
||||
|
||||
change {
|
||||
mutate {
|
||||
_currentAnimation.value = null
|
||||
_currentNinjaMotion.value = null
|
||||
_currentNinjaGeometry.value = null
|
||||
@ -292,7 +292,7 @@ class ViewerStore(
|
||||
try {
|
||||
val ninjaMotion = animationAssetLoader.loadAnimation(animation.filePath)
|
||||
|
||||
change {
|
||||
mutate {
|
||||
_currentNinjaMotion.value = ninjaMotion
|
||||
_animationPlaying.value = true
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.web.shared.JSON_FORMAT
|
||||
import world.phantasmal.web.viewer.loading.AnimationAssetLoader
|
||||
import world.phantasmal.web.viewer.loading.CharacterClassAssetLoader
|
||||
import world.phantasmal.web.viewer.stores.ViewerStore
|
||||
@ -37,9 +38,7 @@ class TestComponents(private val ctx: TestContext) {
|
||||
var httpClient: HttpClient by default {
|
||||
HttpClient {
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
serializer = KotlinxSerializer(JSON_FORMAT)
|
||||
}
|
||||
}.also {
|
||||
ctx.disposer.add(disposable { it.cancel() })
|
||||
|
@ -1,9 +1,9 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.observable.mutateDeferred
|
||||
import world.phantasmal.webui.dom.div
|
||||
|
||||
class LazyLoader(
|
||||
@ -21,11 +21,11 @@ class LazyLoader(
|
||||
if (v && !initialized) {
|
||||
initialized = true
|
||||
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
addChild(createWidget())
|
||||
}, 0)
|
||||
mutateDeferred {
|
||||
if (!disposed) {
|
||||
addChild(createWidget())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,8 +54,8 @@ class TabContainer<T : Tab>(
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeNow(selfOrAncestorVisible, ctrl::visibleChanged)
|
||||
override fun selfAndAncestorsVisibleChanged(visible: Boolean) {
|
||||
ctrl.visibleChanged(visible)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -13,7 +12,9 @@ import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.DisposableSupervisedScope
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.cell.*
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.nullCell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.*
|
||||
|
||||
@ -29,7 +30,9 @@ abstract class Widget(
|
||||
val enabled: Cell<Boolean> = trueCell(),
|
||||
val tooltip: Cell<String?> = nullCell(),
|
||||
) : DisposableContainer() {
|
||||
private val _ancestorVisible = mutableCell(true)
|
||||
protected var ancestorsVisible = true
|
||||
private set
|
||||
|
||||
private val _children = mutableListOf<Widget>()
|
||||
private val _size = HTMLElementSizeCell()
|
||||
|
||||
@ -39,14 +42,13 @@ abstract class Widget(
|
||||
observeNow(visible) { visible ->
|
||||
el.hidden = !visible
|
||||
|
||||
// TODO: Remove this hack.
|
||||
window.setTimeout({
|
||||
if (disposed) return@setTimeout
|
||||
val selfAndAncestorsVisible = visible && ancestorsVisible
|
||||
|
||||
for (child in children) {
|
||||
setAncestorVisible(child, visible && ancestorVisible.value)
|
||||
}
|
||||
}, 0)
|
||||
selfAndAncestorsVisibleChanged(selfAndAncestorsVisible)
|
||||
|
||||
for (child in children) {
|
||||
setAncestorsVisible(child, selfAndAncestorsVisible)
|
||||
}
|
||||
}
|
||||
|
||||
observeNow(enabled) { enabled ->
|
||||
@ -82,16 +84,6 @@ abstract class Widget(
|
||||
*/
|
||||
val element: HTMLElement by elementDelegate
|
||||
|
||||
/**
|
||||
* True if this widget's ancestors are [visible], false otherwise.
|
||||
*/
|
||||
val ancestorVisible: Cell<Boolean> get() = _ancestorVisible
|
||||
|
||||
/**
|
||||
* True if this widget and all of its ancestors are [visible], false otherwise.
|
||||
*/
|
||||
val selfOrAncestorVisible: Cell<Boolean> = visible and ancestorVisible
|
||||
|
||||
val size: Cell<Size> get() = _size
|
||||
|
||||
val children: List<Widget> get() = _children
|
||||
@ -155,7 +147,7 @@ abstract class Widget(
|
||||
}
|
||||
|
||||
_children.add(child)
|
||||
setAncestorVisible(child, selfOrAncestorVisible.value)
|
||||
setAncestorsVisible(child, visible.value && ancestorsVisible)
|
||||
appendChild(child.element)
|
||||
return child
|
||||
}
|
||||
@ -205,7 +197,11 @@ abstract class Widget(
|
||||
addDisposable(bindDisposableChildrenTo(this, list, create))
|
||||
}
|
||||
|
||||
fun Element.onDrag(
|
||||
protected open fun selfAndAncestorsVisibleChanged(visible: Boolean) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
protected fun Element.onDrag(
|
||||
onPointerDown: (e: PointerEvent) -> Boolean,
|
||||
onPointerMove: (movedX: Int, movedY: Int, e: PointerEvent) -> Boolean,
|
||||
onPointerUp: (e: PointerEvent) -> Unit = {},
|
||||
@ -229,13 +225,15 @@ abstract class Widget(
|
||||
STYLE_EL.append(style)
|
||||
}
|
||||
|
||||
protected fun setAncestorVisible(widget: Widget, visible: Boolean) {
|
||||
widget._ancestorVisible.value = visible
|
||||
protected fun setAncestorsVisible(widget: Widget, ancestorsVisible: Boolean) {
|
||||
widget.ancestorsVisible = ancestorsVisible
|
||||
|
||||
if (!widget.visible.value) return
|
||||
|
||||
widget.children.forEach {
|
||||
setAncestorVisible(it, widget.selfOrAncestorVisible.value)
|
||||
widget.selfAndAncestorsVisibleChanged(ancestorsVisible)
|
||||
|
||||
for (child in widget.children) {
|
||||
setAncestorsVisible(child, ancestorsVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package world.phantasmal.webui.widgets
|
||||
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import org.w3c.dom.Node
|
||||
import world.phantasmal.observable.cell.Cell
|
||||
import world.phantasmal.observable.cell.falseCell
|
||||
@ -10,7 +8,6 @@ import world.phantasmal.observable.cell.mutableCell
|
||||
import world.phantasmal.observable.cell.trueCell
|
||||
import world.phantasmal.webui.dom.div
|
||||
import world.phantasmal.webui.test.WebuiTestSuite
|
||||
import kotlin.js.Promise
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
@ -45,48 +42,42 @@ class WidgetTests : WebuiTestSuite {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ancestorVisible_and_selfOrAncestorVisible_update_when_visible_changes() = testAsync {
|
||||
val parentVisible = mutableCell(true)
|
||||
val childVisible = mutableCell(true)
|
||||
val grandChild = DummyWidget()
|
||||
val child = DummyWidget(childVisible, grandChild)
|
||||
val parent = disposer.add(DummyWidget(parentVisible, child))
|
||||
fun ancestorVisible_updates_and_selfAndAncestorsVisibleChanged_is_called_when_visible_changes() =
|
||||
testAsync {
|
||||
val parentVisible = mutableCell(true)
|
||||
val childVisible = mutableCell(true)
|
||||
val grandChild = DummyWidget()
|
||||
val child = DummyWidget(childVisible, grandChild)
|
||||
val parent = disposer.add(DummyWidget(parentVisible, child))
|
||||
|
||||
parent.element // Ensure widgets are fully initialized.
|
||||
parent.element // Ensure widgets are fully initialized.
|
||||
|
||||
assertTrue(parent.ancestorVisible.value)
|
||||
assertTrue(parent.selfOrAncestorVisible.value)
|
||||
assertTrue(child.ancestorVisible.value)
|
||||
assertTrue(child.selfOrAncestorVisible.value)
|
||||
assertTrue(grandChild.ancestorVisible.value)
|
||||
assertTrue(grandChild.selfOrAncestorVisible.value)
|
||||
assertTrue(parent.publicAncestorsVisible)
|
||||
assertEquals(true, parent.selfAndAncestorsVisible)
|
||||
assertTrue(child.publicAncestorsVisible)
|
||||
assertEquals(true, child.selfAndAncestorsVisible)
|
||||
assertTrue(grandChild.publicAncestorsVisible)
|
||||
assertEquals(true, grandChild.selfAndAncestorsVisible)
|
||||
|
||||
parentVisible.value = false
|
||||
setTimeoutHack()
|
||||
parentVisible.value = false
|
||||
|
||||
assertTrue(parent.ancestorVisible.value)
|
||||
assertFalse(parent.selfOrAncestorVisible.value)
|
||||
assertFalse(child.ancestorVisible.value)
|
||||
assertFalse(child.selfOrAncestorVisible.value)
|
||||
assertFalse(grandChild.ancestorVisible.value)
|
||||
assertFalse(grandChild.selfOrAncestorVisible.value)
|
||||
assertTrue(parent.publicAncestorsVisible)
|
||||
assertEquals(false, parent.selfAndAncestorsVisible)
|
||||
assertFalse(child.publicAncestorsVisible)
|
||||
assertEquals(false, child.selfAndAncestorsVisible)
|
||||
assertFalse(grandChild.publicAncestorsVisible)
|
||||
assertEquals(false, grandChild.selfAndAncestorsVisible)
|
||||
|
||||
childVisible.value = false
|
||||
parentVisible.value = true
|
||||
setTimeoutHack()
|
||||
childVisible.value = false
|
||||
parentVisible.value = true
|
||||
|
||||
assertTrue(parent.ancestorVisible.value)
|
||||
assertTrue(parent.selfOrAncestorVisible.value)
|
||||
assertTrue(child.ancestorVisible.value)
|
||||
assertFalse(child.selfOrAncestorVisible.value)
|
||||
assertFalse(grandChild.ancestorVisible.value)
|
||||
assertFalse(grandChild.selfOrAncestorVisible.value)
|
||||
}
|
||||
|
||||
// TODO: Remove test setTimeout hack when setTimeout hack in Widget visible observer is removed.
|
||||
private suspend fun setTimeoutHack() {
|
||||
Promise<Unit> { resolve, _ -> window.setTimeout(resolve, 10) }.await()
|
||||
}
|
||||
assertTrue(parent.publicAncestorsVisible)
|
||||
assertEquals(true, parent.selfAndAncestorsVisible)
|
||||
assertTrue(child.publicAncestorsVisible)
|
||||
assertEquals(false, child.selfAndAncestorsVisible)
|
||||
assertFalse(grandChild.publicAncestorsVisible)
|
||||
assertEquals(false, grandChild.selfAndAncestorsVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun added_child_widgets_have_ancestorVisible_and_selfOrAncestorVisible_set_correctly() =
|
||||
@ -94,10 +85,10 @@ class WidgetTests : WebuiTestSuite {
|
||||
val parent = disposer.add(DummyWidget(visible = falseCell()))
|
||||
val child = parent.addChild(DummyWidget())
|
||||
|
||||
assertTrue(parent.ancestorVisible.value)
|
||||
assertFalse(parent.selfOrAncestorVisible.value)
|
||||
assertFalse(child.ancestorVisible.value)
|
||||
assertFalse(child.selfOrAncestorVisible.value)
|
||||
assertTrue(parent.publicAncestorsVisible)
|
||||
assertEquals(false, parent.selfAndAncestorsVisible)
|
||||
assertFalse(child.publicAncestorsVisible)
|
||||
assertEquals(false, child.selfAndAncestorsVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -149,6 +140,10 @@ class WidgetTests : WebuiTestSuite {
|
||||
visible: Cell<Boolean> = trueCell(),
|
||||
private val child: Widget? = null,
|
||||
) : Widget(visible = visible) {
|
||||
val publicAncestorsVisible: Boolean get() = ancestorsVisible
|
||||
var selfAndAncestorsVisible: Boolean? = null
|
||||
private set
|
||||
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
child?.let { addChild(it) }
|
||||
@ -156,5 +151,9 @@ class WidgetTests : WebuiTestSuite {
|
||||
|
||||
fun <T : Widget> addChild(child: T): T =
|
||||
element.addChild(child)
|
||||
|
||||
override fun selfAndAncestorsVisibleChanged(visible: Boolean) {
|
||||
selfAndAncestorsVisible = visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user