refactor compose-html

This commit is contained in:
Alexander Nozik 2024-02-12 20:04:10 +03:00
parent 38d6a9c419
commit d90c1edc6c
21 changed files with 144 additions and 89 deletions

View File

@ -30,7 +30,6 @@ kscience {
commonMain {
implementation(projects.visionforgeSolid)
implementation(projects.visionforgeComposeHtml)
}
jvmMain {
implementation("org.apache.commons:commons-math3:3.6.1")
@ -40,12 +39,15 @@ kscience {
implementation("ch.qos.logback:logback-classic:1.2.11")
}
jsMain {
// implementation(projects.visionforgeComposeHtml)
implementation(projects.visionforgeThreejs)
//implementation(devNpm("webpack-bundle-analyzer", "4.4.0"))
}
}
kotlin{
explicitApi = null
}
kotlin.explicitApi = null
application {
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")

View File

@ -4,9 +4,8 @@ plugins {
}
kscience {
jvm()
js()
// wasm()
jvm()
}
kotlin {
@ -15,23 +14,22 @@ kotlin {
commonMain {
dependencies {
api(projects.visionforgeCore)
}
}
jvmMain{
//need this to placate compose compiler in MPP applications
dependencies{
api(compose.runtime)
}
}
val jvmMain by getting {
jsMain{
dependencies {
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(compose.runtime)
api(compose.html.core)
}
}
}

View File

@ -9,20 +9,19 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
/**
* An [ElementVisionRenderer] that could be used directly in Compose-html as well as a stand-alone renderer
*/
public interface ComposeVisionRenderer: ElementVisionRenderer {
public interface ComposeHtmlVisionRenderer : ElementVisionRenderer {
@Composable
public fun DOMScope<Element>.render(client: VisionClient, name: Name, vision: Vision, meta: Meta)
public fun DOMScope<Element>.render(name: Name, vision: Vision, meta: Meta)
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
renderComposable(element) {
Style(VisionForgeStyles)
render(client, name, vision, meta)
render(name, vision, meta)
}
}

View File

@ -48,7 +48,7 @@ public fun Vision(
}
DisposableEffect(vision, name, renderer, meta) {
renderer.render(scopeElement, client, name, vision, meta)
renderer.render(scopeElement, name, vision, meta)
onDispose {
scopeElement.clear()
}

View File

@ -5,7 +5,7 @@ plugins {
kscience {
jvm()
wasm()
// wasm()
}
kotlin {
@ -16,6 +16,10 @@ kotlin {
api(projects.visionforgeCore)
api(compose.runtime)
api(compose.foundation)
}
}
jvmMain{
dependencies{
api(compose.material)
api(compose.preview)
}

View File

@ -0,0 +1,15 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
public interface ComposeVisionRenderer {
public fun rateVision(vision: Vision): Int
@Composable
public fun render(client: VisionClient, name: Name, vision: Vision, meta: Meta)
public companion object
}

View File

@ -0,0 +1,24 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.Vision
/**
* Render an Element vision via injected vision renderer inside compose-html
*/
@Composable
public fun Vision(
context: Context,
vision: Vision,
name: Name = "@vision[${vision.hashCode().toString(16)}]".asName(),
meta: Meta = Meta.EMPTY,
modifier: Modifier = Modifier,
) {
}

View File

@ -8,7 +8,7 @@ kscience {
jvm()
js()
native()
// wasm()
wasm()
useCoroutines()
commonMain {
api("space.kscience:dataforge-context:$dataforgeVersion")

View File

@ -2,12 +2,11 @@ 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.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
@ -23,6 +22,9 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr {
public interface ControlVision : Vision {
public val controlEventFlow: SharedFlow<VisionControlEvent>
/**
* Fire a [VisionControlEvent] on this [ControlVision]
*/
public suspend fun dispatchControlEvent(event: VisionControlEvent)
override suspend fun receiveEvent(event: VisionEvent) {
@ -32,6 +34,28 @@ public interface ControlVision : Vision {
}
}
public fun ControlVision.asyncControlEvent(
event: VisionControlEvent,
scope: CoroutineScope = manager?.context ?: error("Can't fire asynchronous event for an orphan vision. Provide a scope."),
) {
scope.launch { dispatchControlEvent(event) }
}
@Serializable
public abstract class AbstractControlVision : AbstractVision(), ControlVision {
@Transient
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
override val controlEventFlow: SharedFlow<VisionControlEvent>
get() = mutableControlEventFlow
override suspend fun dispatchControlEvent(event: VisionControlEvent) {
mutableControlEventFlow.emit(event)
}
}
/**
* An event for submitting changes

View File

@ -2,21 +2,21 @@ package space.kscience.visionforge.html
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.html.DIV
import kotlinx.html.InputType
import kotlinx.html.div
import kotlinx.html.stream.createHTML
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.*
@Serializable
public abstract class VisionOfHtml : AbstractVision() {
public interface VisionOfHtml : Vision {
public var classes: Set<String>
get() = properties[::classes.name, false].stringList?.toSet() ?: emptySet()
set(value) {
@ -26,7 +26,7 @@ public abstract class VisionOfHtml : AbstractVision() {
@Serializable
@SerialName("html.plain")
public class VisionOfPlainHtml : VisionOfHtml() {
public class VisionOfPlainHtml : AbstractVision(), VisionOfHtml {
public var content: String? by properties.string()
}
@ -59,26 +59,11 @@ public enum class InputFeedbackMode {
NONE
}
@Serializable
public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{
@Transient
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
override val controlEventFlow: SharedFlow<VisionControlEvent>
get() = mutableControlEventFlow
override suspend fun dispatchControlEvent(event: VisionControlEvent) {
mutableControlEventFlow.emit(event)
}
}
@Serializable
@SerialName("html.input")
public open class VisionOfHtmlInput(
public val inputType: String,
) : VisionOfHtmlControl() {
) : AbstractControlVision(), VisionOfHtml {
public var value: Value? by properties.value()
public var disabled: Boolean by properties.boolean { false }
public var fieldName: String? by properties.string()

View File

@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.AbstractControlVision
import space.kscience.visionforge.DataControl
import space.kscience.visionforge.onSubmit
@ -18,7 +19,7 @@ import space.kscience.visionforge.onSubmit
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlControl(), DataControl {
) : AbstractControlVision(), DataControl, VisionOfHtml {
public var values: Meta? by properties.node()
}
@ -45,7 +46,7 @@ public fun VisionOfHtmlForm.onFormSubmit(scope: CoroutineScope, block: (Meta?) -
@Serializable
@SerialName("html.button")
public class VisionOfHtmlButton : VisionOfHtmlControl(), DataControl {
public class VisionOfHtmlButton : AbstractControlVision(), DataControl, VisionOfHtml {
public var label: String? by properties.string()
}

View File

@ -30,7 +30,6 @@ public interface ElementVisionRenderer {
*/
public fun render(
element: Element,
client: VisionClient,
name: Name,
vision: Vision,
meta: Meta = Meta.EMPTY,
@ -49,7 +48,7 @@ public interface ElementVisionRenderer {
public class SingleTypeVisionRenderer<T : Vision>(
public val kClass: KClass<T>,
private val acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, vision: T, meta: Meta) -> Unit,
) : ElementVisionRenderer {
override fun rateVision(vision: Vision): Int =
@ -57,19 +56,18 @@ public class SingleTypeVisionRenderer<T : Vision>(
override fun render(
element: Element,
client: VisionClient,
name: Name,
vision: Vision,
meta: Meta,
) {
element.clear()
element.append {
renderFunction(name, client, kClass.cast(vision), meta)
renderFunction(name, kClass.cast(vision), meta)
}
}
}
public inline fun <reified T : Vision> ElementVisionRenderer(
acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
noinline renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
noinline renderFunction: TagConsumer<HTMLElement>.(name: Name, vision: T, meta: Meta) -> Unit,
): ElementVisionRenderer = SingleTypeVisionRenderer(T::class, acceptRating, renderFunction)

View File

@ -96,7 +96,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
vision.setAsRoot(visionManager)
val renderer: ElementVisionRenderer =
findRendererFor(vision) ?: error("Could not find renderer for ${vision::class}")
renderer.render(element, this, name, vision, outputMeta)
//subscribe to a backwards events propagation for control visions
if(vision is ControlVision){
vision.controlEventFlow.onEach {
sendEvent(name,it)
}.launchIn(context)
}
renderer.render(element, name, vision, outputMeta)
}
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {

View File

@ -49,7 +49,7 @@ public fun VisionClient.sendMetaEvent(targetName: Name, payload: MetaRepr): Unit
}
internal val formVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
@ -69,22 +69,18 @@ internal val formVisionRenderer: ElementVisionRenderer =
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.context.launch {
client.sendEvent(name, VisionSubmitEvent(name = name, payload = formData))
}
vision.asyncControlEvent(VisionSubmitEvent(name = name, payload = formData))
console.info("Sent form data: ${formData.toMap()}")
false
}
}
internal val buttonVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlButton> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfHtmlButton> { name, vision, _ ->
button(type = ButtonType.button).also { button ->
button.subscribeToVision(vision)
button.onclick = {
client.context.launch {
client.sendEvent(name, VisionSubmitEvent(name = name))
}
vision.asyncControlEvent(VisionSubmitEvent(name = name))
}
vision.useProperty(VisionOfHtmlButton::label) {
button.innerHTML = it ?: ""

View File

@ -35,7 +35,7 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
}
internal val htmlVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfPlainHtml> { _, _, vision, _ ->
ElementVisionRenderer<VisionOfPlainHtml> { _, vision, _ ->
div().also { div ->
div.subscribeToVision(vision)
vision.useProperty(VisionOfPlainHtml::content) {
@ -47,17 +47,18 @@ internal val htmlVisionRenderer: ElementVisionRenderer =
internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlInput>(
acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
) { name, client, vision, _ ->
) { name, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
htmlInputElement.onchange = {
client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent( VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
}
htmlInputElement.oninput = {
client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent(VisionInputEvent(htmlInputElement.value.asValue(), name))
}
htmlInputElement.subscribeToInput(vision)
@ -68,17 +69,17 @@ internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<
}
internal val checkboxVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfCheckbox> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfCheckbox> { name, vision, _ ->
input {
type = InputType.checkBox
}.also { htmlInputElement ->
htmlInputElement.onchange = {
client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent(VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
}
htmlInputElement.oninput = {
client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent(VisionInputEvent(htmlInputElement.value.asValue(), name))
}
@ -90,17 +91,17 @@ internal val checkboxVisionRenderer: ElementVisionRenderer =
}
internal val textVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfTextField> { name, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
htmlInputElement.onchange = {
client.sendEventAsync(name, VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent(VisionValueChangeEvent(htmlInputElement.value.asValue(), name))
}
htmlInputElement.oninput = {
client.sendEventAsync(name, VisionInputEvent(htmlInputElement.value.asValue(), name))
vision.asyncControlEvent(VisionInputEvent(htmlInputElement.value.asValue(), name))
}
htmlInputElement.subscribeToInput(vision)
@ -111,20 +112,20 @@ internal val textVisionRenderer: ElementVisionRenderer =
}
internal val numberVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
input {
type = InputType.number
}.also { htmlInputElement ->
htmlInputElement.onchange = {
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEventAsync(name, VisionValueChangeEvent(it.asValue(), name))
vision.asyncControlEvent(VisionValueChangeEvent(it.asValue(), name))
}
}
htmlInputElement.oninput = {
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEventAsync(name, VisionInputEvent(it.asValue(), name))
vision.asyncControlEvent(VisionInputEvent(it.asValue(), name))
}
}
@ -137,7 +138,7 @@ internal val numberVisionRenderer: ElementVisionRenderer =
}
internal val rangeVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfRangeField> { name, client, vision, _ ->
ElementVisionRenderer<VisionOfRangeField> { name, vision, _ ->
input {
type = InputType.range
min = vision.min.toString()
@ -147,13 +148,13 @@ internal val rangeVisionRenderer: ElementVisionRenderer =
htmlInputElement.onchange = {
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEventAsync(name, VisionValueChangeEvent(it.asValue(), name))
vision.asyncControlEvent(VisionValueChangeEvent(it.asValue(), name))
}
}
htmlInputElement.oninput = {
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEventAsync(name, VisionInputEvent(it.asValue(), name))
vision.asyncControlEvent(VisionInputEvent(it.asValue(), name))
}
}

View File

@ -27,7 +27,7 @@ public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element,name: Name, vision: Vision, meta: Meta) {
require(vision is VisionOfMarkup) { "The vision is not a markup vision" }
val div = document.createElement("div")
val flavour = when (vision.format) {

View File

@ -10,7 +10,10 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.plotly.PlotlyConfig
import space.kscience.plotly.plot
import space.kscience.visionforge.*
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.JsVisionClient
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin
public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
public val visionClient: JsVisionClient by require(JsVisionClient)
@ -24,7 +27,7 @@ public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
val config = PlotlyConfig.read(meta)
element.plot(config, plot)

View File

@ -14,7 +14,6 @@ import space.kscience.dataforge.names.asName
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.JsVisionClient
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
import tabulator.Tabulator
import tabulator.TabulatorFull
@ -35,7 +34,7 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found")

View File

@ -14,10 +14,10 @@ kscience {
commonMain {
api(projects.visionforgeSolid)
api(projects.visionforgeComposeHtml)
}
jsMain {
api(projects.visionforgeComposeHtml)
implementation(npm("three", "0.143.0"))
implementation(npm("three-csg-ts", "3.1.13"))
implementation(npm("three.meshline", "1.4.0"))

View File

@ -10,7 +10,7 @@ import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.*
import space.kscience.visionforge.*
import space.kscience.visionforge.html.ComposeVisionRenderer
import space.kscience.visionforge.html.ComposeHtmlVisionRenderer
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.compose.ThreeView
@ -22,7 +22,7 @@ import three.objects.Group as ThreeGroup
/**
* A plugin that handles Three Object3D representation of Visions.
*/
public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer {
public class ThreePlugin : AbstractPlugin(), ComposeHtmlVisionRenderer {
override val tag: PluginTag get() = Companion.tag
public val solids: Solids by require(Solids)
@ -186,7 +186,7 @@ public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer {
}
@Composable
override fun DOMScope<Element>.render(client: VisionClient, name: Name, vision: Vision, meta: Meta) {
override fun DOMScope<Element>.render(name: Name, vision: Vision, meta: Meta) {
require(vision is Solid) { "Expected Solid but found ${vision::class}" }
ThreeView(context, vision, null, Canvas3DOptions.read(meta))
}

View File

@ -1,3 +1,4 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose)
@ -22,7 +23,6 @@ kscience {
commonMain {
api(projects.visionforgeSolid)
api(projects.visionforgeComposeHtml)
}
jvmMain {