From 7b9fe54363b0f4c8e3b35b0166dc24499494684a Mon Sep 17 00:00:00 2001 From: Igor Dunaev Date: Fri, 17 Nov 2023 22:17:03 +0300 Subject: [PATCH 1/6] Write Javadocs for ThreePlugin --- .../visionforge/solid/three/ThreePlugin.kt | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt index 7051f887..f5ee8876 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt @@ -10,12 +10,14 @@ import space.kscience.dataforge.names.* import space.kscience.visionforge.* import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.specifications.Canvas3DOptions -import space.kscience.visionforge.solid.three.set import three.core.Object3D import kotlin.collections.set import kotlin.reflect.KClass import three.objects.Group as ThreeGroup +/** + * A plugin that handles Three Object3D representation of Visions. + */ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { override val tag: PluginTag get() = Companion.tag @@ -48,6 +50,13 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { as ThreeFactory? } + /** + * Build an Object3D representation of the given [Solid]. + * + * @param vision 3D vision to build a representation of; + * @param observe whether the constructed Object3D should be changed when its + * parent vision changes. + */ public suspend fun buildObject3D(vision: Solid, observe: Boolean = true): Object3D = when (vision) { is ThreeJsVision -> vision.render(this) is SolidReference -> ThreeReferenceFactory.build(this, vision, observe) @@ -125,6 +134,16 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { private val canvasCache = HashMap() + /** + * Return a [ThreeCanvas] object attached to the given [Element]. + * If there is no canvas bound, a new canvas object is created + * and returned. + * + * @param element HTML element to which the canvas is + * (or should be if it is created by this call) attached; + * @param options canvas options that are applied to a newly + * created [ThreeCanvas] in case it does not exist. + */ public fun getOrCreateCanvas( element: Element, options: Canvas3DOptions, @@ -142,6 +161,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { override fun rateVision(vision: Vision): Int = if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING + /** + * Render the given [Solid] Vision in a [ThreeCanvas] attached + * to the [element]. Canvas objects are cached, so subsequent calls + * with the same [element] value do not create new canvas objects, + * but they replace existing content, so multiple Visions cannot be + * displayed in a single [ThreeCanvas]. + * + * @param element HTML element [ThreeCanvas] should be + * attached to; + * @param vision Vision to render; + * @param options options that are applied to a canvas + * in case it is not in the cache and should be created. + */ internal fun renderSolid( element: Element, vision: Solid, @@ -165,6 +197,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { } } +/** + * Render the given [Solid] Vision in a [ThreeCanvas] attached + * to the [element]. Canvas objects are cached, so subsequent calls + * with the same [element] value do not create new canvas objects, + * but they replace existing content, so multiple Visions cannot be + * displayed in a single [ThreeCanvas]. + * + * @param element HTML element [ThreeCanvas] should be + * attached to; + * @param obj Vision to render; + * @param optionsBuilder option builder that is applied to a canvas + * in case it is not in the cache and should be created. + */ public fun ThreePlugin.render( element: HTMLElement, obj: Solid, @@ -207,4 +252,4 @@ internal fun Object3D.findChild(name: Name): Object3D? { name.length == 1 -> this.children.find { it.name == name.tokens.first().toString() } else -> findChild(name.tokens.first().asName())?.findChild(name.cutFirst()) } -} \ No newline at end of file +} From 71f7f59cb3e1d7911f51fa851b916a0f5317d8b2 Mon Sep 17 00:00:00 2001 From: Igor Dunaev Date: Mon, 20 Nov 2023 17:29:53 +0300 Subject: [PATCH 2/6] More docs for ThreePlugin and VisionContainer --- .../kscience/visionforge/VisionContainer.kt | 17 +++++++++++++++++ .../visionforge/solid/three/ThreePlugin.kt | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionContainer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionContainer.kt index c74f027d..09edf4c1 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionContainer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionContainer.kt @@ -10,10 +10,17 @@ import space.kscience.visionforge.VisionChildren.Companion.STATIC_TOKEN_BODY @DslMarker public annotation class VisionBuilder +/** + * A container interface with read access to its content + * using DataForge [Name] objects as keys. + */ public interface VisionContainer { public fun getChild(name: Name): V? } +/** + * A container interface with write/replace/delete access to its content. + */ public interface MutableVisionContainer { //TODO add documentation public fun setChild(name: Name?, child: V?) @@ -61,12 +68,22 @@ public inline fun VisionChildren.forEach(block: (NameToken, Vision) -> Unit) { keys.forEach { block(it, get(it)!!) } } +/** + * A serializable representation of [Vision] children container + * with the ability to modify the container content. + */ public interface MutableVisionChildren : VisionChildren, MutableVisionContainer { public override val parent: MutableVisionGroup public operator fun set(token: NameToken, value: Vision?) + /** + * Set child [Vision] by name. + * @param name child name. Pass null to add a static child. Note that static children cannot + * be removed, replaced or accessed by name by other means. + * @param child new child value. Pass null to delete the child. + */ override fun setChild(name: Name?, child: Vision?) { when { name == null -> { diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt index f5ee8876..bb5e0c79 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/ThreePlugin.kt @@ -53,9 +53,9 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { /** * Build an Object3D representation of the given [Solid]. * - * @param vision 3D vision to build a representation of; - * @param observe whether the constructed Object3D should be changed when its - * parent vision changes. + * @param vision [Solid] object to build a representation of; + * @param observe whether the constructed Object3D should be changed when the + * original [Vision] changes. */ public suspend fun buildObject3D(vision: Solid, observe: Boolean = true): Object3D = when (vision) { is ThreeJsVision -> vision.render(this) From c5c3868786f4d6a38eb39c4e88cae5bc87261f3a Mon Sep 17 00:00:00 2001 From: Igor Dunaev Date: Sun, 10 Dec 2023 16:02:26 +0300 Subject: [PATCH 3/6] Add event display tutorial --- docs/images/event-display-final.png | Bin 0 -> 23507 bytes docs/images/event-display-selection.png | Bin 0 -> 29623 bytes docs/tutorials/tutorial-event-display.md | 416 +++++++++++++++++++++++ 3 files changed, 416 insertions(+) create mode 100644 docs/images/event-display-final.png create mode 100644 docs/images/event-display-selection.png create mode 100644 docs/tutorials/tutorial-event-display.md diff --git a/docs/images/event-display-final.png b/docs/images/event-display-final.png new file mode 100644 index 0000000000000000000000000000000000000000..d492e15b1057e10c99ac39d92aed37bf716902ab GIT binary patch literal 23507 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 literal 0 HcmV?d00001 diff --git a/docs/images/event-display-selection.png b/docs/images/event-display-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..cb50a83e73f5a324a46fc5c9b4fcf3ad34205798 GIT binary patch literal 29623 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 literal 0 HcmV?d00001 diff --git a/docs/tutorials/tutorial-event-display.md b/docs/tutorials/tutorial-event-display.md new file mode 100644 index 00000000..e257178d --- /dev/null +++ b/docs/tutorials/tutorial-event-display.md @@ -0,0 +1,416 @@ +# Event Display Tutorial + +In this tutorial, we will explore properties of Visions and build a simple front-end application. You may find a complete project [here](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo). + +__NOTE:__ You will need Kotlin Multiplatform 1.9.0 or higher to complete this tutorial! + +### Starting the Project + +We will use Idea's default project template for Kotlin Multiplatform. To initialize the project, go to *File -> New -> Project...*, Then choose *Full-Stack Web Application* project template and +*Kotlin Gradle* build system. Then select *Next -> Finish*. You will end up with a project with some sample code. + +To check that everything is working correctly, run *application -> run* Gradle target. You should see a greeting page when you open `http://localhost:8080` in a web browser. + +We will use Kotlin React as our main UI library and Ktor Netty both as a web server. Our event display frontend and server will reside in `jsMain` and `jvmMain` directories respectively. +Before we start, we have to load necessary dependencies: + +* Add SciProgCentre maven repo in `build.gradle.kts` file: +```kotlin +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") + + // Add either the line below: + maven("https://repo.kotlin.link") + + // Or this line: + maven("https://maven.sciprog.center/kscience") + +} +``` + +* Add `visionforge-threejs-server` into the list of JS dependencies of your project: +```kotlin +kotlin { + sourceSets { + val jsMain by getting { + dependencies { + implementation("space.kscience:visionforge-threejs-server:0.3.0-dev-14") + } + } + } +} +``` + +Refresh build model in the Idea to make sure the dependencies are successfully resolved. + +__NOTE:__ In previous versions of VisionForge, some imports may be broken. If these dependencies fail to resolve, replace `space.kscience:visionforge-threejs-server:0.3.0-dev-14` with `space.kscience:visionforge-threejs:0.3.0-dev-14`. The resulting bundle will lack a React component used in the tutorial (see "Managing Visions"). You may copy and paste it directly from either [VisionForge](https://git.sciprog.center/kscience/visionforge/src/branch/master/ui/react/src/main/kotlin/space/kscience/visionforge/react/ThreeCanvasComponent.kt) or the [tutorial repo](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo/src/branch/main/src/jsMain/kotlin/canvas/ThreeCanvasComponent.kt), or even come up with a better implementation if your own. + +### Setting up Page Markup + +We need to create a page layout and set up Netty to serve our page to clients. There is nothing special related to VisionForge, so feel free to copy and paste the code below. + +File: `src/jvmMain/.../Server.kt` +```kotlin +// ... imports go here + +fun HTML.index() { + head { + // Compatibility headers + meta { charset = "UTF-8" } + meta { + name = "viewport" + content = "width=device-width, initial-scale=1.0" + } + meta { + httpEquiv = "X-UA-Compatible" + content = "IE=edge" + } + title("VF Demo") + } + // Link to our react script + body { + script(src = "/static/vf-demo.js") {} + } +} + +fun main() { + // Seting up Netty + embeddedServer(Netty, port = 8080, host = "127.0.0.1") { + routing { + get("/") { + call.respondHtml(HttpStatusCode.OK, HTML::index) + } + static("/static") { + resources() + } + } + }.start(wait = true) +} +``` + +File: `src/jsMain/.../Client.kt` +```kotlin + +fun main() { + val container = document.createElement("div") + document.body!!.appendChild(container) + + val eventDisplay = EventDisplay.create {} + createRoot(container).render(eventDisplay) +} +``` + +File: `src/jsMain/.../Display.kt` +```kotlin +// All markup goes here: +val EventDisplay = FC { + // Global CSS rules + Global { + styles { + "html,\n" + + "body" { + height = 100.vh + width = 100.vw + margin = 0.px + } + "body > div" { + height = 100.vh + width = 100.vw + display = Display.flex + flexDirection = FlexDirection.column + justifyContent = JustifyContent.start + alignItems = AlignItems.center + } + "*,\n" + + "*:before,\n" + + "*:after" { + boxSizing = BoxSizing.borderBox + } + } + } + + div { + css { + height = 100.pct + width = 100.pct + display = Display.flex + flexDirection = FlexDirection.column + alignItems = AlignItems.center + } + div { + css { + width = 100.pct + display = Display.flex + flexDirection = FlexDirection.row + alignItems = AlignItems.center + justifyContent = JustifyContent.center + + } + input { + css { + margin = 5.px + padding = 5.px + } + type = InputType.button + value = "Update Events" + } + input { + css { + margin = 5.px + padding = 5.px + } + type = InputType.button + value = "Update Geometry" + } + } + div { + css { + width = 98.pct + height = 1.pct + margin = 5.px + display = Display.flex + flexGrow = number(1.0) + justifyContent = JustifyContent.center + alignItems = AlignItems.center + backgroundColor = Color("#b3b3b3") + } + + } + } +} +``` + +After setting everything up, you should see a gray rectangle with two buttons above it when opening `localhost:8080`. + +### Managing Visions + +We are approaching the main part of the tutorial - the place where we will create a working demo. In particle accelerator experiments, event displays are employed to visualise particle collision events. Essentially, it requires drawing a detector setup and visual interpretation of events: tracks, detector hits etc. Usually, a number of events share a common detector setup (e.g. if these events occured in a single experiment run). It makes sense to update and re-render only event information, while keeping detector geometry constant between updates. + +Visions (namely, the `SolidGroup` class) allow us to create an object tree for our displayed event. `SolidGroup` can hold other Visions as its child nodes, access these nodes by names and update/delete them. We will use this property to update our event display efficiently. + +To display Visions as actual 3D object, we will use `ThreePlugin` that renders Visions using *three.js* library. The plugin allows us to create a Three.js representation of a vision that will observe changes of its correspondent Vision. This way we can update only Visions without diving deep into three.js stuff. Using observable Visions is also efficient: Three.js representations are not generated from scratch after each Vision update but are modified too. + +First, let's simulate data load operations: +* Add state variables to our `EventDisplay` React component. These variables will be treated as data loaded from a remote server. In real life, these may be JSON string with event data: +```kotlin +val EventDisplay = FC { + // ... + + var eventData: kotlin.Float? by useState(null) + var geometryData: kotlin.Float? by useState(null) + + // ... +} +``` +* Write two simple functions that will convert data to a Vision. In this case, we will simply parameters of solids like color of size; in real life, these functions will usually take raw data and convert it into Visions. +```kotlin +fun generateEvents(radius: Float): SolidGroup { + val count = Random.nextInt(10, 20) + return SolidGroup { + repeat(count) { + sphere(radius) { + x = 5.0 * (Random.nextFloat() - 0.5) + y = 2.0 * (Random.nextFloat() - 0.5) + z = 2.0 * (Random.nextFloat() - 0.5) + color(Colors.red) + } + } + } +} + +fun generateGeometry(distance: Float): SolidGroup { + return SolidGroup { + box(10, 3, 3) { + x = 0.0 + y = -distance + z = 0.0 + color(Colors.gray) + } + box(10, 3, 3) { + x = 0.0 + y = distance + z = 0.0 + color(Colors.gray) + } + } +} +``` +* Then, let's create our main Vision and add a static light source: +```kotlin +val EventDisplay = FC { + // ... + + val containedVision: SolidGroup by useState(SolidGroup { + ambientLight { + color(Colors.white) + } + }) + + // ... +} +``` + +* A `Context` object is required to hold plugins like `ThreePlugin`. It is also necessary to make Visions observable: we have to root our main Vision in the context. Declare a global `Context` in the same file with `EventDisplay` component: +```kotlin +val viewContext = Context { + plugin(Solids) + plugin(ThreePlugin) +} + +``` + +* Import `ThreeCanvasComponent` from VisionForge. This is a React component that handles all display work. It creates three.js canvas, attaches it to its own parent element and creates and draws `Object3D` on the canvas. We will attach this component to a +separate React component. Note order for Visions to update their Three.js representations, these Visions need to be rooted in a `Context`. This way Visions will be observed for changes, and any such change will trigger an update of the corresponding Three.js object. +```kotlin +external interface EventViewProps: Props { + var displayedVision: Solid? + var context: Context +} + +val EventView = FC { props -> + ThreeCanvasComponent { + solid = props.displayedVision + context = props.context + } + // Make displayedVision observed: + useEffect(props.displayedVision) { + props.displayedVision?.setAsRoot(props.context.visionManager) + } +} +``` + +__NOTE:__ If you had problems with dependency resolution, `ThreeCanvasComponent` may missing from your import scope. You may find a compatible implementation [here](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo/src/branch/main/src/jsMain/kotlin/canvas/ThreeCanvasComponent.kt). + +* Finally, we need to attach EventView to our main component and connect raw data updates to Vision updates using React hooks: +```kotlin +// ... + +// Names used as keys to access and update Visions +// Refer to DataForge documentation for more details +val EVENTS_NAME = "DEMO_EVENTS".parseAsName(false) +val GEOMETRY_NAME = "DEMO_GEOMETRY".parseAsName(false) + +// ... + +val EventDisplay = FC { + // ... + + useEffect(eventData) { + eventData?.let { + containedVision.setChild(EVENTS_NAME, generateEvents(it)) + } + } + useEffect(geometryData) { + geometryData?.let { + containedVision.setChild(GEOMETRY_NAME, generateGeometry(it)) + } + } + // ... + + div { + // ... + + div { + css { + width = 98.pct + height = 1.pct + flexGrow = number(1.0) + margin = 5.px + display = Display.flex + justifyContent = JustifyContent.center + alignItems = AlignItems.center + } + // Replace the gray rectangle with an EventView: + EventView { + displayedVision = containedVision + context = viewContext + } + } + } +// ... +} +``` + +When we press either of the buttons, corresponding raw data changes. This update triggers `UseEffect` hook, which generates new event or geometry data and replaces the old data in the main Vision. Three.js representation is then updated to match our new Vision, so that changes are visible on the canvas. + +Recompile the project and go on `http://localhost:8080`. See how the displayed scene changes with each click: for example, when you update geometry, only the distance between "magnets" varies, but spheres remain intact. + +### Clearing the Scene + +We can erase children Visions from the scene completely. To do so, we cat pass `null` to the function `setChild` as `child` argument. Add these lines to the hooks that update Visions to remove the corresponding Vision from our diplayed `SolidGroup` when raw data changes to `null`: +```kotlin +useEffect(eventData) { + // ... + + if (eventData == null) { + containedVision.setChild(EVENT_NAME, null) + } +} +useEffect(geometryData) { + // ... + + if (geometryData == null) { + containedVision.setChild(GEOMETRY_NAME, null) + } +} +``` +To test how this works, let's create an erase button that will completely clear the scene: +```kotlin +val EventDisplay = FC { + // ... + + div { + // ... + + input { + css { + margin = 5.px + padding = 5.px + backgroundColor = NamedColor.lightcoral + color = NamedColor.white + } + type = InputType.button + value = "Clear Scene" + onClick = { + geometryData = null + eventData = null + } + } + } + + // ... +} +``` +![Picture of an event display with a red button with a caption "Clear Scene" added to the two previous buttons](../images/event-display-final.png "Scene clearing function") + +### Making Selection Fine-Grained + +You may feel annoyed by how selection works in our demo. That's right, selecting the whole detector or the entire event array is not that useful. This is due to the fact that VisionForge selects object based on names. We used names to distinguish SolidGroups, but in fact not only groups but every single Vision can have a name. But when we were randomly generating Vision, we did not use any names, did we? Right, Vision can be nameless, in which case they are treated as a monolithic object together with their parent. So it should be clear now that when we were selecting a single rectangle, we were in fact selecting the whole pair of rectangles and the other one went lit up as well. + +Fortunately, every `Solid` constructor takes a `name` parameter after essential parameters, so it should be easy to fix it. Go to the generator functions and add the change construction invocations to the following: +```kotlin +fun generateGeometry(distance: Float): SolidGroup { + return SolidGroup { + box(10, 3, 3, "Magnet1") { + // ... + } + box(10, 3, 3, "Magnet2") { + // ... + } + } +} +``` +For the events part, we will use the index in a loop as a name: +```kotlin +fun generateEvents(radius: Float): SolidGroup { + // ... + repeat(count) { + sphere(radius, it.toString()) { + // ... +} +``` + +After you update the build, it should be possible to select only one sphere or rectangle: + +![Picture of an event display with only one sphere between the magnets selected](../images/event-display-selection.png "Selection demonstration") \ No newline at end of file From e36e4abb7f6fa2e6d08b47be3d5da5ff08d1e3f3 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Dec 2023 09:59:43 +0300 Subject: [PATCH 4/6] Advanced backwards events --- build.gradle.kts | 2 +- visionforge-core/api/visionforge-core.api | 50 ++++++++++-- .../kscience/visionforge/ControlVision.kt | 23 ++++++ .../kscience/visionforge/VisionClient.kt | 6 ++ .../kscience/visionforge/VisionManager.kt | 1 + .../kscience/visionforge/VisionProperties.kt | 6 +- .../kscience/visionforge/html/VisionOfHtml.kt | 18 +++-- .../visionforge/meta/VisionPropertyTest.kt | 4 +- .../kscience/visionforge/inputRenderers.kt | 80 +++++++------------ .../visionforge/solid/SolidPropertyTest.kt | 1 + 10 files changed, 122 insertions(+), 69 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 57e42d2b..5e015498 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ val dataforgeVersion by extra("0.7.1") allprojects { group = "space.kscience" - version = "0.3.0-RC" + version = "0.3.0" } subprojects { diff --git a/visionforge-core/api/visionforge-core.api b/visionforge-core/api/visionforge-core.api index 0cd2b904..89e926ec 100644 --- a/visionforge-core/api/visionforge-core.api +++ b/visionforge-core/api/visionforge-core.api @@ -225,6 +225,8 @@ public abstract interface class space/kscience/visionforge/ControlVision : space public final class space/kscience/visionforge/ControlVisionKt { public static final fun VisionClickEvent (Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/visionforge/VisionClickEvent; public static synthetic fun VisionClickEvent$default (Lspace/kscience/dataforge/meta/Meta;Lspace/kscience/dataforge/names/Name;ILjava/lang/Object;)Lspace/kscience/visionforge/VisionClickEvent; + public static final fun VisionInputEvent (Lspace/kscience/dataforge/meta/Value;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/visionforge/VisionInputEvent; + public static synthetic fun VisionInputEvent$default (Lspace/kscience/dataforge/meta/Value;Lspace/kscience/dataforge/names/Name;ILjava/lang/Object;)Lspace/kscience/visionforge/VisionInputEvent; public static final fun VisionValueChangeEvent (Lspace/kscience/dataforge/meta/Value;Lspace/kscience/dataforge/names/Name;)Lspace/kscience/visionforge/VisionValueChangeEvent; public static synthetic fun VisionValueChangeEvent$default (Lspace/kscience/dataforge/meta/Value;Lspace/kscience/dataforge/names/Name;ILjava/lang/Object;)Lspace/kscience/visionforge/VisionValueChangeEvent; public static final fun onClick (Lspace/kscience/visionforge/ClickControl;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; @@ -495,6 +497,7 @@ public final class space/kscience/visionforge/VisionClientKt { public static final fun notifyPropertyChanged (Lspace/kscience/visionforge/VisionClient;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Ljava/lang/String;)V public static final fun notifyPropertyChanged (Lspace/kscience/visionforge/VisionClient;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Lspace/kscience/dataforge/meta/Meta;)V public static final fun notifyPropertyChanged (Lspace/kscience/visionforge/VisionClient;Lspace/kscience/dataforge/names/Name;Ljava/lang/String;Z)V + public static final fun sendEventAsync (Lspace/kscience/visionforge/VisionClient;Lspace/kscience/dataforge/names/Name;Lspace/kscience/visionforge/VisionEvent;)Lkotlinx/coroutines/Job; } public abstract interface class space/kscience/visionforge/VisionContainer { @@ -560,6 +563,30 @@ public final class space/kscience/visionforge/VisionGroupKt { public static synthetic fun group$default (Lspace/kscience/visionforge/MutableVisionContainer;Lspace/kscience/dataforge/names/Name;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/visionforge/SimpleVisionGroup; } +public final class space/kscience/visionforge/VisionInputEvent : space/kscience/visionforge/VisionControlEvent { + public static final field Companion Lspace/kscience/visionforge/VisionInputEvent$Companion; + public fun (Lspace/kscience/dataforge/meta/Meta;)V + public fun getMeta ()Lspace/kscience/dataforge/meta/Meta; + public final fun getName ()Lspace/kscience/dataforge/names/Name; + public final fun getValue ()Lspace/kscience/dataforge/meta/Value; + public fun toString ()Ljava/lang/String; +} + +public final class space/kscience/visionforge/VisionInputEvent$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lspace/kscience/visionforge/VisionInputEvent$$serializer; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lspace/kscience/visionforge/VisionInputEvent; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lspace/kscience/visionforge/VisionInputEvent;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class space/kscience/visionforge/VisionInputEvent$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class space/kscience/visionforge/VisionKt { public static final fun getVisible (Lspace/kscience/visionforge/Vision;)Ljava/lang/Boolean; public static final fun onPropertyChange (Lspace/kscience/visionforge/Vision;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; @@ -649,8 +676,8 @@ public final class space/kscience/visionforge/VisionPropertiesKt { public static final fun get (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;)Lspace/kscience/dataforge/meta/Meta; public static synthetic fun get$default (Lspace/kscience/visionforge/MutableVisionProperties;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lspace/kscience/dataforge/meta/MutableMeta; public static synthetic fun get$default (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lspace/kscience/dataforge/meta/Meta; - public static final fun getValue (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;)Lspace/kscience/dataforge/meta/Value; - public static synthetic fun getValue$default (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Lspace/kscience/dataforge/meta/Value; + public static final fun getValue (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;ZLjava/lang/Boolean;)Lspace/kscience/dataforge/meta/Value; + public static synthetic fun getValue$default (Lspace/kscience/visionforge/VisionProperties;Ljava/lang/String;ZLjava/lang/Boolean;ILjava/lang/Object;)Lspace/kscience/dataforge/meta/Value; public static final fun invoke (Lspace/kscience/visionforge/MutableVisionProperties;Lkotlin/jvm/functions/Function1;)V public static final fun remove (Lspace/kscience/visionforge/MutableVisionProperties;Ljava/lang/String;)V public static final fun remove (Lspace/kscience/visionforge/MutableVisionProperties;Lspace/kscience/dataforge/names/Name;)V @@ -781,8 +808,8 @@ public abstract class space/kscience/visionforge/html/VisionOfHtml : space/kscie public static final field Companion Lspace/kscience/visionforge/html/VisionOfHtml$Companion; public fun ()V public synthetic fun (ILspace/kscience/dataforge/meta/MutableMeta;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public final fun getClasses ()Ljava/util/List; - public final fun setClasses (Ljava/util/List;)V + public final fun getClasses ()Ljava/util/Set; + public final fun setClasses (Ljava/util/Set;)V public static final synthetic fun write$Self (Lspace/kscience/visionforge/html/VisionOfHtml;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V } @@ -813,9 +840,16 @@ public final class space/kscience/visionforge/html/VisionOfHtmlButton$Companion } public abstract class space/kscience/visionforge/html/VisionOfHtmlControl : space/kscience/visionforge/html/VisionOfHtml, space/kscience/visionforge/ControlVision { + public static final field Companion Lspace/kscience/visionforge/html/VisionOfHtmlControl$Companion; public fun ()V + public synthetic fun (ILspace/kscience/dataforge/meta/MutableMeta;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V public fun dispatchControlEvent (Lspace/kscience/visionforge/VisionControlEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getControlEventFlow ()Lkotlinx/coroutines/flow/SharedFlow; + public static final synthetic fun write$Self (Lspace/kscience/visionforge/html/VisionOfHtmlControl;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public final class space/kscience/visionforge/html/VisionOfHtmlControl$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class space/kscience/visionforge/html/VisionOfHtmlForm : space/kscience/visionforge/html/VisionOfHtmlControl { @@ -849,11 +883,9 @@ public final class space/kscience/visionforge/html/VisionOfHtmlFormKt { public class space/kscience/visionforge/html/VisionOfHtmlInput : space/kscience/visionforge/html/VisionOfHtmlControl { public static final field Companion Lspace/kscience/visionforge/html/VisionOfHtmlInput$Companion; - public synthetic fun (ILjava/lang/String;Lspace/kscience/visionforge/html/InputFeedbackMode;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (Ljava/lang/String;Lspace/kscience/visionforge/html/InputFeedbackMode;)V - public synthetic fun (Ljava/lang/String;Lspace/kscience/visionforge/html/InputFeedbackMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILspace/kscience/dataforge/meta/MutableMeta;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Ljava/lang/String;)V public final fun getDisabled ()Z - public final fun getFeedbackMode ()Lspace/kscience/visionforge/html/InputFeedbackMode; public final fun getFieldName ()Ljava/lang/String; public final fun getInputType ()Ljava/lang/String; public final fun getValue ()Lspace/kscience/dataforge/meta/Value; @@ -891,6 +923,8 @@ public final class space/kscience/visionforge/html/VisionOfHtmlKt { public static synthetic fun htmlRangeField$default (Lspace/kscience/visionforge/html/VisionOutput;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionOfRangeField; public static final fun htmlTextField (Lspace/kscience/visionforge/html/VisionOutput;Lkotlin/jvm/functions/Function1;)Lspace/kscience/visionforge/html/VisionOfTextField; public static synthetic fun htmlTextField$default (Lspace/kscience/visionforge/html/VisionOutput;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionOfTextField; + public static final fun onInput (Lspace/kscience/visionforge/html/VisionOfHtmlInput;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; + public static synthetic fun onInput$default (Lspace/kscience/visionforge/html/VisionOfHtmlInput;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; public static final fun onValueChange (Lspace/kscience/visionforge/html/VisionOfHtmlInput;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; public static synthetic fun onValueChange$default (Lspace/kscience/visionforge/html/VisionOfHtmlInput;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; } 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 dc27662f..627a2aa1 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -83,9 +83,32 @@ public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEven override fun toString(): String = meta.toString() } + public fun VisionValueChangeEvent(value: Value?, name: Name? = null): VisionValueChangeEvent = VisionValueChangeEvent( Meta { this.value = value name?.let { set("name", it.toString()) } } ) + + +@Serializable +@SerialName("control.input") +public class VisionInputEvent(override val meta: Meta) : VisionControlEvent() { + + public val value: Value? get() = meta.value + + /** + * The name of a control that fired the event + */ + public val name: Name? get() = meta["name"]?.string?.parseAsName() + + override fun toString(): String = meta.toString() +} + +public fun VisionInputEvent(value: Value?, name: Name? = null): VisionInputEvent = VisionInputEvent( + Meta { + this.value = value + name?.let { set("name", it.toString()) } + } +) 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 7a62de7d..2d77dfd0 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -1,5 +1,7 @@ package space.kscience.visionforge +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import space.kscience.dataforge.context.Plugin import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.Name @@ -16,6 +18,10 @@ public interface VisionClient: Plugin { public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) } +public fun VisionClient.sendEventAsync(targetName: Name, event: VisionEvent): Job = context.launch { + sendEvent(targetName, event) +} + public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) { notifyPropertyChanged(visionName, propertyName.parseAsName(true), item) } 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 9b5a21ac..84ed8286 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt @@ -85,6 +85,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont subclass(VisionMetaEvent.serializer()) subclass(VisionClickEvent.serializer()) subclass(VisionValueChangeEvent.serializer()) + subclass(VisionInputEvent.serializer()) } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt index 3c375c62..b7a66625 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionProperties.kt @@ -265,14 +265,14 @@ public abstract class AbstractVisionProperties( public fun VisionProperties.getValue( name: String, - inherit: Boolean? = null, + inherit: Boolean, includeStyles: Boolean? = null, ): Value? = getValue(name.parseAsName(), inherit, includeStyles) /** * Get [Vision] property using key as a String */ -public fun VisionProperties.get( +public operator fun VisionProperties.get( name: String, inherit: Boolean? = null, includeStyles: Boolean? = null, @@ -292,7 +292,7 @@ public fun MutableVisionProperties.root( /** * Get [Vision] property using key as a String */ -public fun MutableVisionProperties.get( +public operator fun MutableVisionProperties.get( name: String, inherit: Boolean? = null, includeStyles: Boolean? = null, 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 0738ddc6..2bd7b9de 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 @@ -12,15 +12,16 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.asName -import space.kscience.visionforge.AbstractVision -import space.kscience.visionforge.ControlVision -import space.kscience.visionforge.VisionControlEvent -import space.kscience.visionforge.VisionValueChangeEvent +import space.kscience.visionforge.* @Serializable public abstract class VisionOfHtml : AbstractVision() { - public var classes: List by properties.stringList(*emptyArray()) + public var classes: Set + get() = properties.get(::classes.name,false).stringList?.toSet() ?: emptySet() + set(value) { + properties[::classes.name] = value.map { it.asValue() } + } } @Serializable @@ -58,6 +59,7 @@ public enum class InputFeedbackMode { NONE } +@Serializable public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{ @Transient @@ -76,7 +78,6 @@ public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{ @SerialName("html.input") public open class VisionOfHtmlInput( public val inputType: String, - public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE, ) : VisionOfHtmlControl() { public var value: Value? by properties.value() public var disabled: Boolean by properties.boolean { false } @@ -92,6 +93,11 @@ public fun VisionOfHtmlInput.onValueChange( callback: suspend VisionValueChangeEvent.() -> Unit, ): Job = controlEventFlow.filterIsInstance().onEach(callback).launchIn(scope) +public fun VisionOfHtmlInput.onInput( + scope: CoroutineScope = manager?.context ?: error("Coroutine context is not resolved for $this"), + callback: suspend VisionInputEvent.() -> Unit, +): Job = controlEventFlow.filterIsInstance().onEach(callback).launchIn(scope) + @Suppress("UnusedReceiverParameter") public inline fun VisionOutput.htmlInput( inputType: String, 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 b1ca970a..e8ebe406 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.get("fff.ddd").apply { + vision.properties["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.get("fff").updateWith(TestScheme) { + vision.properties["fff"].updateWith(TestScheme) { ddd = 2 } assertEquals(2, vision.properties.getValue("fff.ddd")?.int) 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 4112370a..3deb00da 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -1,18 +1,14 @@ package space.kscience.visionforge -import kotlinx.coroutines.launch import kotlinx.dom.clear import kotlinx.html.InputType import kotlinx.html.div import kotlinx.html.js.input import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLInputElement -import org.w3c.dom.events.Event -import space.kscience.dataforge.meta.Value import space.kscience.dataforge.meta.asValue import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.string -import space.kscience.dataforge.names.Name import space.kscience.visionforge.html.* /** @@ -26,13 +22,6 @@ internal fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { } } - -private fun VisionClient.sendInputEvent(name: Name, value: Value?) { - context.launch { - sendEvent(name, VisionValueChangeEvent(value, name)) - } -} - /** * Subscribes the HTML input element to a given vision. * @@ -62,16 +51,13 @@ internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer< input { type = InputType.text }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { - client.sendInputEvent(name, htmlInputElement.value.asValue()) + + htmlInputElement.onchange = { + client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name)) } - - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} + htmlInputElement.oninput = { + client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name)) } htmlInputElement.subscribeToInput(vision) @@ -86,18 +72,16 @@ internal val checkboxVisionRenderer: ElementVisionRenderer = input { type = InputType.checkBox }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { - client.sendInputEvent(name, htmlInputElement.checked.asValue()) + + htmlInputElement.onchange = { + client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name)) } - - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} + htmlInputElement.oninput = { + client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name)) } + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfCheckbox::checked) { htmlInputElement.checked = it ?: false @@ -110,16 +94,13 @@ internal val textVisionRenderer: ElementVisionRenderer = input { type = InputType.text }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { - client.sendInputEvent(name, htmlInputElement.value.asValue()) + + htmlInputElement.onchange = { + client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name)) } - - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} + htmlInputElement.oninput = { + client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name)) } htmlInputElement.subscribeToInput(vision) @@ -135,18 +116,19 @@ internal val numberVisionRenderer: ElementVisionRenderer = type = InputType.number }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { + htmlInputElement.onchange = { htmlInputElement.value.toDoubleOrNull()?.let { - client.sendInputEvent(name, htmlInputElement.value.asValue()) + client.sendEventAsync(name, VisionValueChangeEvent(it.asValue(), name)) } } - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} + htmlInputElement.oninput = { + htmlInputElement.value.toDoubleOrNull()?.let { + client.sendEventAsync(name, VisionInputEvent(it.asValue(), name)) + } } + + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfNumberField::value) { htmlInputElement.valueAsNumber = it?.double ?: 0.0 @@ -163,18 +145,18 @@ internal val rangeVisionRenderer: ElementVisionRenderer = step = vision.step.toString() }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { + htmlInputElement.onchange = { htmlInputElement.value.toDoubleOrNull()?.let { - client.sendInputEvent(name, htmlInputElement.value.asValue()) + client.sendEventAsync(name, VisionValueChangeEvent(it.asValue(), name)) } } - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} + htmlInputElement.oninput = { + htmlInputElement.value.toDoubleOrNull()?.let { + client.sendEventAsync(name, VisionInputEvent(it.asValue(), name)) + } } + htmlInputElement.subscribeToInput(vision) vision.useProperty(VisionOfRangeField::value) { htmlInputElement.valueAsNumber = it?.double ?: 0.0 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 4991c12d..7b559ab7 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 @@ -4,6 +4,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest +import space.kscience.dataforge.meta.getValue import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.set import space.kscience.dataforge.meta.string From 2e2524450d910c65cb763caa50d510626c372f28 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 21 Dec 2023 09:28:45 +0300 Subject: [PATCH 5/6] update form builders --- .../src/jvmMain/kotlin/formServer.kt | 6 ++--- .../kscience/visionforge/ControlVision.kt | 3 ++- .../visionforge/html/VisionOfHtmlForm.kt | 26 ++++++++++++------- .../kscience/visionforge/formRenderers.kt | 12 ++++++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt index 21d2d4a7..9a1dc0a2 100644 --- a/demo/playground/src/jvmMain/kotlin/formServer.kt +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -10,7 +10,7 @@ import space.kscience.dataforge.context.request import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.VisionOfHtmlForm import space.kscience.visionforge.html.VisionPage -import space.kscience.visionforge.html.bindForm +import space.kscience.visionforge.html.bindToVision import space.kscience.visionforge.onPropertyChange import space.kscience.visionforge.server.close import space.kscience.visionforge.server.openInBrowser @@ -36,7 +36,7 @@ fun main() { visionManager, VisionPage.scriptHeader("js/visionforge-playground.js"), ) { - bindForm(form) { + form { label { htmlFor = "fname" +"First name:" @@ -66,9 +66,9 @@ fun main() { type = InputType.submit value = "Submit" } + vision(bindToVision(form)) } println(form.values) - vision(form) } }.start(false) 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 627a2aa1..546869bb 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -39,7 +39,8 @@ public interface ControlVision : Vision { @Serializable @SerialName("control.click") public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() { - public val payload: Meta? by meta.node() + public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY + public val name: Name? get() = meta["name"].string?.parseAsName() override fun toString(): String = meta.toString() 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 c128ec6f..950a55aa 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 @@ -1,8 +1,8 @@ package space.kscience.visionforge.html +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.html.FORM -import kotlinx.html.TagConsumer -import kotlinx.html.form import kotlinx.html.id import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -10,6 +10,7 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.node import space.kscience.dataforge.meta.string import space.kscience.visionforge.ClickControl +import space.kscience.visionforge.onClick /** * @param formId an id of the element in rendered DOM, this form is bound to @@ -18,18 +19,25 @@ import space.kscience.visionforge.ClickControl @SerialName("html.form") public class VisionOfHtmlForm( public val formId: String, -) : VisionOfHtmlControl() { +) : VisionOfHtmlControl(), ClickControl { public var values: Meta? by properties.node() } -public fun TagConsumer.bindForm( - visionOfForm: VisionOfHtmlForm, - builder: FORM.() -> Unit, -): R = form { - this.id = visionOfForm.formId - builder() +/** + * Create a [VisionOfHtmlForm] and bind this form to the id + */ +public fun FORM.bindToVision(id: String): VisionOfHtmlForm { + this.id = id + return VisionOfHtmlForm(id) } +public fun FORM.bindToVision(vision: VisionOfHtmlForm): VisionOfHtmlForm { + this.id = vision.formId + return vision +} + +public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) } + @Serializable @SerialName("html.button") diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt index 6a360af8..781fa677 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt @@ -15,8 +15,10 @@ import space.kscience.dataforge.names.Name import space.kscience.visionforge.html.VisionOfHtmlButton import space.kscience.visionforge.html.VisionOfHtmlForm - -internal fun FormData.toMeta(): Meta { +/** + * Convert form data to Meta + */ +public fun FormData.toMeta(): Meta { @Suppress("UNUSED_VARIABLE") val formData = this //val res = js("Object.fromEntries(formData);") val `object` = js("{}") @@ -67,8 +69,10 @@ internal val formVisionRenderer: ElementVisionRenderer = form.onsubmit = { event -> event.preventDefault() val formData = FormData(form).toMeta() - client.sendMetaEvent(name, formData) - console.info("Sent: ${formData.toMap()}") + client.context.launch { + client.sendEvent(name, VisionClickEvent(name = name, payload = formData)) + } + console.info("Sent form data: ${formData.toMap()}") false } } From 4fd5c634bb7984d81da49af7638827c733ce72c7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 24 Dec 2023 19:45:05 +0300 Subject: [PATCH 6/6] Fix vision sharing between pages --- CHANGELOG.md | 42 ++++++++----- build.gradle.kts | 2 +- .../src/jsMain/kotlin/JsPlaygroundApp.kt | 3 +- .../src/jsMain/kotlin/gravityDemo.kt | 5 +- .../src/jsMain/kotlin/markupComponent.kt | 2 +- .../src/jvmMain/kotlin/formServer.kt | 6 +- demo/sat-demo/build.gradle.kts | 4 +- visionforge-core/api/visionforge-core.api | 29 +++++++-- .../visionforge/html/HtmlVisionRenderer.kt | 34 +++++----- .../visionforge/html/VisionOfHtmlForm.kt | 22 ++++--- .../src/jvmMain/kotlin/VisionForge.kt | 8 +-- .../visionforge/server/VisionServer.kt | 62 ++++++++++++------- 12 files changed, 135 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2c3478..dc833937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,31 @@ # Changelog -## [Unreleased] +## Unreleased + ### Added + +### Changed +- **Breaking API** Move vision cache to upper level for renderers to avoid re-creating visions for page reload. +- **Breaking API** Forms refactor + +### Deprecated + +### Removed + +### Fixed + +### Security + +## 0.3.0 - 2023-12-23 + +### Added + - Context receivers flag - MeshLine for thick lines - Custom client-side events and thier processing in VisionServer ### Changed + - 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 @@ -17,17 +36,14 @@ - Naming of Canvas3D options. - Lights are added to the scene instead of 3D options. -### Deprecated - -### Removed - ### Fixed + - Jupyter integration for IDEA and Jupyter lab. -### Security +## 0.2.0 -## [0.2.0] ### Added + - Server module - Change collector - Customizable accessors for colors @@ -38,8 +54,8 @@ - Markdown module - Tables module - ### Changed + - Vision does not implement ItemProvider anymore. Property changes are done via `getProperty`/`setProperty` and `property` delegate. - Point3D and Point2D are made separate classes instead of expect/actual (to split up different engines. - JavaFX support moved to a separate module @@ -54,16 +70,10 @@ - Property listeners are not triggered if there are no changes. - Feedback websocket connection in the client. - -### Deprecated - ### Removed + - Primary modules dependencies on UI - ### Fixed + - Version conflicts - - -### Security - diff --git a/build.gradle.kts b/build.gradle.kts index 5e015498..f35e8fe3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ val dataforgeVersion by extra("0.7.1") allprojects { group = "space.kscience" - version = "0.3.0" + version = "0.3.1" } subprojects { diff --git a/demo/js-playground/src/jsMain/kotlin/JsPlaygroundApp.kt b/demo/js-playground/src/jsMain/kotlin/JsPlaygroundApp.kt index dd0d0b26..c0ee12c7 100644 --- a/demo/js-playground/src/jsMain/kotlin/JsPlaygroundApp.kt +++ b/demo/js-playground/src/jsMain/kotlin/JsPlaygroundApp.kt @@ -4,6 +4,7 @@ import ringui.SmartTabs import ringui.Tab import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request +import space.kscience.plotly.Plotly.plot import space.kscience.plotly.models.Trace import space.kscience.plotly.scatter import space.kscience.visionforge.Application @@ -97,7 +98,7 @@ private class JsPlaygroundApp : Application { Tab("plotly") { Plotly { attrs { - plot = space.kscience.plotly.Plotly.plot { + plot = plot { scatter { x(1, 2, 3) y(5, 8, 7) diff --git a/demo/js-playground/src/jsMain/kotlin/gravityDemo.kt b/demo/js-playground/src/jsMain/kotlin/gravityDemo.kt index a4bc9057..494874a6 100644 --- a/demo/js-playground/src/jsMain/kotlin/gravityDemo.kt +++ b/demo/js-playground/src/jsMain/kotlin/gravityDemo.kt @@ -11,6 +11,7 @@ import space.kscience.visionforge.markup.VisionOfMarkup import space.kscience.visionforge.react.flexRow import space.kscience.visionforge.ring.ThreeCanvasWithControls import space.kscience.visionforge.ring.solid +import space.kscience.visionforge.setAsRoot import space.kscience.visionforge.solid.* import styled.css import styled.styledDiv @@ -27,7 +28,9 @@ val GravityDemo = fc { props -> val energyTrace = Trace { name = "energy" } - val markup = VisionOfMarkup() + val markup = VisionOfMarkup().apply { + setAsRoot(props.solids.visionManager) + } styledDiv { css { diff --git a/demo/js-playground/src/jsMain/kotlin/markupComponent.kt b/demo/js-playground/src/jsMain/kotlin/markupComponent.kt index a47c28cf..0e2c3c9e 100644 --- a/demo/js-playground/src/jsMain/kotlin/markupComponent.kt +++ b/demo/js-playground/src/jsMain/kotlin/markupComponent.kt @@ -44,7 +44,7 @@ val Markup = fc("Markup") { props -> css { width = 100.pct height = 100.pct - border= Border(2.pt, BorderStyle.solid, Color.blue) + border = Border(2.pt, BorderStyle.solid, Color.blue) padding = Padding(left = 8.pt) backgroundColor = Color.white flex = Flex(1.0) diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt index 9a1dc0a2..c0539d0a 100644 --- a/demo/playground/src/jvmMain/kotlin/formServer.kt +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -10,7 +10,7 @@ import space.kscience.dataforge.context.request import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.VisionOfHtmlForm import space.kscience.visionforge.html.VisionPage -import space.kscience.visionforge.html.bindToVision +import space.kscience.visionforge.html.visionOfForm import space.kscience.visionforge.onPropertyChange import space.kscience.visionforge.server.close import space.kscience.visionforge.server.openInBrowser @@ -36,7 +36,7 @@ fun main() { visionManager, VisionPage.scriptHeader("js/visionforge-playground.js"), ) { - form { + visionOfForm(form) { label { htmlFor = "fname" +"First name:" @@ -66,8 +66,8 @@ fun main() { type = InputType.submit value = "Submit" } - vision(bindToVision(form)) } + vision(form) println(form.values) } diff --git a/demo/sat-demo/build.gradle.kts b/demo/sat-demo/build.gradle.kts index 6cf37859..42356e3d 100644 --- a/demo/sat-demo/build.gradle.kts +++ b/demo/sat-demo/build.gradle.kts @@ -9,7 +9,9 @@ kscience { // useSerialization { // json() // } - jvm() + jvm{ + withJava() + } jvmMain{ implementation("io.ktor:ktor-server-cio") implementation(projects.visionforgeThreejs.visionforgeThreejsServer) diff --git a/visionforge-core/api/visionforge-core.api b/visionforge-core/api/visionforge-core.api index 89e926ec..c36083e8 100644 --- a/visionforge-core/api/visionforge-core.api +++ b/visionforge-core/api/visionforge-core.api @@ -750,10 +750,10 @@ public abstract interface class space/kscience/visionforge/html/HtmlVisionFragme public final class space/kscience/visionforge/html/HtmlVisionRendererKt { public static final fun appendTo (Lspace/kscience/visionforge/html/HtmlVisionFragment;Lspace/kscience/visionforge/html/VisionTagConsumer;)V - public static final fun visionFragment (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V - public static final fun visionFragment (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V - public static synthetic fun visionFragment$default (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V - public static synthetic fun visionFragment$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V + public static final fun visionFragment (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V + public static final fun visionFragment (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V + public static synthetic fun visionFragment$default (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V + public static synthetic fun visionFragment$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V } public final class space/kscience/visionforge/html/InputFeedbackMode : java/lang/Enum { @@ -782,6 +782,21 @@ public final class space/kscience/visionforge/html/ResourceLocation : java/lang/ public abstract interface annotation class space/kscience/visionforge/html/VisionDSL : java/lang/annotation/Annotation { } +public final class space/kscience/visionforge/html/VisionDisplay { + public fun (Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;)V + public final fun component1 ()Lspace/kscience/visionforge/VisionManager; + public final fun component2 ()Lspace/kscience/visionforge/Vision; + public final fun component3 ()Lspace/kscience/dataforge/meta/Meta; + public final fun copy (Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/visionforge/html/VisionDisplay; + public static synthetic fun copy$default (Lspace/kscience/visionforge/html/VisionDisplay;Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionDisplay; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeta ()Lspace/kscience/dataforge/meta/Meta; + public final fun getVision ()Lspace/kscience/visionforge/Vision; + public final fun getVisionManager ()Lspace/kscience/visionforge/VisionManager; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class space/kscience/visionforge/html/VisionOfCheckbox : space/kscience/visionforge/html/VisionOfHtmlInput { public static final field Companion Lspace/kscience/visionforge/html/VisionOfCheckbox$Companion; public fun ()V @@ -852,7 +867,7 @@ public final class space/kscience/visionforge/html/VisionOfHtmlControl$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class space/kscience/visionforge/html/VisionOfHtmlForm : space/kscience/visionforge/html/VisionOfHtmlControl { +public final class space/kscience/visionforge/html/VisionOfHtmlForm : space/kscience/visionforge/html/VisionOfHtmlControl, space/kscience/visionforge/ClickControl { public static final field Companion Lspace/kscience/visionforge/html/VisionOfHtmlForm$Companion; public fun (Ljava/lang/String;)V public final fun getFormId ()Ljava/lang/String; @@ -876,9 +891,11 @@ public final class space/kscience/visionforge/html/VisionOfHtmlForm$Companion { } public final class space/kscience/visionforge/html/VisionOfHtmlFormKt { - public static final fun bindForm (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static final fun button (Lspace/kscience/visionforge/html/VisionOutput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lspace/kscience/visionforge/html/VisionOfHtmlButton; public static synthetic fun button$default (Lspace/kscience/visionforge/html/VisionOutput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionOfHtmlButton; + public static final fun onSubmit (Lspace/kscience/visionforge/html/VisionOfHtmlForm;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job; + public static final fun visionOfForm (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Ljava/lang/String;Lkotlinx/html/FormEncType;Lkotlinx/html/FormMethod;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static synthetic fun visionOfForm$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Ljava/lang/String;Lkotlinx/html/FormEncType;Lkotlinx/html/FormMethod;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; } public class space/kscience/visionforge/html/VisionOfHtmlInput : space/kscience/visionforge/html/VisionOfHtmlControl { diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt index d71e848a..801d23aa 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt @@ -8,19 +8,23 @@ import space.kscience.dataforge.names.asName import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager -public fun interface HtmlVisionFragment{ +public fun interface HtmlVisionFragment { public fun VisionTagConsumer<*>.append() } public fun HtmlVisionFragment.appendTo(consumer: VisionTagConsumer<*>): Unit = consumer.append() +public data class VisionDisplay(val visionManager: VisionManager, val vision: Vision, val meta: Meta) + /** * Render a fragment in the given consumer and return a map of extracted visions - * @param context a context used to create a vision fragment + * @param visionManager a context plugin used to create a vision fragment * @param embedData embed Vision initial state in the HTML * @param fetchDataUrl fetch data after first render from given url * @param updatesUrl receive push updates from the server at given url * @param idPrefix a prefix to be used before vision ids + * @param displayCache external cache for Vision displays. It is required to avoid re-creating visions on page update + * @param fragment the fragment to render */ public fun TagConsumer<*>.visionFragment( visionManager: VisionManager, @@ -28,39 +32,31 @@ public fun TagConsumer<*>.visionFragment( fetchDataUrl: String? = null, updatesUrl: String? = null, idPrefix: String? = null, - onVisionRendered: (Name, Vision) -> Unit = { _, _ -> }, + displayCache: MutableMap = mutableMapOf(), fragment: HtmlVisionFragment, ) { - val collector: MutableMap> = mutableMapOf() - val consumer = object : VisionTagConsumer(this@visionFragment, visionManager, idPrefix) { override fun TagConsumer.vision(name: Name?, buildOutput: VisionOutput.() -> Vision): T { //Avoid re-creating cached visions val actualName = name ?: NameToken( DEFAULT_VISION_NAME, - buildOutput.hashCode().toUInt().toString() + buildOutput.hashCode().toString(16) ).asName() - val (output, vision) = collector.getOrPut(actualName) { + val display = displayCache.getOrPut(actualName) { val output = VisionOutput(context, actualName) val vision = output.buildOutput() - onVisionRendered(actualName, vision) - output to vision + VisionDisplay(output.visionManager, vision, output.meta) } - return addVision(actualName, output.visionManager, vision, output.meta) + return addVision(actualName, display.visionManager, display.vision, display.meta) } override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) { - val (_, actualVision) = collector.getOrPut(name) { - val output = VisionOutput(context, name) - onVisionRendered(name, vision) - output to vision - } - + displayCache[name] = VisionDisplay(manager, vision, outputMeta) // Toggle update mode updatesUrl?.let { @@ -76,7 +72,7 @@ public fun TagConsumer<*>.visionFragment( type = "text/json" attributes["class"] = OUTPUT_DATA_CLASS unsafe { - +"\n${manager.encodeToString(actualVision)}\n" + +"\n${manager.encodeToString(vision)}\n" } } } @@ -91,8 +87,8 @@ public fun FlowContent.visionFragment( embedData: Boolean = true, fetchDataUrl: String? = null, updatesUrl: String? = null, - onVisionRendered: (Name, Vision) -> Unit = { _, _ -> }, idPrefix: String? = null, + displayCache: MutableMap = mutableMapOf(), fragment: HtmlVisionFragment, ): Unit = consumer.visionFragment( visionManager = visionManager, @@ -100,6 +96,6 @@ public fun FlowContent.visionFragment( fetchDataUrl = fetchDataUrl, updatesUrl = updatesUrl, idPrefix = idPrefix, - onVisionRendered = onVisionRendered, + displayCache = displayCache, fragment = fragment ) \ No newline at end of file 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 950a55aa..baa7326a 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 @@ -2,8 +2,7 @@ package space.kscience.visionforge.html import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.html.FORM -import kotlinx.html.id +import kotlinx.html.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta @@ -23,18 +22,23 @@ public class VisionOfHtmlForm( public var values: Meta? by properties.node() } + /** * Create a [VisionOfHtmlForm] and bind this form to the id */ -public fun FORM.bindToVision(id: String): VisionOfHtmlForm { - this.id = id - return VisionOfHtmlForm(id) +@HtmlTagMarker +public inline fun > C.visionOfForm( + vision: VisionOfHtmlForm, + action: String? = null, + encType: FormEncType? = null, + method: FormMethod? = null, + classes: String? = null, + crossinline block: FORM.() -> Unit = {}, +) : T = form(action, encType, method, classes){ + this.id = vision.formId + block() } -public fun FORM.bindToVision(vision: VisionOfHtmlForm): VisionOfHtmlForm { - this.id = vision.formId - return vision -} public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) } diff --git a/visionforge-jupyter/src/jvmMain/kotlin/VisionForge.kt b/visionforge-jupyter/src/jvmMain/kotlin/VisionForge.kt index 49d5fe23..8cb2dcd5 100644 --- a/visionforge-jupyter/src/jvmMain/kotlin/VisionForge.kt +++ b/visionforge-jupyter/src/jvmMain/kotlin/VisionForge.kt @@ -17,9 +17,9 @@ import space.kscience.dataforge.context.info import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.Name -import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.HtmlVisionFragment +import space.kscience.visionforge.html.VisionDisplay import space.kscience.visionforge.html.visionFragment import space.kscience.visionforge.server.VisionRoute import space.kscience.visionforge.server.serveVisionData @@ -142,7 +142,7 @@ public class VisionForge( //server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment) val cellRoute = "content-${counter++}" - val collector: MutableMap = mutableMapOf() + val cache: MutableMap = mutableMapOf() val url = engine.environment.connectors.first().let { url { @@ -153,13 +153,13 @@ public class VisionForge( } } - engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), collector) + engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), cache) visionFragment( visionManager, embedData = true, updatesUrl = url, - onVisionRendered = { name, vision -> collector[name] = vision }, + displayCache = cache, fragment = fragment ) } else { diff --git a/visionforge-server/src/jvmMain/kotlin/space/kscience/visionforge/server/VisionServer.kt b/visionforge-server/src/jvmMain/kotlin/space/kscience/visionforge/server/VisionServer.kt index d89bf1c0..bb14b39a 100644 --- a/visionforge-server/src/jvmMain/kotlin/space/kscience/visionforge/server/VisionServer.kt +++ b/visionforge-server/src/jvmMain/kotlin/space/kscience/visionforge/server/VisionServer.kt @@ -1,31 +1,50 @@ package space.kscience.visionforge.server -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.html.* -import io.ktor.server.http.content.* -import io.ktor.server.plugins.* -import io.ktor.server.plugins.cors.routing.* -import io.ktor.server.request.* -import io.ktor.server.response.* +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLProtocol +import io.ktor.http.path +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.application.log +import io.ktor.server.engine.EngineConnectorConfig +import io.ktor.server.html.respondHtml +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.request.header +import io.ktor.server.request.host +import io.ktor.server.request.port +import io.ktor.server.response.header +import io.ktor.server.response.respond +import io.ktor.server.response.respondText import io.ktor.server.routing.* -import io.ktor.server.util.* -import io.ktor.server.websocket.* -import io.ktor.util.pipeline.* -import io.ktor.websocket.* +import io.ktor.server.util.getOrFail +import io.ktor.server.util.url +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.application +import io.ktor.server.websocket.webSocket +import io.ktor.websocket.Frame import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.html.* +import kotlinx.html.body +import kotlinx.html.head +import kotlinx.html.header +import kotlinx.html.meta import kotlinx.serialization.encodeToString import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware -import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.Configurable +import space.kscience.dataforge.meta.ObservableMutableMeta +import space.kscience.dataforge.meta.enum +import space.kscience.dataforge.meta.long import space.kscience.dataforge.names.Name -import space.kscience.visionforge.* +import space.kscience.visionforge.Vision +import space.kscience.visionforge.VisionEvent +import space.kscience.visionforge.VisionManager +import space.kscience.visionforge.flowChanges import space.kscience.visionforge.html.* import kotlin.time.Duration.Companion.milliseconds @@ -72,7 +91,6 @@ 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 */ @@ -137,8 +155,8 @@ public fun Application.serveVisionData( public fun Application.serveVisionData( configuration: VisionRoute, - data: Map, -): Unit = serveVisionData(configuration) { data[it] } + data: Map, +): Unit = serveVisionData(configuration) { data[it]?.vision } /** * Serve a page, potentially containing any number of visions at a given [route] with given [header]. @@ -154,10 +172,10 @@ public fun Application.visionPage( ) { require(WebSockets) - val collector: MutableMap = mutableMapOf() + val cache: MutableMap = mutableMapOf() //serve data - serveVisionData(configuration, collector) + serveVisionData(configuration, cache) //filled pages routing { @@ -193,7 +211,7 @@ public fun Application.visionPage( path(route, "ws") } } else null, - onVisionRendered = { name, vision -> collector[name] = vision }, + displayCache = cache, fragment = visionFragment ) }