forked from kscience/visionforge
Add compose-html
This commit is contained in:
parent
ed71ba9ccb
commit
e6bdb67262
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,10 +21,7 @@ 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.dataforge.names.*
|
||||
import space.kscience.visionforge.hidden
|
||||
|
||||
|
||||
@ -39,19 +36,17 @@ 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
|
||||
*/
|
||||
@Composable
|
||||
private fun PropertyEditorItem(
|
||||
/**
|
||||
* Root config object - always non-null
|
||||
*/
|
||||
public fun PropertyEditor(
|
||||
scope: CoroutineScope,
|
||||
meta: MutableMeta,
|
||||
getPropertyState: (Name) -> EditorPropertyState,
|
||||
scope: CoroutineScope,
|
||||
updates: Flow<Name>,
|
||||
name: Name,
|
||||
rootDescriptor: MetaDescriptor?,
|
||||
name: Name = Name.EMPTY,
|
||||
rootDescriptor: MetaDescriptor? = null,
|
||||
initialExpanded: Boolean? = null,
|
||||
) {
|
||||
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
|
||||
@ -145,7 +140,7 @@ private fun PropertyEditorItem(
|
||||
Div({
|
||||
classes(TreeStyles.treeItem)
|
||||
}) {
|
||||
PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded)
|
||||
PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +154,8 @@ public fun PropertyEditor(
|
||||
descriptor: MetaDescriptor? = null,
|
||||
expanded: Boolean? = null,
|
||||
) {
|
||||
PropertyEditorItem(
|
||||
PropertyEditor(
|
||||
scope = scope,
|
||||
meta = properties,
|
||||
getPropertyState = { name ->
|
||||
if (properties[name] != null) {
|
||||
@ -170,7 +166,6 @@ public fun PropertyEditor(
|
||||
EditorPropertyState.Undefined
|
||||
}
|
||||
},
|
||||
scope = scope,
|
||||
updates = callbackFlow {
|
||||
properties.onChange(scope) { name ->
|
||||
scope.launch {
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) }
|
@ -1,6 +1,7 @@
|
||||
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,
|
||||
@ -33,3 +34,11 @@ public fun StyleScope.marginAll(
|
||||
) {
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user