Event Display Tutorial #77

Merged
altavir merged 5 commits from teldufalsari/visionforge:teldufalsari/dev into dev 2023-12-24 15:30:21 +03:00
69 changed files with 1910 additions and 334 deletions
Showing only changes of commit c0cf852c62 - Show all commits

View File

@ -7,7 +7,7 @@
- Custom client-side events and thier processing in VisionServer
### Changed
- Color accessor property is now `colorProperty`. Color uses `invoke` instead of `set`
- Color accessor property is now `colorProperty`. Color uses non-nullable `invoke` instead of `set`.
- API update for server and pages
- Edges moved to solids module for easier construction
- Visions **must** be rooted in order to subscribe to updates.

View File

@ -7,12 +7,12 @@ plugins {
// id("org.jetbrains.kotlinx.kover") version "0.5.0"
}
val dataforgeVersion by extra("0.6.2")
val dataforgeVersion by extra("0.7.1")
val fxVersion by extra("11")
allprojects {
group = "space.kscience"
version = "0.3.0-dev-14"
version = "0.3.0-dev-17"
}
subprojects {

View File

@ -34,6 +34,6 @@ class GDMLVisionTest {
val child = cubes[Name.of("composite-000","segment-0")]
assertNotNull(child)
child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue())
assertEquals("red", child.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).string)
assertEquals("red", child.properties[SolidMaterial.MATERIAL_COLOR_KEY].string)
}
}

View File

@ -13,7 +13,6 @@ kscience {
useKtor()
fullStack(
"muon-monitor.js",
development = true,
jvmConfig = { withJava() },
jsConfig = { useCommonJs() }
) {
@ -47,9 +46,6 @@ application {
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
}
//TODO ???
tasks.getByName("jsBrowserProductionWebpack").dependsOn("jsDevelopmentExecutableCompileSync")
//distributions {
// main {
// contents {

View File

@ -71,7 +71,7 @@ class Model(val manager: VisionManager) {
fun reset() {
map.values.forEach {
it.properties.setMeta(SolidMaterial.MATERIAL_COLOR_KEY, null)
it.properties[SolidMaterial.MATERIAL_COLOR_KEY] = null
}
tracks.children.clear()
}

View File

@ -54,9 +54,6 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"jupyter": {
"outputs_hidden": false
},
"tags": []
},
"outputs": [],
@ -83,9 +80,6 @@
"language": "kotlin",
"name": "kotlin"
},
"ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false
},
"language_info": {
"codemirror_mode": "text/x-kotlin",
"file_extension": ".kt",
@ -94,6 +88,9 @@
"nbconvert_exporter": "",
"pygments_lexer": "kotlin",
"version": "1.8.20"
},
"ktnbPluginMetadata": {
"projectLibraries": []
}
},
"nbformat": 4,

View File

@ -0,0 +1,45 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"USE(JupyterCommonIntegration())"
]
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [],
"metadata": {
"collapsed": false
}
}
],
"metadata": {
"kernelspec": {
"display_name": "Kotlin",
"language": "kotlin",
"name": "kotlin"
},
"language_info": {
"name": "kotlin",
"version": "1.9.0",
"mimetype": "text/x-kotlin",
"file_extension": ".kt",
"pygments_lexer": "kotlin",
"codemirror_mode": "text/x-kotlin",
"nbconvert_exporter": ""
},
"ktnbPluginMetadata": {
"projectDependencies": true
}
},
"nbformat": 4,
"nbformat_minor": 0
}

View File

@ -25,10 +25,7 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
"collapsed": false
},
"outputs": [],
"source": [
@ -84,7 +81,7 @@
"version": "1.8.0-dev-3517"
},
"ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false
"projectLibraries": []
}
},
"nbformat": 4,

View File

@ -75,7 +75,7 @@ fun main() {
server.openInBrowser()
while (readln() != "exit") {
while (readlnOrNull() != "exit") {
}

View File

@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G
org.jetbrains.compose.experimental.jscanvas.enabled=true
toolsVersion=0.15.0-kotlin-1.9.20-RC2
toolsVersion=0.15.2-kotlin-1.9.21
#kotlin.experimental.tryK2=true
#kscience.wasm.disabled=true

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -45,7 +45,7 @@ include(
":ui:ring",
// ":ui:material",
":ui:bootstrap",
// ":ui:compose",
":ui:compose",
":visionforge-core",
":visionforge-solid",
// ":visionforge-fx",

View File

@ -29,7 +29,7 @@ public fun RBuilder.visionPropertyEditor(
this.descriptor = descriptor
this.scope = vision.manager?.context ?: error("Orphan vision could not be observed")
this.getPropertyState = { name ->
val ownMeta = vision.properties.own?.getMeta(name)
val ownMeta = vision.properties.own?.get(name)
if (ownMeta != null && !ownMeta.isEmpty()) {
EditorPropertyState.Defined
} else if (vision.properties.root().getValue(name) != null) {

View File

@ -0,0 +1,42 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose)
// id("org.jetbrains.compose") version "1.5.11"
// id("com.android.library")
}
kscience{
jvm()
js()
// wasm()
}
kotlin {
// android()
sourceSets {
val commonMain by getting {
dependencies {
}
}
val jvmMain by getting {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(compose.material)
api(compose.preview)
}
}
val jsMain by getting{
dependencies {
api(compose.html.core)
api("app.softwork:bootstrap-compose:0.1.15")
api("app.softwork:bootstrap-compose-icons:0.1.15")
api(projects.visionforge.visionforgeThreejs)
}
}
}
}

View File

@ -0,0 +1,87 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.dom.A
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.isLeaf
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.lastOrNull
import space.kscience.dataforge.names.plus
@Composable
private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) {
var expanded: Boolean by remember { mutableStateOf(true) }
val item: Meta? = root[name]
val descriptorItem: MetaDescriptor? = rootDescriptor?.get(name)
val actualValue = item?.value ?: descriptorItem?.defaultValue
val actualMeta = item ?: descriptorItem?.defaultNode
val token = name.lastOrNull()?.toString() ?: ""
FlexRow(attrs = {
classes("metaItem")
style {
alignItems(AlignItems.Center)
}
}) {
if (actualMeta?.isLeaf == false) {
Span({
classes(TreeStyles.treeCaret)
if (expanded) {
classes(TreeStyles.treeCaretDown)
}
onClick { expanded = !expanded }
})
}
Span({
classes(TreeStyles.treeLabel)
if (item == null) {
classes(TreeStyles.treeLabelInactive)
}
}) {
Text(token)
}
Div {
A {
Text(actualValue.toString())
}
}
}
if (expanded) {
FlexColumn({
classes(TreeStyles.tree)
}) {
val keys = buildSet {
descriptorItem?.children?.keys?.forEach {
add(NameToken(it))
}
actualMeta!!.items.keys.let { addAll(it) }
}
keys.filter { !it.body.startsWith("@") }.forEach { token ->
Div({
classes(TreeStyles.treeItem)
}) {
MetaViewerItem(root, name + token, rootDescriptor)
}
}
}
}
}
@Composable
public fun MetaViewer(meta: Meta, descriptor: MetaDescriptor? = null) {
MetaViewerItem(meta, Name.EMPTY, descriptor)
}

View File

@ -0,0 +1,44 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.Li
import org.jetbrains.compose.web.dom.Nav
import org.jetbrains.compose.web.dom.Ol
import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.length
@Composable
public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({
attr("aria-label","breadcrumb")
}) {
Ol({classes("breadcrumb")}) {
Li({
classes("breadcrumb-item")
onClick {
link(Name.EMPTY)
}
}) {
Text("\u2302")
}
if (name != null) {
val tokens = ArrayList<NameToken>(name.length)
name.tokens.forEach { token ->
tokens.add(token)
val fullName = Name(tokens.toList())
Text(".")
Li({
classes("breadcrumb-item")
if(tokens.size == name.length) classes("active")
onClick {
link(fullName)
}
}) {
Text(token.toString())
}
}
}
}
}

View File

@ -0,0 +1,183 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.ObservableMutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.remove
import space.kscience.dataforge.names.*
import space.kscience.visionforge.hidden
/**
* The display state of a property
*/
public sealed class EditorPropertyState {
public object Defined : EditorPropertyState()
public class Default(public val source: String = "unknown") : EditorPropertyState()
public object Undefined : EditorPropertyState()
}
/**
* @param meta Root config object - always non-null
* @param rootDescriptor Full path to the displayed node in [meta]. Could be empty
*/
@Composable
public fun PropertyEditor(
scope: CoroutineScope,
meta: MutableMeta,
getPropertyState: (Name) -> EditorPropertyState,
updates: Flow<Name>,
name: Name = Name.EMPTY,
rootDescriptor: MetaDescriptor? = null,
initialExpanded: Boolean? = null,
) {
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
val descriptor: MetaDescriptor? = remember(rootDescriptor, name) { rootDescriptor?.get(name) }
var property: MutableMeta by remember { mutableStateOf(meta.getOrCreate(name)) }
var editorPropertyState: EditorPropertyState by remember { mutableStateOf(getPropertyState(name)) }
val keys = remember(descriptor) {
buildSet {
descriptor?.children?.filterNot {
it.key.startsWith("@") || it.value.hidden
}?.forEach {
add(NameToken(it.key))
}
//ownProperty?.items?.keys?.filterNot { it.body.startsWith("@") }?.let { addAll(it) }
}
}
val token = name.lastOrNull()?.toString() ?: "Properties"
fun update() {
property = meta.getOrCreate(name)
editorPropertyState = getPropertyState(name)
}
LaunchedEffect(meta) {
updates.collect { updatedName ->
if (updatedName == name) {
update()
}
}
}
FlexRow({
style {
alignItems(AlignItems.Center)
}
}) {
if (keys.isNotEmpty()) {
Span({
classes(TreeStyles.treeCaret)
if (expanded) {
classes(TreeStyles.treeCaretDown)
}
onClick { expanded = !expanded }
})
}
Span({
classes(TreeStyles.treeLabel)
if (editorPropertyState != EditorPropertyState.Defined) {
classes(TreeStyles.treeLabelInactive)
}
}) {
Text(token)
}
if (!name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
Div({
style {
width(160.px)
marginAll(1.px, 5.px)
}
}) {
ValueChooser(descriptor, editorPropertyState, property.value) {
property.value = it
editorPropertyState = getPropertyState(name)
}
}
Button({
classes(TreeStyles.propertyEditorButton)
if (editorPropertyState != EditorPropertyState.Defined) {
disabled()
} else {
onClick {
meta.remove(name)
update()
}
}
}) {
Text("\u00D7")
}
}
}
if (expanded) {
FlexColumn({
classes(TreeStyles.tree)
}) {
keys.forEach { token ->
Div({
classes(TreeStyles.treeItem)
}) {
PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded)
}
}
}
}
}
@Composable
public fun PropertyEditor(
scope: CoroutineScope,
properties: ObservableMutableMeta,
descriptor: MetaDescriptor? = null,
expanded: Boolean? = null,
) {
PropertyEditor(
scope = scope,
meta = properties,
getPropertyState = { name ->
if (properties[name] != null) {
EditorPropertyState.Defined
} else if (descriptor?.get(name)?.defaultValue != null) {
EditorPropertyState.Default("descriptor")
} else {
EditorPropertyState.Undefined
}
},
updates = callbackFlow {
properties.onChange(scope) { name ->
scope.launch {
send(name)
}
}
invokeOnClose {
properties.removeListener(scope)
}
},
name = Name.EMPTY,
rootDescriptor = descriptor,
initialExpanded = expanded,
)
}

View File

@ -0,0 +1,102 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLLIElement
public class ComposeTab(
public val key: String,
public val title: String,
public val content: ContentBuilder<HTMLDivElement>,
public val disabled: Boolean,
public val titleExt: ContentBuilder<HTMLLIElement>,
)
@Composable
public fun Tabs(tabs: List<ComposeTab>, activeKey: String) {
var active by remember(activeKey) { mutableStateOf(activeKey) }
Div({ classes("card", "text-center") }) {
Div({ classes("card-header") }) {
Ul({ classes("nav", "nav-tabs", "card-header-tabs") }) {
tabs.forEach { tab ->
Li({
classes("nav-item")
}) {
A(attrs = {
classes("nav-link")
if (active == tab.key) {
classes("active")
}
if (tab.disabled) {
classes("disabled")
}
onClick {
active = tab.key
}
}) {
Text(tab.title)
}
tab.titleExt.invoke(this)
}
}
}
}
tabs.find { it.key == active }?.let { tab ->
Div({ classes("card-body") }) {
tab.content.invoke(this)
}
}
}
}
public class TabBuilder internal constructor(public val key: String) {
private var title: String = key
public var disabled: Boolean = false
private var content: ContentBuilder<HTMLDivElement> = {}
private var titleExt: ContentBuilder<HTMLLIElement> = {}
@Composable
public fun Content(content: ContentBuilder<HTMLDivElement>) {
this.content = content
}
@Composable
public fun Title(title: String, titleExt: ContentBuilder<HTMLLIElement> = {}) {
this.title = title
this.titleExt = titleExt
}
internal fun build(): ComposeTab = ComposeTab(
key,
title,
content,
disabled,
titleExt
)
}
public class TabsBuilder {
public var active: String = ""
internal val tabs: MutableList<ComposeTab> = mutableListOf()
@Composable
public fun Tab(key: String, builder: @Composable TabBuilder.() -> Unit) {
tabs.add(TabBuilder(key).apply { builder() }.build())
}
public fun addTab(tab: ComposeTab) {
tabs.add(tab)
}
}
@Composable
public fun Tabs(builder: @Composable TabsBuilder.() -> Unit) {
val result = TabsBuilder().apply { builder() }
Tabs(result.tabs, result.active)
}

View File

@ -0,0 +1,52 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import kotlinx.dom.clear
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.solid.Solid
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.ThreeCanvas
import space.kscience.visionforge.solid.three.ThreePlugin
@Composable
public fun ThreeCanvas(
context: Context,
options: Canvas3DOptions?,
solid: Solid?,
selected: Name?,
) {
val three: ThreePlugin by derivedStateOf { context.request(ThreePlugin) }
Div({
style {
maxWidth(100.vw)
maxHeight(100.vh)
width(100.percent)
height(100.percent)
}
}) {
var canvas: ThreeCanvas? = null
DisposableEffect(options) {
canvas = ThreeCanvas(three, scopeElement, options ?: Canvas3DOptions())
onDispose {
scopeElement.clear()
canvas = null
}
}
LaunchedEffect(solid) {
if (solid != null) {
canvas?.render(solid)
} else {
canvas?.clear()
}
}
LaunchedEffect(selected) {
canvas?.select(selected)
}
}
}

View File

@ -0,0 +1,80 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Text
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.encodeToString
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
@Composable
internal fun CanvasControls(
vision: Vision?,
options: Canvas3DOptions,
) {
FlexColumn {
FlexRow({
style {
border {
width(1.px)
style(LineStyle.Solid)
color(Color("blue"))
}
padding(4.px)
}
}) {
vision?.let { vision ->
Button({
onClick { event ->
val json = vision.encodeToString()
event.stopPropagation();
event.preventDefault();
val fileSaver = kotlinext.js.require<dynamic>("file-saver")
val blob = Blob(arrayOf(json), BlobPropertyBag("text/json;charset=utf-8"))
fileSaver.saveAs(blob, "object.json") as Unit
}
}) {
Text("Export")
}
}
}
PropertyEditor(
scope = vision?.manager?.context ?: Global,
properties = options.meta,
descriptor = Canvas3DOptions.descriptor,
expanded = false
)
}
}
@Composable
public fun ThreeControls(
vision: Vision?,
canvasOptions: Canvas3DOptions,
selected: Name?,
onSelect: (Name?) -> Unit,
tabBuilder: @Composable TabsBuilder.() -> Unit = {},
) {
Tabs {
active = "Tree"
vision?.let { vision ->
Tab("Tree") {
CardTitle("Vision tree")
VisionTree(vision, Name.EMPTY, selected, onSelect)
}
}
Tab("Settings") {
CardTitle("Canvas configuration")
CanvasControls(vision, canvasOptions)
}
tabBuilder()
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.visionforge.compose
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
@Composable
public fun ThreeJs(){
Surface {
}
}

View File

@ -0,0 +1,169 @@
@file:OptIn(ExperimentalComposeWebApi::class)
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.Card
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import space.kscience.visionforge.*
import space.kscience.visionforge.solid.Solid
import space.kscience.visionforge.solid.SolidGroup
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
@Composable
public fun ThreeCanvasWithControls(
solids: Solids,
builderOfSolid: Deferred<Solid?>,
initialSelected: Name?,
options: Canvas3DOptions?,
tabBuilder: @Composable TabsBuilder.() -> Unit = {},
) {
var selected: Name? by remember { mutableStateOf(initialSelected) }
var solid: Solid? by remember { mutableStateOf(null) }
LaunchedEffect(builderOfSolid) {
solids.context.launch {
solid = builderOfSolid.await()
//ensure that the solid is properly rooted
if (solid?.parent == null) {
solid?.setAsRoot(solids.context.visionManager)
}
}
}
val optionsWithSelector = remember(options) {
(options ?: Canvas3DOptions()).apply {
this.onSelect = {
selected = it
}
}
}
val selectedVision: Vision? = remember(builderOfSolid, selected) {
selected?.let {
when {
it.isEmpty() -> solid
else -> (solid as? SolidGroup)?.get(it)
}
}
}
FlexRow({
style {
height(100.percent)
width(100.percent)
flexWrap(FlexWrap.Wrap)
alignItems(AlignItems.Stretch)
alignContent(AlignContent.Stretch)
}
}) {
FlexColumn({
style {
height(100.percent)
minWidth(600.px)
flex(10, 1, 600.px)
position(Position.Relative)
}
}) {
if (solid == null) {
Div({
style {
position(Position.Fixed)
width(100.percent)
height(100.percent)
zIndex(1000)
top(40.percent)
left(0.px)
opacity(0.5)
filter {
opacity(50.percent)
}
}
}) {
Div({ classes("d-flex", " justify-content-center") }) {
Div({
classes("spinner-grow", "text-primary")
style {
width(3.cssRem)
height(3.cssRem)
zIndex(20)
}
attr("role", "status")
}) {
Span({ classes("sr-only") }) { Text("Loading 3D vision") }
}
}
}
} else {
ThreeCanvas(solids.context, optionsWithSelector, solid, selected)
}
selectedVision?.let { vision ->
Div({
style {
position(Position.Absolute)
top(5.px)
right(5.px)
width(450.px)
}
}) {
Card(
headerAttrs = {
// border = true
},
header = {
NameCrumbs(selected) { selected = it }
}
) {
PropertyEditor(
scope = solids.context,
meta = vision.properties.root(),
getPropertyState = { name ->
if (vision.properties.own?.get(name) != null) {
EditorPropertyState.Defined
} else if (vision.properties.root()[name] != null) {
// TODO differentiate
EditorPropertyState.Default()
} else {
EditorPropertyState.Undefined
}
},
updates = vision.properties.changes,
rootDescriptor = vision.descriptor
)
}
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->
P {
B { Text("Styles: ") }
Text(styles.joinToString(separator = ", "))
}
}
}
}
}
}
FlexColumn({
style {
paddingAll(4.px)
minWidth(400.px)
height(100.percent)
overflowY("auto")
flex(1, 10, 300.px)
}
}) {
ThreeControls(solid, optionsWithSelector, selected, onSelect = { selected = it }, tabBuilder = tabBuilder)
}
}

View File

@ -0,0 +1,94 @@
package space.kscience.visionforge.compose
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.css.*
@OptIn(ExperimentalComposeWebApi::class)
public object TreeStyles : StyleSheet() {
/**
* Remove default bullets
*/
public val tree: String by style {
paddingLeft(5.px)
marginLeft(0.px)
listStyleType("none")
}
/**
* Style the caret/arrow
*/
public val treeCaret: String by style {
cursor("pointer")
userSelect(UserSelect.none)
/* Create the caret/arrow with a unicode, and style it */
before {
content("\u25B6")
color(Color.black)
display(DisplayStyle.InlineBlock)
marginRight(6.px)
}
}
/**
* Rotate the caret/arrow icon when clicked on (using JavaScript)
*/
public val treeCaretDown: String by style {
before {
content("\u25B6")
color(Color.black)
display(DisplayStyle.InlineBlock)
marginRight(6.px)
transform { rotate(90.deg) }
}
}
public val treeItem: String by style {
alignItems(AlignItems.Center)
paddingLeft(10.px)
border {
left {
width(1.px)
color(Color.lightgray)
style = LineStyle.Dashed
}
}
}
public val treeLabel: String by style {
border(style = LineStyle.None)
paddingAll(left = 4.pt, right = 4.pt)
textAlign("left")
flex(1)
}
public val treeLabelInactive: String by style {
color(Color.lightgray)
}
public val treeLabelSelected: String by style {
backgroundColor(Color.lightblue)
}
public val propertyEditorButton: String by style {
width(24.px)
alignSelf(AlignSelf.Stretch)
marginAll(1.px, 5.px)
backgroundColor(Color.white)
border{
style(LineStyle.Solid)
}
borderRadius(2.px)
textAlign("center")
textDecoration("none")
cursor("pointer")
disabled {
cursor("auto")
border{
style(LineStyle.Dashed)
}
color(Color.lightgray)
}
}
}

View File

@ -0,0 +1,96 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.css.Color
import org.jetbrains.compose.web.css.color
import org.jetbrains.compose.web.css.cursor
import org.jetbrains.compose.web.css.textDecorationLine
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.lastOrNull
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.names.startsWith
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionGroup
import space.kscience.visionforge.asSequence
import space.kscience.visionforge.compose.TreeStyles.hover
import space.kscience.visionforge.compose.TreeStyles.invoke
import space.kscience.visionforge.isEmpty
@Composable
private fun TreeLabel(
vision: Vision,
name: Name,
selected: Name?,
clickCallback: (Name) -> Unit,
) {
Span({
classes(TreeStyles.treeLabel)
if (name == selected) {
classes(TreeStyles.treeLabelSelected)
}
style {
color(Color("#069"))
cursor("pointer")
hover.invoke {
textDecorationLine("underline")
}
}
onClick { clickCallback(name) }
}) {
Text(name.lastOrNull()?.toString() ?: "World")
}
}
@Composable
public fun VisionTree(
vision: Vision,
name: Name = Name.EMPTY,
selected: Name? = null,
clickCallback: (Name) -> Unit,
): Unit {
var expanded: Boolean by remember { mutableStateOf(selected?.startsWith(name) ?: false) }
//display as node if any child is visible
if (vision is VisionGroup) {
FlexRow {
if (vision.children.keys.any { !it.body.startsWith("@") }) {
Span({
classes(TreeStyles.treeCaret)
if (expanded) {
classes(TreeStyles.treeCaretDown)
}
onClick {
expanded = !expanded
}
})
}
TreeLabel(vision, name, selected, clickCallback)
}
if (expanded) {
FlexColumn({
classes(TreeStyles.tree)
}) {
vision.children.asSequence()
.filter { !it.first.toString().startsWith("@") } // ignore statics and other hidden children
.sortedBy { (it.second as? VisionGroup)?.children?.isEmpty() ?: true } // ignore empty groups
.forEach { (childToken, child) ->
Div({ classes(TreeStyles.treeItem) }) {
VisionTree(
child,
name + childToken,
selected,
clickCallback
)
}
}
}
}
} else {
TreeLabel(vision, name, selected, clickCallback)
}
}

View File

@ -0,0 +1,8 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.H5
import org.jetbrains.compose.web.dom.Text
@Composable
public fun CardTitle(title: String): Unit = H5({ classes("card-title") }) { Text(title) }

View File

@ -0,0 +1,44 @@
package space.kscience.visionforge.compose
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword
public enum class UserSelect {
inherit, initial, revert, revertLayer, unset,
none, auto, text, contain, all;
}
public fun StyleScope.userSelect(value: UserSelect) {
property("user-select", value.name)
}
public fun StyleScope.content(value: String) {
property("content", "'$value'")
}
public fun StyleScope.paddingAll(
top: CSSNumeric = 0.pt,
right: CSSNumeric = top,
bottom: CSSNumeric = top,
left: CSSNumeric = right,
) {
padding(top, right, bottom, left)
}
public fun StyleScope.marginAll(
top: CSSNumeric = 0.pt,
right: CSSNumeric = top,
bottom: CSSNumeric = top,
left: CSSNumeric = right,
) {
margin(top, right, bottom, left)
}
public fun StyleScope.zIndex(value: Int) {
property("z-index", "$value")
}
public fun StyleScope.zIndex(value: CSSAutoKeyword) {
property("z-index", value)
}

View File

@ -0,0 +1,41 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.FlexDirection
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.flexDirection
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.ElementScope
import org.w3c.dom.HTMLDivElement
@Composable
public fun FlexColumn(
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: @Composable ElementScope<HTMLDivElement>.() -> Unit,
): Unit = Div(
attrs = {
style {
display(DisplayStyle.Flex)
flexDirection(FlexDirection.Column)
}
attrs?.invoke(this)
},
content
)
@Composable
public fun FlexRow(
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: @Composable ElementScope<HTMLDivElement>.() -> Unit,
): Unit = Div(
attrs = {
style {
display(DisplayStyle.Flex)
flexDirection(FlexDirection.Row)
}
attrs?.invoke(this)
},
content
)

View File

@ -0,0 +1,268 @@
@file:Suppress("UNUSED_PARAMETER")
package space.kscience.visionforge.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.percent
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Option
import org.jetbrains.compose.web.dom.Select
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLOptionElement
import org.w3c.dom.asList
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.allowedValues
import space.kscience.visionforge.Colors
import space.kscience.visionforge.widgetType
import three.math.Color
@Composable
public fun StringValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
var stringValue by remember { mutableStateOf(value?.string ?: "") }
Input(type = InputType.Text) {
style {
width(100.percent)
}
value(stringValue)
onKeyDown { event ->
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
stringValue = (event.target as HTMLInputElement).value
onValueChange(stringValue.asValue())
}
}
onChange {
stringValue = it.target.value
}
}
}
@Composable
public fun BooleanValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
Input(type = InputType.Checkbox) {
style {
width(100.percent)
}
//this.attributes["indeterminate"] = (props.item == null).toString()
checked(value?.boolean ?: false)
onChange {
val newValue = it.target.checked
onValueChange(newValue.asValue())
}
}
}
@Composable
public fun NumberValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
var innerValue by remember { mutableStateOf(value?.string ?: "") }
Input(type = InputType.Number) {
style {
width(100.percent)
}
value(innerValue)
onKeyDown { event ->
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
innerValue = (event.target as HTMLInputElement).value
val number = innerValue.toDoubleOrNull()
if (number == null) {
console.error("The input value $innerValue is not a number")
} else {
onValueChange(number.asValue())
}
}
}
onChange {
innerValue = it.target.value
}
descriptor?.attributes?.get("step").number?.let {
step(it)
}
descriptor?.attributes?.get("min").string?.let {
min(it)
}
descriptor?.attributes?.get("max").string?.let {
max(it)
}
}
}
@Composable
public fun ComboValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
var selected by remember { mutableStateOf(value?.string ?: "") }
Select({
style {
width(100.percent)
}
onChange {
selected = it.target.value
onValueChange(selected.asValue())
}
}, multiple = false) {
descriptor?.allowedValues?.forEach {
Option(it.string, { if (it == value) selected() }) {
Text(it.string)
}
}
}
}
@Composable
public fun ColorValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
Input(type = InputType.Color) {
style {
width(100.percent)
marginAll(0.px)
}
value(
value?.let { value ->
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
else "#" + Color(value.string).getHexString()
} ?: "#000000"
)
onChange {
onValueChange(it.target.value.asValue())
}
}
}
@Composable
public fun MultiSelectChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
Select({
onChange { event ->
val newSelected = event.target.selectedOptions.asList()
.map { (it as HTMLOptionElement).value.asValue() }
onValueChange(newSelected.asValue())
}
}, multiple = true) {
descriptor?.allowedValues?.forEach { optionValue ->
Option(optionValue.string, {
value?.list?.let { if (optionValue in it) selected() }
}) {
Text(optionValue.string)
}
}
}
}
@Composable
public fun RangeValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
var innerValue by remember { mutableStateOf(value?.double) }
var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) }
FlexRow {
if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
Input(type = InputType.Checkbox) {
if (!rangeDisabled) defaultChecked()
onChange {
val checkBoxValue = it.target.checked
rangeDisabled = !checkBoxValue
onValueChange(
if (!checkBoxValue) {
null
} else {
innerValue?.asValue()
}
)
}
}
}
}
Input(type = InputType.Range) {
style {
width(100.percent)
}
if (rangeDisabled) disabled()
value(innerValue?.toString() ?: "")
onChange {
val newValue = it.target.value
onValueChange(newValue.toDoubleOrNull()?.asValue())
innerValue = newValue.toDoubleOrNull()
}
descriptor?.attributes?.get("min").string?.let {
min(it)
}
descriptor?.attributes?.get("max").string?.let {
max(it)
}
descriptor?.attributes?.get("step").number?.let {
step(it)
}
}
}
@Composable
public fun ValueChooser(
descriptor: MetaDescriptor?,
state: EditorPropertyState,
value: Value?,
onValueChange: (Value?) -> Unit,
) {
val rawInput by remember { mutableStateOf(false) }
val type = descriptor?.valueTypes?.firstOrNull()
when {
rawInput -> StringValueChooser(descriptor, state, value, onValueChange)
descriptor?.widgetType == "color" -> ColorValueChooser(descriptor, state, value, onValueChange)
descriptor?.widgetType == "multiSelect" -> MultiSelectChooser(descriptor, state, value, onValueChange)
descriptor?.widgetType == "range" -> RangeValueChooser(descriptor, state, value, onValueChange)
type == ValueType.BOOLEAN -> BooleanValueChooser(descriptor, state, value, onValueChange)
type == ValueType.NUMBER -> NumberValueChooser(descriptor, state, value, onValueChange)
descriptor?.allowedValues?.isNotEmpty() ?: false -> ComboValueChooser(descriptor, state, value, onValueChange)
//TODO handle lists
else -> StringValueChooser(descriptor, state, value, onValueChange)
}
}

View File

@ -16,9 +16,8 @@ import react.dom.attrs
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.ObservableMutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRequirement
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.remove
import space.kscience.dataforge.names.*
import space.kscience.visionforge.hidden
@ -146,7 +145,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
}
+token
}
if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
if (!props.name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
styledDiv {
css {
//+TreeStyles.resizeableInput
@ -185,7 +184,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
}
+"\u00D7"
attrs {
if (editorPropertyState!= EditorPropertyState.Defined) {
if (editorPropertyState != EditorPropertyState.Defined) {
disabled = true
} else {
onClickFunction = removeClick

View File

@ -12,7 +12,7 @@ import react.dom.attrs
import react.fc
import react.useState
import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.meta.descriptors.ValueRequirement
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
@ -43,7 +43,7 @@ public val RangeValueChooser: FC<ValueChooserProps> = fc("RangeValueChooser") {
}
flexRow {
if (props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
if (props.descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
styledInput(type = InputType.checkBox) {
attrs {
defaultChecked = rangeDisabled.not()

View File

@ -0,0 +1,63 @@
package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.meta.MutableMeta
@Serializable
@SerialName("control")
public abstract class VisionControlEvent : VisionEvent, MetaRepr {
public abstract val meta: Meta
override fun toMeta(): Meta = meta
}
public interface ControlVision : Vision {
public val controlEventFlow: SharedFlow<VisionControlEvent>
public fun dispatchControlEvent(event: VisionControlEvent)
override fun receiveEvent(event: VisionEvent) {
if (event is VisionControlEvent) {
dispatchControlEvent(event)
} else super.receiveEvent(event)
}
}
/**
* @param payload The optional payload associated with the click event.
*/
@Serializable
@SerialName("control.click")
public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() {
override val meta: Meta get() = Meta { ::payload.name put payload }
}
public interface ClickControl : ControlVision {
/**
* Create and dispatch a click event
*/
public fun click(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder)))
}
}
/**
* Register listener
*/
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
@Serializable
@SerialName("control.valueChange")
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent()

View File

@ -13,7 +13,7 @@ import kotlin.jvm.JvmInline
@JvmInline
public value class StyleSheet(private val owner: Vision) {
private val styleNode: Meta get() = owner.properties.getMeta(STYLESHEET_KEY)
private val styleNode: Meta get() = owner.properties[STYLESHEET_KEY]
public val items: Map<NameToken, Meta> get() = styleNode.items
@ -23,7 +23,7 @@ public value class StyleSheet(private val owner: Vision) {
* Define a style without notifying owner
*/
public fun define(key: String, style: Meta?) {
owner.properties.setMeta(STYLESHEET_KEY + key, style)
owner.properties[STYLESHEET_KEY + key] = style
}
/**
@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) {
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
*/
public fun Vision.getStyle(name: String): Meta? =
properties.own?.getMeta(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
/**
* Resolve a property from all styles

View File

@ -4,11 +4,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn
import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.descriptors.Described
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
@ -17,7 +19,7 @@ import space.kscience.visionforge.Vision.Companion.TYPE
/**
* A root type for display hierarchy
*/
@Type(TYPE)
@DfType(TYPE)
public interface Vision : Described {
/**
@ -45,6 +47,14 @@ public interface Vision : Described {
}
}
/**
* Receive and process a generic [VisionEvent].
*/
public fun receiveEvent(event: VisionEvent) {
if(event is VisionChange) update(event)
else manager?.logger?.warn { "Undispatched event: $event" }
}
override val descriptor: MetaDescriptor?
public companion object {

View File

@ -63,8 +63,7 @@ public data class VisionChange(
public val vision: Vision? = null,
public val properties: Meta? = null,
public val children: Map<Name, VisionChange>? = null,
)
) : VisionEvent
/**
* An update for a [Vision]

View File

@ -13,12 +13,11 @@ import space.kscience.dataforge.names.parseAsName
public interface VisionClient: Plugin {
public val visionManager: VisionManager
public suspend fun sendEvent(event: VisionEvent)
public suspend fun sendEvent(targetName: Name, event: VisionEvent)
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
}
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
}
@ -35,8 +34,8 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
}
public fun VisionClient.sendEvent(visionName: Name, event: MetaRepr): Unit {
public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit {
context.launch {
sendEvent(VisionMetaEvent(visionName, event.toMeta()))
sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
}
}

View File

@ -3,18 +3,14 @@ package space.kscience.visionforge
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.names.Name
/**
* An event propagated from client to a server
*/
@Serializable
public sealed interface VisionEvent{
public val targetName: Name
public companion object{
public sealed interface VisionEvent {
public companion object {
public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
}
}
@ -24,17 +20,4 @@ public sealed interface VisionEvent{
*/
@Serializable
@SerialName("meta")
public class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent
@Serializable
@SerialName("change")
public class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent
public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
/**
* Set the payload to be sent to server on click
*/
public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit){
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
}
public class VisionMetaEvent(public val meta: Meta) : VisionEvent

View File

@ -13,10 +13,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.VisionOfCheckbox
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
import space.kscience.visionforge.html.VisionOfTextField
import space.kscience.visionforge.html.*
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
override val tag: PluginTag get() = Companion.tag
@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
defaultDeserializer { SimpleVisionGroup.serializer() }
subclass(NullVision.serializer())
subclass(SimpleVisionGroup.serializer())
subclass(VisionOfHtmlInput.serializer())
subclass(VisionOfNumberField.serializer())
subclass(VisionOfTextField.serializer())
subclass(VisionOfCheckbox.serializer())
subclass(VisionOfRangeField.serializer())
subclass(VisionOfHtmlForm.serializer())
}
}

View File

@ -34,13 +34,13 @@ public interface VisionProperties : MetaProvider {
* @param inherit toggles parent node property lookup. Null means inference from descriptor.
* @param includeStyles toggles inclusion of properties from styles.
*/
public fun getMeta(
public fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean? = null,
): Meta
override fun getMeta(name: Name): Meta? = getMeta(name, null, null)
override fun get(name: Name): Meta? = get(name, null, null)
public val changes: Flow<Name>
@ -54,7 +54,7 @@ public interface VisionProperties : MetaProvider {
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
override fun getMeta(
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
@ -65,7 +65,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
includeStyles,
)
public fun setMeta(
public fun set(
name: Name,
node: Meta?,
notify: Boolean,
@ -77,10 +77,10 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
notify: Boolean,
)
override fun getMeta(name: Name): MutableMeta = getMeta(name, null, null)
override fun get(name: Name): MutableMeta = get(name, null, null)
override fun setMeta(name: Name, node: Meta?) {
setMeta(name, node, true)
override fun set(name: Name, node: Meta?) {
set(name, node, true)
}
override fun setValue(name: Name, value: Value?) {
@ -89,7 +89,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
}
public fun MutableVisionProperties.remove(name: Name) {
setMeta(name, null)
set(name, null)
}
public fun MutableVisionProperties.remove(name: String) {
@ -114,7 +114,7 @@ private class VisionPropertiesItem(
override val items: Map<NameToken, MutableMeta>
get() {
val metaKeys = properties.own?.getMeta(nodeName)?.items?.keys ?: emptySet()
val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet()
val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet()
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
val inheritFlag = descriptor?.inherited ?: inherit
@ -148,8 +148,8 @@ private class VisionPropertiesItem(
default
)
override fun setMeta(name: Name, node: Meta?) {
properties.setMeta(nodeName + name, node)
override fun set(name: Name, node: Meta?) {
properties[nodeName + name] = node
}
override fun toString(): String = Meta.toString(this)
@ -202,16 +202,16 @@ public abstract class AbstractVisionProperties(
return descriptor?.defaultValue
}
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
override fun set(name: Name, node: Meta?, notify: Boolean) {
//ignore if the value is the same as existing
if (own?.getMeta(name) == node) return
if (own?.get(name) == node) return
if (name.isEmpty()) {
properties = node?.asMutableMeta()
} else if (node == null) {
properties?.setMeta(name, node)
properties?.set(name, node)
} else {
getOrCreateProperties().setMeta(name, node)
getOrCreateProperties()[name] = node
}
if (notify) {
invalidate(name)
@ -223,7 +223,7 @@ public abstract class AbstractVisionProperties(
if (own?.getValue(name) == value) return
if (value == null) {
properties?.getMeta(name)?.value = null
properties?.get(name)?.value = null
} else {
getOrCreateProperties().setValue(name, value)
}
@ -272,11 +272,11 @@ public fun VisionProperties.getValue(
/**
* Get [Vision] property using key as a String
*/
public fun VisionProperties.getMeta(
public fun VisionProperties.get(
name: String,
inherit: Boolean? = null,
includeStyles: Boolean? = null,
): Meta = getMeta(name.parseAsName(), inherit, includeStyles)
): Meta = get(name.parseAsName(), inherit, includeStyles)
/**
* The root property node with given inheritance and style flags
@ -286,17 +286,17 @@ public fun VisionProperties.getMeta(
public fun MutableVisionProperties.root(
inherit: Boolean? = null,
includeStyles: Boolean? = null,
): MutableMeta = getMeta(Name.EMPTY, inherit, includeStyles)
): MutableMeta = get(Name.EMPTY, inherit, includeStyles)
/**
* Get [Vision] property using key as a String
*/
public fun MutableVisionProperties.getMeta(
public fun MutableVisionProperties.get(
name: String,
inherit: Boolean? = null,
includeStyles: Boolean? = null,
): MutableMeta = getMeta(name.parseAsName(), inherit, includeStyles)
): MutableMeta = get(name.parseAsName(), inherit, includeStyles)
//
//public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit =

View File

@ -17,10 +17,10 @@ public fun Vision.flowProperty(
includeStyles: Boolean? = null,
): Flow<Meta> = flow {
//Pass initial value.
emit(properties.getMeta(propertyName, inherit, includeStyles))
emit(properties.get(propertyName, inherit, includeStyles))
properties.changes.collect { name ->
if (name.startsWith(propertyName)) {
emit(properties.getMeta(propertyName, inherit, includeStyles))
emit(properties.get(propertyName, inherit, includeStyles))
}
}
}

View File

@ -0,0 +1,119 @@
package space.kscience.visionforge.html
import kotlinx.html.InputType
import kotlinx.html.TagConsumer
import kotlinx.html.stream.createHTML
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.AbstractVision
@Serializable
public abstract class VisionOfHtml : AbstractVision() {
public var classes: List<String> by properties.stringList(*emptyArray())
}
@Serializable
@SerialName("html.plain")
public class VisionOfPlainHtml : VisionOfHtml() {
public var content: String? by properties.string()
}
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) {
content = createHTML().apply(block).finalize()
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.html(
block: VisionOfPlainHtml.() -> Unit,
): VisionOfPlainHtml = VisionOfPlainHtml().apply(block)
@Serializable
public enum class InputFeedbackMode {
/**
* Fire feedback event on `onchange` event
*/
ONCHANGE,
/**
* Fire feedback event on `oninput` event
*/
ONINPUT,
/**
* provide only manual feedback
*/
NONE
}
@Serializable
@SerialName("html.input")
public open class VisionOfHtmlInput(
public val inputType: String,
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
) : VisionOfHtml() {
public var value: Value? by properties.value()
public var disabled: Boolean by properties.boolean { false }
public var fieldName: String? by properties.string()
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlInput(
inputType: String,
block: VisionOfHtmlInput.() -> Unit = {},
): VisionOfHtmlInput = VisionOfHtmlInput(inputType).apply(block)
@Serializable
@SerialName("html.text")
public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) {
public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlTextField(
block: VisionOfTextField.() -> Unit = {},
): VisionOfTextField = VisionOfTextField().apply(block)
@Serializable
@SerialName("html.checkbox")
public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) {
public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlCheckBox(
block: VisionOfCheckbox.() -> Unit = {},
): VisionOfCheckbox = VisionOfCheckbox().apply(block)
@Serializable
@SerialName("html.number")
public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) {
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlNumberField(
block: VisionOfNumberField.() -> Unit = {},
): VisionOfNumberField = VisionOfNumberField().apply(block)
@Serializable
@SerialName("html.range")
public class VisionOfRangeField(
public val min: Double,
public val max: Double,
public val step: Double = 1.0,
) : VisionOfHtmlInput(InputType.range.realValue) {
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlRangeField(
min: Double,
max: Double,
step: Double = 1.0,
block: VisionOfRangeField.() -> Unit = {},
): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block)

View File

@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
/**
* @param formId an id of the element in rendered DOM, this form is bound to
*/
@Serializable
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlInput() {
public var values: Meta? by mutableProperties.node()
) : VisionOfHtml() {
public var values: Meta? by properties.node()
}
public fun <R> TagConsumer<R>.bindForm(

View File

@ -1,58 +0,0 @@
package space.kscience.visionforge.html
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.number
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.Vision
//TODO replace by something
internal val Vision.mutableProperties get() = properties.getMeta(Name.EMPTY, false, false)
@Serializable
public abstract class VisionOfHtmlInput : AbstractVision() {
public var disabled: Boolean by mutableProperties.boolean { false }
}
@Serializable
@SerialName("html.text")
public class VisionOfTextField(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var text: String? by mutableProperties.string()
}
@Serializable
@SerialName("html.checkbox")
public class VisionOfCheckbox(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var checked: Boolean? by mutableProperties.boolean()
}
@Serializable
@SerialName("html.number")
public class VisionOfNumberField(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var value: Number? by mutableProperties.number()
}
@Serializable
@SerialName("html.range")
public class VisionOfRangeField(
public val min: Double,
public val max: Double,
public val step: Double = 1.0,
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var value: Number? by mutableProperties.number()
}

View File

@ -23,10 +23,10 @@ public fun Vision.useProperty(
callback: (Meta) -> Unit,
): Job {
//Pass initial value.
callback(properties.getMeta(propertyName, inherit, includeStyles))
callback(properties.get(propertyName, inherit, includeStyles))
return properties.changes.onEach { name ->
if (name.startsWith(propertyName)) {
callback(properties.getMeta(propertyName, inherit, includeStyles))
callback(properties.get(propertyName, inherit, includeStyles))
}
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
}
@ -39,9 +39,22 @@ public fun Vision.useProperty(
callback: (Meta) -> Unit,
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
/**
* Observe changes to the specific property without passing the initial value.
*/
public fun <V : Vision, T> V.onPropertyChange(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: suspend V.(T) -> Unit,
): Job = properties.changes.onEach { name ->
if (name.startsWith(property.name.asName())) {
callback(property.get(this))
}
}.launchIn(scope)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,
scope: CoroutineScope? = manager?.context,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
@ -50,5 +63,5 @@ public fun <V : Vision, T> V.useProperty(
if (name.startsWith(property.name.asName())) {
callback(property.get(this@useProperty))
}
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
}.launchIn(scope)
}

View File

@ -1,7 +1,6 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
@ -24,7 +23,6 @@ private class TestScheme : Scheme() {
companion object : SchemeSpec<TestScheme>(::TestScheme)
}
@OptIn(ExperimentalCoroutinesApi::class)
internal class VisionPropertyTest {
private val manager = Global.request(VisionManager)
@ -42,7 +40,7 @@ internal class VisionPropertyTest {
@Test
fun testPropertyEdit() {
val vision = manager.group()
vision.properties.getMeta("fff.ddd").apply {
vision.properties.get("fff.ddd").apply {
value = 2.asValue()
}
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
@ -52,7 +50,7 @@ internal class VisionPropertyTest {
@Test
fun testPropertyUpdate() {
val vision = manager.group()
vision.properties.getMeta("fff").updateWith(TestScheme) {
vision.properties.get("fff").updateWith(TestScheme) {
ddd = 2
}
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
@ -87,7 +85,7 @@ internal class VisionPropertyTest {
child.properties.remove("test")
assertEquals(11, child.properties.getMeta("test", inherit = true).int)
assertEquals(11, child.properties.get("test", inherit = true).int)
// assertEquals(11, deferred.await()?.int)
// assertEquals(2, callCounter)
subscription.cancel()

View File

@ -9,8 +9,8 @@ import kotlinx.serialization.serializerOrNull
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
@ -20,13 +20,13 @@ import kotlin.reflect.cast
/**
* A browser renderer for a [Vision].
*/
@Type(ElementVisionRenderer.TYPE)
@DfType(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
* higher value to "steal" rendering job
*/
public fun rateVision(vision: Vision): Int

View File

@ -67,7 +67,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
private val mutex = Mutex()
private val changeCollector = VisionChangeBuilder()
private val rootChangeCollector = VisionChangeBuilder()
/**
* Communicate vision property changed from rendering engine to model
@ -75,21 +76,20 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
context.launch {
mutex.withLock {
changeCollector.propertyChanged(visionName, propertyName, item)
rootChangeCollector.propertyChanged(visionName, propertyName, item)
}
}
}
private val eventCollector by lazy {
MutableSharedFlow<VisionEvent>(meta["feedback.eventCache"].int ?: 100)
MutableSharedFlow<Pair<Name, VisionEvent>>(meta["feedback.eventCache"].int ?: 100)
}
/**
* Send a custom feedback event
*/
override suspend fun sendEvent(event: VisionEvent) {
eventCollector.emit(event)
override suspend fun sendEvent(targetName: Name, event: VisionEvent) {
eventCollector.emit(targetName to event)
}
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
@ -98,7 +98,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
renderer.render(element, name, vision, outputMeta)
}
private fun startVisionUpdate(element: Element, name: Name, vision: Vision?, outputMeta: Meta) {
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
val endpoint = resolveEndpoint(element)
@ -110,9 +110,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name.toString())
searchParams.append("name", visionName.toString())
}
logger.info { "Updating vision data from $wsUrl" }
//Individual websocket for this vision
@ -120,25 +121,25 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
val event: VisionEvent = visionManager.jsonFormat.decodeFromString(
VisionEvent.serializer(),
stringData
)
// If change contains root vision replacement, do it
change.vision?.let { vision ->
renderVision(element, name, vision, outputMeta)
if (event is VisionChange) {
event.vision?.let { vision ->
renderVision(element, visionName, vision, outputMeta)
}
}
logger.debug { "Got update $change for output with name $name" }
if (vision == null) error("Can't update vision because it is not loaded.")
vision.update(change)
logger.debug { "Got $event for output with name $visionName" }
vision.receiveEvent(event)
} else {
logger.error { "WebSocket message data is not a string" }
}
}
//Backward change propagation
var feedbackJob: Job? = null
@ -146,32 +147,35 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300
onopen = {
feedbackJob = visionManager.context.launch {
eventCollector.filter { it.targetName == name }.onEach {
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it))
//launch a separate coroutine to send events to the backend
eventCollector.filter { it.first == visionName }.onEach {
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second))
}.launchIn(this)
//aggregate atomic changes
while (isActive) {
delay(feedbackAggregationTime.milliseconds)
val change = changeCollector[name] ?: continue
if (!change.isEmpty()) {
val visionChangeCollector = rootChangeCollector[name]
if (visionChangeCollector?.isEmpty() == false) {
mutex.withLock {
eventCollector.emit(VisionChangeEvent(name, change.deepCopy(visionManager)))
change.reset()
eventCollector.emit(visionName to visionChangeCollector.deepCopy(visionManager))
rootChangeCollector.reset()
}
}
}
}
logger.info { "WebSocket feedback channel established for output '$name'" }
logger.info { "WebSocket feedback channel established for output '$visionName'" }
}
onclose = {
feedbackJob?.cancel()
logger.info { "WebSocket feedback channel closed for output '$name'" }
logger.info { "WebSocket feedback channel closed for output '$visionName'" }
}
onerror = {
feedbackJob?.cancel()
logger.error { "WebSocket feedback channel error for output '$name'" }
logger.error { "WebSocket feedback channel error for output '$visionName'" }
}
}
}
@ -240,9 +244,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
}
//Try to load vision via websocket
element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
startVisionUpdate(element, name, null, outputMeta)
}
// element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
// startVisionUpdate(element, name, null, outputMeta)
// }
else -> error("No embedded vision data / fetch url for $name")
}
@ -251,9 +255,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
listOf(
numberVisionRenderer(this),
textVisionRenderer(this),
formVisionRenderer(this)
htmlVisionRenderer,
inputVisionRenderer,
checkboxVisionRenderer,
numberVisionRenderer,
textVisionRenderer,
rangeVisionRenderer,
formVisionRenderer
).associateByName()
} else super<AbstractPlugin>.content(target)

View File

@ -2,67 +2,174 @@ package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.html.InputType
import kotlinx.html.div
import kotlinx.html.js.input
import kotlinx.html.js.label
import kotlinx.html.js.onChangeFunction
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.get
import org.w3c.xhr.FormData
import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.DynamicMeta
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toMap
import space.kscience.dataforge.meta.valueSequence
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
import space.kscience.visionforge.html.VisionOfTextField
import space.kscience.dataforge.meta.*
import space.kscience.visionforge.html.*
internal fun textVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfTextField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
}
}
input {
type = InputType.text
this.name = fieldName
vision.useProperty(VisionOfTextField::text) {
value = it ?: ""
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfTextField::text.name, value)
}
/**
* Subscribes the HTML element to a given vision.
*
* @param vision The vision to subscribe to.
*/
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
vision.useProperty(VisionOfHtml::classes) {
classList.value = classes.joinToString(separator = " ")
}
}
internal fun numberVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
}
}
input {
type = InputType.text
this.name = fieldName
vision.useProperty(VisionOfNumberField::value) {
value = it?.toDouble() ?: 0.0
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value)
}
/**
* Subscribes the HTML input element to a given vision.
*
* @param inputVision The input vision to subscribe to.
*/
private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
subscribeToVision(inputVision)
inputVision.useProperty(VisionOfHtmlInput::disabled) {
disabled = it
}
}
internal val htmlVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfPlainHtml> { _, vision, _ ->
div {}.also { div ->
div.subscribeToVision(vision)
vision.useProperty(VisionOfPlainHtml::content) {
div.textContent = it
}
}
}
internal val inputVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.value = htmlInputElement.value.asValue()
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfHtmlInput::value) {
htmlInputElement.value = it?.string ?: ""
}
}
}
internal val checkboxVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfCheckbox> { _, vision, _ ->
input {
type = InputType.checkBox
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.checked = htmlInputElement.checked
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfCheckbox::checked) {
htmlInputElement.checked = it ?: false
}
}
}
internal val textVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { _, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.text = htmlInputElement.value
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfTextField::text) {
htmlInputElement.value = it ?: ""
}
}
}
internal val numberVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfNumberField::value) {
htmlInputElement.valueAsNumber = it?.double ?: 0.0
}
}
}
internal val rangeVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfRangeField> { _, vision, _ ->
input {
type = InputType.text
min = vision.min.toString()
max = vision.max.toString()
step = vision.step.toString()
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfRangeField::value) {
htmlInputElement.valueAsNumber = it?.double ?: 0.0
}
}
}
internal fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);")
@ -86,28 +193,29 @@ internal fun FormData.toMeta(): Meta {
return DynamicMeta(`object`)
}
internal fun formVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
internal val formVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { _, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
client.logger.debug{"Adding hooks to form with id = '$vision.formId'"}
form.subscribeToVision(vision)
vision.useProperty(VisionOfHtmlForm::values) { values ->
client.logger.debug{"Updating form '${vision.formId}' with values $values"}
val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
vision.useProperty(VisionOfHtmlForm::values) { values ->
vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" }
val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
}
}
}
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
console.info("Sent: ${formData.toMap()}")
false
}
}
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
vision.values = formData
console.info("Sent: ${formData.toMap()}")
false
}
}

View File

@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.*
@ -17,7 +16,6 @@ import kotlin.random.nextUInt
/**
* A base class for different Jupyter VF integrations
*/
@DFExperimental
public abstract class VisionForgeIntegration(
public val visionManager: VisionManager,
) : JupyterIntegration(), ContextAware {

View File

@ -1,14 +1,14 @@
package space.kscience.visionforge.jupyter
import kotlinx.html.*
import kotlinx.html.div
import kotlinx.html.p
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml
import space.kscience.plotly.Plot
import space.kscience.plotly.PlotlyPage
import space.kscience.plotly.StaticPlotlyRenderer
import space.kscience.tables.*
import space.kscience.tables.Table
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage
@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision
import space.kscience.visionforge.visionManager
@DFExperimental
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
override fun Builder.afterLoaded(vf: VisionForge) {

View File

@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val plotlyVersion = "0.6.0"
val plotlyVersion = "0.6.1"
kscience {
jvm()

View File

@ -33,8 +33,8 @@ public class VisionOfPlotly private constructor(
@Transient
override val properties: MutableVisionProperties = object : MutableVisionProperties {
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
meta.setMeta(name, node)
override fun set(name: Name, node: Meta?, notify: Boolean) {
meta[name] = node
}
override fun setValue(name: Name, value: Value?, notify: Boolean) {
@ -45,11 +45,11 @@ public class VisionOfPlotly private constructor(
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
override fun getMeta(
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): MutableMeta = meta.getMeta(name) ?: MutableMeta()
): MutableMeta = meta[name] ?: MutableMeta()
override fun getValue(
name: Name,

View File

@ -14,6 +14,7 @@ import io.ktor.server.util.*
import io.ktor.server.websocket.*
import io.ktor.util.pipeline.*
import io.ktor.websocket.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -31,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
public class VisionRoute(
public val route: String,
public val visionManager: VisionManager,
override val meta: ObservableMutableMeta = MutableMeta(),
override val meta: ObservableMutableMeta = ObservableMutableMeta(),
) : Configurable, ContextAware {
public enum class Mode {
@ -71,14 +72,11 @@ public class VisionRoute(
/**
* Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service.
*
* @return a [Flow] of backward events, including vision change events
*/
public fun Application.serveVisionData(
configuration: VisionRoute,
onEvent: suspend Vision.(VisionEvent) -> Unit = { event ->
if (event is VisionChangeEvent) {
update(event.change)
}
},
resolveVision: (Name) -> Vision?,
) {
require(WebSockets)
@ -102,16 +100,17 @@ public fun Application.serveVisionData(
val event = configuration.visionManager.jsonFormat.decodeFromString(
VisionEvent.serializer(), data
)
vision.onEvent(event)
vision.receiveEvent(event)
}
}
try {
withContext(configuration.context.coroutineContext) {
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { event ->
val json = configuration.visionManager.jsonFormat.encodeToString(
VisionChange.serializer(),
update
VisionEvent.serializer(),
event
)
application.log.debug("Sending update for $name: \n$json")
outgoing.send(Frame.Text(json))
@ -147,6 +146,8 @@ public fun Application.serveVisionData(
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
*
* @return a [Flow] containing backward propagated events, including vision change events
*/
public fun Application.visionPage(
route: String,
@ -154,7 +155,7 @@ public fun Application.visionPage(
headers: Collection<HtmlFragment>,
connector: EngineConnectorConfig? = null,
visionFragment: HtmlVisionFragment,
) {
){
require(WebSockets)
val collector: MutableMap<Name, Vision> = mutableMapOf()

View File

@ -34,33 +34,33 @@ public fun Vision.colorProperty(
ColorAccessor(properties.root(true), propertyName ?: property.name.asName())
}
public var ColorAccessor?.string: String?
get() = this?.value?.let { if (it == Null) null else it.string }
public var ColorAccessor.string: String?
get() = value?.let { if (it == Null) null else it.string }
set(value) {
this?.value = value?.asValue()
this.value = value?.asValue()
}
/**
* Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string
*/
public operator fun ColorAccessor?.invoke(webColor: String) {
this?.value = webColor.asValue()
public operator fun ColorAccessor.invoke(webColor: String) {
value = webColor.asValue()
}
/**
* Set color as RGB integer
*/
public operator fun ColorAccessor?.invoke(rgb: Int) {
this?.value = Colors.rgbToString(rgb).asValue()
public operator fun ColorAccessor.invoke(rgb: Int) {
value = Colors.rgbToString(rgb).asValue()
}
/**
* Set color as RGB
*/
public operator fun ColorAccessor?.invoke(r: UByte, g: UByte, b: UByte) {
this?.value = Colors.rgbToString(r, g, b).asValue()
public operator fun ColorAccessor.invoke(r: UByte, g: UByte, b: UByte) {
value = Colors.rgbToString(r, g, b).asValue()
}
public fun ColorAccessor?.clear() {
this?.value = null
public fun ColorAccessor.clear() {
value = null
}

View File

@ -39,7 +39,7 @@ public inline fun MutableVisionContainer<Solid>.composite(
}
val res = Composite(type, children[0], children[1])
res.properties.setMeta(Name.EMPTY, group.properties.own)
res.properties[Name.EMPTY] = group.properties.own
setChild(name, res)
return res

View File

@ -92,7 +92,7 @@ public class Extruded(
}
internal fun build(): Extruded = Extruded(shape, layers).apply {
this.properties.setMeta(Name.EMPTY, this@Builder.properties)
this.properties[Name.EMPTY] = this@Builder.properties
}
}

View File

@ -190,7 +190,7 @@ internal fun float32Vector(
override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) {
if (value == null) {
thisRef.properties.setMeta(name, null)
thisRef.properties[name] = null
} else {
thisRef.properties[name + X_KEY] = value.x
thisRef.properties[name + Y_KEY] = value.y

View File

@ -110,12 +110,12 @@ public val Solid.color: ColorAccessor
get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY)
public var Solid.material: SolidMaterial?
get() = SolidMaterial.read(properties.getMeta(MATERIAL_KEY))
set(value) = properties.setMeta(MATERIAL_KEY, value?.meta)
get() = SolidMaterial.read(properties[MATERIAL_KEY])
set(value) = properties.set(MATERIAL_KEY, value?.meta)
@VisionBuilder
public fun Solid.material(builder: SolidMaterial.() -> Unit) {
properties.getMeta(MATERIAL_KEY).updateWith(SolidMaterial, builder)
properties[MATERIAL_KEY].updateWith(SolidMaterial, builder)
}
public var Solid.opacity: Number?
@ -128,5 +128,5 @@ public var Solid.opacity: Number?
@VisionBuilder
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled
SolidMaterial.write(properties.getMeta(SolidMaterial.EDGES_MATERIAL_KEY)).apply(block)
SolidMaterial.write(properties[SolidMaterial.EDGES_MATERIAL_KEY]).apply(block)
}

View File

@ -162,7 +162,7 @@ internal class SolidReferenceChild(
override val properties: MutableVisionProperties = object : MutableVisionProperties {
override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor
override val own: MutableMeta by lazy { owner.properties.getMeta(childToken(childName).asName()) }
override val own: MutableMeta by lazy { owner.properties[childToken(childName).asName()] }
override fun getValue(
name: Name,
@ -170,8 +170,8 @@ internal class SolidReferenceChild(
includeStyles: Boolean?,
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
own.setMeta(name, node)
override fun set(name: Name, node: Meta?, notify: Boolean) {
own[name] = node
}
override fun setValue(name: Name, value: Value?, notify: Boolean) {

View File

@ -155,7 +155,7 @@ public class Surface(
}
internal fun build(): Surface = Surface(layers).apply {
properties.setMeta(Name.EMPTY, this@Builder.properties)
properties[Name.EMPTY] = this@Builder.properties
}
}

View File

@ -36,9 +36,9 @@ internal fun Meta.toVector2D(): Float32Vector2D =
//}
internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector(
getMeta(X_KEY).float ?: default,
getMeta(Y_KEY).float ?: default,
getMeta(Z_KEY).float ?: default
get(X_KEY).float ?: default,
get(Y_KEY).float ?: default,
get(Z_KEY).float ?: default
)

View File

@ -19,7 +19,7 @@ internal fun Solid.updateFrom(other: Solid): Solid {
scaleX *= other.scaleX
scaleY *= other.scaleY
scaleZ *= other.scaleZ
properties.setMeta(Name.EMPTY, other.properties.root())
properties[Name.EMPTY] = other.properties.root()
return this
}

View File

@ -94,7 +94,7 @@ class SolidPropertyTest {
}
}
}
assertEquals("#555555", box?.color.string)
assertEquals("#555555", box?.color?.string)
}
@Test

View File

@ -31,7 +31,7 @@ class SolidReferenceTest {
fun testReferenceSerialization(){
val serialized = Solids.jsonForSolids.encodeToJsonElement(groupWithReference)
val deserialized = Solids.jsonForSolids.decodeFromJsonElement(SolidGroup.serializer(), serialized)
assertEquals(groupWithReference.items["test"]?.color.string, deserialized.items["test"]?.color.string)
assertEquals(groupWithReference.items["test"]?.color?.string, deserialized.items["test"]?.color?.string)
assertEquals("blue", (deserialized.children.getChild("test") as Solid).color.string)
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val tablesVersion = "0.2.0-dev-4"
val tablesVersion = "0.3.0"
kscience {
jvm()
@ -22,8 +22,8 @@ kscience {
api("space.kscience:tables-kt:${tablesVersion}")
}
dependencies(jsMain) {
implementation(npm("tabulator-tables", "5.4.4"))
implementation(npm("@types/tabulator-tables", "5.4.8"))
implementation(npm("tabulator-tables", "5.5.2"))
implementation(npm("@types/tabulator-tables", "5.5.3"))
}
useSerialization()
}

View File

@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.int
import space.kscience.tables.ColumnHeader
import space.kscience.tables.ColumnTable
import space.kscience.tables.fill
import space.kscience.tables.get
import kotlin.math.pow
import kotlin.test.Test
@ -18,7 +19,7 @@ internal class VisionOfTableTest {
val y by ColumnHeader.typed<Value>()
val table = ColumnTable<Value>(100) {
x.fill { it.asValue() }
fill(x, null) { it.asValue() }
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
}

View File

@ -1,6 +1,6 @@
package space.kscience.visionforge.solid.three
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.startsWith
import space.kscience.visionforge.Vision
@ -17,7 +17,7 @@ import kotlin.reflect.KClass
/**
* Builder and updater for three.js object
*/
@Type(TYPE)
@DfType(TYPE)
public interface ThreeFactory<in T : Vision> {
public val type: KClass<in T>

View File

@ -24,7 +24,7 @@ public object ThreeLineFactory : ThreeFactory<PolyLine> {
}
val material = ThreeMaterials.getLineMaterial(
vision.properties.getMeta(SolidMaterial.MATERIAL_KEY),
vision.properties[SolidMaterial.MATERIAL_KEY],
false
)

View File

@ -83,7 +83,7 @@ public object ThreeMaterials {
private val visionMaterialCache = HashMap<Vision, Material>()
internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) {
buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY)).apply {
buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY]).apply {
cached = true
}
}
@ -133,11 +133,11 @@ public fun Mesh.setMaterial(vision: Vision) {
} else {
material = vision.parent?.let { parent ->
//TODO cache parent material
ThreeMaterials.buildMaterial(parent.properties.getMeta(SolidMaterial.MATERIAL_KEY))
ThreeMaterials.buildMaterial(parent.properties[SolidMaterial.MATERIAL_KEY])
} ?: ThreeMaterials.cacheMaterial(vision)
}
} else {
material = ThreeMaterials.buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY))
material = ThreeMaterials.buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY])
}
}
@ -153,18 +153,18 @@ public fun Mesh.updateMaterialProperty(vision: Vision, propertyName: Name) {
when (propertyName) {
SolidMaterial.MATERIAL_COLOR_KEY -> {
material.asDynamic().color =
vision.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).threeColor()
vision.properties[SolidMaterial.MATERIAL_COLOR_KEY].threeColor()
?: ThreeMaterials.DEFAULT_COLOR
}
SolidMaterial.SPECULAR_COLOR_KEY -> {
material.asDynamic().specular =
vision.properties.getMeta(SolidMaterial.SPECULAR_COLOR_KEY).threeColor()
vision.properties[SolidMaterial.SPECULAR_COLOR_KEY].threeColor()
?: ThreeMaterials.DEFAULT_COLOR
}
SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> {
material.asDynamic().emissive = vision.properties.getMeta(SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY)
material.asDynamic().emissive = vision.properties[SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY]
.threeColor()
?: ThreeMaterials.BLACK_COLOR
}

View File

@ -76,7 +76,7 @@ public fun Mesh.applyEdges(vision: Solid) {
val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments
//inherited edges definition, enabled by default
if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) {
val material = ThreeMaterials.getLineMaterial(vision.properties.getMeta(EDGES_MATERIAL_KEY), true)
val material = ThreeMaterials.getLineMaterial(vision.properties[EDGES_MATERIAL_KEY], true)
if (edges == null) {
add(
LineSegments(