Add compose-html

This commit is contained in:
Alexander Nozik 2023-11-20 10:03:44 +03:00
parent f0048a4d46
commit ed71ba9ccb
9 changed files with 612 additions and 99 deletions

View File

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

View File

@ -1,11 +1,12 @@
package space.kscience.visionforge.compose package space.kscience.visionforge.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import kotlinx.html.js.onClickFunction
import org.jetbrains.compose.web.css.AlignItems import org.jetbrains.compose.web.css.AlignItems
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.Span
import org.w3c.dom.events.Event import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.get import space.kscience.dataforge.meta.descriptors.get
@ -17,10 +18,6 @@ import space.kscience.dataforge.names.lastOrNull
import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.plus
private val MetaViewerItem: FC<MetaViewerProps> = fc("MetaViewerItem") { props ->
metaViewerItem(props)
}
@Composable @Composable
private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) { private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) {
var expanded: Boolean by remember { mutableStateOf(true) } var expanded: Boolean by remember { mutableStateOf(true) }
@ -29,11 +26,7 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto
val actualValue = item?.value ?: descriptorItem?.defaultValue val actualValue = item?.value ?: descriptorItem?.defaultValue
val actualMeta = item ?: descriptorItem?.defaultNode val actualMeta = item ?: descriptorItem?.defaultNode
val token = name.lastOrNull()?.toString() ?: props.rootName ?: "" val token = name.lastOrNull()?.toString() ?: ""
val expanderClick: (Event) -> Unit = {
expanded = !expanded
}
FlexRow(attrs = { FlexRow(attrs = {
classes("metaItem") classes("metaItem")
@ -42,42 +35,34 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto
} }
}) { }) {
if (actualMeta?.isLeaf == false) { if (actualMeta?.isLeaf == false) {
Span(attrs = { Span({
classes(TreeStyles.treeCaret)
if (expanded) {
classes(TreeStyles.treeCaretDown)
}
onClick { expanded = !expanded }
}) })
styledSpan {
css {
+TreeStyles.treeCaret
if (expanded) {
+TreeStyles.treeCaredDown
}
}
attrs {
onClickFunction = expanderClick
}
}
} }
styledSpan { Span({
css { classes(TreeStyles.treeLabel)
+TreeStyles.treeLabel
if (item == null) { if (item == null) {
+TreeStyles.treeLabelInactive classes(TreeStyles.treeLabelInactive)
} }
}) {
Text(token)
} }
+token
} Div {
styledDiv { A {
a { Text(actualValue.toString())
+actualValue.toString()
} }
} }
} }
if (expanded) { if (expanded) {
flexColumn { FlexColumn({
css { classes(TreeStyles.tree)
+TreeStyles.tree }) {
}
val keys = buildSet { val keys = buildSet {
descriptorItem?.children?.keys?.forEach { descriptorItem?.children?.keys?.forEach {
add(NameToken(it)) add(NameToken(it))
@ -86,45 +71,17 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto
} }
keys.filter { !it.body.startsWith("@") }.forEach { token -> keys.filter { !it.body.startsWith("@") }.forEach { token ->
styledDiv { Div({
css { classes(TreeStyles.treeItem)
+TreeStyles.treeItem }) {
MetaViewerItem(root, name + token, rootDescriptor)
} }
child(MetaViewerItem) {
attrs {
this.key = props.name.toString()
this.root = props.root
this.name = props.name + token
this.descriptor = props.descriptor
}
}
//configEditor(props.root, props.name + token, props.descriptor, props.default)
} }
} }
} }
} }
@Composable
} public fun MetaViewer(meta: Meta, descriptor: MetaDescriptor? = null) {
MetaViewerItem(meta, Name.EMPTY, descriptor)
@JsExport
public val MetaViewer: FC<MetaViewerProps> = fc("MetaViewer") { props ->
child(MetaViewerItem) {
attrs {
this.key = ""
this.root = props.root
this.name = Name.EMPTY
this.descriptor = props.descriptor
}
}
}
public fun RBuilder.metaViewer(meta: Meta, descriptor: MetaDescriptor? = null, key: Any? = null) {
child(MetaViewer) {
attrs {
this.key = key?.toString() ?: ""
this.root = meta
this.descriptor = descriptor
}
}
} }

View File

@ -0,0 +1,189 @@
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.ValueRequirement
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.remove
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.lastOrNull
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 rootDescriptor Full path to the displayed node in [meta]. Could be empty
*/
@Composable
private fun PropertyEditorItem(
/**
* Root config object - always non-null
*/
meta: MutableMeta,
getPropertyState: (Name) -> EditorPropertyState,
scope: CoroutineScope,
updates: Flow<Name>,
name: Name,
rootDescriptor: MetaDescriptor?,
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?.valueRequirement != ValueRequirement.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)
}) {
PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded)
}
}
}
}
}
@Composable
public fun PropertyEditor(
scope: CoroutineScope,
properties: ObservableMutableMeta,
descriptor: MetaDescriptor? = null,
expanded: Boolean? = null,
) {
PropertyEditorItem(
meta = properties,
getPropertyState = { name ->
if (properties[name] != null) {
EditorPropertyState.Defined
} else if (descriptor?.get(name)?.defaultValue != null) {
EditorPropertyState.Default("descriptor")
} else {
EditorPropertyState.Undefined
}
},
scope = scope,
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,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

@ -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

@ -1,8 +1,10 @@
package space.kscience.visionforge.compose package space.kscience.visionforge.compose
import kotlinx.css.* import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
@OptIn(ExperimentalComposeWebApi::class)
public object TreeStyles : StyleSheet() { public object TreeStyles : StyleSheet() {
/** /**
* Remove default bullets * Remove default bullets
@ -16,12 +18,12 @@ public object TreeStyles : StyleSheet() {
/** /**
* Style the caret/arrow * Style the caret/arrow
*/ */
public val treeCaret by style { public val treeCaret: String by style {
cursor("pointer") cursor("pointer")
userSelect = UserSelect.none userSelect(UserSelect.none)
/* Create the caret/arrow with a unicode, and style it */ /* Create the caret/arrow with a unicode, and style it */
before { before {
content = "\u25B6".quoted content("\u25B6")
color(Color.black) color(Color.black)
display(DisplayStyle.InlineBlock) display(DisplayStyle.InlineBlock)
marginRight(6.px) marginRight(6.px)
@ -31,9 +33,9 @@ public object TreeStyles : StyleSheet() {
/** /**
* Rotate the caret/arrow icon when clicked on (using JavaScript) * Rotate the caret/arrow icon when clicked on (using JavaScript)
*/ */
public val treeCaredDown by style { public val treeCaretDown: String by style {
before { before {
content = "\u25B6".quoted content("\u25B6")
color(Color.black) color(Color.black)
display(DisplayStyle.InlineBlock) display(DisplayStyle.InlineBlock)
marginRight(6.px) marginRight(6.px)
@ -53,19 +55,40 @@ public object TreeStyles : StyleSheet() {
} }
} }
public val treeLabel by style { public val treeLabel: String by style {
border(style = LineStyle.None) border(style = LineStyle.None)
padding(left = 4.pt, right = 4.pt, top = 0.pt, bottom = 0.pt) paddingAll(left = 4.pt, right = 4.pt)
textAlign("left") textAlign("left")
flex(1) flex(1)
} }
public val treeLabelInactive: RuleSet by css { public val treeLabelInactive: String by style {
color = Color.lightGray color(Color.lightgray)
} }
public val treeLabelSelected: RuleSet by css { public val treeLabelSelected: String by style {
backgroundColor = Color.lightBlue 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,35 @@
package space.kscience.visionforge.compose
import org.jetbrains.compose.web.css.*
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)
}

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.ValueRequirement
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?.valueRequirement != ValueRequirement.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

@ -47,7 +47,7 @@ public interface Vision : Described {
} }
public fun onMetaEvent(meta: Meta){ public fun onMetaEvent(meta: Meta){
//Do nothing by default
} }
/** /**