From 6b8e166978af9b0cc1ec8e0d989bb7c5fb13be47 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 31 Dec 2021 13:59:27 +0300 Subject: [PATCH] Forms implemented --- .../visionforge/gdml/demo/GDMLAppComponent.kt | 4 +- demo/jupyter-playground/build.gradle.kts | 35 ------ .../kotlin/ru/mipt/npm/muon/monitor/Model.kt | 4 +- .../mipt/npm/muon/monitor/MMAppComponent.kt | 3 - demo/playground/build.gradle.kts | 28 +++-- .../kotlin/VisionForgePlayGroundForJupyter.kt | 37 +++---- .../src/jvmMain/kotlin/formServer.kt | 67 ++++++++++++ .../kotlin/{gdmCurve.kt => gdmlCurve.kt} | 2 +- .../src/jvmMain/kotlin/generateSchema.kt | 20 ++-- .../main/kotlin/ru/mipt/npm/sat/geometry.kt | 2 - .../src/jvmMain/kotlin/JupyterPluginBase.kt | 7 -- settings.gradle.kts | 1 - .../ThreeViewWithControls.kt | 2 +- .../space/kscience/visionforge/VisionBase.kt | 2 +- .../kscience/visionforge/VisionChange.kt | 36 ++++--- .../kscience/visionforge/VisionGroupBase.kt | 5 +- .../kscience/visionforge/VisionManager.kt | 8 ++ .../visionforge/html/VisionOfHtmlForm.kt | 26 +++++ .../visionforge/html/VisionOfHtmlInput.kt | 53 +++++++++ .../visionforge/html/VisionTagConsumer.kt | 6 +- .../visionforge/ElementVisionRenderer.kt | 74 +++++++++++++ .../kscience/visionforge/VisionClient.kt | 18 +++- .../kscience/visionforge/elementOutput.kt | 27 ----- .../kscience/visionforge/inputRenderers.kt | 101 ++++++++++++++++++ .../space/kscience/visionforge/FormTest.kt | 22 ++++ .../visionforge/three/server/VisionServer.kt | 2 +- .../visionforge/solid/VisionUpdateTest.kt | 4 +- 27 files changed, 445 insertions(+), 151 deletions(-) delete mode 100644 demo/jupyter-playground/build.gradle.kts rename demo/{jupyter-playground/src/main => playground/src/jvmMain}/kotlin/VisionForgePlayGroundForJupyter.kt (71%) create mode 100644 demo/playground/src/jvmMain/kotlin/formServer.kt rename demo/playground/src/jvmMain/kotlin/{gdmCurve.kt => gdmlCurve.kt} (99%) create mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt create mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt create mode 100644 visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt delete mode 100644 visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/elementOutput.kt create mode 100644 visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt create mode 100644 visionforge-core/src/jsTest/kotlin/space/kscience/visionforge/FormTest.kt diff --git a/demo/gdml/src/jsMain/kotlin/space/kscience/visionforge/gdml/demo/GDMLAppComponent.kt b/demo/gdml/src/jsMain/kotlin/space/kscience/visionforge/gdml/demo/GDMLAppComponent.kt index e5787050..823297ec 100644 --- a/demo/gdml/src/jsMain/kotlin/space/kscience/visionforge/gdml/demo/GDMLAppComponent.kt +++ b/demo/gdml/src/jsMain/kotlin/space/kscience/visionforge/gdml/demo/GDMLAppComponent.kt @@ -21,7 +21,7 @@ import space.kscience.visionforge.gdml.markLayers import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.ring.ThreeCanvasWithControls import space.kscience.visionforge.ring.tab -import space.kscience.visionforge.root +import space.kscience.visionforge.setAsRoot import space.kscience.visionforge.solid.Solid import space.kscience.visionforge.solid.Solids import styled.css @@ -50,7 +50,7 @@ val GDMLApp = fc("GDMLApp") { props -> name.endsWith(".gdml") || name.endsWith(".xml") -> { val gdml = Gdml.decodeFromString(data) gdml.toVision().apply { - root(visionManager) + setAsRoot(visionManager) console.info("Marking layers for file $name") markLayers() } diff --git a/demo/jupyter-playground/build.gradle.kts b/demo/jupyter-playground/build.gradle.kts deleted file mode 100644 index 62f92348..00000000 --- a/demo/jupyter-playground/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - kotlin("jvm") - kotlin("jupyter.api") - id("com.github.johnrengelman.shadow") version "6.1.0" -} - -repositories { - jcenter() - mavenCentral() - maven("https://repo.kotlin.link") -} - -dependencies { - implementation(project(":demo:playground")) -} - -tasks.withType { - kotlinOptions { - jvmTarget = ru.mipt.npm.gradle.KScienceVersions.JVM_TARGET.toString() - } -} - -extensions.findByType()?.apply { - targetCompatibility = ru.mipt.npm.gradle.KScienceVersions.JVM_TARGET -} - -tasks.withType { - useJUnitPlatform() -} - -tasks.processJupyterApiResources { - libraryProducers = listOf("playground.VisionForgePlayGroundForJupyter") -} - -tasks.findByName("shadowJar")?.dependsOn("processJupyterApiResources") \ 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 0c5e0af0..2dab49a2 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 @@ -5,7 +5,7 @@ import ru.mipt.npm.muon.monitor.Monitor.LOWER_LAYER_Z import ru.mipt.npm.muon.monitor.Monitor.UPPER_LAYER_Z import space.kscience.visionforge.VisionManager import space.kscience.visionforge.removeAll -import space.kscience.visionforge.root +import space.kscience.visionforge.setAsRoot import space.kscience.visionforge.setProperty import space.kscience.visionforge.solid.* import kotlin.math.PI @@ -37,7 +37,7 @@ class Model(val manager: VisionManager) { var tracks: SolidGroup val root: SolidGroup = SolidGroup().apply { - root(this@Model.manager) + setAsRoot(this@Model.manager) material { wireframe color("darkgreen") diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index 879f6af7..558317cb 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -76,9 +76,6 @@ val MMApp = fc("Muon monitor") { props -> attrs { onClickFunction = { context.launch { -// val event = props.connection.get( -// "http://localhost:8080/event" -// ) val event = window.fetch( "http://localhost:8080/event", RequestInit("GET") diff --git a/demo/playground/build.gradle.kts b/demo/playground/build.gradle.kts index f06c209e..aa9e7c07 100644 --- a/demo/playground/build.gradle.kts +++ b/demo/playground/build.gradle.kts @@ -1,14 +1,13 @@ plugins { kotlin("multiplatform") + kotlin("jupyter.api") + id("com.github.johnrengelman.shadow") version "7.1.2" } -repositories{ - jcenter() - maven("https://kotlin.bintray.com/kotlinx") - maven("https://dl.bintray.com/kotlin/kotlin-eap") - maven("https://dl.bintray.com/mipt-npm/dataforge") - maven("https://dl.bintray.com/mipt-npm/kscience") - maven("https://dl.bintray.com/mipt-npm/dev") +repositories { + mavenCentral() + maven("https://jitpack.io") + maven("https://repo.kotlin.link") } kotlin { @@ -27,7 +26,8 @@ kotlin { binaries.executable() } - jvm{ + jvm { + withJava() compilations.all { kotlinOptions.jvmTarget = "11" } @@ -37,7 +37,7 @@ kotlin { } afterEvaluate { - val jsBrowserDistribution = tasks.getByName("jsBrowserDevelopmentExecutableDistribution") + val jsBrowserDistribution = tasks.getByName("jsBrowserDevelopmentExecutableDistribution") tasks.getByName("jvmProcessResources") { dependsOn(jsBrowserDistribution) @@ -59,14 +59,14 @@ kotlin { } } - val jsMain by getting{ + val jsMain by getting { dependencies { api(project(":ui:ring")) api(project(":visionforge-threejs")) } } - val jvmMain by getting{ + val jvmMain by getting { dependencies { api(project(":visionforge-server")) api("ch.qos.logback:logback-classic:1.2.3") @@ -75,3 +75,9 @@ kotlin { } } } + +tasks.withType { + libraryProducers = listOf("space.kscience.visionforge.examples.VisionForgePlayGroundForJupyter") +} + +tasks.findByName("shadowJar")?.dependsOn("processJupyterApiResources") \ No newline at end of file diff --git a/demo/jupyter-playground/src/main/kotlin/VisionForgePlayGroundForJupyter.kt b/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt similarity index 71% rename from demo/jupyter-playground/src/main/kotlin/VisionForgePlayGroundForJupyter.kt rename to demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt index 5ada35ec..b820cf71 100644 --- a/demo/jupyter-playground/src/main/kotlin/VisionForgePlayGroundForJupyter.kt +++ b/demo/playground/src/jvmMain/kotlin/VisionForgePlayGroundForJupyter.kt @@ -1,12 +1,9 @@ -package playground +package space.kscience.visionforge.examples -import kotlinx.html.div -import kotlinx.html.id -import kotlinx.html.script import kotlinx.html.stream.createHTML -import kotlinx.html.unsafe import org.jetbrains.kotlinx.jupyter.api.HTML -import org.jetbrains.kotlinx.jupyter.api.libraries.* +import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration +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 @@ -15,7 +12,7 @@ import space.kscience.visionforge.Vision import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.Page -import space.kscience.visionforge.html.embedVisionFragment +import space.kscience.visionforge.html.embedAndRenderVisionFragment import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.asVision import space.kscience.visionforge.solid.Solids @@ -29,27 +26,19 @@ public class VisionForgePlayGroundForJupyter : JupyterIntegration() { plugin(PlotlyPlugin) } - private val jsBundle = ResourceFallbacksBundle(listOf( - ResourceLocation("js/visionforge-playground.js", ResourcePathType.CLASSPATH_PATH)) - ) - private val jsResource = LibraryResource(name = "VisionForge", type = ResourceType.JS, bundles = listOf(jsBundle)) - private var counter = 0 - private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().div { - val id = "visionforge.vision[${counter++}]" - div { - this.id = id - embedVisionFragment(context.visionManager, fragment = fragment) - } - script { - type = "text/javascript" - unsafe { +"window.renderAllVisionsById(\"$id\");" } - } - } + private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply { + embedAndRenderVisionFragment(context.visionManager, counter++, fragment = fragment) + }.finalize() override fun Builder.onLoaded() { - resource(jsResource) + + resources { + js("VisionForge"){ + classPath("js/visionforge-playground.js") + } + } import( "space.kscience.gdml.*", diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt new file mode 100644 index 00000000..bdc95017 --- /dev/null +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -0,0 +1,67 @@ +package space.kscience.visionforge.examples + +import kotlinx.html.* +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.fetch +import space.kscience.dataforge.names.asName +import space.kscience.visionforge.VisionManager +import space.kscience.visionforge.html.visionOfForm +import space.kscience.visionforge.onPropertyChange +import space.kscience.visionforge.three.server.close +import space.kscience.visionforge.three.server.openInBrowser +import space.kscience.visionforge.three.server.serve +import space.kscience.visionforge.three.server.useScript + +fun main() { + val visionManager = Global.fetch(VisionManager) + + val server = visionManager.serve { + useScript("js/visionforge-playground.js") + page { + val form = visionOfForm("form") { + label { + htmlFor = "fname" + +"First name:" + } + br() + input { + type = InputType.text + id = "fname" + name = "fname" + value = "John" + } + br() + label { + htmlFor = "lname" + +"Last name:" + } + br() + input { + type = InputType.text + id = "lname" + name = "lname" + value = "Doe" + } + br() + br() + input { + type = InputType.submit + value = "Submit" + } + } + + vision("form".asName(), form) + form.onPropertyChange { + println(this) + } + } + } + + server.openInBrowser() + + while (readln() != "exit") { + + } + + server.close() +} \ No newline at end of file diff --git a/demo/playground/src/jvmMain/kotlin/gdmCurve.kt b/demo/playground/src/jvmMain/kotlin/gdmlCurve.kt similarity index 99% rename from demo/playground/src/jvmMain/kotlin/gdmCurve.kt rename to demo/playground/src/jvmMain/kotlin/gdmlCurve.kt index cbdac68a..1a646d97 100644 --- a/demo/playground/src/jvmMain/kotlin/gdmCurve.kt +++ b/demo/playground/src/jvmMain/kotlin/gdmlCurve.kt @@ -226,7 +226,7 @@ fun main() { } } }.toVision { - configure { parent, solid, material -> + configure { _, solid, _ -> //disable visibility for the world box if(solid.name == "world"){ visible = false diff --git a/demo/playground/src/jvmMain/kotlin/generateSchema.kt b/demo/playground/src/jvmMain/kotlin/generateSchema.kt index 75c6f9cd..8331ac62 100644 --- a/demo/playground/src/jvmMain/kotlin/generateSchema.kt +++ b/demo/playground/src/jvmMain/kotlin/generateSchema.kt @@ -6,16 +6,18 @@ import kotlinx.serialization.json.Json import space.kscience.visionforge.solid.SolidGroup import space.kscience.visionforge.solid.Solids +private val json = Json { + serializersModule = Solids.serializersModuleForSolids + prettyPrintIndent = " " + prettyPrint = true + ignoreUnknownKeys = true + isLenient = true + coerceInputValues = true + encodeDefaults = true +} + @ExperimentalSerializationApi fun main() { - val schema = Json { - serializersModule = Solids.serializersModuleForSolids - prettyPrintIndent = " " - prettyPrint = true - ignoreUnknownKeys = true - isLenient = true - coerceInputValues = true - encodeDefaults = true - }.encodeToSchema(SolidGroup.serializer(), generateDefinitions = false) + val schema = json.encodeToSchema(SolidGroup.serializer(), generateDefinitions = false) println(schema) } \ No newline at end of file diff --git a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt index 546c7b51..93650fb2 100644 --- a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt +++ b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt @@ -1,13 +1,11 @@ package ru.mipt.npm.sat import space.kscience.dataforge.meta.set -import space.kscience.dataforge.misc.DFExperimental import space.kscience.visionforge.solid.* import space.kscience.visionforge.style import space.kscience.visionforge.useStyle import kotlin.math.PI -@DFExperimental internal fun visionOfSatellite( layers: Int = 10, layerHeight: Number = 10, diff --git a/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt b/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt index f912a88f..e071ac45 100644 --- a/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt +++ b/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt @@ -6,7 +6,6 @@ import io.ktor.server.engine.embeddedServer import kotlinx.html.stream.createHTML import org.jetbrains.kotlinx.jupyter.api.HTML import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration -import org.jetbrains.kotlinx.jupyter.api.libraries.resources import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.meta.get @@ -53,12 +52,6 @@ public abstract class JupyterPluginBase( server = null } - resources { - js("three") { - classPath("js/gdml-jupyter.js") - } - } - import( "kotlinx.html.*", "space.kscience.visionforge.html.Page", diff --git a/settings.gradle.kts b/settings.gradle.kts index fb02e336..dc65b446 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,7 +59,6 @@ include( ":demo:muon-monitor", ":demo:sat-demo", ":demo:playground", - ":demo:jupyter-playground", ":demo:plotly-fx", ":demo:js-playground", ":jupyter:visionforge-jupyter-base", diff --git a/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt b/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt index e9cf62bf..97c9a083 100644 --- a/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt +++ b/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt @@ -83,7 +83,7 @@ public val ThreeCanvasWithControls: FC = fc("Three useEffect { props.context.launch { solid = props.builderOfSolid.await().also { - it?.root(props.context.visionManager) + it?.setAsRoot(props.context.visionManager) } } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt index 6e2e95d0..003fec79 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt @@ -109,7 +109,7 @@ public open class VisionBase( override fun hashCode(): Int = Meta.hashCode(this) } - override val meta: ObservableMutableMeta get() = VisionProperties(Name.EMPTY) + final override val meta: ObservableMutableMeta get() = VisionProperties(Name.EMPTY) override fun getPropertyValue( name: Name, 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 c4f18712..ad0d0d70 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionChange.kt @@ -14,6 +14,17 @@ import space.kscience.dataforge.values.Null import kotlin.jvm.Synchronized import kotlin.time.Duration +/** + * Create a deep copy of given Vision without external connections. + */ +private fun Vision.deepCopy(): Vision { + //Assuming that unrooted visions are already isolated + val manager = this.manager ?: return this + //TODO replace by efficient deep copy + val json = manager.encodeToJsonElement(this) + return manager.decodeFromJson(json) +} + /** * An update for a [Vision] or a [VisionGroup] */ @@ -50,20 +61,14 @@ public class VisionChangeBuilder : VisionContainerBuilder { /** * Isolate collected changes by creating detached copies of given visions */ - public fun isolate(manager: VisionManager): VisionChange = VisionChange( + public fun deepCopy(): VisionChange = VisionChange( reset, - vision?.isolate(manager), + vision?.deepCopy(), if (propertyChange.isEmpty()) null else propertyChange.seal(), - if (children.isEmpty()) null else children.mapValues { it.value.isolate(manager) } + if (children.isEmpty()) null else children.mapValues { it.value.deepCopy() } ) } -private fun Vision.isolate(manager: VisionManager): Vision { - //TODO replace by efficient deep copy - val json = manager.encodeToJsonElement(this) - return manager.decodeFromJson(json) -} - /** * @param delete flag showing that this vision child should be removed * @param vision a new value for vision content @@ -78,8 +83,8 @@ public data class VisionChange( public val children: Map? = null, ) -public inline fun VisionChange(manager: VisionManager, block: VisionChangeBuilder.() -> Unit): VisionChange = - VisionChangeBuilder().apply(block).isolate(manager) +public inline fun VisionChange(block: VisionChangeBuilder.() -> Unit): VisionChange = + VisionChangeBuilder().apply(block).deepCopy() @OptIn(DFExperimental::class) @@ -115,9 +120,10 @@ private fun CoroutineScope.collectChange( } } -@DFExperimental +/** + * Generate a flow of changes of this vision and its children + */ public fun Vision.flowChanges( - manager: VisionManager, collectionDuration: Duration, ): Flow = flow { @@ -126,7 +132,7 @@ public fun Vision.flowChanges( collectChange(Name.EMPTY, this@flowChanges) { collector } //Send initial vision state - val initialChange = VisionChange(vision = isolate(manager)) + val initialChange = VisionChange(vision = deepCopy()) emit(initialChange) while (currentCoroutineContext().isActive) { @@ -135,7 +141,7 @@ public fun Vision.flowChanges( //Propagate updates only if something is changed if (!collector.isEmpty()) { //emit changes - emit(collector.isolate(manager)) + emit(collector.deepCopy()) //Reset the collector collector = VisionChangeBuilder() } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroupBase.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroupBase.kt index e8594df2..873b5996 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroupBase.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionGroupBase.kt @@ -160,8 +160,9 @@ public open class VisionGroupBase( internal class RootVisionGroup(override val manager: VisionManager) : VisionGroupBase() /** - * Designate this [VisionGroup] as a root group and assign a [VisionManager] as its parent + * Designate this [VisionGroup] as a root and assign a [VisionManager] as its parent */ -public fun Vision.root(manager: VisionManager) { +public fun Vision.setAsRoot(manager: VisionManager) { + if (parent != null) error("This Vision already has a parent. It could not be set as root") parent = RootVisionGroup(manager) } \ No newline at end of file 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 ff4c29e1..118fd47c 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt @@ -13,6 +13,10 @@ 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 kotlin.reflect.KClass public class VisionManager(meta: Meta) : AbstractPlugin(meta) { @@ -66,6 +70,10 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { default { VisionBase.serializer() } subclass(VisionBase.serializer()) subclass(VisionGroupBase.serializer()) + subclass(VisionOfNumberField.serializer()) + subclass(VisionOfTextField.serializer()) + subclass(VisionOfCheckbox.serializer()) + subclass(VisionOfHtmlForm.serializer()) } } 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 new file mode 100644 index 00000000..a3070e58 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt @@ -0,0 +1,26 @@ +package space.kscience.visionforge.html + +import kotlinx.html.FORM +import kotlinx.html.TagConsumer +import kotlinx.html.form +import kotlinx.html.id +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.node + +@Serializable +@SerialName("html.form") +public class VisionOfHtmlForm( + public val formId: String, +) : VisionOfHtmlInput() { + public var values: Meta? by meta.node() +} + +public inline fun TagConsumer.visionOfForm(id: String, crossinline builder: FORM.() -> Unit): VisionOfHtmlForm { + form { + this.id = id + builder() + } + return VisionOfHtmlForm(id) +} \ No newline at end of file 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 new file mode 100644 index 00000000..084c5b6b --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt @@ -0,0 +1,53 @@ +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.visionforge.VisionBase + +@Serializable +public abstract class VisionOfHtmlInput : VisionBase() { + public var disabled: Boolean by meta.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 meta.string() +} + +@Serializable +@SerialName("html.checkbox") +public class VisionOfCheckbox( + public val label: String? = null, + public val name: String? = null, +) : VisionOfHtmlInput() { + public var checked: Boolean? by meta.boolean() +} + +@Serializable +@SerialName("html.number") +public class VisionOfNumberField( + public val label: String? = null, + public val name: String? = null, +) : VisionOfHtmlInput() { + public var value: Number? by meta.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 meta.number() +} + diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt index 27725721..c047222b 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt @@ -11,7 +11,7 @@ import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.asName import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager -import space.kscience.visionforge.root +import space.kscience.visionforge.setAsRoot import kotlin.collections.set @DslMarker @@ -81,11 +81,11 @@ public abstract class VisionTagConsumer( @OptIn(DFExperimental::class) public inline fun TagConsumer.vision( name: Name, - visionProvider: VisionOutput.() -> Vision, + @OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision, ): T { val output = VisionOutput(manager) val vision = output.visionProvider() - vision.root(manager) + vision.setAsRoot(manager) return vision(name, vision, output.meta) } diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt new file mode 100644 index 00000000..bf9f00df --- /dev/null +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/ElementVisionRenderer.kt @@ -0,0 +1,74 @@ +package space.kscience.visionforge + +import kotlinx.dom.clear +import kotlinx.html.TagConsumer +import kotlinx.html.dom.append +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializerOrNull +import org.w3c.dom.Element +import org.w3c.dom.HTMLElement +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.misc.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 +import kotlin.reflect.KClass +import kotlin.reflect.cast + +/** + * A browser renderer for a [Vision]. + */ +@Type(ElementVisionRenderer.TYPE) +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 + */ + public fun rateVision(vision: Vision): Int + + /** + * Display the [vision] inside a given [element] replacing its current content. + * @param meta additional parameters for rendering container + */ + public fun render(element: Element, vision: Vision, meta: Meta = Meta.EMPTY) + + public companion object { + public const val TYPE: String = "elementVisionRenderer" + public const val ZERO_RATING: Int = 0 + public const val DEFAULT_RATING: Int = 10 + } +} + +/** + * A browser renderer for element of given type + */ +public class SingleTypeVisionRenderer( + public val kClass: KClass, + private val acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING, + private val renderFunction: TagConsumer.(vision: T, meta: Meta) -> Unit, +) : ElementVisionRenderer { + + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + override val name: Name + get() = kClass.serializerOrNull()?.descriptor?.serialName?.parseAsName() + ?: kClass.toString().asName() + + override fun rateVision(vision: Vision): Int = + if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING + + override fun render(element: Element, vision: Vision, meta: Meta) { + element.clear() + element.append { + renderFunction(kClass.cast(vision), meta) + } + } +} + +public inline fun ElementVisionRenderer( + acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING, + noinline renderFunction: TagConsumer.(vision: T, meta: Meta) -> Unit, +): ElementVisionRenderer = SingleTypeVisionRenderer(T::class, acceptRating, renderFunction) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt index 60f7a627..c236a19d 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -10,6 +10,9 @@ import org.w3c.dom.url.URL import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta 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.visionforge.html.RENDER_FUNCTION_NAME import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE @@ -19,6 +22,9 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_A import kotlin.reflect.KClass import kotlin.time.Duration.Companion.milliseconds +/** + * A Kotlin-browser plugin that renders visions based on provided renderers and governs communication with the server. + */ public class VisionClient : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag private val visionManager: VisionManager by require(VisionManager) @@ -102,10 +108,12 @@ public class VisionClient : AbstractPlugin() { //Backward change propagation var feedbackJob: Job? = null + //Feedback changes aggregation time in milliseconds + val feedbackAggregationTime = meta["aggregationTime"]?.int ?: 300 + onopen = { feedbackJob = vision.flowChanges( - visionManager, - 300.milliseconds + feedbackAggregationTime.milliseconds ).onEach { change -> send(visionManager.encodeToString(change)) }.launchIn(visionManager.context) @@ -180,6 +188,12 @@ public class VisionClient : AbstractPlugin() { } } + override fun content(target: String): Map = if (target == ElementVisionRenderer.TYPE) mapOf( + numberVisionRenderer.name to numberVisionRenderer, + textVisionRenderer.name to textVisionRenderer, + formVisionRenderer.name to formVisionRenderer + ) else super.content(target) + public companion object : PluginFactory { override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient() diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/elementOutput.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/elementOutput.kt deleted file mode 100644 index f85fdabb..00000000 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/elementOutput.kt +++ /dev/null @@ -1,27 +0,0 @@ -package space.kscience.visionforge - -import org.w3c.dom.Element -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.misc.Type - -@Type(ElementVisionRenderer.TYPE) -public interface ElementVisionRenderer { - - /** - * 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 - */ - public fun rateVision(vision: Vision): Int - - /** - * Display the [vision] inside a given [element] replacing its current content - */ - public fun render(element: Element, vision: Vision, meta: Meta = Meta.EMPTY) - - public companion object { - public const val TYPE: String = "elementVisionRenderer" - public const val ZERO_RATING: Int = 0 - public const val DEFAULT_RATING: Int = 10 - } -} \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt new file mode 100644 index 00000000..0d67a821 --- /dev/null +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -0,0 +1,101 @@ +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.HTMLFormElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.get +import org.w3c.xhr.FormData +import space.kscience.dataforge.meta.DynamicMeta +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.valueSequence +import space.kscience.visionforge.html.VisionOfHtmlForm +import space.kscience.visionforge.html.VisionOfNumberField +import space.kscience.visionforge.html.VisionOfTextField + +public val textVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { vision, _ -> + val name = vision.name ?: "input[${vision.hashCode().toUInt()}]" + vision.label?.let { + label { + htmlFor = name + +it + } + } + input { + type = InputType.text + this.name = name + vision.useProperty(VisionOfTextField::text) { + value = it ?: "" + } + onChangeFunction = { + vision.text = value + } + } +} + +public val numberVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { vision, _ -> + val name = vision.name ?: "input[${vision.hashCode().toUInt()}]" + vision.label?.let { + label { + htmlFor = name + +it + } + } + input { + type = InputType.text + this.name = name + vision.useProperty(VisionOfNumberField::value) { + value = it?.toDouble() ?: 0.0 + } + onChangeFunction = { + vision.value = value.toDoubleOrNull() + } + } +} + +internal fun FormData.toMeta(): Meta { + @Suppress("UNUSED_VARIABLE") val formData = this + //val res = js("Object.fromEntries(formData);") + val `object` = js("{}") + //language=JavaScript + js(""" + formData.forEach(function(value, key){ + // Reflect.has in favor of: object.hasOwnProperty(key) + if(!Reflect.has(object, key)){ + object[key] = value; + return; + } + if(!Array.isArray(object[key])){ + object[key] = [object[key]]; + } + object[key].push(value); + }); + """) + return DynamicMeta(`object`) +} + +public val formVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { vision, _ -> + + val form = document.getElementById(vision.formId) as? HTMLFormElement + ?: error("An element with id = '${vision.formId} is not a form") + + console.info("Adding hooks to form '$form'") + + vision.useProperty(VisionOfHtmlForm::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() + console.log(formData.toString()) + vision.values = formData + false + } +} \ No newline at end of file diff --git a/visionforge-core/src/jsTest/kotlin/space/kscience/visionforge/FormTest.kt b/visionforge-core/src/jsTest/kotlin/space/kscience/visionforge/FormTest.kt new file mode 100644 index 00000000..58bd2b09 --- /dev/null +++ b/visionforge-core/src/jsTest/kotlin/space/kscience/visionforge/FormTest.kt @@ -0,0 +1,22 @@ +package space.kscience.visionforge + +import org.w3c.xhr.FormData +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.stringList +import kotlin.test.Test +import kotlin.test.assertEquals + +class FormTest { + @Test + fun testFormConversion() { + val fd = FormData() + fd.append("a", "22") + fd.append("b", "1") + fd.append("b", "2") + val meta = fd.toMeta() + assertEquals(22, meta["a"].int) + assertEquals(listOf("1","2"), meta["b"]?.stringList) + } + +} \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt index 82b27952..5065c765 100644 --- a/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt @@ -144,7 +144,7 @@ public class VisionServer internal constructor( try { withContext(visionManager.context.coroutineContext) { - vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update -> + vision.flowChanges(updateInterval.milliseconds).collect { update -> val json = visionManager.jsonFormat.encodeToString( VisionChange.serializer(), update 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 a08085d6..26b4b40d 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 @@ -20,7 +20,7 @@ class VisionUpdateTest { val targetVision = SolidGroup { box(200,200,200, name = "origin") } - val dif = VisionChange(visionManager){ + val dif = VisionChange{ group("top") { color(123) box(100,100,100) @@ -36,7 +36,7 @@ class VisionUpdateTest { @Test fun testVisionChangeSerialization(){ - val change = VisionChange(visionManager){ + val change = VisionChange{ group("top") { color(123) box(100,100,100)