forked from kscience/visionforge
Add direct event processing to Vision
This commit is contained in:
parent
399be206be
commit
1ea5ef86e6
@ -12,7 +12,7 @@ val fxVersion by extra("11")
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.3.0-dev-15"
|
version = "0.3.0-dev-16"
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -45,7 +45,7 @@ include(
|
|||||||
":ui:ring",
|
":ui:ring",
|
||||||
// ":ui:material",
|
// ":ui:material",
|
||||||
":ui:bootstrap",
|
":ui:bootstrap",
|
||||||
// ":ui:compose",
|
":ui:compose",
|
||||||
":visionforge-core",
|
":visionforge-core",
|
||||||
":visionforge-solid",
|
":visionforge-solid",
|
||||||
// ":visionforge-fx",
|
// ":visionforge-fx",
|
||||||
|
@ -11,6 +11,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
|||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.misc.Type
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
|
import space.kscience.dataforge.names.isEmpty
|
||||||
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
||||||
import space.kscience.visionforge.Vision.Companion.TYPE
|
import space.kscience.visionforge.Vision.Companion.TYPE
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ public interface Vision : Described {
|
|||||||
/**
|
/**
|
||||||
* Update this vision using a dif represented by [VisionChange].
|
* Update this vision using a dif represented by [VisionChange].
|
||||||
*/
|
*/
|
||||||
public fun update(change: VisionChange) {
|
public fun receiveChange(change: VisionChange) {
|
||||||
if (change.children?.isNotEmpty() == true) {
|
if (change.children?.isNotEmpty() == true) {
|
||||||
error("Vision is not a group")
|
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?
|
override val descriptor: MetaDescriptor?
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
@ -11,10 +11,15 @@ import space.kscience.dataforge.names.Name
|
|||||||
* An event propagated from client to a server
|
* An event propagated from client to a server
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public sealed interface VisionEvent{
|
public sealed interface VisionEvent {
|
||||||
public val targetName: Name
|
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")
|
public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,17 +29,21 @@ public sealed interface VisionEvent{
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("meta")
|
@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
|
@Serializable
|
||||||
@SerialName("change")
|
@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")
|
public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the payload to be sent to server on click
|
* 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)
|
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
|
||||||
}
|
}
|
@ -6,10 +6,7 @@ import space.kscience.dataforge.meta.Meta
|
|||||||
import space.kscience.dataforge.meta.ValueType
|
import space.kscience.dataforge.meta.ValueType
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
import space.kscience.dataforge.meta.descriptors.value
|
import space.kscience.dataforge.meta.descriptors.value
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.dataforge.names.NameToken
|
|
||||||
import space.kscience.dataforge.names.parseAsName
|
|
||||||
import space.kscience.dataforge.names.plus
|
|
||||||
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
||||||
import space.kscience.visionforge.Vision.Companion.STYLE_KEY
|
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 interface VisionGroup : Vision {
|
||||||
public val children: VisionChildren
|
public val children: VisionChildren
|
||||||
|
|
||||||
override fun update(change: VisionChange) {
|
override fun receiveChange(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
if (change.vision != null || change.vision == NullVision) {
|
if (change.vision != null || change.vision == NullVision) {
|
||||||
error("VisionGroup is read-only")
|
error("VisionGroup is read-only")
|
||||||
} else {
|
} else {
|
||||||
children.getChild(name)?.update(change)
|
children.getChild(name)?.receiveChange(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
updateProperties(it, Name.EMPTY)
|
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 {
|
public interface MutableVisionGroup : VisionGroup {
|
||||||
@ -37,12 +43,12 @@ public interface MutableVisionGroup : VisionGroup {
|
|||||||
|
|
||||||
public fun createGroup(): MutableVisionGroup
|
public fun createGroup(): MutableVisionGroup
|
||||||
|
|
||||||
override fun update(change: VisionChange) {
|
override fun receiveChange(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
when {
|
when {
|
||||||
change.vision == NullVision -> children.setChild(name, null)
|
change.vision == NullVision -> children.setChild(name, null)
|
||||||
change.vision != null -> children.setChild(name, change.vision)
|
change.vision != null -> children.setChild(name, change.vision)
|
||||||
else -> children.getChild(name)?.update(change)
|
else -> children.getChild(name)?.receiveChange(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
|
@ -20,6 +20,7 @@ import space.kscience.dataforge.meta.MetaSerializer
|
|||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.isEmpty
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
import space.kscience.visionforge.html.VisionTagConsumer
|
import space.kscience.visionforge.html.VisionTagConsumer
|
||||||
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
|
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
|
||||||
@ -120,19 +121,21 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
onmessage = { messageEvent ->
|
onmessage = { messageEvent ->
|
||||||
val stringData: String? = messageEvent.data as? String
|
val stringData: String? = messageEvent.data as? String
|
||||||
if (stringData != null) {
|
if (stringData != null) {
|
||||||
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
|
val event: VisionEvent = visionManager.jsonFormat.decodeFromString(
|
||||||
VisionChange.serializer(),
|
VisionEvent.serializer(),
|
||||||
stringData
|
stringData
|
||||||
)
|
)
|
||||||
|
|
||||||
// If change contains root vision replacement, do it
|
// If change contains root vision replacement, do it
|
||||||
change.vision?.let { vision ->
|
if(event is VisionChangeEvent && event.targetName.isEmpty()) {
|
||||||
renderVision(element, name, vision, outputMeta)
|
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.")
|
if (vision == null) error("Can't update vision because it is not loaded.")
|
||||||
vision.update(change)
|
vision.receiveEvent(event)
|
||||||
} else {
|
} else {
|
||||||
logger.error { "WebSocket message data is not a string" }
|
logger.error { "WebSocket message data is not a string" }
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import io.ktor.server.util.*
|
|||||||
import io.ktor.server.websocket.*
|
import io.ktor.server.websocket.*
|
||||||
import io.ktor.util.pipeline.*
|
import io.ktor.util.pipeline.*
|
||||||
import io.ktor.websocket.*
|
import io.ktor.websocket.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -71,14 +72,11 @@ public class VisionRoute(
|
|||||||
/**
|
/**
|
||||||
* Serve visions in a given [route] without providing a page template.
|
* Serve visions in a given [route] without providing a page template.
|
||||||
* [visions] could be changed during the service.
|
* [visions] could be changed during the service.
|
||||||
|
*
|
||||||
|
* @return a [Flow] of backward events, including vision change events
|
||||||
*/
|
*/
|
||||||
public fun Application.serveVisionData(
|
public fun Application.serveVisionData(
|
||||||
configuration: VisionRoute,
|
configuration: VisionRoute,
|
||||||
onEvent: suspend Vision.(VisionEvent) -> Unit = { event ->
|
|
||||||
if (event is VisionChangeEvent) {
|
|
||||||
update(event.change)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolveVision: (Name) -> Vision?,
|
resolveVision: (Name) -> Vision?,
|
||||||
) {
|
) {
|
||||||
require(WebSockets)
|
require(WebSockets)
|
||||||
@ -102,16 +100,18 @@ public fun Application.serveVisionData(
|
|||||||
val event = configuration.visionManager.jsonFormat.decodeFromString(
|
val event = configuration.visionManager.jsonFormat.decodeFromString(
|
||||||
VisionEvent.serializer(), data
|
VisionEvent.serializer(), data
|
||||||
)
|
)
|
||||||
vision.onEvent(event)
|
|
||||||
|
vision.receiveEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
withContext(configuration.context.coroutineContext) {
|
withContext(configuration.context.coroutineContext) {
|
||||||
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
|
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
|
||||||
|
val event = VisionChangeEvent(Name.EMPTY, update)
|
||||||
val json = configuration.visionManager.jsonFormat.encodeToString(
|
val json = configuration.visionManager.jsonFormat.encodeToString(
|
||||||
VisionChange.serializer(),
|
VisionEvent.serializer(),
|
||||||
update
|
event
|
||||||
)
|
)
|
||||||
application.log.debug("Sending update for $name: \n$json")
|
application.log.debug("Sending update for $name: \n$json")
|
||||||
outgoing.send(Frame.Text(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].
|
* 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(
|
public fun Application.visionPage(
|
||||||
route: String,
|
route: String,
|
||||||
@ -154,7 +156,7 @@ public fun Application.visionPage(
|
|||||||
headers: Collection<HtmlFragment>,
|
headers: Collection<HtmlFragment>,
|
||||||
connector: EngineConnectorConfig? = null,
|
connector: EngineConnectorConfig? = null,
|
||||||
visionFragment: HtmlVisionFragment,
|
visionFragment: HtmlVisionFragment,
|
||||||
) {
|
){
|
||||||
require(WebSockets)
|
require(WebSockets)
|
||||||
|
|
||||||
val collector: MutableMap<Name, Vision> = mutableMapOf()
|
val collector: MutableMap<Name, Vision> = mutableMapOf()
|
||||||
|
@ -28,7 +28,7 @@ internal class VisionUpdateTest {
|
|||||||
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
||||||
propertyChanged("origin".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 }
|
assertTrue { targetVision.children.getChild("top") is SolidGroup }
|
||||||
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
Loading…
Reference in New Issue
Block a user