diff --git a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt index 29be7aca..2740b1e5 100644 --- a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt +++ b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/NameCrumbs.kt @@ -17,7 +17,7 @@ public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({ classes("breadcrumb") style { property("--bs-breadcrumb-divider", "'.'") - property("--bs-breadcrumb-item-padding-x",".2rem") + property("--bs-breadcrumb-item-padding-x",".1rem") } }) { Li({ diff --git a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt index 0892b9af..cff16298 100644 --- a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt +++ b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt @@ -1,16 +1,16 @@ package space.kscience.visionforge.compose import androidx.compose.runtime.* +import app.softwork.bootstrapcompose.CloseButton import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose 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 @@ -35,13 +35,13 @@ public sealed class EditorPropertyState { } /** - * @param meta Root config object - always non-null - * @param rootDescriptor Full path to the displayed node in [meta]. Could be empty + * @param rootMeta Root config object - always non-null + * @param rootDescriptor Full path to the displayed node in [rootMeta]. Could be empty */ @Composable public fun PropertyEditor( scope: CoroutineScope, - meta: MutableMeta, + rootMeta: MutableMeta, getPropertyState: (Name) -> EditorPropertyState, updates: Flow, name: Name = Name.EMPTY, @@ -50,11 +50,11 @@ public fun PropertyEditor( ) { 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 property: MutableMeta by remember { mutableStateOf(rootMeta.getOrCreate(name)) } var editorPropertyState: EditorPropertyState by remember { mutableStateOf(getPropertyState(name)) } - val keys = remember(descriptor) { + val keys by derivedStateOf { buildSet { descriptor?.children?.filterNot { it.key.startsWith("@") || it.value.hidden @@ -68,11 +68,11 @@ public fun PropertyEditor( val token = name.lastOrNull()?.toString() ?: "Properties" fun update() { - property = meta.getOrCreate(name) + property = rootMeta.getOrCreate(name) editorPropertyState = getPropertyState(name) } - LaunchedEffect(meta) { + LaunchedEffect(rootMeta) { updates.collect { updatedName -> if (updatedName == name) { update() @@ -116,18 +116,9 @@ public fun PropertyEditor( } } - Button({ - classes(TreeStyles.propertyEditorButton) - if (editorPropertyState != EditorPropertyState.Defined) { - disabled() - } else { - onClick { - meta.remove(name) - update() - } - } - }) { - Text("\u00D7") + CloseButton(editorPropertyState != EditorPropertyState.Defined){ + rootMeta.remove(name) + update() } } } @@ -139,7 +130,7 @@ public fun PropertyEditor( Div({ classes(TreeStyles.treeItem) }) { - PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded) + PropertyEditor(scope, rootMeta, getPropertyState, updates, name + token, rootDescriptor, expanded) } } } @@ -155,7 +146,7 @@ public fun PropertyEditor( ) { PropertyEditor( scope = scope, - meta = properties, + rootMeta = properties, getPropertyState = { name -> if (properties[name] != null) { EditorPropertyState.Defined @@ -172,9 +163,7 @@ public fun PropertyEditor( } } - invokeOnClose { - properties.removeListener(scope) - } + awaitClose { properties.removeListener(scope) } }, name = Name.EMPTY, rootDescriptor = descriptor, diff --git a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt index ce953304..6073cdda 100644 --- a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt +++ b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/Tabs.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.* import app.softwork.bootstrapcompose.Card import app.softwork.bootstrapcompose.NavbarLink import app.softwork.bootstrapcompose.Styling +import org.jetbrains.compose.web.css.overflowY import org.jetbrains.compose.web.dom.* import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLDivElement @@ -51,6 +52,11 @@ public fun Tabs( } } } + }, + bodyAttrs = { + style { + overflowY("auto") + } } ) { activeTab?.content?.invoke(this) diff --git a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt index 2de59f90..39a4d669 100644 --- a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt +++ b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/VisionTree.kt @@ -4,7 +4,6 @@ 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 @@ -15,8 +14,6 @@ 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 @@ -35,10 +32,6 @@ private fun TreeLabel( style { color(Color("#069")) cursor("pointer") - hover.invoke { - textDecorationLine("underline") - } - } onClick { clickCallback(name) } }) { diff --git a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt index cf1ad9f8..f0f2451c 100644 --- a/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt +++ b/visionforge-compose-html/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt @@ -4,14 +4,10 @@ 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.* @@ -29,20 +25,16 @@ public fun StringValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { - var stringValue by remember { mutableStateOf(value?.string ?: "") } + var stringValue by remember(value, descriptor) { mutableStateOf(value?.string ?: "") } Input(type = InputType.Text) { - style { - width(100.percent) - } + classes("w-100") value(stringValue) - onKeyDown { event -> - if (event.type == "keydown" && event.asDynamic().key == "Enter") { - stringValue = (event.target as HTMLInputElement).value - onValueChange(stringValue.asValue()) - } + onChange { event -> + stringValue = event.value } - onChange { - stringValue = it.target.value + onInput { event -> + stringValue = event.value + onValueChange(event.value.asValue()) } } } @@ -55,16 +47,18 @@ public fun BooleanValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { + var innerValue by remember(value, descriptor) { + mutableStateOf( + value?.boolean ?: descriptor?.defaultValue?.boolean + ) + } Input(type = InputType.Checkbox) { - style { - width(100.percent) - } - //this.attributes["indeterminate"] = (props.item == null).toString() - checked(value?.boolean ?: false) + classes("w-100") + checked(innerValue ?: false) - onChange { - val newValue = it.target.checked - onValueChange(newValue.asValue()) + onInput { event -> + innerValue = event.value + onValueChange(event.value.asValue()) } } } @@ -76,25 +70,18 @@ public fun NumberValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { - var innerValue by remember { mutableStateOf(value?.string ?: "") } + var innerValue by remember(value, descriptor) { mutableStateOf(value?.number) } Input(type = InputType.Number) { - style { - width(100.percent) + classes("w-100") + + value(innerValue ?: descriptor?.defaultValue?.number ?: 0.0) + + onChange { event -> + innerValue = event.value } - 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 + onInput { event -> + innerValue = event.value + onValueChange(event.value?.asValue()) } descriptor?.attributes?.get("step").number?.let { step(it) @@ -116,11 +103,10 @@ public fun ComboValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { - var selected by remember { mutableStateOf(value?.string ?: "") } + var selected by remember(value, descriptor) { mutableStateOf(value?.string ?: "") } Select({ - style { - width(100.percent) - } + classes("w-100") + onChange { selected = it.target.value onValueChange(selected.asValue()) @@ -142,11 +128,11 @@ public fun ColorValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { + var innerValue by remember { mutableStateOf(value?.string ?: descriptor?.defaultValue?.string) } + Input(type = InputType.Color) { - style { - width(100.percent) - marginAll(0.px) - } + classes("w-100") + value( value?.let { value -> if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) @@ -154,8 +140,12 @@ public fun ColorValueChooser( //else "#" + Color(value.string).getHexString() } ?: "#000000" ) - onChange { - onValueChange(it.target.value.asValue()) + onChange { event -> + innerValue = event.value + } + onInput { event -> + innerValue = event.value + onValueChange(event.value.asValue()) } } } @@ -194,7 +184,7 @@ public fun RangeValueChooser( value: Value?, onValueChange: (Value?) -> Unit, ) { - var innerValue by remember { mutableStateOf(value?.double) } + var innerValue by remember(value, descriptor) { mutableStateOf(value?.double) } var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) } @@ -219,9 +209,8 @@ public fun RangeValueChooser( } Input(type = InputType.Range) { - style { - width(100.percent) - } + classes("w-100") + if (rangeDisabled) disabled() value(innerValue?.toString() ?: "") onChange { diff --git a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt index 245953e0..f2e890b1 100644 --- a/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt +++ b/visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three/compose/ThreeView.kt @@ -14,7 +14,6 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.isEmpty -import space.kscience.visionforge.Vision import space.kscience.visionforge.compose.* import space.kscience.visionforge.root import space.kscience.visionforge.solid.Solid @@ -81,15 +80,6 @@ public fun ThreeView( } } - val selectedVision: Vision? by derivedStateOf { - selected?.let { - when { - it.isEmpty() -> solid - else -> (solid as? SolidGroup)?.get(it) - } - } - } - if (optionsSnapshot.controls.enabled) { Row( @@ -147,47 +137,57 @@ public fun ThreeView( SimpleThreeView(solids.context, optionsSnapshot, solid, selected) } - selectedVision?.let { vision -> - Card( - attrs = { - style { - position(Position.Absolute) - top(5.px) - right(5.px) - width(450.px) - } - }, - headerAttrs = { - // border = true - }, - header = { - NameCrumbs(selected) { selected = it } - }, - footer = { - vision.styles.takeIf { it.isNotEmpty() }?.let { styles -> - P { - B { Text("Styles: ") } - Text(styles.joinToString(separator = ", ")) - } - } + key(selected) { + selected?.let { + when { + it.isEmpty() -> solid + else -> (solid as? SolidGroup)?.get(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 + }?.let { vision -> + Card( + attrs = { + style { + position(Position.Absolute) + top(5.px) + right(5.px) + width(450.px) + overflowY("auto") } }, - updates = vision.properties.changes, - rootDescriptor = vision.descriptor - ) + headerAttrs = { + style { + alignItems(AlignItems.Center) + } + }, + header = { + NameCrumbs(selected) { selected = it } + }, + footer = { + vision.styles.takeIf { it.isNotEmpty() }?.let { styles -> + P { + B { Text("Styles: ") } + Text(styles.joinToString(separator = ", ")) + } + } + } + ) { + PropertyEditor( + scope = solids.context, + rootMeta = 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 + ) + } } } } @@ -204,7 +204,6 @@ public fun ThreeView( paddingAll(4.px) minWidth(400.px) height(100.percent) - overflowY("auto") } } ) {