From 399be206be9e2c455db4895aacc770efd0ff7a86 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 13 Nov 2023 21:45:37 +0300 Subject: [PATCH 01/13] Non-nullable accessor for colors --- CHANGELOG.md | 2 +- build.gradle.kts | 2 +- demo/muon-monitor/build.gradle.kts | 4 - gradle.properties | 2 +- ui/compose/build.gradle.kts | 41 ++++++ .../visionforge/compose/MetaViewer.kt | 130 ++++++++++++++++++ .../visionforge/compose/TreeStyles.kt | 71 ++++++++++ .../kscience/visionforge/compose/layouts.kt | 41 ++++++ .../visionforge/solid/ColorAccessor.kt | 22 +-- .../visionforge/solid/SolidPropertyTest.kt | 2 +- .../visionforge/solid/SolidReferenceTest.kt | 2 +- visionforge-tables/build.gradle.kts | 6 +- 12 files changed, 302 insertions(+), 23 deletions(-) create mode 100644 ui/compose/build.gradle.kts create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/layouts.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 87fe2343..bf2c3478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Custom client-side events and thier processing in VisionServer ### Changed -- Color accessor property is now `colorProperty`. Color uses `invoke` instead of `set` +- Color accessor property is now `colorProperty`. Color uses non-nullable `invoke` instead of `set`. - API update for server and pages - Edges moved to solids module for easier construction - Visions **must** be rooted in order to subscribe to updates. diff --git a/build.gradle.kts b/build.gradle.kts index 4e97dbe6..9f982d94 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-14" + version = "0.3.0-dev-15" } subprojects { diff --git a/demo/muon-monitor/build.gradle.kts b/demo/muon-monitor/build.gradle.kts index e453492b..d01b9dc9 100644 --- a/demo/muon-monitor/build.gradle.kts +++ b/demo/muon-monitor/build.gradle.kts @@ -13,7 +13,6 @@ kscience { useKtor() fullStack( "muon-monitor.js", - development = true, jvmConfig = { withJava() }, jsConfig = { useCommonJs() } ) { @@ -47,9 +46,6 @@ application { mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt") } -//TODO ??? -tasks.getByName("jsBrowserProductionWebpack").dependsOn("jsDevelopmentExecutableCompileSync") - //distributions { // main { // contents { diff --git a/gradle.properties b/gradle.properties index c8070279..9413b93e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G org.jetbrains.compose.experimental.jscanvas.enabled=true -toolsVersion=0.15.0-kotlin-1.9.20-RC2 +toolsVersion=0.15.0-kotlin-1.9.20 #kotlin.experimental.tryK2=true #kscience.wasm.disabled=true diff --git a/ui/compose/build.gradle.kts b/ui/compose/build.gradle.kts new file mode 100644 index 00000000..a2f2c4a7 --- /dev/null +++ b/ui/compose/build.gradle.kts @@ -0,0 +1,41 @@ + +plugins { + id("space.kscience.gradle.mpp") + id("org.jetbrains.compose") version "1.5.10" +// id("com.android.library") +} + +kscience{ + jvm() + js() +// wasm() +} + +kotlin { +// android() + sourceSets { + val commonMain by getting { + dependencies { + + } + } + + val jvmMain by getting { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + api(compose.preview) + } + } + + val jsMain by getting{ + dependencies { + api(compose.html.core) + api("app.softwork:bootstrap-compose:0.1.15") + api("app.softwork:bootstrap-compose-icons:0.1.15") + api(projects.visionforge.visionforgeThreejs) + } + } + } +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt new file mode 100644 index 00000000..93cbddfc --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt @@ -0,0 +1,130 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import kotlinx.html.js.onClickFunction +import org.jetbrains.compose.web.css.AlignItems +import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.dom.Span +import org.w3c.dom.events.Event +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.descriptors.MetaDescriptor +import space.kscience.dataforge.meta.descriptors.get +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.isLeaf +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.lastOrNull +import space.kscience.dataforge.names.plus + + +private val MetaViewerItem: FC = fc("MetaViewerItem") { props -> + metaViewerItem(props) +} + +@Composable +private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) { + var expanded: Boolean by remember { mutableStateOf(true) } + val item: Meta? = root[name] + val descriptorItem: MetaDescriptor? = rootDescriptor?.get(name) + val actualValue = item?.value ?: descriptorItem?.defaultValue + val actualMeta = item ?: descriptorItem?.defaultNode + + val token = name.lastOrNull()?.toString() ?: props.rootName ?: "" + + val expanderClick: (Event) -> Unit = { + expanded = !expanded + } + + FlexRow(attrs = { + classes("metaItem") + style { + alignItems(AlignItems.Center) + } + }) { + if (actualMeta?.isLeaf == false) { + Span(attrs = { + + }) + styledSpan { + css { + +TreeStyles.treeCaret + if (expanded) { + +TreeStyles.treeCaredDown + } + } + attrs { + onClickFunction = expanderClick + } + } + } + + styledSpan { + css { + +TreeStyles.treeLabel + if (item == null) { + +TreeStyles.treeLabelInactive + } + } + +token + } + styledDiv { + a { + +actualValue.toString() + } + } + } + if (expanded) { + flexColumn { + css { + +TreeStyles.tree + } + val keys = buildSet { + descriptorItem?.children?.keys?.forEach { + add(NameToken(it)) + } + actualMeta!!.items.keys.let { addAll(it) } + } + + keys.filter { !it.body.startsWith("@") }.forEach { token -> + styledDiv { + css { + +TreeStyles.treeItem + } + child(MetaViewerItem) { + attrs { + this.key = props.name.toString() + this.root = props.root + this.name = props.name + token + this.descriptor = props.descriptor + } + } + //configEditor(props.root, props.name + token, props.descriptor, props.default) + } + } + } + } + + +} + +@JsExport +public val MetaViewer: FC = fc("MetaViewer") { props -> + child(MetaViewerItem) { + attrs { + this.key = "" + this.root = props.root + this.name = Name.EMPTY + this.descriptor = props.descriptor + } + } +} + +public fun RBuilder.metaViewer(meta: Meta, descriptor: MetaDescriptor? = null, key: Any? = null) { + child(MetaViewer) { + attrs { + this.key = key?.toString() ?: "" + this.root = meta + this.descriptor = descriptor + } + } +} diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt new file mode 100644 index 00000000..29074464 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt @@ -0,0 +1,71 @@ +package space.kscience.visionforge.compose + +import kotlinx.css.* +import org.jetbrains.compose.web.css.* + +public object TreeStyles : StyleSheet() { + /** + * Remove default bullets + */ + public val tree: String by style { + paddingLeft(5.px) + marginLeft(0.px) + listStyleType("none") + } + + /** + * Style the caret/arrow + */ + public val treeCaret by style { + cursor("pointer") + userSelect = UserSelect.none + /* Create the caret/arrow with a unicode, and style it */ + before { + content = "\u25B6".quoted + color(Color.black) + display(DisplayStyle.InlineBlock) + marginRight(6.px) + } + } + + /** + * Rotate the caret/arrow icon when clicked on (using JavaScript) + */ + public val treeCaredDown by style { + before { + content = "\u25B6".quoted + color(Color.black) + display(DisplayStyle.InlineBlock) + marginRight(6.px) + transform { rotate(90.deg) } + } + } + + public val treeItem: String by style { + alignItems(AlignItems.Center) + paddingLeft(10.px) + border { + left{ + width(1.px) + color(Color.lightgray) + style = LineStyle.Dashed + } + } + } + + public val treeLabel by style { + border(style = LineStyle.None) + padding(left = 4.pt, right = 4.pt, top = 0.pt, bottom = 0.pt) + textAlign("left") + flex(1) + } + + public val treeLabelInactive: RuleSet by css { + color = Color.lightGray + } + + public val treeLabelSelected: RuleSet by css { + backgroundColor = Color.lightBlue + } + +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/layouts.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/layouts.kt new file mode 100644 index 00000000..882f1f1c --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/layouts.kt @@ -0,0 +1,41 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.css.DisplayStyle +import org.jetbrains.compose.web.css.FlexDirection +import org.jetbrains.compose.web.css.display +import org.jetbrains.compose.web.css.flexDirection +import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.ElementScope +import org.w3c.dom.HTMLDivElement + +@Composable +public fun FlexColumn( + attrs: AttrBuilderContext? = null, + content: @Composable ElementScope.() -> Unit, +): Unit = Div( + attrs = { + style { + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Column) + } + attrs?.invoke(this) + }, + content +) + +@Composable +public fun FlexRow( + attrs: AttrBuilderContext? = null, + content: @Composable ElementScope.() -> Unit, +): Unit = Div( + attrs = { + style { + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Row) + } + attrs?.invoke(this) + }, + content +) \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/ColorAccessor.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/ColorAccessor.kt index 60789027..14a22797 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/ColorAccessor.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/ColorAccessor.kt @@ -34,33 +34,33 @@ public fun Vision.colorProperty( ColorAccessor(properties.root(true), propertyName ?: property.name.asName()) } -public var ColorAccessor?.string: String? - get() = this?.value?.let { if (it == Null) null else it.string } +public var ColorAccessor.string: String? + get() = value?.let { if (it == Null) null else it.string } set(value) { - this?.value = value?.asValue() + this.value = value?.asValue() } /** * Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string */ -public operator fun ColorAccessor?.invoke(webColor: String) { - this?.value = webColor.asValue() +public operator fun ColorAccessor.invoke(webColor: String) { + value = webColor.asValue() } /** * Set color as RGB integer */ -public operator fun ColorAccessor?.invoke(rgb: Int) { - this?.value = Colors.rgbToString(rgb).asValue() +public operator fun ColorAccessor.invoke(rgb: Int) { + value = Colors.rgbToString(rgb).asValue() } /** * Set color as RGB */ -public operator fun ColorAccessor?.invoke(r: UByte, g: UByte, b: UByte) { - this?.value = Colors.rgbToString(r, g, b).asValue() +public operator fun ColorAccessor.invoke(r: UByte, g: UByte, b: UByte) { + value = Colors.rgbToString(r, g, b).asValue() } -public fun ColorAccessor?.clear() { - this?.value = null +public fun ColorAccessor.clear() { + value = null } \ No newline at end of file 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 e4b0619d..4991c12d 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 @@ -94,7 +94,7 @@ class SolidPropertyTest { } } } - assertEquals("#555555", box?.color.string) + assertEquals("#555555", box?.color?.string) } @Test diff --git a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidReferenceTest.kt b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidReferenceTest.kt index d8d971bb..512f3807 100644 --- a/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidReferenceTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/space/kscience/visionforge/solid/SolidReferenceTest.kt @@ -31,7 +31,7 @@ class SolidReferenceTest { fun testReferenceSerialization(){ val serialized = Solids.jsonForSolids.encodeToJsonElement(groupWithReference) val deserialized = Solids.jsonForSolids.decodeFromJsonElement(SolidGroup.serializer(), serialized) - assertEquals(groupWithReference.items["test"]?.color.string, deserialized.items["test"]?.color.string) + assertEquals(groupWithReference.items["test"]?.color?.string, deserialized.items["test"]?.color?.string) assertEquals("blue", (deserialized.children.getChild("test") as Solid).color.string) } } \ No newline at end of file diff --git a/visionforge-tables/build.gradle.kts b/visionforge-tables/build.gradle.kts index cf813e6e..33988cf0 100644 --- a/visionforge-tables/build.gradle.kts +++ b/visionforge-tables/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("space.kscience.gradle.mpp") } -val tablesVersion = "0.2.0-dev-4" +val tablesVersion = "0.2.1" kscience { jvm() @@ -22,8 +22,8 @@ kscience { api("space.kscience:tables-kt:${tablesVersion}") } dependencies(jsMain) { - implementation(npm("tabulator-tables", "5.4.4")) - implementation(npm("@types/tabulator-tables", "5.4.8")) + implementation(npm("tabulator-tables", "5.5.2")) + implementation(npm("@types/tabulator-tables", "5.5.3")) } useSerialization() } From 1ea5ef86e66abb12198847288f9c689e61ace89c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 15 Nov 2023 10:26:37 +0300 Subject: [PATCH 02/13] Add direct event processing to Vision --- build.gradle.kts | 2 +- settings.gradle.kts | 2 +- .../space/kscience/visionforge/Vision.kt | 17 +++++++++++++- .../space/kscience/visionforge/VisionEvent.kt | 19 +++++++++++----- .../space/kscience/visionforge/VisionGroup.kt | 22 ++++++++++++------- .../kscience/visionforge/JsVisionClient.kt | 15 ++++++++----- .../visionforge/server/VisionServer.kt | 20 +++++++++-------- .../visionforge/solid/VisionUpdateTest.kt | 2 +- 8 files changed, 67 insertions(+), 32 deletions(-) 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( From c7d4bdfa5f989c320412bca67ab90e8be2b0e17c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 15 Nov 2023 10:42:56 +0300 Subject: [PATCH 03/13] Add direct event processing to Vision --- settings.gradle.kts | 2 +- .../space/kscience/visionforge/Vision.kt | 11 +++-------- .../space/kscience/visionforge/VisionChange.kt | 3 +-- .../space/kscience/visionforge/VisionClient.kt | 6 +++--- .../space/kscience/visionforge/VisionEvent.kt | 16 +--------------- .../space/kscience/visionforge/VisionGroup.kt | 14 ++++---------- .../kscience/visionforge/JsVisionClient.kt | 18 ++++++++---------- .../visionforge/server/VisionServer.kt | 3 +-- 8 files changed, 22 insertions(+), 51 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8611d0aa..31119a06 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 8cfe7a27..06cd6d8f 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -11,7 +11,6 @@ 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 @@ -50,13 +49,9 @@ 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") + when (event) { + is VisionChange -> receiveChange(event) + else -> TODO() } } 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 9915c035..87bff794 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt @@ -63,8 +63,7 @@ public data class VisionChange( public val vision: Vision? = null, public val properties: Meta? = null, public val children: Map? = null, -) - +) : VisionEvent /** * An update for a [Vision] diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt index cc1c38b8..76d8aa80 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -13,7 +13,7 @@ import space.kscience.dataforge.names.parseAsName public interface VisionClient: Plugin { public val visionManager: VisionManager - public suspend fun sendEvent(event: VisionEvent) + public suspend fun sendEvent(targetName: Name, event: VisionEvent) public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) } @@ -35,8 +35,8 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) } -public fun VisionClient.sendEvent(visionName: Name, event: MetaRepr): Unit { +public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit { context.launch { - sendEvent(VisionMetaEvent(visionName, event.toMeta())) + sendEvent(targetName, VisionMetaEvent(payload.toMeta())) } } \ No newline at end of file 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 f1d8d3ed..84d36217 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt @@ -12,13 +12,6 @@ import space.kscience.dataforge.names.Name */ @Serializable public sealed interface VisionEvent { - public val targetName: Name - - /** - * 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") } @@ -29,15 +22,8 @@ public sealed interface VisionEvent { */ @Serializable @SerialName("meta") -public data class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent { - override fun changeTarget(newTarget: Name): VisionMetaEvent = VisionMetaEvent(newTarget, meta) -} +public class VisionMetaEvent(public val meta: Meta) : VisionEvent -@Serializable -@SerialName("change") -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") 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 ca3d6338..1edfa40a 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt @@ -6,7 +6,10 @@ 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.* +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.visionforge.AbstractVisionGroup.Companion.updateProperties import space.kscience.visionforge.Vision.Companion.STYLE_KEY @@ -26,15 +29,6 @@ public interface VisionGroup : Vision { 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 { 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 3042be5a..b9e0ef93 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -20,7 +20,6 @@ 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 @@ -82,15 +81,14 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { } private val eventCollector by lazy { - MutableSharedFlow(meta["feedback.eventCache"].int ?: 100) + MutableSharedFlow>(meta["feedback.eventCache"].int ?: 100) } - /** * Send a custom feedback event */ - override suspend fun sendEvent(event: VisionEvent) { - eventCollector.emit(event) + override suspend fun sendEvent(targetName: Name, event: VisionEvent) { + eventCollector.emit(targetName to event) } private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) { @@ -127,8 +125,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { ) // If change contains root vision replacement, do it - if(event is VisionChangeEvent && event.targetName.isEmpty()) { - event.change.vision?.let { vision -> + if(event is VisionChange) { + event.vision?.let { vision -> renderVision(element, name, vision, outputMeta) } } @@ -150,8 +148,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { onopen = { feedbackJob = visionManager.context.launch { - eventCollector.filter { it.targetName == name }.onEach { - send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it)) + eventCollector.filter { it.first == name }.onEach { + send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second)) }.launchIn(this) while (isActive) { @@ -159,7 +157,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { val change = changeCollector[name] ?: continue if (!change.isEmpty()) { mutex.withLock { - eventCollector.emit(VisionChangeEvent(name, change.deepCopy(visionManager))) + eventCollector.emit(name to change.deepCopy(visionManager)) change.reset() } } 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 1831d641..4c5e093a 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 @@ -107,8 +107,7 @@ public fun Application.serveVisionData( try { withContext(configuration.context.coroutineContext) { - vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update -> - val event = VisionChangeEvent(Name.EMPTY, update) + vision.flowChanges(configuration.updateInterval.milliseconds).onEach { event -> val json = configuration.visionManager.jsonFormat.encodeToString( VisionEvent.serializer(), event From f0048a4d46c433e14ad0558dd3014120396983a3 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 15 Nov 2023 10:49:19 +0300 Subject: [PATCH 04/13] Add direct event processing to Vision --- .../commonMain/kotlin/space/kscience/visionforge/Vision.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 06cd6d8f..55d1391b 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.asValue import space.kscience.dataforge.meta.boolean import space.kscience.dataforge.meta.descriptors.Described @@ -45,13 +46,17 @@ public interface Vision : Described { } } + public fun onMetaEvent(meta: Meta){ + + } + /** * Receive and process a generic [VisionEvent]. */ public fun receiveEvent(event: VisionEvent) { when (event) { is VisionChange -> receiveChange(event) - else -> TODO() + is VisionMetaEvent -> onMetaEvent(event.meta) } } From ed71ba9ccbf42e03779f480b2407c99ab8712046 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 20 Nov 2023 10:03:44 +0300 Subject: [PATCH 05/13] Add compose-html --- settings.gradle.kts | 2 +- .../visionforge/compose/MetaViewer.kt | 103 ++----- .../visionforge/compose/PropertyEditor.kt | 189 ++++++++++++ .../visionforge/compose/ThreeCanvas.kt | 52 ++++ .../kscience/visionforge/compose/ThreeJS.kt | 11 - .../visionforge/compose/TreeStyles.kt | 49 +++- .../space/kscience/visionforge/compose/css.kt | 35 +++ .../visionforge/compose/valueChooser.kt | 268 ++++++++++++++++++ .../space/kscience/visionforge/Vision.kt | 2 +- 9 files changed, 612 insertions(+), 99 deletions(-) create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt delete mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt 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/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt index 93cbddfc..5aaf4140 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt @@ -1,11 +1,12 @@ package space.kscience.visionforge.compose import androidx.compose.runtime.* -import kotlinx.html.js.onClickFunction import org.jetbrains.compose.web.css.AlignItems import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.dom.A +import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Span -import org.w3c.dom.events.Event +import org.jetbrains.compose.web.dom.Text import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.get @@ -17,10 +18,6 @@ import space.kscience.dataforge.names.lastOrNull import space.kscience.dataforge.names.plus -private val MetaViewerItem: FC = fc("MetaViewerItem") { props -> - metaViewerItem(props) -} - @Composable private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) { var expanded: Boolean by remember { mutableStateOf(true) } @@ -29,11 +26,7 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto val actualValue = item?.value ?: descriptorItem?.defaultValue val actualMeta = item ?: descriptorItem?.defaultNode - val token = name.lastOrNull()?.toString() ?: props.rootName ?: "" - - val expanderClick: (Event) -> Unit = { - expanded = !expanded - } + val token = name.lastOrNull()?.toString() ?: "" FlexRow(attrs = { classes("metaItem") @@ -42,42 +35,34 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto } }) { if (actualMeta?.isLeaf == false) { - Span(attrs = { - + Span({ + classes(TreeStyles.treeCaret) + if (expanded) { + classes(TreeStyles.treeCaretDown) + } + onClick { expanded = !expanded } }) - styledSpan { - css { - +TreeStyles.treeCaret - if (expanded) { - +TreeStyles.treeCaredDown - } - } - attrs { - onClickFunction = expanderClick - } - } } - styledSpan { - css { - +TreeStyles.treeLabel - if (item == null) { - +TreeStyles.treeLabelInactive - } + Span({ + classes(TreeStyles.treeLabel) + if (item == null) { + classes(TreeStyles.treeLabelInactive) } - +token + }) { + Text(token) } - styledDiv { - a { - +actualValue.toString() + + Div { + A { + Text(actualValue.toString()) } } } if (expanded) { - flexColumn { - css { - +TreeStyles.tree - } + FlexColumn({ + classes(TreeStyles.tree) + }) { val keys = buildSet { descriptorItem?.children?.keys?.forEach { add(NameToken(it)) @@ -86,45 +71,17 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto } keys.filter { !it.body.startsWith("@") }.forEach { token -> - styledDiv { - css { - +TreeStyles.treeItem - } - child(MetaViewerItem) { - attrs { - this.key = props.name.toString() - this.root = props.root - this.name = props.name + token - this.descriptor = props.descriptor - } - } - //configEditor(props.root, props.name + token, props.descriptor, props.default) + Div({ + classes(TreeStyles.treeItem) + }) { + MetaViewerItem(root, name + token, rootDescriptor) } } } } - - } -@JsExport -public val MetaViewer: FC = fc("MetaViewer") { props -> - child(MetaViewerItem) { - attrs { - this.key = "" - this.root = props.root - this.name = Name.EMPTY - this.descriptor = props.descriptor - } - } -} - -public fun RBuilder.metaViewer(meta: Meta, descriptor: MetaDescriptor? = null, key: Any? = null) { - child(MetaViewer) { - attrs { - this.key = key?.toString() ?: "" - this.root = meta - this.descriptor = descriptor - } - } +@Composable +public fun MetaViewer(meta: Meta, descriptor: MetaDescriptor? = null) { + MetaViewerItem(meta, Name.EMPTY, descriptor) } diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt new file mode 100644 index 00000000..f82b9e05 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt @@ -0,0 +1,189 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import org.jetbrains.compose.web.attributes.disabled +import org.jetbrains.compose.web.css.AlignItems +import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.meta.ObservableMutableMeta +import space.kscience.dataforge.meta.descriptors.MetaDescriptor +import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.get +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.remove +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.isEmpty +import space.kscience.dataforge.names.lastOrNull +import space.kscience.visionforge.hidden + + +/** + * The display state of a property + */ +public sealed class EditorPropertyState { + public object Defined : EditorPropertyState() + public class Default(public val source: String = "unknown") : EditorPropertyState() + public object Undefined : EditorPropertyState() + +} + +/** + * @param rootDescriptor Full path to the displayed node in [meta]. Could be empty + */ +@Composable +private fun PropertyEditorItem( + /** + * Root config object - always non-null + */ + meta: MutableMeta, + getPropertyState: (Name) -> EditorPropertyState, + scope: CoroutineScope, + updates: Flow, + name: Name, + rootDescriptor: MetaDescriptor?, + initialExpanded: Boolean? = null, +) { + var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) } + val descriptor: MetaDescriptor? = remember(rootDescriptor, name) { rootDescriptor?.get(name) } + var property: MutableMeta by remember { mutableStateOf(meta.getOrCreate(name)) } + var editorPropertyState: EditorPropertyState by remember { mutableStateOf(getPropertyState(name)) } + + + val keys = remember(descriptor) { + buildSet { + descriptor?.children?.filterNot { + it.key.startsWith("@") || it.value.hidden + }?.forEach { + add(NameToken(it.key)) + } + //ownProperty?.items?.keys?.filterNot { it.body.startsWith("@") }?.let { addAll(it) } + } + } + + val token = name.lastOrNull()?.toString() ?: "Properties" + + fun update() { + property = meta.getOrCreate(name) + editorPropertyState = getPropertyState(name) + } + + LaunchedEffect(meta) { + updates.collect { updatedName -> + if (updatedName == name) { + update() + } + } + } + + FlexRow({ + style { + alignItems(AlignItems.Center) + } + }) { + if (keys.isNotEmpty()) { + Span({ + classes(TreeStyles.treeCaret) + if (expanded) { + classes(TreeStyles.treeCaretDown) + } + onClick { expanded = !expanded } + }) + } + Span({ + classes(TreeStyles.treeLabel) + if (editorPropertyState != EditorPropertyState.Defined) { + classes(TreeStyles.treeLabelInactive) + } + }) { + Text(token) + } + + if (!name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { + Div({ + style { + width(160.px) + marginAll(1.px, 5.px) + } + }) { + ValueChooser(descriptor, editorPropertyState, property.value) { + property.value = it + editorPropertyState = getPropertyState(name) + } + } + + Button({ + classes(TreeStyles.propertyEditorButton) + if (editorPropertyState != EditorPropertyState.Defined) { + disabled() + } else { + onClick { + meta.remove(name) + update() + } + } + }) { + Text("\u00D7") + } + } + } + if (expanded) { + FlexColumn({ + classes(TreeStyles.tree) + }) { + keys.forEach { token -> + Div({ + classes(TreeStyles.treeItem) + }) { + PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded) + } + } + } + } +} + +@Composable +public fun PropertyEditor( + scope: CoroutineScope, + properties: ObservableMutableMeta, + descriptor: MetaDescriptor? = null, + expanded: Boolean? = null, +) { + PropertyEditorItem( + meta = properties, + getPropertyState = { name -> + if (properties[name] != null) { + EditorPropertyState.Defined + } else if (descriptor?.get(name)?.defaultValue != null) { + EditorPropertyState.Default("descriptor") + } else { + EditorPropertyState.Undefined + } + }, + scope = scope, + updates = callbackFlow { + properties.onChange(scope) { name -> + scope.launch { + send(name) + } + } + + invokeOnClose { + properties.removeListener(scope) + } + }, + name = Name.EMPTY, + rootDescriptor = descriptor, + initialExpanded = expanded, + ) +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt new file mode 100644 index 00000000..9816b5c2 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt @@ -0,0 +1,52 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import kotlinx.dom.clear +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Div +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.names.Name +import space.kscience.visionforge.solid.Solid +import space.kscience.visionforge.solid.specifications.Canvas3DOptions +import space.kscience.visionforge.solid.three.ThreeCanvas +import space.kscience.visionforge.solid.three.ThreePlugin + +@Composable +public fun ThreeCanvas( + context: Context, + options: Canvas3DOptions?, + solid: Solid?, + selected: Name?, +) { + + val three: ThreePlugin by derivedStateOf { context.request(ThreePlugin) } + + Div({ + style { + maxWidth(100.vw) + maxHeight(100.vh) + width(100.percent) + height(100.percent) + } + }) { + var canvas: ThreeCanvas? = null + DisposableEffect(options) { + canvas = ThreeCanvas(three, scopeElement, options ?: Canvas3DOptions()) + onDispose { + scopeElement.clear() + canvas = null + } + } + LaunchedEffect(solid) { + if (solid != null) { + canvas?.render(solid) + } else { + canvas?.clear() + } + } + LaunchedEffect(selected) { + canvas?.select(selected) + } + } +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt deleted file mode 100644 index 6033c712..00000000 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt +++ /dev/null @@ -1,11 +0,0 @@ -package space.kscience.visionforge.compose - -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable - -@Composable -public fun ThreeJs(){ - Surface { - - } -} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt index 29074464..de9001ba 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt @@ -1,8 +1,10 @@ package space.kscience.visionforge.compose -import kotlinx.css.* +import org.jetbrains.compose.web.ExperimentalComposeWebApi import org.jetbrains.compose.web.css.* + +@OptIn(ExperimentalComposeWebApi::class) public object TreeStyles : StyleSheet() { /** * Remove default bullets @@ -16,12 +18,12 @@ public object TreeStyles : StyleSheet() { /** * Style the caret/arrow */ - public val treeCaret by style { + public val treeCaret: String by style { cursor("pointer") - userSelect = UserSelect.none + userSelect(UserSelect.none) /* Create the caret/arrow with a unicode, and style it */ before { - content = "\u25B6".quoted + content("\u25B6") color(Color.black) display(DisplayStyle.InlineBlock) marginRight(6.px) @@ -31,9 +33,9 @@ public object TreeStyles : StyleSheet() { /** * Rotate the caret/arrow icon when clicked on (using JavaScript) */ - public val treeCaredDown by style { + public val treeCaretDown: String by style { before { - content = "\u25B6".quoted + content("\u25B6") color(Color.black) display(DisplayStyle.InlineBlock) marginRight(6.px) @@ -45,7 +47,7 @@ public object TreeStyles : StyleSheet() { alignItems(AlignItems.Center) paddingLeft(10.px) border { - left{ + left { width(1.px) color(Color.lightgray) style = LineStyle.Dashed @@ -53,19 +55,40 @@ public object TreeStyles : StyleSheet() { } } - public val treeLabel by style { + public val treeLabel: String by style { border(style = LineStyle.None) - padding(left = 4.pt, right = 4.pt, top = 0.pt, bottom = 0.pt) + paddingAll(left = 4.pt, right = 4.pt) textAlign("left") flex(1) } - public val treeLabelInactive: RuleSet by css { - color = Color.lightGray + public val treeLabelInactive: String by style { + color(Color.lightgray) } - public val treeLabelSelected: RuleSet by css { - backgroundColor = Color.lightBlue + public val treeLabelSelected: String by style { + backgroundColor(Color.lightblue) + } + + public val propertyEditorButton: String by style { + width(24.px) + alignSelf(AlignSelf.Stretch) + marginAll(1.px, 5.px) + backgroundColor(Color.white) + border{ + style(LineStyle.Solid) + } + borderRadius(2.px) + textAlign("center") + textDecoration("none") + cursor("pointer") + disabled { + cursor("auto") + border{ + style(LineStyle.Dashed) + } + color(Color.lightgray) + } } } \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt new file mode 100644 index 00000000..9f6eb3e4 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt @@ -0,0 +1,35 @@ +package space.kscience.visionforge.compose + +import org.jetbrains.compose.web.css.* + +public enum class UserSelect { + inherit, initial, revert, revertLayer, unset, + + none, auto, text, contain, all; +} + +public fun StyleScope.userSelect(value: UserSelect) { + property("user-select", value.name) +} + +public fun StyleScope.content(value: String) { + property("content", "'$value'") +} + +public fun StyleScope.paddingAll( + top: CSSNumeric = 0.pt, + right: CSSNumeric = top, + bottom: CSSNumeric = top, + left: CSSNumeric = right, +) { + padding(top, right, bottom, left) +} + +public fun StyleScope.marginAll( + top: CSSNumeric = 0.pt, + right: CSSNumeric = top, + bottom: CSSNumeric = top, + left: CSSNumeric = right, +) { + margin(top, right, bottom, left) +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt new file mode 100644 index 00000000..0be879ad --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt @@ -0,0 +1,268 @@ +@file:Suppress("UNUSED_PARAMETER") + +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.Input +import org.jetbrains.compose.web.dom.Option +import org.jetbrains.compose.web.dom.Select +import org.jetbrains.compose.web.dom.Text +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLOptionElement +import org.w3c.dom.asList +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.descriptors.MetaDescriptor +import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.allowedValues +import space.kscience.visionforge.Colors +import space.kscience.visionforge.widgetType +import three.math.Color + + +@Composable +public fun StringValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var stringValue by remember { mutableStateOf(value?.string ?: "") } + Input(type = InputType.Text) { + style { + width(100.percent) + } + value(stringValue) + onKeyDown { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + stringValue = (event.target as HTMLInputElement).value + onValueChange(stringValue.asValue()) + } + } + onChange { + stringValue = it.target.value + } + } +} + + +@Composable +public fun BooleanValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Input(type = InputType.Checkbox) { + style { + width(100.percent) + } + //this.attributes["indeterminate"] = (props.item == null).toString() + checked(value?.boolean ?: false) + + onChange { + val newValue = it.target.checked + onValueChange(newValue.asValue()) + } + } +} + +@Composable +public fun NumberValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var innerValue by remember { mutableStateOf(value?.string ?: "") } + Input(type = InputType.Number) { + style { + width(100.percent) + } + value(innerValue) + onKeyDown { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + innerValue = (event.target as HTMLInputElement).value + val number = innerValue.toDoubleOrNull() + if (number == null) { + console.error("The input value $innerValue is not a number") + } else { + onValueChange(number.asValue()) + } + } + } + onChange { + innerValue = it.target.value + } + descriptor?.attributes?.get("step").number?.let { + step(it) + } + descriptor?.attributes?.get("min").string?.let { + min(it) + } + descriptor?.attributes?.get("max").string?.let { + max(it) + } + } +} + + +@Composable +public fun ComboValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var selected by remember { mutableStateOf(value?.string ?: "") } + Select({ + style { + width(100.percent) + } + onChange { + selected = it.target.value + onValueChange(selected.asValue()) + } + }, multiple = false) { + descriptor?.allowedValues?.forEach { + Option(it.string, { if (it == value) selected() }) { + Text(it.string) + } + } + + } +} + +@Composable +public fun ColorValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Input(type = InputType.Color) { + style { + width(100.percent) + marginAll(0.px) + } + value( + value?.let { value -> + if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) + else "#" + Color(value.string).getHexString() + } ?: "#000000" + ) + onChange { + onValueChange(it.target.value.asValue()) + } + } +} + + +@Composable +public fun MultiSelectChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Select({ + onChange { event -> + val newSelected = event.target.selectedOptions.asList() + .map { (it as HTMLOptionElement).value.asValue() } + onValueChange(newSelected.asValue()) + + } + }, multiple = true) { + descriptor?.allowedValues?.forEach { optionValue -> + Option(optionValue.string, { + value?.list?.let { if (optionValue in it) selected() } + }) { + Text(optionValue.string) + } + } + + } +} + +@Composable +public fun RangeValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var innerValue by remember { mutableStateOf(value?.double) } + var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) } + + + FlexRow { + if (descriptor?.valueRequirement != ValueRequirement.REQUIRED) { + Input(type = InputType.Checkbox) { + if (!rangeDisabled) defaultChecked() + + onChange { + val checkBoxValue = it.target.checked + rangeDisabled = !checkBoxValue + onValueChange( + if (!checkBoxValue) { + null + } else { + innerValue?.asValue() + } + ) + } + } + } + } + + Input(type = InputType.Range) { + style { + width(100.percent) + } + if (rangeDisabled) disabled() + value(innerValue?.toString() ?: "") + onChange { + val newValue = it.target.value + onValueChange(newValue.toDoubleOrNull()?.asValue()) + innerValue = newValue.toDoubleOrNull() + } + descriptor?.attributes?.get("min").string?.let { + min(it) + } + descriptor?.attributes?.get("max").string?.let { + max(it) + } + descriptor?.attributes?.get("step").number?.let { + step(it) + } + + } + +} + +@Composable +public fun ValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + val rawInput by remember { mutableStateOf(false) } + + val type = descriptor?.valueTypes?.firstOrNull() + + when { + rawInput -> StringValueChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "color" -> ColorValueChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "multiSelect" -> MultiSelectChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "range" -> RangeValueChooser(descriptor, state, value, onValueChange) + type == ValueType.BOOLEAN -> BooleanValueChooser(descriptor, state, value, onValueChange) + type == ValueType.NUMBER -> NumberValueChooser(descriptor, state, value, onValueChange) + descriptor?.allowedValues?.isNotEmpty() ?: false -> ComboValueChooser(descriptor, state, value, onValueChange) + //TODO handle lists + else -> StringValueChooser(descriptor, state, value, onValueChange) + } +} \ No newline at end of file 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 55d1391b..b4e6cba7 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -47,7 +47,7 @@ public interface Vision : Described { } public fun onMetaEvent(meta: Meta){ - + //Do nothing by default } /** From e6bdb67262f74ec9fd1c69045ad3be2a2d3d4202 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 21 Nov 2023 13:32:02 +0300 Subject: [PATCH 06/13] Add compose-html --- .../visionforge/compose/NameCrumbs.kt | 44 +++++ .../visionforge/compose/PropertyEditor.kt | 23 +-- .../kscience/visionforge/compose/Tabs.kt | 102 +++++++++++ .../visionforge/compose/ThreeControls.kt | 80 +++++++++ .../compose/ThreeViewWithControls.kt | 169 ++++++++++++++++++ .../visionforge/compose/VisionTree.kt | 96 ++++++++++ .../kscience/visionforge/compose/bootstrap.kt | 8 + .../space/kscience/visionforge/compose/css.kt | 9 + 8 files changed, 517 insertions(+), 14 deletions(-) create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeControls.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeViewWithControls.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt create mode 100644 ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/bootstrap.kt diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt new file mode 100644 index 00000000..2becdbb6 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt @@ -0,0 +1,44 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.dom.Li +import org.jetbrains.compose.web.dom.Nav +import org.jetbrains.compose.web.dom.Ol +import org.jetbrains.compose.web.dom.Text +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.length + +@Composable +public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({ + attr("aria-label","breadcrumb") +}) { + Ol({classes("breadcrumb")}) { + Li({ + classes("breadcrumb-item") + onClick { + link(Name.EMPTY) + } + }) { + Text("\u2302") + } + + if (name != null) { + val tokens = ArrayList(name.length) + name.tokens.forEach { token -> + tokens.add(token) + val fullName = Name(tokens.toList()) + Text(".") + Li({ + classes("breadcrumb-item") + if(tokens.size == name.length) classes("active") + onClick { + link(fullName) + } + }) { + Text(token.toString()) + } + } + } + } +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt index f82b9e05..d919cd77 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt @@ -21,10 +21,7 @@ import space.kscience.dataforge.meta.descriptors.ValueRequirement import space.kscience.dataforge.meta.descriptors.get import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.remove -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.NameToken -import space.kscience.dataforge.names.isEmpty -import space.kscience.dataforge.names.lastOrNull +import space.kscience.dataforge.names.* import space.kscience.visionforge.hidden @@ -39,19 +36,17 @@ public sealed class EditorPropertyState { } /** + * @param meta Root config object - always non-null * @param rootDescriptor Full path to the displayed node in [meta]. Could be empty */ @Composable -private fun PropertyEditorItem( - /** - * Root config object - always non-null - */ +public fun PropertyEditor( + scope: CoroutineScope, meta: MutableMeta, getPropertyState: (Name) -> EditorPropertyState, - scope: CoroutineScope, updates: Flow, - name: Name, - rootDescriptor: MetaDescriptor?, + name: Name = Name.EMPTY, + rootDescriptor: MetaDescriptor? = null, initialExpanded: Boolean? = null, ) { var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) } @@ -145,7 +140,7 @@ private fun PropertyEditorItem( Div({ classes(TreeStyles.treeItem) }) { - PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded) + PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded) } } } @@ -159,7 +154,8 @@ public fun PropertyEditor( descriptor: MetaDescriptor? = null, expanded: Boolean? = null, ) { - PropertyEditorItem( + PropertyEditor( + scope = scope, meta = properties, getPropertyState = { name -> if (properties[name] != null) { @@ -170,7 +166,6 @@ public fun PropertyEditor( EditorPropertyState.Undefined } }, - scope = scope, updates = callbackFlow { properties.onChange(scope) { name -> scope.launch { diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt new file mode 100644 index 00000000..cae4e797 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt @@ -0,0 +1,102 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import org.jetbrains.compose.web.dom.* +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLLIElement + + +public class ComposeTab( + public val key: String, + public val title: String, + public val content: ContentBuilder, + public val disabled: Boolean, + public val titleExt: ContentBuilder, +) + +@Composable +public fun Tabs(tabs: List, activeKey: String) { + var active by remember(activeKey) { mutableStateOf(activeKey) } + + Div({ classes("card", "text-center") }) { + Div({ classes("card-header") }) { + + Ul({ classes("nav", "nav-tabs", "card-header-tabs") }) { + tabs.forEach { tab -> + Li({ + classes("nav-item") + }) { + A(attrs = { + classes("nav-link") + if (active == tab.key) { + classes("active") + } + if (tab.disabled) { + classes("disabled") + } + onClick { + active = tab.key + } + }) { + Text(tab.title) + } + tab.titleExt.invoke(this) + } + } + } + } + tabs.find { it.key == active }?.let { tab -> + Div({ classes("card-body") }) { + tab.content.invoke(this) + } + } + } + + +} + +public class TabBuilder internal constructor(public val key: String) { + private var title: String = key + public var disabled: Boolean = false + private var content: ContentBuilder = {} + private var titleExt: ContentBuilder = {} + + @Composable + public fun Content(content: ContentBuilder) { + this.content = content + } + + @Composable + public fun Title(title: String, titleExt: ContentBuilder = {}) { + this.title = title + this.titleExt = titleExt + } + + internal fun build(): ComposeTab = ComposeTab( + key, + title, + content, + disabled, + titleExt + ) +} + +public class TabsBuilder { + public var active: String = "" + internal val tabs: MutableList = mutableListOf() + + @Composable + public fun Tab(key: String, builder: @Composable TabBuilder.() -> Unit) { + tabs.add(TabBuilder(key).apply { builder() }.build()) + } + + public fun addTab(tab: ComposeTab) { + tabs.add(tab) + } +} + +@Composable +public fun Tabs(builder: @Composable TabsBuilder.() -> Unit) { + val result = TabsBuilder().apply { builder() } + Tabs(result.tabs, result.active) +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeControls.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeControls.kt new file mode 100644 index 00000000..c559b946 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeControls.kt @@ -0,0 +1,80 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Text +import org.w3c.files.Blob +import org.w3c.files.BlobPropertyBag +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.names.Name +import space.kscience.visionforge.Vision +import space.kscience.visionforge.encodeToString +import space.kscience.visionforge.solid.specifications.Canvas3DOptions + +@Composable +internal fun CanvasControls( + vision: Vision?, + options: Canvas3DOptions, +) { + FlexColumn { + FlexRow({ + style { + border { + width(1.px) + style(LineStyle.Solid) + color(Color("blue")) + } + padding(4.px) + } + }) { + vision?.let { vision -> + Button({ + onClick { event -> + val json = vision.encodeToString() + event.stopPropagation(); + event.preventDefault(); + + val fileSaver = kotlinext.js.require("file-saver") + val blob = Blob(arrayOf(json), BlobPropertyBag("text/json;charset=utf-8")) + fileSaver.saveAs(blob, "object.json") as Unit + } + }) { + Text("Export") + } + } + } + PropertyEditor( + scope = vision?.manager?.context ?: Global, + properties = options.meta, + descriptor = Canvas3DOptions.descriptor, + expanded = false + ) + + } +} + + +@Composable +public fun ThreeControls( + vision: Vision?, + canvasOptions: Canvas3DOptions, + selected: Name?, + onSelect: (Name?) -> Unit, + tabBuilder: @Composable TabsBuilder.() -> Unit = {}, +) { + Tabs { + active = "Tree" + vision?.let { vision -> + Tab("Tree") { + CardTitle("Vision tree") + VisionTree(vision, Name.EMPTY, selected, onSelect) + } + } + Tab("Settings") { + CardTitle("Canvas configuration") + CanvasControls(vision, canvasOptions) + } + tabBuilder() + } +} diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeViewWithControls.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeViewWithControls.kt new file mode 100644 index 00000000..7a347110 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeViewWithControls.kt @@ -0,0 +1,169 @@ +@file:OptIn(ExperimentalComposeWebApi::class) + +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import app.softwork.bootstrapcompose.Card +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.launch +import org.jetbrains.compose.web.ExperimentalComposeWebApi +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.isEmpty +import space.kscience.visionforge.* +import space.kscience.visionforge.solid.Solid +import space.kscience.visionforge.solid.SolidGroup +import space.kscience.visionforge.solid.Solids +import space.kscience.visionforge.solid.specifications.Canvas3DOptions + +@Composable +public fun ThreeCanvasWithControls( + solids: Solids, + builderOfSolid: Deferred, + initialSelected: Name?, + options: Canvas3DOptions?, + tabBuilder: @Composable TabsBuilder.() -> Unit = {}, +) { + var selected: Name? by remember { mutableStateOf(initialSelected) } + var solid: Solid? by remember { mutableStateOf(null) } + + LaunchedEffect(builderOfSolid) { + solids.context.launch { + solid = builderOfSolid.await() + //ensure that the solid is properly rooted + if (solid?.parent == null) { + solid?.setAsRoot(solids.context.visionManager) + } + } + } + + val optionsWithSelector = remember(options) { + (options ?: Canvas3DOptions()).apply { + this.onSelect = { + selected = it + } + } + } + + val selectedVision: Vision? = remember(builderOfSolid, selected) { + selected?.let { + when { + it.isEmpty() -> solid + else -> (solid as? SolidGroup)?.get(it) + } + } + } + + + FlexRow({ + style { + height(100.percent) + width(100.percent) + flexWrap(FlexWrap.Wrap) + alignItems(AlignItems.Stretch) + alignContent(AlignContent.Stretch) + } + }) { + FlexColumn({ + style { + height(100.percent) + minWidth(600.px) + flex(10, 1, 600.px) + position(Position.Relative) + } + }) { + if (solid == null) { + Div({ + style { + position(Position.Fixed) + width(100.percent) + height(100.percent) + zIndex(1000) + top(40.percent) + left(0.px) + opacity(0.5) + filter { + opacity(50.percent) + } + } + }) { + Div({ classes("d-flex", " justify-content-center") }) { + Div({ + classes("spinner-grow", "text-primary") + style { + width(3.cssRem) + height(3.cssRem) + zIndex(20) + } + attr("role", "status") + }) { + Span({ classes("sr-only") }) { Text("Loading 3D vision") } + } + } + } + } else { + ThreeCanvas(solids.context, optionsWithSelector, solid, selected) + } + + selectedVision?.let { vision -> + Div({ + style { + position(Position.Absolute) + top(5.px) + right(5.px) + width(450.px) + } + }) { + Card( + headerAttrs = { + // border = true + }, + header = { + NameCrumbs(selected) { selected = it } + } + ) { + PropertyEditor( + scope = solids.context, + meta = vision.properties.root(), + getPropertyState = { name -> + if (vision.properties.own?.get(name) != null) { + EditorPropertyState.Defined + } else if (vision.properties.root()[name] != null) { + // TODO differentiate + EditorPropertyState.Default() + } else { + EditorPropertyState.Undefined + } + }, + updates = vision.properties.changes, + rootDescriptor = vision.descriptor + ) + + } + + vision.styles.takeIf { it.isNotEmpty() }?.let { styles -> + P { + B { Text("Styles: ") } + Text(styles.joinToString(separator = ", ")) + } + } + } + } + } + } + FlexColumn({ + style { + paddingAll(4.px) + minWidth(400.px) + height(100.percent) + overflowY("auto") + flex(1, 10, 300.px) + } + }) { + ThreeControls(solid, optionsWithSelector, selected, onSelect = { selected = it }, tabBuilder = tabBuilder) + } +} + + diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt new file mode 100644 index 00000000..2de59f90 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt @@ -0,0 +1,96 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import org.jetbrains.compose.web.css.Color +import org.jetbrains.compose.web.css.color +import org.jetbrains.compose.web.css.cursor +import org.jetbrains.compose.web.css.textDecorationLine +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.lastOrNull +import space.kscience.dataforge.names.plus +import space.kscience.dataforge.names.startsWith +import space.kscience.visionforge.Vision +import space.kscience.visionforge.VisionGroup +import space.kscience.visionforge.asSequence +import space.kscience.visionforge.compose.TreeStyles.hover +import space.kscience.visionforge.compose.TreeStyles.invoke +import space.kscience.visionforge.isEmpty + + +@Composable +private fun TreeLabel( + vision: Vision, + name: Name, + selected: Name?, + clickCallback: (Name) -> Unit, +) { + Span({ + classes(TreeStyles.treeLabel) + if (name == selected) { + classes(TreeStyles.treeLabelSelected) + } + style { + color(Color("#069")) + cursor("pointer") + hover.invoke { + textDecorationLine("underline") + } + + } + onClick { clickCallback(name) } + }) { + Text(name.lastOrNull()?.toString() ?: "World") + } +} + +@Composable +public fun VisionTree( + vision: Vision, + name: Name = Name.EMPTY, + selected: Name? = null, + clickCallback: (Name) -> Unit, +): Unit { + var expanded: Boolean by remember { mutableStateOf(selected?.startsWith(name) ?: false) } + + //display as node if any child is visible + if (vision is VisionGroup) { + FlexRow { + if (vision.children.keys.any { !it.body.startsWith("@") }) { + Span({ + classes(TreeStyles.treeCaret) + if (expanded) { + classes(TreeStyles.treeCaretDown) + } + onClick { + expanded = !expanded + } + }) + } + TreeLabel(vision, name, selected, clickCallback) + } + if (expanded) { + FlexColumn({ + classes(TreeStyles.tree) + }) { + vision.children.asSequence() + .filter { !it.first.toString().startsWith("@") } // ignore statics and other hidden children + .sortedBy { (it.second as? VisionGroup)?.children?.isEmpty() ?: true } // ignore empty groups + .forEach { (childToken, child) -> + Div({ classes(TreeStyles.treeItem) }) { + VisionTree( + child, + name + childToken, + selected, + clickCallback + ) + } + } + } + } + } else { + TreeLabel(vision, name, selected, clickCallback) + } +} diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/bootstrap.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/bootstrap.kt new file mode 100644 index 00000000..8b0ee5b5 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/bootstrap.kt @@ -0,0 +1,8 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.dom.H5 +import org.jetbrains.compose.web.dom.Text + +@Composable +public fun CardTitle(title: String): Unit = H5({ classes("card-title") }) { Text(title) } \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt index 9f6eb3e4..0bf8f7c2 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt @@ -1,6 +1,7 @@ package space.kscience.visionforge.compose import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword public enum class UserSelect { inherit, initial, revert, revertLayer, unset, @@ -32,4 +33,12 @@ public fun StyleScope.marginAll( left: CSSNumeric = right, ) { margin(top, right, bottom, left) +} + +public fun StyleScope.zIndex(value: Int) { + property("z-index", "$value") +} + +public fun StyleScope.zIndex(value: CSSAutoKeyword) { + property("z-index", value) } \ No newline at end of file From 80284a99ef7b7e4c79aa510e27f6a03b03a20225 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 24 Nov 2023 10:02:25 +0300 Subject: [PATCH 07/13] Add click handlers --- .../kscience/visionforge/ControlVision.kt | 52 +++++++++++++++++++ .../space/kscience/visionforge/Vision.kt | 15 ++---- .../space/kscience/visionforge/VisionEvent.kt | 14 +---- .../space/kscience/visionforge/VisionGroup.kt | 8 +-- .../visionforge/solid/VisionUpdateTest.kt | 2 +- 5 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt new file mode 100644 index 00000000..38ee4d7f --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -0,0 +1,52 @@ +package space.kscience.visionforge + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaRepr +import space.kscience.dataforge.meta.MutableMeta + +@Serializable +@SerialName("control") +public abstract class VisionControlEvent : VisionEvent, MetaRepr { + public abstract val meta: Meta + + override fun toMeta(): Meta = meta +} + +public interface ControlVision : Vision { + public val controlEventFlow: Flow + + public fun dispatchControlEvent(event: VisionControlEvent) + + override fun receiveEvent(event: VisionEvent) { + if (event is VisionControlEvent) { + dispatchControlEvent(event) + } else super.receiveEvent(event) + } +} + +@Serializable +@SerialName("control.click") +public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() + + +public interface ClickControl : ControlVision { + public fun click(builder: MutableMeta.() -> Unit = {}) { + dispatchControlEvent(VisionClickEvent(Meta(builder))) + } + + public fun onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job { + return controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) + } + + public companion object { + + } +} \ No newline at end of file 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 b4e6cba7..29965944 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -4,7 +4,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.context.logger +import space.kscience.dataforge.context.warn import space.kscience.dataforge.meta.asValue import space.kscience.dataforge.meta.boolean import space.kscience.dataforge.meta.descriptors.Described @@ -37,7 +38,7 @@ public interface Vision : Described { /** * Update this vision using a dif represented by [VisionChange]. */ - public fun receiveChange(change: VisionChange) { + public fun update(change: VisionChange) { if (change.children?.isNotEmpty() == true) { error("Vision is not a group") } @@ -46,18 +47,12 @@ public interface Vision : Described { } } - public fun onMetaEvent(meta: Meta){ - //Do nothing by default - } - /** * Receive and process a generic [VisionEvent]. */ public fun receiveEvent(event: VisionEvent) { - when (event) { - is VisionChange -> receiveChange(event) - is VisionMetaEvent -> onMetaEvent(event.meta) - } + if(event is VisionChange) update(event) + else manager?.logger?.warn { "Undispatched event: $event" } } override val descriptor: MetaDescriptor? 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 84d36217..de0b2643 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionEvent.kt @@ -3,8 +3,6 @@ package space.kscience.visionforge import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MutableMeta -import space.kscience.dataforge.meta.set import space.kscience.dataforge.names.Name /** @@ -22,14 +20,4 @@ public sealed interface VisionEvent { */ @Serializable @SerialName("meta") -public class VisionMetaEvent(public val meta: Meta) : VisionEvent - - -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) { - properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder) -} \ No newline at end of file +public class VisionMetaEvent(public val meta: Meta) : VisionEvent \ 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 1edfa40a..fd8aaa16 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroup.kt @@ -17,12 +17,12 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY public interface VisionGroup : Vision { public val children: VisionChildren - override fun receiveChange(change: VisionChange) { + override fun update(change: VisionChange) { change.children?.forEach { (name, change) -> if (change.vision != null || change.vision == NullVision) { error("VisionGroup is read-only") } else { - children.getChild(name)?.receiveChange(change) + children.getChild(name)?.update(change) } } change.properties?.let { @@ -37,12 +37,12 @@ public interface MutableVisionGroup : VisionGroup { public fun createGroup(): MutableVisionGroup - override fun receiveChange(change: VisionChange) { + override fun update(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)?.receiveChange(change) + else -> children.getChild(name)?.update(change) } } change.properties?.let { 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 898fae11..0e495aaa 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.receiveChange(dif) + targetVision.update(dif) assertTrue { targetVision.children.getChild("top") is SolidGroup } assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work assertEquals( From 469655092e8c7f871dfb610f1c34555d82da0d81 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 29 Nov 2023 09:41:22 +0300 Subject: [PATCH 08/13] Change controls API --- demo/playground/notebooks/common-demo.ipynb | 9 +- demo/playground/notebooks/controls.ipynb | 45 ++++++ demo/playground/notebooks/dynamic-demo.ipynb | 7 +- .../src/jvmMain/kotlin/formServer.kt | 2 +- .../kscience/visionforge/ControlVision.kt | 29 ++-- .../kscience/visionforge/html/VisionOfHtml.kt | 54 ++++++++ .../visionforge/html/VisionOfHtmlForm.kt | 7 +- .../visionforge/html/VisionOfHtmlInput.kt | 58 -------- .../kscience/visionforge/JsVisionClient.kt | 6 +- .../kscience/visionforge/inputRenderers.kt | 130 +++++++++--------- .../jvmMain/kotlin/VisionForgeIntegration.kt | 2 - .../kotlin/JupyterCommonIntegration.kt | 7 +- 12 files changed, 200 insertions(+), 156 deletions(-) create mode 100644 demo/playground/notebooks/controls.ipynb create mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt delete mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt diff --git a/demo/playground/notebooks/common-demo.ipynb b/demo/playground/notebooks/common-demo.ipynb index 78797545..caa7306f 100644 --- a/demo/playground/notebooks/common-demo.ipynb +++ b/demo/playground/notebooks/common-demo.ipynb @@ -54,9 +54,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "jupyter": { - "outputs_hidden": false - }, "tags": [] }, "outputs": [], @@ -83,9 +80,6 @@ "language": "kotlin", "name": "kotlin" }, - "ktnbPluginMetadata": { - "isAddProjectLibrariesToClasspath": false - }, "language_info": { "codemirror_mode": "text/x-kotlin", "file_extension": ".kt", @@ -94,6 +88,9 @@ "nbconvert_exporter": "", "pygments_lexer": "kotlin", "version": "1.8.20" + }, + "ktnbPluginMetadata": { + "projectLibraries": [] } }, "nbformat": 4, diff --git a/demo/playground/notebooks/controls.ipynb b/demo/playground/notebooks/controls.ipynb new file mode 100644 index 00000000..8552a178 --- /dev/null +++ b/demo/playground/notebooks/controls.ipynb @@ -0,0 +1,45 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "USE(JupyterCommonIntegration())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.0", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectDependencies": true + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/demo/playground/notebooks/dynamic-demo.ipynb b/demo/playground/notebooks/dynamic-demo.ipynb index ac70b4c2..3fcd31e3 100644 --- a/demo/playground/notebooks/dynamic-demo.ipynb +++ b/demo/playground/notebooks/dynamic-demo.ipynb @@ -25,10 +25,7 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } + "collapsed": false }, "outputs": [], "source": [ @@ -84,7 +81,7 @@ "version": "1.8.0-dev-3517" }, "ktnbPluginMetadata": { - "isAddProjectLibrariesToClasspath": false + "projectLibraries": [] } }, "nbformat": 4, diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt index d832d85a..21d2d4a7 100644 --- a/demo/playground/src/jvmMain/kotlin/formServer.kt +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -75,7 +75,7 @@ fun main() { server.openInBrowser() - while (readln() != "exit") { + while (readlnOrNull() != "exit") { } 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 38ee4d7f..e0d44930 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -2,7 +2,7 @@ package space.kscience.visionforge import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -21,7 +21,7 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr { } public interface ControlVision : Vision { - public val controlEventFlow: Flow + public val controlEventFlow: SharedFlow public fun dispatchControlEvent(event: VisionControlEvent) @@ -32,21 +32,32 @@ public interface ControlVision : Vision { } } +/** + * @param payload The optional payload associated with the click event. + */ @Serializable @SerialName("control.click") -public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() +public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() { + override val meta: Meta get() = Meta { ::payload.name put payload } +} public interface ClickControl : ControlVision { + /** + * Create and dispatch a click event + */ public fun click(builder: MutableMeta.() -> Unit = {}) { dispatchControlEvent(VisionClickEvent(Meta(builder))) } +} - public fun onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job { - return controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) - } +/** + * Register listener + */ +public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job = + controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) - public companion object { - } -} \ No newline at end of file +@Serializable +@SerialName("control.valueChange") +public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt new file mode 100644 index 00000000..d9f09ec1 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt @@ -0,0 +1,54 @@ +package space.kscience.visionforge.html + +import kotlinx.html.InputType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.asName +import space.kscience.visionforge.AbstractVision + + +@Serializable +public abstract class VisionOfHtml: AbstractVision(){ + public var classes: List by properties.stringList(*emptyArray()) +} + +@Serializable +@SerialName("html.input") +public open class VisionOfHtmlInput( + public val inputType: String, +) : VisionOfHtml() { + public var value : Value? by properties.value() + public var disabled: Boolean by properties.boolean { false } + public var fieldName: String? by properties.string() +} + + +@Serializable +@SerialName("html.text") +public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) { + public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.checkbox") +public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) { + public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.number") +public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) { + public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.range") +public class VisionOfRangeField( + public val min: Double, + public val max: Double, + public val step: Double = 1.0, +) : VisionOfHtmlInput(InputType.range.realValue) { + public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) +} + 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 d9c0347d..e56af874 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 @@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.node +/** + * @param formId an id of the element in rendered DOM, this form is bound to + */ @Serializable @SerialName("html.form") public class VisionOfHtmlForm( public val formId: String, -) : VisionOfHtmlInput() { - public var values: Meta? by mutableProperties.node() +) : VisionOfHtml() { + public var values: Meta? by properties.node() } public fun TagConsumer.bindForm( diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt deleted file mode 100644 index d2bb2c52..00000000 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt +++ /dev/null @@ -1,58 +0,0 @@ -package space.kscience.visionforge.html - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import space.kscience.dataforge.meta.boolean -import space.kscience.dataforge.meta.number -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.names.Name -import space.kscience.visionforge.AbstractVision -import space.kscience.visionforge.Vision - -//TODO replace by something -internal val Vision.mutableProperties get() = properties.getMeta(Name.EMPTY, false, false) - -@Serializable -public abstract class VisionOfHtmlInput : AbstractVision() { - public var disabled: Boolean by mutableProperties.boolean { false } -} - -@Serializable -@SerialName("html.text") -public class VisionOfTextField( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var text: String? by mutableProperties.string() -} - -@Serializable -@SerialName("html.checkbox") -public class VisionOfCheckbox( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var checked: Boolean? by mutableProperties.boolean() -} - -@Serializable -@SerialName("html.number") -public class VisionOfNumberField( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var value: Number? by mutableProperties.number() -} - -@Serializable -@SerialName("html.range") -public class VisionOfRangeField( - public val min: Double, - public val max: Double, - public val step: Double = 1.0, - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var value: Number? by mutableProperties.number() -} - 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 b9e0ef93..d164e5ea 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -252,9 +252,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { override fun content(target: String): Map = if (target == ElementVisionRenderer.TYPE) { listOf( - numberVisionRenderer(this), - textVisionRenderer(this), - formVisionRenderer(this) + numberVisionRenderer(), + textVisionRenderer(), + formVisionRenderer() ).associateByName() } else super.content(target) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index ff84c403..8b07e177 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -3,66 +3,63 @@ package space.kscience.visionforge import kotlinx.browser.document import kotlinx.html.InputType import kotlinx.html.js.input -import kotlinx.html.js.label import kotlinx.html.js.onChangeFunction +import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.get import org.w3c.xhr.FormData import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.logger -import space.kscience.dataforge.meta.DynamicMeta -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.toMap -import space.kscience.dataforge.meta.valueSequence -import space.kscience.visionforge.html.VisionOfHtmlForm -import space.kscience.visionforge.html.VisionOfNumberField -import space.kscience.visionforge.html.VisionOfTextField +import space.kscience.dataforge.meta.* +import space.kscience.visionforge.html.* -internal fun textVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> - val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" - vision.label?.let { - label { - htmlFor = fieldName - +it - } - } - input { - type = InputType.text - this.name = fieldName - vision.useProperty(VisionOfTextField::text) { - value = it ?: "" - } - onChangeFunction = { - client.notifyPropertyChanged(name, VisionOfTextField::text.name, value) - } + +private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { + vision.useProperty(VisionOfHtml::classes) { + classList.value = classes.joinToString(separator = " ") } } -internal fun numberVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> - val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" - vision.label?.let { - label { - htmlFor = fieldName - +it - } - } - input { - type = InputType.text - this.name = fieldName - vision.useProperty(VisionOfNumberField::value) { - value = it?.toDouble() ?: 0.0 - } - onChangeFunction = { - client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value) - } + +private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { + subscribeToVision(inputVision) + inputVision.useProperty(VisionOfHtmlInput::disabled) { + disabled = it } } + +internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> + input { + type = InputType.text + onChangeFunction = { + notifyPropertyChanged(visionName, VisionOfTextField::text.name, value) + } + }.apply { + subscribeToInput(vision) + vision.useProperty(VisionOfTextField::text) { + value = (it ?: "").asValue() + } + } + } + +internal fun JsVisionClient.numberVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> + input { + type = InputType.text + onChangeFunction = { + notifyPropertyChanged(visionName, VisionOfNumberField::value.name, value) + } + }.apply { + subscribeToInput(vision) + vision.useProperty(VisionOfNumberField::value) { + value = (it?.double ?: 0.0).asValue() + } + } + } + internal fun FormData.toMeta(): Meta { @Suppress("UNUSED_VARIABLE") val formData = this //val res = js("Object.fromEntries(formData);") @@ -86,28 +83,29 @@ internal fun FormData.toMeta(): Meta { return DynamicMeta(`object`) } -internal fun formVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> +internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> - val form = document.getElementById(vision.formId) as? HTMLFormElement - ?: error("An element with id = '${vision.formId} is not a form") + val form = document.getElementById(vision.formId) as? HTMLFormElement + ?: error("An element with id = '${vision.formId} is not a form") - client.logger.debug{"Adding hooks to form with id = '$vision.formId'"} + form.subscribeToVision(vision) - vision.useProperty(VisionOfHtmlForm::values) { values -> - client.logger.debug{"Updating form '${vision.formId}' with values $values"} - val inputs = form.getElementsByTagName("input") - values?.valueSequence()?.forEach { (token, value) -> - (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() + logger.debug { "Adding hooks to form with id = '$vision.formId'" } + + vision.useProperty(VisionOfHtmlForm::values) { values -> + logger.debug { "Updating form '${vision.formId}' with values $values" } + val inputs = form.getElementsByTagName("input") + values?.valueSequence()?.forEach { (token, value) -> + (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() + } } - } - form.onsubmit = { event -> - event.preventDefault() - val formData = FormData(form).toMeta() - client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData) - console.info("Sent: ${formData.toMap()}") - false - } -} \ No newline at end of file + form.onsubmit = { event -> + event.preventDefault() + val formData = FormData(form).toMeta() + notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData) + console.info("Sent: ${formData.toMap()}") + false + } + } \ No newline at end of file diff --git a/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt b/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt index afc2ecc2..935f183c 100644 --- a/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt +++ b/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt @@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware -import space.kscience.dataforge.misc.DFExperimental import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.* @@ -17,7 +16,6 @@ import kotlin.random.nextUInt /** * A base class for different Jupyter VF integrations */ -@DFExperimental public abstract class VisionForgeIntegration( public val visionManager: VisionManager, ) : JupyterIntegration(), ContextAware { diff --git a/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt b/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt index 6200bd5d..2f7988f8 100644 --- a/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt +++ b/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt @@ -1,14 +1,14 @@ package space.kscience.visionforge.jupyter -import kotlinx.html.* +import kotlinx.html.div +import kotlinx.html.p import org.jetbrains.kotlinx.jupyter.api.libraries.resources import space.kscience.dataforge.context.Context -import space.kscience.dataforge.misc.DFExperimental import space.kscience.gdml.Gdml import space.kscience.plotly.Plot import space.kscience.plotly.PlotlyPage import space.kscience.plotly.StaticPlotlyRenderer -import space.kscience.tables.* +import space.kscience.tables.Table import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.VisionPage @@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision import space.kscience.visionforge.visionManager -@DFExperimental public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) { override fun Builder.afterLoaded(vf: VisionForge) { From 7561ddad364527cfc1ccfc63aa43781c5949c599 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 2 Dec 2023 23:01:15 +0300 Subject: [PATCH 09/13] Fix html input renderers --- .../kscience/visionforge/VisionClient.kt | 34 ++--- .../kscience/visionforge/VisionManager.kt | 7 +- .../kscience/visionforge/html/VisionOfHtml.kt | 35 ++++- .../space/kscience/visionforge/useProperty.kt | 17 ++- .../visionforge/ElementVisionRenderer.kt | 2 +- .../kscience/visionforge/JsVisionClient.kt | 79 ++++++---- .../kscience/visionforge/inputRenderers.kt | 139 +++++++++++++++--- 7 files changed, 234 insertions(+), 79 deletions(-) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt index 76d8aa80..9d6a5561 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -2,10 +2,8 @@ package space.kscience.visionforge import kotlinx.coroutines.launch import space.kscience.dataforge.context.Plugin -import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaRepr import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.parseAsName /** * A feedback client that communicates with a server and provides ability to propagate events and changes back to the model @@ -15,25 +13,25 @@ public interface VisionClient: Plugin { public suspend fun sendEvent(targetName: Name, event: VisionEvent) - public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) +// public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) } -public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) { - notifyPropertyChanged(visionName, propertyName.parseAsName(true), item) -} - -public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) { - notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -} - -public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) { - notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -} - -public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) { - notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -} +//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) { +// notifyPropertyChanged(visionName, propertyName.parseAsName(true), item) +//} +// +//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) { +// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +//} +// +//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) { +// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +//} +// +//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) { +// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +//} public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit { context.launch { 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 e4ca1cdb..8799a73f 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt @@ -13,10 +13,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.toJson import space.kscience.dataforge.meta.toMeta import space.kscience.dataforge.names.Name -import space.kscience.visionforge.html.VisionOfCheckbox -import space.kscience.visionforge.html.VisionOfHtmlForm -import space.kscience.visionforge.html.VisionOfNumberField -import space.kscience.visionforge.html.VisionOfTextField +import space.kscience.visionforge.html.* public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer { override val tag: PluginTag get() = Companion.tag @@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont defaultDeserializer { SimpleVisionGroup.serializer() } subclass(NullVision.serializer()) subclass(SimpleVisionGroup.serializer()) + subclass(VisionOfHtmlInput.serializer()) subclass(VisionOfNumberField.serializer()) subclass(VisionOfTextField.serializer()) subclass(VisionOfCheckbox.serializer()) + subclass(VisionOfRangeField.serializer()) subclass(VisionOfHtmlForm.serializer()) } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt index d9f09ec1..51458047 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt @@ -1,6 +1,8 @@ package space.kscience.visionforge.html import kotlinx.html.InputType +import kotlinx.html.TagConsumer +import kotlinx.html.stream.createHTML import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.* @@ -9,16 +11,45 @@ import space.kscience.visionforge.AbstractVision @Serializable -public abstract class VisionOfHtml: AbstractVision(){ +public abstract class VisionOfHtml : AbstractVision() { public var classes: List by properties.stringList(*emptyArray()) } +@Serializable +@SerialName("html.plain") +public class VisionOfPlainHtml : VisionOfHtml() { + public var content: String? by properties.string() +} + +public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) { + content = createHTML().apply(block).finalize() +} + +@Serializable +public enum class InputFeedbackMode{ + /** + * Fire feedback event on `onchange` event + */ + ONCHANGE, + + /** + * Fire feedback event on `oninput` event + */ + ONINPUT, + + /** + * provide only manual feedback + */ + NONE +} + @Serializable @SerialName("html.input") public open class VisionOfHtmlInput( public val inputType: String, + public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE ) : VisionOfHtml() { - public var value : Value? by properties.value() + public var value: Value? by properties.value() public var disabled: Boolean by properties.boolean { false } public var fieldName: 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 f6f95a6f..cee3a002 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt @@ -39,9 +39,22 @@ public fun Vision.useProperty( callback: (Meta) -> Unit, ): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback) +/** + * 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"), + 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, + scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), callback: V.(T) -> Unit, ): Job { //Pass initial value. @@ -50,5 +63,5 @@ public fun V.useProperty( if (name.startsWith(property.name.asName())) { callback(property.get(this@useProperty)) } - }.launchIn(scope ?: error("Orphan Vision can't observe properties")) + }.launchIn(scope) } \ 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 75ec785a..8842e08e 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt @@ -26,7 +26,7 @@ public interface ElementVisionRenderer : Named { /** * Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer * can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify - * higher value in order to "steal" rendering job + * higher value to "steal" rendering job */ public fun rateVision(vision: Vision): Int 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 d164e5ea..388ed2af 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -65,20 +65,20 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null - private val mutex = Mutex() +// private val mutex = Mutex() - private val changeCollector = VisionChangeBuilder() - /** - * Communicate vision property changed from rendering engine to model - */ - override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) { - context.launch { - mutex.withLock { - changeCollector.propertyChanged(visionName, propertyName, item) - } - } - } + +// /** +// * Communicate vision property changed from rendering engine to model +// */ +// private fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) { +// context.launch { +// mutex.withLock { +// changeCollector.propertyChanged(visionName, propertyName, item) +// } +// } +// } private val eventCollector by lazy { MutableSharedFlow>(meta["feedback.eventCache"].int ?: 100) @@ -97,7 +97,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { renderer.render(element, name, vision, outputMeta) } - private fun startVisionUpdate(element: Element, name: Name, vision: Vision?, outputMeta: Meta) { + private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) { element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) { val endpoint = resolveEndpoint(element) @@ -109,9 +109,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { URL(attr.value) }.apply { protocol = "ws" - searchParams.append("name", name.toString()) + searchParams.append("name", visionName.toString()) } + logger.info { "Updating vision data from $wsUrl" } //Individual websocket for this vision @@ -125,14 +126,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { ) // If change contains root vision replacement, do it - if(event is VisionChange) { + if (event is VisionChange) { event.vision?.let { vision -> - renderVision(element, name, vision, outputMeta) + renderVision(element, visionName, vision, outputMeta) } } - logger.debug { "Got $event for output with name $name" } - if (vision == null) error("Can't update vision because it is not loaded.") + logger.debug { "Got $event for output with name $visionName" } vision.receiveEvent(event) } else { logger.error { "WebSocket message data is not a string" } @@ -147,32 +147,44 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300 onopen = { + + + val mutex = Mutex() + + val changeCollector = VisionChangeBuilder() + feedbackJob = visionManager.context.launch { - eventCollector.filter { it.first == name }.onEach { + //launch a separate coroutine to send events to the backend + eventCollector.filter { it.first == visionName }.onEach { send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second)) }.launchIn(this) + //launch backward property propagation + vision.properties.changes.onEach { propertyName: Name -> + changeCollector.propertyChanged(visionName, propertyName, vision.properties.getMeta(propertyName)) + }.launchIn(this) + + //aggregate atomic changes while (isActive) { delay(feedbackAggregationTime.milliseconds) - val change = changeCollector[name] ?: continue - if (!change.isEmpty()) { + if (!changeCollector.isEmpty()) { mutex.withLock { - eventCollector.emit(name to change.deepCopy(visionManager)) - change.reset() + eventCollector.emit(visionName to changeCollector.deepCopy(visionManager)) + changeCollector.reset() } } } } - logger.info { "WebSocket feedback channel established for output '$name'" } + logger.info { "WebSocket feedback channel established for output '$visionName'" } } onclose = { feedbackJob?.cancel() - logger.info { "WebSocket feedback channel closed for output '$name'" } + logger.info { "WebSocket feedback channel closed for output '$visionName'" } } onerror = { feedbackJob?.cancel() - logger.error { "WebSocket feedback channel error for output '$name'" } + logger.error { "WebSocket feedback channel error for output '$visionName'" } } } } @@ -241,9 +253,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { } //Try to load vision via websocket - element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> { - startVisionUpdate(element, name, null, outputMeta) - } +// element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> { +// startVisionUpdate(element, name, null, outputMeta) +// } else -> error("No embedded vision data / fetch url for $name") } @@ -252,9 +264,12 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { override fun content(target: String): Map = if (target == ElementVisionRenderer.TYPE) { listOf( - numberVisionRenderer(), - textVisionRenderer(), - formVisionRenderer() + inputVisionRenderer, + checkboxVisionRenderer, + numberVisionRenderer, + textVisionRenderer, + rangeVisionRenderer, + formVisionRenderer ).associateByName() } else super.content(target) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index 8b07e177..4e984cb0 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -3,10 +3,10 @@ package space.kscience.visionforge import kotlinx.browser.document import kotlinx.html.InputType import kotlinx.html.js.input -import kotlinx.html.js.onChangeFunction import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement +import org.w3c.dom.events.Event import org.w3c.dom.get import org.w3c.xhr.FormData import space.kscience.dataforge.context.debug @@ -14,7 +14,11 @@ import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.* import space.kscience.visionforge.html.* - +/** + * Subscribes the HTML element to a given vision. + * + * @param vision The vision to subscribe to. + */ private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { vision.useProperty(VisionOfHtml::classes) { classList.value = classes.joinToString(separator = " ") @@ -22,6 +26,11 @@ private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { } +/** + * Subscribes the HTML input element to a given vision. + * + * @param inputVision The input vision to subscribe to. + */ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { subscribeToVision(inputVision) inputVision.useProperty(VisionOfHtmlInput::disabled) { @@ -29,33 +38,123 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { } } - -internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer = - ElementVisionRenderer { visionName, vision, _ -> +internal val inputVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ -> input { type = InputType.text - onChangeFunction = { - notifyPropertyChanged(visionName, VisionOfTextField::text.name, value) - } }.apply { + val onEvent: (Event) -> Unit = { + vision.value = value.asValue() + } + + + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> onchange = onEvent + + InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.NONE -> {} + } + subscribeToInput(vision) - vision.useProperty(VisionOfTextField::text) { - value = (it ?: "").asValue() + vision.useProperty(VisionOfHtmlInput::value) { + this@apply.value = it?.string ?: "" } } } -internal fun JsVisionClient.numberVisionRenderer(): ElementVisionRenderer = - ElementVisionRenderer { visionName, vision, _ -> +internal val checkboxVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { _, vision, _ -> + input { + type = InputType.checkBox + }.apply { + val onEvent: (Event) -> Unit = { + vision.checked = checked + } + + + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> onchange = onEvent + + InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.NONE -> {} + } + + subscribeToInput(vision) + vision.useProperty(VisionOfCheckbox::checked) { + this@apply.checked = it ?: false + } + } + } + +internal val textVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { _, vision, _ -> input { type = InputType.text - onChangeFunction = { - notifyPropertyChanged(visionName, VisionOfNumberField::value.name, value) - } }.apply { + val onEvent: (Event) -> Unit = { + vision.text = value + } + + + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> onchange = onEvent + + InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.NONE -> {} + } + + subscribeToInput(vision) + vision.useProperty(VisionOfTextField::text) { + this@apply.value = it ?: "" + } + } + } + +internal val numberVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { _, vision, _ -> + input { + type = InputType.text + }.apply { + + val onEvent: (Event) -> Unit = { + value.toDoubleOrNull()?.let { vision.number = it } + } + + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> onchange = onEvent + + InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.NONE -> {} + } subscribeToInput(vision) vision.useProperty(VisionOfNumberField::value) { - value = (it?.double ?: 0.0).asValue() + this@apply.valueAsNumber = it?.double ?: 0.0 + } + } + } + +internal val rangeVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { _, vision, _ -> + input { + type = InputType.text + min = vision.min.toString() + max = vision.max.toString() + step = vision.step.toString() + }.apply { + + val onEvent: (Event) -> Unit = { + value.toDoubleOrNull()?.let { vision.number = it } + } + + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> onchange = onEvent + + InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.NONE -> {} + } + subscribeToInput(vision) + vision.useProperty(VisionOfRangeField::value) { + this@apply.valueAsNumber = it?.double ?: 0.0 } } } @@ -83,7 +182,7 @@ internal fun FormData.toMeta(): Meta { return DynamicMeta(`object`) } -internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer = +internal val formVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { visionName, vision, _ -> val form = document.getElementById(vision.formId) as? HTMLFormElement @@ -91,10 +190,10 @@ internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer = form.subscribeToVision(vision) - logger.debug { "Adding hooks to form with id = '$vision.formId'" } + vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" } vision.useProperty(VisionOfHtmlForm::values) { values -> - logger.debug { "Updating form '${vision.formId}' with values $values" } + vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" } val inputs = form.getElementsByTagName("input") values?.valueSequence()?.forEach { (token, value) -> (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() @@ -104,7 +203,7 @@ internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer = form.onsubmit = { event -> event.preventDefault() val formData = FormData(form).toMeta() - notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData) + vision.values = formData console.info("Sent: ${formData.toMap()}") false } From c877fcbce37cc6a4f1049a5a8ed7a2762445d739 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 2 Dec 2023 23:07:00 +0300 Subject: [PATCH 10/13] Add plain html input renderer --- .../kscience/visionforge/JsVisionClient.kt | 1 + .../kscience/visionforge/inputRenderers.kt | 71 +++++++++++-------- 2 files changed, 42 insertions(+), 30 deletions(-) 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 388ed2af..6ae70280 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -264,6 +264,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { override fun content(target: String): Map = if (target == ElementVisionRenderer.TYPE) { listOf( + htmlVisionRenderer, inputVisionRenderer, checkboxVisionRenderer, numberVisionRenderer, diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index 4e984cb0..af54bd75 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -2,6 +2,7 @@ package space.kscience.visionforge import kotlinx.browser.document import kotlinx.html.InputType +import kotlinx.html.div import kotlinx.html.js.input import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLFormElement @@ -38,26 +39,36 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { } } +internal val htmlVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { _, vision, _ -> + div {}.also { div -> + div.subscribeToVision(vision) + vision.useProperty(VisionOfPlainHtml::content) { + div.textContent = it + } + } + } + internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ -> input { type = InputType.text - }.apply { + }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - vision.value = value.asValue() + vision.value = htmlInputElement.value.asValue() } when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> onchange = onEvent + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.NONE -> {} } - subscribeToInput(vision) + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfHtmlInput::value) { - this@apply.value = it?.string ?: "" + htmlInputElement.value = it?.string ?: "" } } } @@ -66,22 +77,22 @@ internal val checkboxVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { _, vision, _ -> input { type = InputType.checkBox - }.apply { + }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - vision.checked = checked + vision.checked = htmlInputElement.checked } when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> onchange = onEvent + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.NONE -> {} } - subscribeToInput(vision) + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfCheckbox::checked) { - this@apply.checked = it ?: false + htmlInputElement.checked = it ?: false } } } @@ -90,22 +101,22 @@ internal val textVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { _, vision, _ -> input { type = InputType.text - }.apply { + }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - vision.text = value + vision.text = htmlInputElement.value } when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> onchange = onEvent + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.NONE -> {} } - subscribeToInput(vision) + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfTextField::text) { - this@apply.value = it ?: "" + htmlInputElement.value = it ?: "" } } } @@ -114,21 +125,21 @@ internal val numberVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { _, vision, _ -> input { type = InputType.text - }.apply { + }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - value.toDoubleOrNull()?.let { vision.number = it } + htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } } when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> onchange = onEvent + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.NONE -> {} } - subscribeToInput(vision) + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfNumberField::value) { - this@apply.valueAsNumber = it?.double ?: 0.0 + htmlInputElement.valueAsNumber = it?.double ?: 0.0 } } } @@ -140,21 +151,21 @@ internal val rangeVisionRenderer: ElementVisionRenderer = min = vision.min.toString() max = vision.max.toString() step = vision.step.toString() - }.apply { + }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - value.toDoubleOrNull()?.let { vision.number = it } + htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } } when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> onchange = onEvent + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> oninput = onEvent + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.NONE -> {} } - subscribeToInput(vision) + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfRangeField::value) { - this@apply.valueAsNumber = it?.double ?: 0.0 + htmlInputElement.valueAsNumber = it?.double ?: 0.0 } } } From fbb402de90463f893d016e72329958c067e87ce8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 3 Dec 2023 20:17:48 +0300 Subject: [PATCH 11/13] add builders for html field --- .../kscience/visionforge/html/VisionOfHtml.kt | 38 ++++++++++++++++++- .../visionforge/meta/VisionPropertyTest.kt | 2 - .../kscience/visionforge/inputRenderers.kt | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt index 51458047..9cc223e5 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt @@ -25,8 +25,13 @@ public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) { content = createHTML().apply(block).finalize() } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.html( + block: VisionOfPlainHtml.() -> Unit, +): VisionOfPlainHtml = VisionOfPlainHtml().apply(block) + @Serializable -public enum class InputFeedbackMode{ +public enum class InputFeedbackMode { /** * Fire feedback event on `onchange` event */ @@ -47,13 +52,18 @@ public enum class InputFeedbackMode{ @SerialName("html.input") public open class VisionOfHtmlInput( public val inputType: String, - public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE + public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE, ) : VisionOfHtml() { public var value: Value? by properties.value() public var disabled: Boolean by properties.boolean { false } public var fieldName: String? by properties.string() } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.htmlInput( + inputType: String, + block: VisionOfHtmlInput.() -> Unit = {}, +): VisionOfHtmlInput = VisionOfHtmlInput(inputType).apply(block) @Serializable @SerialName("html.text") @@ -61,18 +71,34 @@ public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) { public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName()) } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.htmlTextField( + block: VisionOfTextField.() -> Unit = {}, +): VisionOfTextField = VisionOfTextField().apply(block) + + @Serializable @SerialName("html.checkbox") public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) { public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName()) } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.htmlCheckBox( + block: VisionOfCheckbox.() -> Unit = {}, +): VisionOfCheckbox = VisionOfCheckbox().apply(block) + @Serializable @SerialName("html.number") public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) { public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.htmlNumberField( + block: VisionOfNumberField.() -> Unit = {}, +): VisionOfNumberField = VisionOfNumberField().apply(block) + @Serializable @SerialName("html.range") public class VisionOfRangeField( @@ -83,3 +109,11 @@ public class VisionOfRangeField( public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) } +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.htmlRangeField( + min: Double, + max: Double, + step: Double = 1.0, + block: VisionOfRangeField.() -> Unit = {}, +): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block) + 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 a6ea86a2..38abe34c 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,7 +1,6 @@ package space.kscience.visionforge.meta import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.take @@ -24,7 +23,6 @@ private class TestScheme : Scheme() { companion object : SchemeSpec(::TestScheme) } -@OptIn(ExperimentalCoroutinesApi::class) internal class VisionPropertyTest { private val manager = Global.request(VisionManager) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index af54bd75..e1410752 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -194,7 +194,7 @@ internal fun FormData.toMeta(): Meta { } internal val formVisionRenderer: ElementVisionRenderer = - ElementVisionRenderer { visionName, vision, _ -> + ElementVisionRenderer { _, vision, _ -> val form = document.getElementById(vision.formId) as? HTMLFormElement ?: error("An element with id = '${vision.formId} is not a form") From 595512959c4e358a0eb86d11f24f104bd0284013 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 5 Dec 2023 16:39:15 +0300 Subject: [PATCH 12/13] DataForge 0.7.1 and other version updates --- build.gradle.kts | 4 +- .../visionforge/gdml/GDMLVisionTest.kt | 2 +- .../kotlin/ru/mipt/npm/muon/monitor/Model.kt | 2 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../bootstrap/visionPropertyEditor.kt | 2 +- ui/compose/build.gradle.kts | 3 +- .../visionforge/compose/PropertyEditor.kt | 5 +-- .../visionforge/compose/valueChooser.kt | 4 +- .../visionforge/react/PropertyEditor.kt | 7 ++-- .../visionforge/react/RangeValueChooser.kt | 4 +- .../space/kscience/visionforge/StyleSheet.kt | 6 +-- .../space/kscience/visionforge/Vision.kt | 4 +- .../kscience/visionforge/VisionProperties.kt | 42 +++++++++---------- .../kscience/visionforge/flowProperty.kt | 4 +- .../space/kscience/visionforge/useProperty.kt | 4 +- .../visionforge/meta/VisionPropertyTest.kt | 6 +-- .../visionforge/ElementVisionRenderer.kt | 4 +- .../kscience/visionforge/JsVisionClient.kt | 2 +- visionforge-plotly/build.gradle.kts | 2 +- .../visionforge/plotly/VisionOfPlotly.kt | 8 ++-- .../visionforge/server/VisionServer.kt | 2 +- .../kscience/visionforge/solid/Composite.kt | 2 +- .../kscience/visionforge/solid/Extruded.kt | 2 +- .../space/kscience/visionforge/solid/Solid.kt | 2 +- .../visionforge/solid/SolidMaterial.kt | 8 ++-- .../visionforge/solid/SolidReference.kt | 6 +-- .../kscience/visionforge/solid/Surface.kt | 2 +- .../kscience/visionforge/solid/geometry.kt | 6 +-- .../solid/transform/RemoveSingleChild.kt | 2 +- visionforge-tables/build.gradle.kts | 2 +- .../visionforge/tables/VisionOfTableTest.kt | 3 +- .../visionforge/solid/three/ThreeFactory.kt | 4 +- .../solid/three/ThreeLineFactory.kt | 2 +- .../visionforge/solid/three/ThreeMaterials.kt | 12 +++--- .../solid/three/ThreeMeshFactory.kt | 2 +- 36 files changed, 88 insertions(+), 88 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2da86b96..b578696d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,12 +7,12 @@ plugins { // id("org.jetbrains.kotlinx.kover") version "0.5.0" } -val dataforgeVersion by extra("0.6.2") +val dataforgeVersion by extra("0.7.1") val fxVersion by extra("11") allprojects { group = "space.kscience" - version = "0.3.0-dev-16" + version = "0.3.0-dev-17" } subprojects { diff --git a/demo/gdml/src/commonTest/kotlin/space/kscience/visionforge/gdml/GDMLVisionTest.kt b/demo/gdml/src/commonTest/kotlin/space/kscience/visionforge/gdml/GDMLVisionTest.kt index ba9cb333..6ade4ec8 100644 --- a/demo/gdml/src/commonTest/kotlin/space/kscience/visionforge/gdml/GDMLVisionTest.kt +++ b/demo/gdml/src/commonTest/kotlin/space/kscience/visionforge/gdml/GDMLVisionTest.kt @@ -34,6 +34,6 @@ class GDMLVisionTest { val child = cubes[Name.of("composite-000","segment-0")] assertNotNull(child) child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue()) - assertEquals("red", child.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).string) + assertEquals("red", child.properties[SolidMaterial.MATERIAL_COLOR_KEY].string) } } \ No newline at end of file diff --git a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt index 7f7958b9..34b750e9 100644 --- a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt +++ b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt @@ -71,7 +71,7 @@ class Model(val manager: VisionManager) { fun reset() { map.values.forEach { - it.properties.setMeta(SolidMaterial.MATERIAL_COLOR_KEY, null) + it.properties[SolidMaterial.MATERIAL_COLOR_KEY] = null } tracks.children.clear() } diff --git a/gradle.properties b/gradle.properties index 9413b93e..4e33feb5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G org.jetbrains.compose.experimental.jscanvas.enabled=true -toolsVersion=0.15.0-kotlin-1.9.20 +toolsVersion=0.15.2-kotlin-1.9.21 #kotlin.experimental.tryK2=true #kscience.wasm.disabled=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..a5952066 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ui/bootstrap/src/jsMain/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt b/ui/bootstrap/src/jsMain/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt index 8e7e1208..8264ae43 100644 --- a/ui/bootstrap/src/jsMain/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt +++ b/ui/bootstrap/src/jsMain/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt @@ -29,7 +29,7 @@ public fun RBuilder.visionPropertyEditor( this.descriptor = descriptor this.scope = vision.manager?.context ?: error("Orphan vision could not be observed") this.getPropertyState = { name -> - val ownMeta = vision.properties.own?.getMeta(name) + val ownMeta = vision.properties.own?.get(name) if (ownMeta != null && !ownMeta.isEmpty()) { EditorPropertyState.Defined } else if (vision.properties.root().getValue(name) != null) { diff --git a/ui/compose/build.gradle.kts b/ui/compose/build.gradle.kts index a2f2c4a7..f77c8e26 100644 --- a/ui/compose/build.gradle.kts +++ b/ui/compose/build.gradle.kts @@ -1,7 +1,8 @@ plugins { id("space.kscience.gradle.mpp") - id("org.jetbrains.compose") version "1.5.10" + alias(spclibs.plugins.compose) +// id("org.jetbrains.compose") version "1.5.11" // id("com.android.library") } diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt index d919cd77..0892b9af 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt @@ -17,9 +17,8 @@ import org.jetbrains.compose.web.dom.Text import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.ObservableMutableMeta import space.kscience.dataforge.meta.descriptors.MetaDescriptor -import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.ValueRestriction import space.kscience.dataforge.meta.descriptors.get -import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.remove import space.kscience.dataforge.names.* import space.kscience.visionforge.hidden @@ -104,7 +103,7 @@ public fun PropertyEditor( Text(token) } - if (!name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { + if (!name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) { Div({ style { width(160.px) diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt index 0be879ad..b6aefee7 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt @@ -16,7 +16,7 @@ import org.w3c.dom.HTMLOptionElement import org.w3c.dom.asList import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.descriptors.MetaDescriptor -import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.ValueRestriction import space.kscience.dataforge.meta.descriptors.allowedValues import space.kscience.visionforge.Colors import space.kscience.visionforge.widgetType @@ -199,7 +199,7 @@ public fun RangeValueChooser( FlexRow { - if (descriptor?.valueRequirement != ValueRequirement.REQUIRED) { + if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) { Input(type = InputType.Checkbox) { if (!rangeDisabled) defaultChecked() diff --git a/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/PropertyEditor.kt b/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/PropertyEditor.kt index 58986076..b13e7304 100644 --- a/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/PropertyEditor.kt +++ b/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/PropertyEditor.kt @@ -16,9 +16,8 @@ import react.dom.attrs import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.ObservableMutableMeta import space.kscience.dataforge.meta.descriptors.MetaDescriptor -import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.ValueRestriction import space.kscience.dataforge.meta.descriptors.get -import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.remove import space.kscience.dataforge.names.* import space.kscience.visionforge.hidden @@ -146,7 +145,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { } +token } - if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { + if (!props.name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) { styledDiv { css { //+TreeStyles.resizeableInput @@ -185,7 +184,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { } +"\u00D7" attrs { - if (editorPropertyState!= EditorPropertyState.Defined) { + if (editorPropertyState != EditorPropertyState.Defined) { disabled = true } else { onClickFunction = removeClick diff --git a/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt b/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt index 4a82a6e9..bec34fc1 100644 --- a/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt +++ b/ui/react/src/jsMain/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt @@ -12,7 +12,7 @@ import react.dom.attrs import react.fc import react.useState import space.kscience.dataforge.meta.asValue -import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.ValueRestriction import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string @@ -43,7 +43,7 @@ public val RangeValueChooser: FC = fc("RangeValueChooser") { } flexRow { - if (props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) { + if (props.descriptor?.valueRestriction != ValueRestriction.REQUIRED) { styledInput(type = InputType.checkBox) { attrs { defaultChecked = rangeDisabled.not() 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 00213644..ed893162 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/StyleSheet.kt @@ -13,7 +13,7 @@ import kotlin.jvm.JvmInline @JvmInline public value class StyleSheet(private val owner: Vision) { - private val styleNode: Meta get() = owner.properties.getMeta(STYLESHEET_KEY) + private val styleNode: Meta get() = owner.properties[STYLESHEET_KEY] public val items: Map get() = styleNode.items @@ -23,7 +23,7 @@ public value class StyleSheet(private val owner: Vision) { * Define a style without notifying owner */ public fun define(key: String, style: Meta?) { - owner.properties.setMeta(STYLESHEET_KEY + key, style) + owner.properties[STYLESHEET_KEY + key] = style } /** @@ -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?.getMeta(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name) + properties.own?.get(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 29965944..78f917a9 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -10,7 +10,7 @@ import space.kscience.dataforge.meta.asValue import space.kscience.dataforge.meta.boolean import space.kscience.dataforge.meta.descriptors.Described import space.kscience.dataforge.meta.descriptors.MetaDescriptor -import space.kscience.dataforge.misc.Type +import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties @@ -19,7 +19,7 @@ import space.kscience.visionforge.Vision.Companion.TYPE /** * A root type for display hierarchy */ -@Type(TYPE) +@DfType(TYPE) public interface Vision : Described { /** 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 dfb06b50..3c375c62 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt @@ -34,13 +34,13 @@ public interface VisionProperties : MetaProvider { * @param inherit toggles parent node property lookup. Null means inference from descriptor. * @param includeStyles toggles inclusion of properties from styles. */ - public fun getMeta( + public fun get( name: Name, inherit: Boolean?, includeStyles: Boolean? = null, ): Meta - override fun getMeta(name: Name): Meta? = getMeta(name, null, null) + override fun get(name: Name): Meta? = get(name, null, null) public val changes: Flow @@ -54,7 +54,7 @@ public interface VisionProperties : MetaProvider { public interface MutableVisionProperties : VisionProperties, MutableMetaProvider { - override fun getMeta( + override fun get( name: Name, inherit: Boolean?, includeStyles: Boolean?, @@ -65,7 +65,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider includeStyles, ) - public fun setMeta( + public fun set( name: Name, node: Meta?, notify: Boolean, @@ -77,10 +77,10 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider notify: Boolean, ) - override fun getMeta(name: Name): MutableMeta = getMeta(name, null, null) + override fun get(name: Name): MutableMeta = get(name, null, null) - override fun setMeta(name: Name, node: Meta?) { - setMeta(name, node, true) + override fun set(name: Name, node: Meta?) { + set(name, node, true) } override fun setValue(name: Name, value: Value?) { @@ -89,7 +89,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider } public fun MutableVisionProperties.remove(name: Name) { - setMeta(name, null) + set(name, null) } public fun MutableVisionProperties.remove(name: String) { @@ -114,7 +114,7 @@ private class VisionPropertiesItem( override val items: Map get() { - val metaKeys = properties.own?.getMeta(nodeName)?.items?.keys ?: emptySet() + val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet() val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet() val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet() val inheritFlag = descriptor?.inherited ?: inherit @@ -148,8 +148,8 @@ private class VisionPropertiesItem( default ) - override fun setMeta(name: Name, node: Meta?) { - properties.setMeta(nodeName + name, node) + override fun set(name: Name, node: Meta?) { + properties[nodeName + name] = node } override fun toString(): String = Meta.toString(this) @@ -202,16 +202,16 @@ public abstract class AbstractVisionProperties( return descriptor?.defaultValue } - override fun setMeta(name: Name, node: Meta?, notify: Boolean) { + override fun set(name: Name, node: Meta?, notify: Boolean) { //ignore if the value is the same as existing - if (own?.getMeta(name) == node) return + if (own?.get(name) == node) return if (name.isEmpty()) { properties = node?.asMutableMeta() } else if (node == null) { - properties?.setMeta(name, node) + properties?.set(name, node) } else { - getOrCreateProperties().setMeta(name, node) + getOrCreateProperties()[name] = node } if (notify) { invalidate(name) @@ -223,7 +223,7 @@ public abstract class AbstractVisionProperties( if (own?.getValue(name) == value) return if (value == null) { - properties?.getMeta(name)?.value = null + properties?.get(name)?.value = null } else { getOrCreateProperties().setValue(name, value) } @@ -272,11 +272,11 @@ public fun VisionProperties.getValue( /** * Get [Vision] property using key as a String */ -public fun VisionProperties.getMeta( +public fun VisionProperties.get( name: String, inherit: Boolean? = null, includeStyles: Boolean? = null, -): Meta = getMeta(name.parseAsName(), inherit, includeStyles) +): Meta = get(name.parseAsName(), inherit, includeStyles) /** * The root property node with given inheritance and style flags @@ -286,17 +286,17 @@ public fun VisionProperties.getMeta( public fun MutableVisionProperties.root( inherit: Boolean? = null, includeStyles: Boolean? = null, -): MutableMeta = getMeta(Name.EMPTY, inherit, includeStyles) +): MutableMeta = get(Name.EMPTY, inherit, includeStyles) /** * Get [Vision] property using key as a String */ -public fun MutableVisionProperties.getMeta( +public fun MutableVisionProperties.get( name: String, inherit: Boolean? = null, includeStyles: Boolean? = null, -): MutableMeta = getMeta(name.parseAsName(), inherit, includeStyles) +): MutableMeta = get(name.parseAsName(), inherit, includeStyles) // //public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit = 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 f8d128e1..60c39c20 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/flowProperty.kt @@ -17,10 +17,10 @@ public fun Vision.flowProperty( includeStyles: Boolean? = null, ): Flow = flow { //Pass initial value. - emit(properties.getMeta(propertyName, inherit, includeStyles)) + emit(properties.get(propertyName, inherit, includeStyles)) properties.changes.collect { name -> if (name.startsWith(propertyName)) { - emit(properties.getMeta(propertyName, inherit, includeStyles)) + emit(properties.get(propertyName, inherit, includeStyles)) } } } 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 cee3a002..8ffb272a 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/useProperty.kt @@ -23,10 +23,10 @@ public fun Vision.useProperty( callback: (Meta) -> Unit, ): Job { //Pass initial value. - callback(properties.getMeta(propertyName, inherit, includeStyles)) + callback(properties.get(propertyName, inherit, includeStyles)) return properties.changes.onEach { name -> if (name.startsWith(propertyName)) { - callback(properties.getMeta(propertyName, inherit, includeStyles)) + callback(properties.get(propertyName, inherit, includeStyles)) } }.launchIn(scope ?: error("Orphan Vision can't observe properties")) } 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 38abe34c..b1ca970a 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 @@ -40,7 +40,7 @@ internal class VisionPropertyTest { @Test fun testPropertyEdit() { val vision = manager.group() - vision.properties.getMeta("fff.ddd").apply { + vision.properties.get("fff.ddd").apply { value = 2.asValue() } assertEquals(2, vision.properties.getValue("fff.ddd")?.int) @@ -50,7 +50,7 @@ internal class VisionPropertyTest { @Test fun testPropertyUpdate() { val vision = manager.group() - vision.properties.getMeta("fff").updateWith(TestScheme) { + vision.properties.get("fff").updateWith(TestScheme) { ddd = 2 } assertEquals(2, vision.properties.getValue("fff.ddd")?.int) @@ -85,7 +85,7 @@ internal class VisionPropertyTest { child.properties.remove("test") - assertEquals(11, child.properties.getMeta("test", inherit = true).int) + assertEquals(11, child.properties.get("test", inherit = true).int) // assertEquals(11, deferred.await()?.int) // assertEquals(2, callCounter) subscription.cancel() 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 8842e08e..8e1254d9 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt @@ -9,8 +9,8 @@ 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.misc.Type import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.parseAsName @@ -20,7 +20,7 @@ import kotlin.reflect.cast /** * A browser renderer for a [Vision]. */ -@Type(ElementVisionRenderer.TYPE) +@DfType(ElementVisionRenderer.TYPE) public interface ElementVisionRenderer : Named { /** 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 6ae70280..17958f97 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -161,7 +161,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { //launch backward property propagation vision.properties.changes.onEach { propertyName: Name -> - changeCollector.propertyChanged(visionName, propertyName, vision.properties.getMeta(propertyName)) + changeCollector.propertyChanged(visionName, propertyName, vision.properties[propertyName]) }.launchIn(this) //aggregate atomic changes diff --git a/visionforge-plotly/build.gradle.kts b/visionforge-plotly/build.gradle.kts index 9d313266..c4df8593 100644 --- a/visionforge-plotly/build.gradle.kts +++ b/visionforge-plotly/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("space.kscience.gradle.mpp") } -val plotlyVersion = "0.6.0" +val plotlyVersion = "0.6.1" kscience { jvm() 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 d5333dda..19d98171 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 @@ -33,8 +33,8 @@ public class VisionOfPlotly private constructor( @Transient override val properties: MutableVisionProperties = object : MutableVisionProperties { - override fun setMeta(name: Name, node: Meta?, notify: Boolean) { - meta.setMeta(name, node) + override fun set(name: Name, node: Meta?, notify: Boolean) { + meta[name] = node } override fun setValue(name: Name, value: Value?, notify: Boolean) { @@ -45,11 +45,11 @@ public class VisionOfPlotly private constructor( override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor - override fun getMeta( + override fun get( name: Name, inherit: Boolean?, includeStyles: Boolean?, - ): MutableMeta = meta.getMeta(name) ?: MutableMeta() + ): MutableMeta = meta[name] ?: MutableMeta() override fun getValue( name: Name, 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 4c5e093a..3c5e397a 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 @@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds public class VisionRoute( public val route: String, public val visionManager: VisionManager, - override val meta: ObservableMutableMeta = MutableMeta(), + override val meta: ObservableMutableMeta = ObservableMutableMeta(), ) : Configurable, ContextAware { public enum class Mode { 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 3cc6ff61..3ae6a681 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 @@ -39,7 +39,7 @@ public inline fun MutableVisionContainer.composite( } val res = Composite(type, children[0], children[1]) - res.properties.setMeta(Name.EMPTY, group.properties.own) + res.properties[Name.EMPTY] = group.properties.own setChild(name, res) return res diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Extruded.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Extruded.kt index 8276722e..c2157480 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Extruded.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Extruded.kt @@ -92,7 +92,7 @@ public class Extruded( } internal fun build(): Extruded = Extruded(shape, layers).apply { - this.properties.setMeta(Name.EMPTY, this@Builder.properties) + this.properties[Name.EMPTY] = this@Builder.properties } } 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 a42427da..aab583a0 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 @@ -190,7 +190,7 @@ internal fun float32Vector( override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) { if (value == null) { - thisRef.properties.setMeta(name, null) + thisRef.properties[name] = null } else { thisRef.properties[name + X_KEY] = value.x thisRef.properties[name + Y_KEY] = value.y diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidMaterial.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidMaterial.kt index 40857b14..c411cf4c 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidMaterial.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/SolidMaterial.kt @@ -110,12 +110,12 @@ public val Solid.color: ColorAccessor get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY) public var Solid.material: SolidMaterial? - get() = SolidMaterial.read(properties.getMeta(MATERIAL_KEY)) - set(value) = properties.setMeta(MATERIAL_KEY, value?.meta) + get() = SolidMaterial.read(properties[MATERIAL_KEY]) + set(value) = properties.set(MATERIAL_KEY, value?.meta) @VisionBuilder public fun Solid.material(builder: SolidMaterial.() -> Unit) { - properties.getMeta(MATERIAL_KEY).updateWith(SolidMaterial, builder) + properties[MATERIAL_KEY].updateWith(SolidMaterial, builder) } public var Solid.opacity: Number? @@ -128,5 +128,5 @@ public var Solid.opacity: Number? @VisionBuilder public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) { properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled - SolidMaterial.write(properties.getMeta(SolidMaterial.EDGES_MATERIAL_KEY)).apply(block) + SolidMaterial.write(properties[SolidMaterial.EDGES_MATERIAL_KEY]).apply(block) } \ No newline at end of file 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 32e69520..b832865c 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 @@ -162,7 +162,7 @@ internal class SolidReferenceChild( override val properties: MutableVisionProperties = object : MutableVisionProperties { override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor - override val own: MutableMeta by lazy { owner.properties.getMeta(childToken(childName).asName()) } + override val own: MutableMeta by lazy { owner.properties[childToken(childName).asName()] } override fun getValue( name: Name, @@ -170,8 +170,8 @@ internal class SolidReferenceChild( includeStyles: Boolean?, ): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles) - override fun setMeta(name: Name, node: Meta?, notify: Boolean) { - own.setMeta(name, node) + override fun set(name: Name, node: Meta?, notify: Boolean) { + own[name] = node } override fun setValue(name: Name, value: Value?, notify: Boolean) { diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Surface.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Surface.kt index 35f9b75f..cf442c62 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Surface.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/Surface.kt @@ -155,7 +155,7 @@ public class Surface( } internal fun build(): Surface = Surface(layers).apply { - properties.setMeta(Name.EMPTY, this@Builder.properties) + properties[Name.EMPTY] = this@Builder.properties } } diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/geometry.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/geometry.kt index 58c3021d..8f799f2e 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/geometry.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/geometry.kt @@ -36,9 +36,9 @@ internal fun Meta.toVector2D(): Float32Vector2D = //} internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector( - getMeta(X_KEY).float ?: default, - getMeta(Y_KEY).float ?: default, - getMeta(Z_KEY).float ?: default + get(X_KEY).float ?: default, + get(Y_KEY).float ?: default, + get(Z_KEY).float ?: default ) diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/transform/RemoveSingleChild.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/transform/RemoveSingleChild.kt index cdc9caaf..bcf3baa9 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/transform/RemoveSingleChild.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/transform/RemoveSingleChild.kt @@ -19,7 +19,7 @@ internal fun Solid.updateFrom(other: Solid): Solid { scaleX *= other.scaleX scaleY *= other.scaleY scaleZ *= other.scaleZ - properties.setMeta(Name.EMPTY, other.properties.root()) + properties[Name.EMPTY] = other.properties.root() return this } diff --git a/visionforge-tables/build.gradle.kts b/visionforge-tables/build.gradle.kts index 33988cf0..f4197864 100644 --- a/visionforge-tables/build.gradle.kts +++ b/visionforge-tables/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("space.kscience.gradle.mpp") } -val tablesVersion = "0.2.1" +val tablesVersion = "0.3.0" kscience { jvm() diff --git a/visionforge-tables/src/commonTest/kotlin/space/kscience/visionforge/tables/VisionOfTableTest.kt b/visionforge-tables/src/commonTest/kotlin/space/kscience/visionforge/tables/VisionOfTableTest.kt index e1210ffa..def7017b 100644 --- a/visionforge-tables/src/commonTest/kotlin/space/kscience/visionforge/tables/VisionOfTableTest.kt +++ b/visionforge-tables/src/commonTest/kotlin/space/kscience/visionforge/tables/VisionOfTableTest.kt @@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.int import space.kscience.tables.ColumnHeader import space.kscience.tables.ColumnTable +import space.kscience.tables.fill import space.kscience.tables.get import kotlin.math.pow import kotlin.test.Test @@ -18,7 +19,7 @@ internal class VisionOfTableTest { val y by ColumnHeader.typed() val table = ColumnTable(100) { - x.fill { it.asValue() } + fill(x, null) { it.asValue() } y.values = x.values.map { it?.double?.pow(2)?.asValue() } } diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeFactory.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeFactory.kt index 122a2c30..09b81053 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeFactory.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeFactory.kt @@ -1,6 +1,6 @@ package space.kscience.visionforge.solid.three -import space.kscience.dataforge.misc.Type +import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.startsWith import space.kscience.visionforge.Vision @@ -17,7 +17,7 @@ import kotlin.reflect.KClass /** * Builder and updater for three.js object */ -@Type(TYPE) +@DfType(TYPE) public interface ThreeFactory { public val type: KClass diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeLineFactory.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeLineFactory.kt index 42722254..68bfd7b2 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeLineFactory.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeLineFactory.kt @@ -24,7 +24,7 @@ public object ThreeLineFactory : ThreeFactory { } val material = ThreeMaterials.getLineMaterial( - vision.properties.getMeta(SolidMaterial.MATERIAL_KEY), + vision.properties[SolidMaterial.MATERIAL_KEY], false ) 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 49667c8e..8e1b2e3f 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 @@ -83,7 +83,7 @@ public object ThreeMaterials { private val visionMaterialCache = HashMap() internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) { - buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY)).apply { + buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY]).apply { cached = true } } @@ -133,11 +133,11 @@ public fun Mesh.setMaterial(vision: Vision) { } else { material = vision.parent?.let { parent -> //TODO cache parent material - ThreeMaterials.buildMaterial(parent.properties.getMeta(SolidMaterial.MATERIAL_KEY)) + ThreeMaterials.buildMaterial(parent.properties[SolidMaterial.MATERIAL_KEY]) } ?: ThreeMaterials.cacheMaterial(vision) } } else { - material = ThreeMaterials.buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY)) + material = ThreeMaterials.buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY]) } } @@ -153,18 +153,18 @@ public fun Mesh.updateMaterialProperty(vision: Vision, propertyName: Name) { when (propertyName) { SolidMaterial.MATERIAL_COLOR_KEY -> { material.asDynamic().color = - vision.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).threeColor() + vision.properties[SolidMaterial.MATERIAL_COLOR_KEY].threeColor() ?: ThreeMaterials.DEFAULT_COLOR } SolidMaterial.SPECULAR_COLOR_KEY -> { material.asDynamic().specular = - vision.properties.getMeta(SolidMaterial.SPECULAR_COLOR_KEY).threeColor() + vision.properties[SolidMaterial.SPECULAR_COLOR_KEY].threeColor() ?: ThreeMaterials.DEFAULT_COLOR } SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> { - material.asDynamic().emissive = vision.properties.getMeta(SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY) + material.asDynamic().emissive = vision.properties[SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY] .threeColor() ?: ThreeMaterials.BLACK_COLOR } diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMeshFactory.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMeshFactory.kt index 4ba19998..2e5fc0ca 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMeshFactory.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreeMeshFactory.kt @@ -76,7 +76,7 @@ public fun Mesh.applyEdges(vision: Solid) { val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments //inherited edges definition, enabled by default if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) { - val material = ThreeMaterials.getLineMaterial(vision.properties.getMeta(EDGES_MATERIAL_KEY), true) + val material = ThreeMaterials.getLineMaterial(vision.properties[EDGES_MATERIAL_KEY], true) if (edges == null) { add( LineSegments( From 9fc6f1e34ccfaf2c49775422e436a48ecdf077c6 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 5 Dec 2023 17:43:53 +0300 Subject: [PATCH 13/13] revert chagnes to client change collector --- .../kscience/visionforge/VisionClient.kt | 33 +++++++-------- .../kscience/visionforge/JsVisionClient.kt | 41 ++++++++----------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt index 9d6a5561..20974f0c 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -2,8 +2,10 @@ package space.kscience.visionforge import kotlinx.coroutines.launch import space.kscience.dataforge.context.Plugin +import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaRepr import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.parseAsName /** * A feedback client that communicates with a server and provides ability to propagate events and changes back to the model @@ -13,25 +15,24 @@ public interface VisionClient: Plugin { public suspend fun sendEvent(targetName: Name, event: VisionEvent) -// public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) + public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) } +public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) { + notifyPropertyChanged(visionName, propertyName.parseAsName(true), item) +} -//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) { -// notifyPropertyChanged(visionName, propertyName.parseAsName(true), item) -//} -// -//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) { -// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -//} -// -//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) { -// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -//} -// -//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) { -// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -//} +public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) { + notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +} + +public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) { + notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +} + +public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) { + notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) +} public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit { context.launch { 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 17958f97..c7ada5fe 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -65,20 +65,21 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null -// private val mutex = Mutex() + private val mutex = Mutex() + private val rootChangeCollector = VisionChangeBuilder() -// /** -// * Communicate vision property changed from rendering engine to model -// */ -// private fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) { -// context.launch { -// mutex.withLock { -// changeCollector.propertyChanged(visionName, propertyName, item) -// } -// } -// } + /** + * Communicate vision property changed from rendering engine to model + */ + override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) { + context.launch { + mutex.withLock { + rootChangeCollector.propertyChanged(visionName, propertyName, item) + } + } + } private val eventCollector by lazy { MutableSharedFlow>(meta["feedback.eventCache"].int ?: 100) @@ -139,7 +140,6 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { } } - //Backward change propagation var feedbackJob: Job? = null @@ -148,29 +148,20 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { onopen = { - - val mutex = Mutex() - - val changeCollector = VisionChangeBuilder() - feedbackJob = visionManager.context.launch { //launch a separate coroutine to send events to the backend eventCollector.filter { it.first == visionName }.onEach { send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second)) }.launchIn(this) - //launch backward property propagation - vision.properties.changes.onEach { propertyName: Name -> - changeCollector.propertyChanged(visionName, propertyName, vision.properties[propertyName]) - }.launchIn(this) - //aggregate atomic changes while (isActive) { delay(feedbackAggregationTime.milliseconds) - if (!changeCollector.isEmpty()) { + val visionChangeCollector = rootChangeCollector[name] + if (visionChangeCollector?.isEmpty() == false) { mutex.withLock { - eventCollector.emit(visionName to changeCollector.deepCopy(visionManager)) - changeCollector.reset() + eventCollector.emit(visionName to visionChangeCollector.deepCopy(visionManager)) + rootChangeCollector.reset() } } }