From cbf3f4941abe44f8e91d48aca3d450dc7d912030 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 12 Feb 2024 10:16:01 +0300 Subject: [PATCH] Fixed all tests --- build.gradle.kts | 4 +- .../kotlin/ru/mipt/npm/root/dRootToSolid.kt | 7 +- .../src/jvmMain/kotlin/controlVision.kt | 4 +- .../kscience/visionforge/AbstractVision.kt | 16 ++--- .../kscience/visionforge/ControlVision.kt | 24 +++---- .../space/kscience/visionforge/StyleSheet.kt | 6 +- .../space/kscience/visionforge/Vision.kt | 16 +---- .../kscience/visionforge/VisionChange.kt | 4 +- .../kscience/visionforge/VisionManager.kt | 2 +- .../kscience/visionforge/VisionProperties.kt | 68 ++++++++++--------- .../kscience/visionforge/flowProperty.kt | 4 +- .../visionforge/html/VisionOfHtmlForm.kt | 10 +-- .../space/kscience/visionforge/useProperty.kt | 53 +++++++++------ .../kscience/visionforge/html/HtmlTagTest.kt | 2 +- .../visionforge/meta/VisionPropertyTest.kt | 38 +++++++---- .../visionforge/ElementVisionRenderer.kt | 13 +--- .../kscience/visionforge/JsVisionClient.kt | 5 +- .../kscience/visionforge/formRenderers.kt | 4 +- .../visionforge/plotly/VisionOfPlotly.kt | 35 +++------- .../kscience/visionforge/solid/Composite.kt | 3 +- .../space/kscience/visionforge/solid/Solid.kt | 2 +- .../visionforge/solid/SolidReference.kt | 27 ++++---- .../visionforge/solid/SolidPropertyTest.kt | 24 ++----- .../visionforge/solid/three/ThreeMaterials.kt | 2 +- .../visionforge/solid/three/ThreePlugin.kt | 3 +- .../solid/three/compose/ThreeView.kt | 4 +- 26 files changed, 171 insertions(+), 209 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0888408f..debd7323 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,14 +3,14 @@ import space.kscience.gradle.useSPCTeam plugins { id("space.kscience.gradle.project") -// id("org.jetbrains.kotlinx.kover") version "0.5.0" + id("org.jetbrains.kotlinx.kover") version "0.5.0" } val dataforgeVersion by extra("0.8.0") allprojects { group = "space.kscience" - version = "0.4.0-dev-2" + version = "0.4.0-dev-3" } subprojects { diff --git a/cern-root-loader/src/commonMain/kotlin/ru/mipt/npm/root/dRootToSolid.kt b/cern-root-loader/src/commonMain/kotlin/ru/mipt/npm/root/dRootToSolid.kt index 2c501fd7..e6536d6f 100644 --- a/cern-root-loader/src/commonMain/kotlin/ru/mipt/npm/root/dRootToSolid.kt +++ b/cern-root-loader/src/commonMain/kotlin/ru/mipt/npm/root/dRootToSolid.kt @@ -1,9 +1,6 @@ package ru.mipt.npm.root -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.int -import space.kscience.dataforge.meta.set +import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.plus @@ -365,7 +362,7 @@ private fun buildVolume(volume: DGeoVolume, context: RootToSolidContext): Solid? } return if (group.children.isEmpty()) { null - } else if (group.items.size == 1 && group.properties.own == null) { + } else if (group.items.size == 1 && group.properties.own.isEmpty()) { group.items.values.first().apply { parent = null } } else { group diff --git a/demo/playground/src/jvmMain/kotlin/controlVision.kt b/demo/playground/src/jvmMain/kotlin/controlVision.kt index 5cd1c2d2..a0383ea2 100644 --- a/demo/playground/src/jvmMain/kotlin/controlVision.kt +++ b/demo/playground/src/jvmMain/kotlin/controlVision.kt @@ -7,7 +7,7 @@ import kotlinx.html.h2 import kotlinx.html.p import space.kscience.visionforge.VisionControlEvent import space.kscience.visionforge.html.* -import space.kscience.visionforge.onClick +import space.kscience.visionforge.onSubmit import kotlin.time.Duration.Companion.seconds @@ -59,7 +59,7 @@ fun main() = serve { vision { button("Click me") { - onClick(context) { + onSubmit(context) { pushEvent(this) } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/AbstractVision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/AbstractVision.kt index 0cba6a37..655a7684 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/AbstractVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/AbstractVision.kt @@ -1,8 +1,6 @@ package space.kscience.visionforge -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient +import kotlinx.serialization.* import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.descriptors.MetaDescriptor @@ -14,16 +12,12 @@ public abstract class AbstractVision : Vision { override var parent: Vision? = null @SerialName("properties") - protected var propertiesInternal: MutableMeta? = null + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault(EncodeDefault.Mode.NEVER) + protected var propertiesInternal: MutableMeta = MutableMeta() final override val properties: MutableVisionProperties by lazy { - object : AbstractVisionProperties(this) { - override var properties: MutableMeta? - get() = propertiesInternal - set(value) { - propertiesInternal = value - } - } + AbstractVisionProperties(this, propertiesInternal) } override val descriptor: MetaDescriptor? get() = null diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt index 546869bb..36dfe003 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -34,11 +34,11 @@ public interface ControlVision : Vision { /** - * @param payload The optional payload associated with the click event. + * An event for submitting changes */ @Serializable -@SerialName("control.click") -public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() { +@SerialName("control.submit") +public class VisionSubmitEvent(override val meta: Meta) : VisionControlEvent() { public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY public val name: Name? get() = meta["name"].string?.parseAsName() @@ -46,28 +46,28 @@ public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() { override fun toString(): String = meta.toString() } -public fun VisionClickEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionClickEvent = VisionClickEvent( +public fun VisionSubmitEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionSubmitEvent = VisionSubmitEvent( Meta { - VisionClickEvent::payload.name put payload - VisionClickEvent::name.name put name.toString() + VisionSubmitEvent::payload.name put payload + VisionSubmitEvent::name.name put name.toString() } ) -public interface ClickControl : ControlVision { +public interface DataControl : ControlVision { /** - * Create and dispatch a click event + * Create and dispatch submit event */ - public suspend fun click(builder: MutableMeta.() -> Unit = {}) { - dispatchControlEvent(VisionClickEvent(Meta(builder))) + public suspend fun submit(builder: MutableMeta.() -> Unit = {}) { + dispatchControlEvent(VisionSubmitEvent(Meta(builder))) } } /** * Register listener */ -public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job = - controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) +public fun DataControl.onSubmit(scope: CoroutineScope, block: suspend VisionSubmitEvent.() -> Unit): Job = + controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) @Serializable diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt index ed893162..cff29e61 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt @@ -67,7 +67,7 @@ internal fun Vision.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?) * List of names of styles applied to this object. Order matters. Not inherited. */ public var Vision.styles: List - get() = properties.own?.getValue(Vision.STYLE_KEY)?.stringList ?: emptyList() + get() = properties.own[Vision.STYLE_KEY]?.stringList ?: emptyList() set(value) { properties.setValue(Vision.STYLE_KEY, value.map { it.asValue() }.asValue()) } @@ -83,7 +83,7 @@ public val Vision.styleSheet: StyleSheet get() = StyleSheet(this) * The style with given name does not necessary exist at the moment. */ public fun Vision.useStyle(name: String, notify: Boolean = true) { - val newStyle = properties.own?.get(Vision.STYLE_KEY)?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue()) + val newStyle = properties.own[Vision.STYLE_KEY]?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue()) properties.setValue(Vision.STYLE_KEY, newStyle.asValue(), notify) } @@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) { * Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision]. */ public fun Vision.getStyle(name: String): Meta? = - properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name) + properties.own[StyleSheet.STYLESHEET_KEY + name] ?: parent?.getStyle(name) /** * Resolve a property from all styles diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt index 886a1574..4062b356 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -1,9 +1,5 @@ package space.kscience.visionforge -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.warn import space.kscience.dataforge.meta.asValue @@ -73,14 +69,4 @@ public var Vision.visible: Boolean? get() = properties.getValue(Vision.VISIBLE_KEY)?.boolean set(value) { properties.setValue(Vision.VISIBLE_KEY, value?.asValue()) - } - -/** - * Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled - */ -public fun Vision.onPropertyChange( - scope: CoroutineScope, - callback: suspend (Name) -> Unit, -): Job = properties.changes.onEach { - callback(it) -}.launchIn(scope) \ No newline at end of file + } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt index 87bff794..00386948 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt @@ -147,8 +147,8 @@ private fun CoroutineScope.collectChange( ) { //Collect properties change - source.properties.changes.onEach { propertyName -> - val newItem = source.properties.own?.get(propertyName) + source.properties.flowChanges().onEach { propertyName -> + val newItem = source.properties.own[propertyName] collector.propertyChanged(name, propertyName, newItem) }.launchIn(this) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt index 84ed8286..2bee6a4f 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt @@ -83,7 +83,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont polymorphic(VisionEvent::class) { subclass(VisionChange.serializer()) subclass(VisionMetaEvent.serializer()) - subclass(VisionClickEvent.serializer()) + subclass(VisionSubmitEvent.serializer()) subclass(VisionValueChangeEvent.serializer()) subclass(VisionInputEvent.serializer()) } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt index 74bbb504..752b7bd3 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt @@ -1,9 +1,8 @@ package space.kscience.visionforge -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch import kotlinx.serialization.Transient import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.descriptors.MetaDescriptor @@ -15,7 +14,7 @@ public interface VisionProperties : MetaProvider { /** * Raw Visions own properties without styles, defaults, etc. */ - public val own: Meta? + public val own: Meta public val descriptor: MetaDescriptor? @@ -41,7 +40,7 @@ public interface VisionProperties : MetaProvider { override fun get(name: Name): Meta? = get(name, null, null) - public val changes: Flow + public fun flowChanges(): Flow /** * Notify all listeners that a property has been changed and should be invalidated. @@ -112,7 +111,7 @@ private class VisionPropertiesItem( override val items: Map get() { - val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet() + val metaKeys = properties.own[nodeName]?.items?.keys ?: emptySet() val descriptorKeys = descriptor?.nodes?.map { NameToken(it.key) } ?: emptySet() val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet() val inheritFlag = descriptor?.inherited ?: inherit @@ -158,25 +157,12 @@ private class VisionPropertiesItem( /** * A base implementation of [MutableVisionProperties] */ -public abstract class AbstractVisionProperties( +public open class AbstractVisionProperties( public val vision: Vision, + final override val own: MutableMeta, ) : MutableVisionProperties { override val descriptor: MetaDescriptor? get() = vision.descriptor - protected abstract var properties: MutableMeta? - - override val own: Meta? get() = properties - - @JvmSynchronized - protected fun getOrCreateProperties(): MutableMeta { - if (properties == null) { - //TODO check performance issues - val newProperties = MutableMeta() - properties = newProperties - } - return properties!! - } - private val descriptorCache = HashMap() override fun getValue( @@ -184,7 +170,7 @@ public abstract class AbstractVisionProperties( inherit: Boolean?, includeStyles: Boolean?, ): Value? { - own?.get(name)?.value?.let { return it } + own[name]?.value?.let { return it } val descriptor = descriptor?.let { descriptor -> descriptorCache.getOrPut(name) { descriptor[name] } } val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true @@ -202,14 +188,26 @@ public abstract class AbstractVisionProperties( override fun set(name: Name, node: Meta?, notify: Boolean) { //ignore if the value is the same as existing - if (own?.get(name) == node) return + if (own[name] == node) return if (name.isEmpty()) { - properties = node?.asMutableMeta() + if (node == null) { + own.items.keys.forEach { + remove(it.asName()) + } + } else { + (own.items.keys - node.items.keys).forEach { + remove(it.asName()) + } + node.items.forEach { (token, item) -> + set(token, item) + } + } + } else if (node == null) { - properties?.set(name, node) + own[name] = node } else { - getOrCreateProperties()[name] = node + own[name] = node } if (notify) { invalidate(name) @@ -218,12 +216,12 @@ public abstract class AbstractVisionProperties( override fun setValue(name: Name, value: Value?, notify: Boolean) { //ignore if the value is the same as existing - if (own?.getValue(name) == value) return + if (own.getValue(name) == value) return if (value == null) { - properties?.get(name)?.value = null + own[name]?.value = null } else { - getOrCreateProperties().setValue(name, value) + own.setValue(name, value) } if (notify) { invalidate(name) @@ -231,12 +229,20 @@ public abstract class AbstractVisionProperties( } @Transient - protected val changesInternal: MutableSharedFlow = MutableSharedFlow(500, onBufferOverflow = BufferOverflow.DROP_OLDEST) - override val changes: SharedFlow get() = changesInternal + protected val changesInternal: MutableSharedFlow = MutableSharedFlow() + + override fun flowChanges(): Flow = changesInternal override fun invalidate(propertyName: Name) { //send update signal - changesInternal.tryEmit(propertyName) + val manager = vision.manager + if (manager != null) { + manager.context.launch { + changesInternal.emit(propertyName) + } + } else { + changesInternal.tryEmit(propertyName) + } //notify children if there are any if (vision is VisionGroup) { diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt index 60c39c20..8d1bbb72 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt @@ -18,7 +18,7 @@ public fun Vision.flowProperty( ): Flow = flow { //Pass initial value. emit(properties.get(propertyName, inherit, includeStyles)) - properties.changes.collect { name -> + properties.flowChanges().collect { name -> if (name.startsWith(propertyName)) { emit(properties.get(propertyName, inherit, includeStyles)) } @@ -41,7 +41,7 @@ public fun Vision.flowPropertyValue( ): Flow = flow { //Pass initial value. emit(properties.getValue(propertyName, inherit, includeStyles)) - properties.changes.collect { name -> + properties.flowChanges().collect { name -> if (name.startsWith(propertyName)) { emit(properties.getValue(propertyName, inherit, includeStyles)) } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt index baa7326a..3f6d46ca 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt @@ -8,8 +8,8 @@ import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.node import space.kscience.dataforge.meta.string -import space.kscience.visionforge.ClickControl -import space.kscience.visionforge.onClick +import space.kscience.visionforge.DataControl +import space.kscience.visionforge.onSubmit /** * @param formId an id of the element in rendered DOM, this form is bound to @@ -18,7 +18,7 @@ import space.kscience.visionforge.onClick @SerialName("html.form") public class VisionOfHtmlForm( public val formId: String, -) : VisionOfHtmlControl(), ClickControl { +) : VisionOfHtmlControl(), DataControl { public var values: Meta? by properties.node() } @@ -40,12 +40,12 @@ public inline fun > C.visionOfForm( } -public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) } +public fun VisionOfHtmlForm.onFormSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onSubmit(scope) { block(payload) } @Serializable @SerialName("html.button") -public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl { +public class VisionOfHtmlButton : VisionOfHtmlControl(), DataControl { public var label: String? by properties.string() } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt index c396714b..07ed4bd9 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt @@ -2,6 +2,7 @@ package space.kscience.visionforge import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import space.kscience.dataforge.meta.Meta @@ -19,12 +20,12 @@ public fun Vision.useProperty( propertyName: Name, inherit: Boolean? = null, includeStyles: Boolean? = null, - scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."), callback: (Meta) -> Unit, ): Job { //Pass initial value. callback(properties.get(propertyName, inherit, includeStyles)) - return properties.changes.onEach { name -> + return properties.flowChanges().onEach { name -> if (name.startsWith(propertyName)) { callback(properties.get(propertyName, inherit, includeStyles)) } @@ -35,33 +36,41 @@ public fun Vision.useProperty( propertyName: String, inherit: Boolean? = null, includeStyles: Boolean? = null, - scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."), callback: (Meta) -> Unit, ): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback) +public fun V.useProperty( + property: KProperty1, + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."), + callback: V.(T) -> Unit, +): Job { + //Pass initial value. + callback(property.get(this)) + return properties.flowChanges().onEach { name -> + if (name.startsWith(property.name.asName())) { + callback(property.get(this@useProperty)) + } + }.launchIn(scope) +} + +/** + * Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled + */ +public fun Vision.onPropertyChange( + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."), + callback: suspend (Name) -> Unit, +): Job = properties.flowChanges().onEach { + callback(it) +}.launchIn(scope) + /** * Observe changes to the specific property without passing the initial value. */ public fun V.onPropertyChange( property: KProperty1, - scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."), callback: suspend V.(T) -> Unit, -): Job = properties.changes.onEach { name -> - if (name.startsWith(property.name.asName())) { - callback(property.get(this)) - } -}.launchIn(scope) - -public fun V.useProperty( - property: KProperty1, - scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), - callback: V.(T) -> Unit, -): Job { - //Pass initial value. +): Job = properties.flowChanges().filter { it.startsWith(property.name.asName()) }.onEach { callback(property.get(this)) - return properties.changes.onEach { name -> - if (name.startsWith(property.name.asName())) { - callback(property.get(this@useProperty)) - } - }.launchIn(scope) -} \ No newline at end of file +}.launchIn(scope) \ No newline at end of file diff --git a/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/html/HtmlTagTest.kt b/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/html/HtmlTagTest.kt index 20868b96..b4cb115d 100644 --- a/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/html/HtmlTagTest.kt +++ b/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/html/HtmlTagTest.kt @@ -55,7 +55,7 @@ class HtmlTagTest { div { h2 { +"Properties" } ul { - vision.properties.own?.items?.forEach { + vision.properties.own.items.forEach { li { a { +it.key.toString() } p { +it.value.toString() } diff --git a/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/meta/VisionPropertyTest.kt b/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/meta/VisionPropertyTest.kt index e8ebe406..8b800add 100644 --- a/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/meta/VisionPropertyTest.kt +++ b/visionforge-core/src/commonTest/kotlin/space/kscience/visionforge/meta/VisionPropertyTest.kt @@ -1,11 +1,10 @@ package space.kscience.visionforge.meta import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.test.runTest import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.request @@ -92,7 +91,7 @@ internal class VisionPropertyTest { } @Test - fun testChildrenPropertyFlow() = runTest(timeout = 200.milliseconds) { + fun testChildrenPropertyFlow() = runTest(timeout = 500.milliseconds) { val group = Global.request(VisionManager).group { properties { @@ -109,17 +108,32 @@ internal class VisionPropertyTest { val child = group.children["child"]!! - launch { - val list = child.flowPropertyValue("test", inherit = true).take(3).map { it?.int }.toList() - assertEquals(22, list.first()) - //assertEquals(11, list[1]) //a race - assertEquals(33, list.last()) + val semaphore = Semaphore(1, 1) + + val changesFlow = child.flowPropertyValue("test", inherit = true).map { + semaphore.release() + it!!.int } - //wait for subscription to be created - delay(5) + val collectedValues = ArrayList(5) + val collectorJob = changesFlow.onEach { + collectedValues.add(it) + }.launchIn(this) + + assertEquals(22, child.properties["test", true].int) + + semaphore.acquire() child.properties.remove("test") + + assertEquals(11, child.properties["test", true].int) + + semaphore.acquire() group.properties["test"] = 33 + assertEquals(33, child.properties["test", true].int) + + semaphore.acquire() + collectorJob.cancel() + assertEquals(listOf(22, 11, 33), collectedValues) } } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt index f67172e5..dc0c7941 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt @@ -3,17 +3,11 @@ package space.kscience.visionforge import kotlinx.dom.clear import kotlinx.html.TagConsumer import kotlinx.html.dom.append -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.serializerOrNull import org.w3c.dom.Element import org.w3c.dom.HTMLElement import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DfType -import space.kscience.dataforge.misc.Named import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.asName -import space.kscience.dataforge.names.parseAsName import kotlin.reflect.KClass import kotlin.reflect.cast @@ -21,7 +15,7 @@ import kotlin.reflect.cast * A browser renderer for a [Vision]. */ @DfType(ElementVisionRenderer.TYPE) -public interface ElementVisionRenderer : Named { +public interface ElementVisionRenderer { /** * Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer @@ -58,11 +52,6 @@ public class SingleTypeVisionRenderer( private val renderFunction: TagConsumer.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit, ) : ElementVisionRenderer { - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - override val name: Name - get() = kClass.serializerOrNull()?.descriptor?.serialName?.parseAsName() - ?: kClass.toString().asName() - override fun rateVision(vision: Vision): Int = if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt index 45af671c..db88bea4 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -21,6 +21,7 @@ import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.parseAsName import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE @@ -262,7 +263,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { rangeVisionRenderer, formVisionRenderer, buttonVisionRenderer - ).associateByName() + ).associateBy { it.toString().asName() } } else super.content(target) public companion object : PluginFactory { @@ -321,7 +322,7 @@ public class VisionClientApplication(public val context: Context) : Application client.renderers.joinToString( prefix = "\n\t", separator = "\n\t" - ) { it.name.toString() } + ) { it.toString() } }" } val element = document.body ?: error("Document does not have a body") diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt index 781fa677..67c5b95e 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt @@ -70,7 +70,7 @@ internal val formVisionRenderer: ElementVisionRenderer = event.preventDefault() val formData = FormData(form).toMeta() client.context.launch { - client.sendEvent(name, VisionClickEvent(name = name, payload = formData)) + client.sendEvent(name, VisionSubmitEvent(name = name, payload = formData)) } console.info("Sent form data: ${formData.toMap()}") false @@ -83,7 +83,7 @@ internal val buttonVisionRenderer: ElementVisionRenderer = button.subscribeToVision(vision) button.onclick = { client.context.launch { - client.sendEvent(name, VisionClickEvent(name = name)) + client.sendEvent(name, VisionSubmitEvent(name = name)) } } vision.useProperty(VisionOfHtmlButton::label) { diff --git a/visionforge-plotly/src/commonMain/kotlin/space/kscience/visionforge/plotly/VisionOfPlotly.kt b/visionforge-plotly/src/commonMain/kotlin/space/kscience/visionforge/plotly/VisionOfPlotly.kt index 19d98171..0e2a3206 100644 --- a/visionforge-plotly/src/commonMain/kotlin/space/kscience/visionforge/plotly/VisionOfPlotly.kt +++ b/visionforge-plotly/src/commonMain/kotlin/space/kscience/visionforge/plotly/VisionOfPlotly.kt @@ -8,12 +8,16 @@ import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.meta.MutableMetaSerializer +import space.kscience.dataforge.meta.ObservableMeta +import space.kscience.dataforge.meta.asObservable import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.names.Name import space.kscience.plotly.Plot import space.kscience.plotly.Plotly import space.kscience.plotly.PlotlyConfig +import space.kscience.visionforge.AbstractVisionProperties import space.kscience.visionforge.MutableVisionProperties import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionBuilder @@ -32,32 +36,9 @@ public class VisionOfPlotly private constructor( override var parent: Vision? = null @Transient - override val properties: MutableVisionProperties = object : MutableVisionProperties { - override fun set(name: Name, node: Meta?, notify: Boolean) { - meta[name] = node - } + override val properties: MutableVisionProperties = object : AbstractVisionProperties(this, meta) { - override fun setValue(name: Name, value: Value?, notify: Boolean) { - meta.setValue(name, value) - } - - override val own: Meta get() = meta - - override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor - - override fun get( - name: Name, - inherit: Boolean?, - includeStyles: Boolean?, - ): MutableMeta = meta[name] ?: MutableMeta() - - override fun getValue( - name: Name, - inherit: Boolean?, - includeStyles: Boolean?, - ): Value? = meta.getValue(name) - - override val changes: Flow = if (meta is ObservableMeta) { + override fun flowChanges(): Flow = if (meta is ObservableMeta) { callbackFlow { meta.onChange(this) { launch { @@ -78,7 +59,7 @@ public class VisionOfPlotly private constructor( } - override val descriptor: MetaDescriptor? = null // TODO add descriptor for Plot + override val descriptor: MetaDescriptor = Plot.descriptor } public fun Plot.asVision(): VisionOfPlotly = VisionOfPlotly(this) diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Composite.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Composite.kt index 3ae6a681..76556860 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Composite.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Composite.kt @@ -2,6 +2,7 @@ package space.kscience.visionforge.solid import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.isEmpty import space.kscience.dataforge.names.Name import space.kscience.visionforge.MutableVisionContainer import space.kscience.visionforge.VisionBuilder @@ -55,7 +56,7 @@ public fun SolidGroup.smartComposite( @VisionBuilder builder: SolidGroup.() -> Unit, ): Solid = if (type == CompositeType.GROUP) { val group = SolidGroup().apply(builder) - if (name == null && group.properties.own == null) { + if (name == null && group.properties.own.isEmpty()) { //append directly to group if no properties are defined group.items.forEach { (_, value) -> value.parent = null diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Solid.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Solid.kt index db2c0948..c1e16daa 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Solid.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Solid.kt @@ -177,7 +177,7 @@ internal fun float32Vector( ): ReadWriteProperty = object : ReadWriteProperty { override fun getValue(thisRef: Solid, property: KProperty<*>): Float32Vector3D? { - val item = thisRef.properties.own?.get(name) ?: return null + val item = thisRef.properties.own[name] ?: return null //using dynamic property accessor because values could change return object : Float32Vector3D { override val x: Float get() = item[X_KEY]?.float ?: defaultX diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidReference.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidReference.kt index b832865c..ec13823e 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidReference.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidReference.kt @@ -6,9 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient +import kotlinx.serialization.* import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.get @@ -51,21 +49,19 @@ public class SolidReference( } override val descriptor: MetaDescriptor get() = prototype.descriptor + @SerialName("properties") - private var propertiesInternal: MutableMeta? = null + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault(EncodeDefault.Mode.NEVER) + private val propertiesInternal: MutableMeta = MutableMeta() override val properties: MutableVisionProperties by lazy { - object : AbstractVisionProperties(this) { - override var properties: MutableMeta? - get() = propertiesInternal - set(value) { - propertiesInternal = value - } + object : AbstractVisionProperties(this, propertiesInternal) { override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? { if (name == Vision.STYLE_KEY) { return buildList { - properties?.getValue(Vision.STYLE_KEY)?.list?.forEach { + own.getValue(Vision.STYLE_KEY)?.list?.forEach { add(it) } prototype.styles.forEach { @@ -74,18 +70,18 @@ public class SolidReference( }.distinct().asValue() } //1. resolve own properties - properties?.getValue(name)?.let { return it } + own.getValue(name)?.let { return it } val descriptor = descriptor?.get(name) val inheritFlag = inherit ?: descriptor?.inherited ?: false val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true //2. Resolve prototype onw properties - prototype.properties.own?.getValue(name)?.let { return it } + prototype.properties.own.getValue(name)?.let { return it } if (stylesFlag) { //3. own styles - own?.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName -> + own.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName -> getStyle(styleName.string)?.getValue(name)?.let { return it } } //4. prototype styles @@ -178,7 +174,8 @@ internal class SolidReferenceChild( own.setValue(name, value) } - override val changes: Flow get() = owner.properties.changes.filter { it.startsWith(childToken(childName)) } + override fun flowChanges(): Flow = + owner.properties.flowChanges().filter { it.startsWith(childToken(childName)) } override fun invalidate(propertyName: Name) { owner.properties.invalidate(childPropertyName(childName, propertyName)) diff --git a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidPropertyTest.kt b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidPropertyTest.kt index 7b559ab7..a68e7daf 100644 --- a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidPropertyTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidPropertyTest.kt @@ -1,18 +1,17 @@ package space.kscience.visionforge.solid -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest import space.kscience.dataforge.meta.getValue import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.set import space.kscience.dataforge.meta.string import space.kscience.dataforge.names.asName -import space.kscience.visionforge.* +import space.kscience.visionforge.getValue +import space.kscience.visionforge.styleSheet +import space.kscience.visionforge.styles +import space.kscience.visionforge.useStyle import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalCoroutinesApi::class) @Suppress("UNUSED_VARIABLE") @@ -29,25 +28,14 @@ class SolidPropertyTest { } @Test - fun testColorUpdate() = runTest(timeout = 200.milliseconds) { + fun testColorUpdate() { val box = Box(10.0f, 10.0f, 10.0f) - val c = CompletableDeferred() - - - val subscription = box.onPropertyChange(this) { key -> - if (key == SolidMaterial.MATERIAL_COLOR_KEY) { - c.complete(box.color.string) - } - } - delay(5) - box.material { color("pink") } - assertEquals("pink", c.await()) - subscription.cancel() + assertEquals("pink", box.properties[SolidMaterial.MATERIAL_COLOR_KEY].string) } @Test diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMaterials.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMaterials.kt index 34cbbde0..3c860111 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMaterials.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMaterials.kt @@ -133,7 +133,7 @@ internal var Material.cached: Boolean public fun Mesh.setMaterial(vision: Vision) { if ( - vision.properties.own?.get(SolidMaterial.MATERIAL_KEY) == null + vision.properties.own[SolidMaterial.MATERIAL_KEY] == null && vision.getStyleNodes(SolidMaterial.MATERIAL_KEY).isEmpty() ) { //if this is a reference, use material of the prototype diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt index 7f7cc29b..5d3fbdc3 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt @@ -14,7 +14,6 @@ import space.kscience.visionforge.compose.ComposeVisionRenderer import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.specifications.Canvas3DOptions import space.kscience.visionforge.solid.three.compose.ThreeView -import space.kscience.visionforge.solid.three.set import three.core.Object3D import kotlin.collections.set import kotlin.reflect.KClass @@ -85,7 +84,7 @@ public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer { updatePosition(vision) //obj.onChildrenChange() if (observe) { - vision.properties.changes.onEach { name -> + vision.properties.flowChanges().onEach { name -> if ( name.startsWith(Solid.POSITION_KEY) || name.startsWith(Solid.ROTATION_KEY) || diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt index 2ea1d930..a9ba1146 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt @@ -138,7 +138,7 @@ public fun ThreeView( scope = context, rootMeta = vision.properties.root(), getPropertyState = { name -> - if (vision.properties.own?.get(name) != null) { + if (vision.properties.own[name] != null) { EditorPropertyState.Defined } else if (vision.properties.root()[name] != null) { // TODO differentiate @@ -148,7 +148,7 @@ public fun ThreeView( } }, name = Name.EMPTY, - updates = vision.properties.changes, + updates = vision.properties.flowChanges(), rootDescriptor = vision.descriptor ) vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->