diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt new file mode 100644 index 00000000..113d77ba --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt @@ -0,0 +1,24 @@ +package hep.dataforge.properties + +import hep.dataforge.meta.Config +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.get +import hep.dataforge.names.Name + +class ConfigProperty(val config: Config, val name: Name) : Property?> { + override var value: MetaItem<*>? + get() = config[name] + set(value) { + config[name] = value + } + + override fun onChange(owner: Any?, callback: (MetaItem<*>?) -> Unit) { + config.onChange(owner) { name, oldItem, newItem -> + if (name == this.name && oldItem != newItem) callback(newItem) + } + } + + override fun removeChangeListener(owner: Any?) { + config.removeListener(owner) + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt new file mode 100644 index 00000000..fdd37132 --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt @@ -0,0 +1,57 @@ +package hep.dataforge.properties + +import hep.dataforge.meta.DFExperimental +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +//TODO move to core + +interface Property { + var value: T + + fun onChange(owner: Any? = null, callback: (T) -> Unit) + fun removeChangeListener(owner: Any? = null) +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun Property.flow() = callbackFlow { + send(value) + onChange(this) { + //TODO add exception handler? + launch { + send(it) + } + } + awaitClose { removeChangeListener(this) } +} + +/** + * Reflect all changes in the [source] property onto this property + * + * @return a mirroring job + */ +fun Property.mirror(source: Property, scope: CoroutineScope): Job { + return scope.launch { + source.flow().collect { + value = it + } + } +} + +/** + * Bi-directional connection between properties + */ +@DFExperimental +fun Property.bind(other: Property) { + onChange(other) { + other.value = it + } + other.onChange { + this.value = it + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt new file mode 100644 index 00000000..48d373d7 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt @@ -0,0 +1,30 @@ +package hep.dataforge.js + +import hep.dataforge.properties.Property +import org.w3c.dom.HTMLInputElement + +fun HTMLInputElement.bindValue(property: Property) { + if (this.onchange != null) error("Input element already bound") + this.onchange = { + property.value = this.value + Unit + } + property.onChange(this) { + if (value != it) { + value = it + } + } +} + +fun HTMLInputElement.bindChecked(property: Property) { + if (this.onchange != null) error("Input element already bound") + this.onchange = { + property.value = this.checked + Unit + } + property.onChange(this) { + if (checked != it) { + checked = it + } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt index eb99704a..9c3b15f7 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt @@ -16,9 +16,11 @@ inline fun TagConsumer.card(title: String, crossinline block: TagCo } inline fun RBuilder.card(title: String, crossinline block: RBuilder.() -> Unit) { - div("card w-100") { + div("card w-100 h-100") { div("card-body") { - h3(classes = "card-title") { +title } + h3(classes = "card-title") { + +title + } block() } } @@ -78,7 +80,7 @@ fun RBuilder.accordion(id: String, elements: List. elements.forEachIndexed { index, (title, builder) -> val headerID = "${id}-${index}-heading" val collapseID = "${id}-${index}-collapse" - div("card") { + div("card p-0 m-0") { div("card-header") { attrs { this.id = headerID diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt index 0138393c..1ed93464 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt @@ -22,7 +22,7 @@ fun

component( } } -fun RFBuilder.initState(init: () -> T): ReadWriteProperty = +fun RFBuilder.state(init: () -> T): ReadWriteProperty = object : ReadWriteProperty { val pair = react.useState(init) override fun getValue(thisRef: Any?, property: KProperty<*>): T { diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt index 1a985f40..6da696f6 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt @@ -2,14 +2,12 @@ package hep.dataforge.vis.editor import hep.dataforge.js.RFBuilder import hep.dataforge.js.component -import hep.dataforge.js.initState -import hep.dataforge.js.memoize +import hep.dataforge.js.state import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.plus -import hep.dataforge.values.Value import kotlinx.html.classes import kotlinx.html.js.onClickFunction import org.w3c.dom.Element @@ -41,15 +39,15 @@ interface ConfigEditorProps : RProps { } private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { - var expanded: Boolean by initState { true } - val item = memoize(props.root, props.name) { props.root[props.name] } - val descriptorItem: ItemDescriptor? = memoize(props.descriptor, props.name) { props.descriptor?.get(props.name) } - val defaultItem = memoize(props.default, props.name) { props.default?.get(props.name) } + var expanded: Boolean by state { true } + val item = props.root[props.name] + val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) + val defaultItem = props.default?.get(props.name) val actualItem: MetaItem? = item ?: defaultItem ?: descriptorItem?.defaultItem() val token = props.name.last()?.toString() ?: "Properties" - var kostyl by initState { false } + var kostyl by state { false } fun update() { kostyl = !kostyl @@ -73,20 +71,6 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { update() } - val valueChanged: (Value?) -> Unit = { value -> - try { - if (value == null) { - props.root.remove(props.name) - } else { - props.root.setValue(props.name, value) - } - update() - } catch (ex: Exception) { - console.error("Can't set config property ${props.name} to $value") - } - } - - when (actualItem) { is MetaItem.NodeItem -> { div { @@ -127,37 +111,30 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { } is MetaItem.ValueItem -> { div { - div("row") { - div("col") { - p("tree-label") { - +token + div("d-flex flex-row align-items-center") { + div("flex-grow-1 p-1 mr-auto tree-label") { + +token + attrs { + if (item == null) { + classes += "tree-label-inactive" + } + } + } + div("d-inline-flex") { + valueChooser(props.root, props.name, actualItem.value, descriptorItem as? ValueDescriptor) + } + div("d-inline-flex p-1") { + button(classes = "btn btn-link") { + +"\u00D7" attrs { if (item == null) { - classes += "tree-label-inactive" + disabled = true + } else { + onClickFunction = removeClick } } } } - div("col") { - child(ValueChooser) { - attrs { - this.value = actualItem.value - this.descriptor = descriptorItem as? ValueDescriptor - this.valueChanged = valueChanged - } - } - } - button(classes = "btn btn-link") { - +"\u00D7" - attrs { - if (item == null) { - disabled = true - } else { - onClickFunction = removeClick - } - } - } - } } } diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt index 54891cc0..af4faaa9 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt @@ -3,7 +3,7 @@ package hep.dataforge.vis.editor import hep.dataforge.js.RFBuilder import hep.dataforge.js.card import hep.dataforge.js.component -import hep.dataforge.js.initState +import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.startsWith @@ -29,7 +29,7 @@ interface TreeState : RState { } private fun RFBuilder.objectTree(props: ObjectTreeProps): Unit { - var expanded: Boolean by initState{ props.selected?.startsWith(props.name) ?: false } + var expanded: Boolean by state{ props.selected?.startsWith(props.name) ?: false } val onClick: (Event) -> Unit = { expanded = !expanded diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt similarity index 56% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt rename to dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt index 642c7ee4..9b71a03a 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt @@ -1,9 +1,11 @@ package hep.dataforge.vis.editor -import hep.dataforge.js.component +import hep.dataforge.meta.Config import hep.dataforge.meta.descriptors.ValueDescriptor import hep.dataforge.meta.get +import hep.dataforge.meta.setValue import hep.dataforge.meta.string +import hep.dataforge.names.Name import hep.dataforge.values.* import hep.dataforge.vis.widgetType import kotlinx.html.InputType @@ -11,51 +13,10 @@ import kotlinx.html.js.onChangeFunction import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLSelectElement import org.w3c.dom.events.Event -import react.RProps -import react.RState -import react.dom.div -import react.dom.input -import react.dom.option -import react.dom.select - -interface ValueChooserProps : RProps { - var value: Value - var descriptor: ValueDescriptor? - var valueChanged: (Value?) -> Unit -} - -interface ValueChooserState : RState { - var value: Value -} - -//class TextValueChooser(props: ValueChooserProps) : RComponent(props) { -// -// override fun ValueChooserState.init(props: ValueChooserProps) { -// this.value = props.value -// } -// -// val valueChanged: (Event) -> Unit = { -// val res = (it.target as HTMLInputElement).value.asValue() -// setState { -// this.value = res -// } -// props.valueChanged(res) -// } -// -// override fun RBuilder.render() { -// input(type = InputType.text, classes = "float-right") { -// attrs { -// this.value = state.value.string -// onChangeFunction = valueChanged -// } -// } -// } -//} - -val ValueChooser = component { props -> -// var state by initState {props.value } - val descriptor = props.descriptor +import react.RBuilder +import react.dom.* +internal fun RBuilder.valueChooser(root: Config, name: Name, value: Value, descriptor: ValueDescriptor?) { val onValueChange: (Event) -> Unit = { val res = when (val t = it.target) { // (it.target as HTMLInputElement).value @@ -67,22 +28,26 @@ val ValueChooser = component { props -> is HTMLSelectElement -> t.value.asValue() else -> error("Unknown event target: $t") } -// state = res - props.valueChanged(res) + + try { + root.setValue(name, res) + } catch (ex: Exception) { + console.error("Can't set config property ${name} to $res") + } } - div { + div() { val type = descriptor?.type?.firstOrNull() when { type == ValueType.BOOLEAN -> { - input(type = InputType.checkBox, classes = "float-right") { + input(type = InputType.checkBox) { attrs { - checked = props.value.boolean + checked = value.boolean onChangeFunction = onValueChange } } } - type == ValueType.NUMBER -> input(type = InputType.number, classes = "float-right") { + type == ValueType.NUMBER -> input(type = InputType.number, classes = "form-control w-100") { attrs { descriptor.attributes["step"].string?.let { step = it @@ -93,11 +58,11 @@ val ValueChooser = component { props -> descriptor.attributes["max"].string?.let { max = it } - this.value = props.value.string + this.defaultValue = value.string onChangeFunction = onValueChange } } - descriptor?.allowedValues?.isNotEmpty() ?: false -> select("float-right") { + descriptor?.allowedValues?.isNotEmpty() ?: false -> select (classes = "w-100") { descriptor!!.allowedValues.forEach { option { +it.string @@ -108,19 +73,18 @@ val ValueChooser = component { props -> onChangeFunction = onValueChange } } - descriptor?.widgetType == "color" -> input(type = InputType.color, classes = "float-right") { + descriptor?.widgetType == "color" -> input(type = InputType.color) { attrs { - this.value = props.value.string + this.value = value.string onChangeFunction = onValueChange } } - else -> input(type = InputType.text, classes = "float-right") { + else -> input(type = InputType.text, classes = "form-control w-100") { attrs { - this.value = props.value.string + this.value = value.string onChangeFunction = onValueChange } } } } - } \ No newline at end of file diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt index a8cb0a11..71766435 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt @@ -56,12 +56,10 @@ class Material3D : Scheme() { defineValue(OPACITY_KEY) { type(ValueType.NUMBER) default(1.0) - configure { - "attributes" to { - this["min"] = 0.0 - this["max"] = 1.0 - this["step"] = 0.1 - } + config["attributes"] = Meta { + this["min"] = 0.0 + this["max"] = 1.0 + this["step"] = 0.1 } } defineValue(WIREFRAME_KEY) { diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt index 1a4c5360..8cd0369d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt @@ -68,18 +68,13 @@ interface VisualObject3D : VisualObject { default(true) } - defineItem(Material3D.MATERIAL_KEY.toString(), Material3D.descriptor) - //TODO replace by descriptor merge defineValue(VisualObject.STYLE_KEY){ type(ValueType.STRING) multiple = true } -// Material3D.MATERIAL_COLOR_KEY put "#ffffff" -// Material3D.MATERIAL_OPACITY_KEY put 1.0 -// Material3D.MATERIAL_WIREFRAME_KEY put false - + defineItem(Material3D.MATERIAL_KEY.toString(), Material3D.descriptor) } } } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt index b7fcae95..0a49e96a 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt @@ -109,10 +109,10 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv element.appendChild(renderer.domElement) - renderer.setSize(max(canvas.minSize, element.offsetWidth), max(canvas.minSize, element.offsetWidth)) + renderer.setSize(max(canvas.minSize, element.clientWidth), max(canvas.minSize, element.clientWidth)) - element.onresize = { - renderer.setSize(element.offsetWidth, element.offsetWidth) + window.onresize = { + renderer.setSize(element.clientWidth, element.clientWidth) camera.updateProjectionMatrix() } diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index 87f3f124..bf05be52 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -3,7 +3,7 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.context.Context import hep.dataforge.js.card import hep.dataforge.js.component -import hep.dataforge.js.initState +import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.isEmpty @@ -40,17 +40,21 @@ private val canvasConfig = Canvas { } val MMApp = component { props -> - var selected by initState { props.selected } - var canvas: ThreeCanvas? by initState { null } + var selected by state { props.selected } + var canvas: ThreeCanvas? by state { null } val select: (Name?) -> Unit = { selected = it } val visual = props.model.root - div("row") { - div("col-lg-3") { + h1("mx-auto") { + +"Muon monitor demo" + } + } + div("row") { + div("col-lg-3 mh-100 px-0") { //tree card("Object tree") { objectTree(visual, selected, select) @@ -156,5 +160,6 @@ val MMApp = component { props -> } } } + } } \ No newline at end of file diff --git a/demo/muon-monitor/src/jsMain/resources/index.html b/demo/muon-monitor/src/jsMain/resources/index.html index 9d5118c6..a229b2a9 100644 --- a/demo/muon-monitor/src/jsMain/resources/index.html +++ b/demo/muon-monitor/src/jsMain/resources/index.html @@ -11,25 +11,6 @@ - -

-

Muon monitor demo

-
- -
-
- -
+
\ No newline at end of file