diff --git a/build.gradle.kts b/build.gradle.kts index 9f982d94..2da86b96 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ val fxVersion by extra("11") allprojects { group = "space.kscience" - version = "0.3.0-dev-15" + version = "0.3.0-dev-16" } subprojects { diff --git a/settings.gradle.kts b/settings.gradle.kts index 31119a06..8611d0aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,7 +45,7 @@ include( ":ui:ring", // ":ui:material", ":ui:bootstrap", -// ":ui:compose", + ":ui:compose", ":visionforge-core", ":visionforge-solid", // ":visionforge-fx", 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 31ff307e..8cfe7a27 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -11,6 +11,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.misc.Type import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.isEmpty import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties import space.kscience.visionforge.Vision.Companion.TYPE @@ -36,7 +37,7 @@ public interface Vision : Described { /** * Update this vision using a dif represented by [VisionChange]. */ - public fun update(change: VisionChange) { + public fun receiveChange(change: VisionChange) { if (change.children?.isNotEmpty() == true) { error("Vision is not a group") } @@ -45,6 +46,20 @@ public interface Vision : Described { } } + /** + * Receive and process a generic [VisionEvent]. + */ + public fun receiveEvent(event: VisionEvent) { + if(event.targetName.isEmpty()) { + when (event) { + is VisionChangeEvent -> receiveChange(event.change) + else -> TODO() + } + } else { + error("Vision is not a group and can't process an event with non-empty target") + } + } + override val descriptor: MetaDescriptor? public companion object { diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt index f123cf46..f1d8d3ed 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt @@ -11,10 +11,15 @@ import space.kscience.dataforge.names.Name * An event propagated from client to a server */ @Serializable -public sealed interface VisionEvent{ +public sealed interface VisionEvent { public val targetName: Name - public companion object{ + /** + * Create a copy of this event with the same type and content, but different [targetName] + */ + public fun changeTarget(newTarget: Name): VisionEvent + + public companion object { public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload") } } @@ -24,17 +29,21 @@ public sealed interface VisionEvent{ */ @Serializable @SerialName("meta") -public class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent +public data class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent { + override fun changeTarget(newTarget: Name): VisionMetaEvent = VisionMetaEvent(newTarget, meta) +} @Serializable @SerialName("change") -public class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent +public data class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent { + override fun changeTarget(newTarget: Name): VisionChangeEvent = VisionChangeEvent(newTarget, change) +} public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload") /** * Set the payload to be sent to server on click */ -public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit){ +public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit) { properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder) } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt index fd8aaa16..ca3d6338 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt @@ -6,10 +6,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.value -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken -import space.kscience.dataforge.names.parseAsName -import space.kscience.dataforge.names.plus +import space.kscience.dataforge.names.* import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties import space.kscience.visionforge.Vision.Companion.STYLE_KEY @@ -17,18 +14,27 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY public interface VisionGroup : Vision { public val children: VisionChildren - override fun update(change: VisionChange) { + override fun receiveChange(change: VisionChange) { change.children?.forEach { (name, change) -> if (change.vision != null || change.vision == NullVision) { error("VisionGroup is read-only") } else { - children.getChild(name)?.update(change) + children.getChild(name)?.receiveChange(change) } } change.properties?.let { updateProperties(it, Name.EMPTY) } } + + override fun receiveEvent(event: VisionEvent) { + if (event.targetName.isEmpty()) { + super.receiveEvent(event) + } else { + val target = children[event.targetName] ?: error("Child vision with name ${event.targetName} not found") + target.receiveEvent(event.changeTarget(Name.EMPTY)) + } + } } public interface MutableVisionGroup : VisionGroup { @@ -37,12 +43,12 @@ public interface MutableVisionGroup : VisionGroup { public fun createGroup(): MutableVisionGroup - override fun update(change: VisionChange) { + override fun receiveChange(change: VisionChange) { change.children?.forEach { (name, change) -> when { change.vision == NullVision -> children.setChild(name, null) change.vision != null -> children.setChild(name, change.vision) - else -> children.getChild(name)?.update(change) + else -> children.getChild(name)?.receiveChange(change) } } change.properties?.let { 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 44d07309..3042be5a 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -20,6 +20,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.isEmpty import space.kscience.dataforge.names.parseAsName import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE @@ -120,19 +121,21 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { onmessage = { messageEvent -> val stringData: String? = messageEvent.data as? String if (stringData != null) { - val change: VisionChange = visionManager.jsonFormat.decodeFromString( - VisionChange.serializer(), + val event: VisionEvent = visionManager.jsonFormat.decodeFromString( + VisionEvent.serializer(), stringData ) // If change contains root vision replacement, do it - change.vision?.let { vision -> - renderVision(element, name, vision, outputMeta) + if(event is VisionChangeEvent && event.targetName.isEmpty()) { + event.change.vision?.let { vision -> + renderVision(element, name, vision, outputMeta) + } } - logger.debug { "Got update $change for output with name $name" } + logger.debug { "Got $event for output with name $name" } if (vision == null) error("Can't update vision because it is not loaded.") - vision.update(change) + vision.receiveEvent(event) } else { logger.error { "WebSocket message data is not a string" } } diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt index cf2f068d..1831d641 100644 --- a/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/server/VisionServer.kt @@ -14,6 +14,7 @@ import io.ktor.server.util.* import io.ktor.server.websocket.* import io.ktor.util.pipeline.* import io.ktor.websocket.* +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -71,14 +72,11 @@ public class VisionRoute( /** * Serve visions in a given [route] without providing a page template. * [visions] could be changed during the service. + * + * @return a [Flow] of backward events, including vision change events */ public fun Application.serveVisionData( configuration: VisionRoute, - onEvent: suspend Vision.(VisionEvent) -> Unit = { event -> - if (event is VisionChangeEvent) { - update(event.change) - } - }, resolveVision: (Name) -> Vision?, ) { require(WebSockets) @@ -102,16 +100,18 @@ public fun Application.serveVisionData( val event = configuration.visionManager.jsonFormat.decodeFromString( VisionEvent.serializer(), data ) - vision.onEvent(event) + + vision.receiveEvent(event) } } try { withContext(configuration.context.coroutineContext) { vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update -> + val event = VisionChangeEvent(Name.EMPTY, update) val json = configuration.visionManager.jsonFormat.encodeToString( - VisionChange.serializer(), - update + VisionEvent.serializer(), + event ) application.log.debug("Sending update for $name: \n$json") outgoing.send(Frame.Text(json)) @@ -147,6 +147,8 @@ public fun Application.serveVisionData( /** * Serve a page, potentially containing any number of visions at a given [route] with given [header]. + * + * @return a [Flow] containing backward propagated events, including vision change events */ public fun Application.visionPage( route: String, @@ -154,7 +156,7 @@ public fun Application.visionPage( headers: Collection, connector: EngineConnectorConfig? = null, visionFragment: HtmlVisionFragment, -) { +){ require(WebSockets) val collector: MutableMap = mutableMapOf() diff --git a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/VisionUpdateTest.kt b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/VisionUpdateTest.kt index 0e495aaa..898fae11 100644 --- a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/VisionUpdateTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/VisionUpdateTest.kt @@ -28,7 +28,7 @@ internal class VisionUpdateTest { propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue())) propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue())) } - targetVision.update(dif) + targetVision.receiveChange(dif) assertTrue { targetVision.children.getChild("top") is SolidGroup } assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work assertEquals(