Fixed all tests

This commit is contained in:
Alexander Nozik 2024-02-12 10:16:01 +03:00
parent 30ad680688
commit cbf3f4941a
26 changed files with 171 additions and 209 deletions

View File

@ -3,14 +3,14 @@ import space.kscience.gradle.useSPCTeam
plugins { plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
// id("org.jetbrains.kotlinx.kover") version "0.5.0" id("org.jetbrains.kotlinx.kover") version "0.5.0"
} }
val dataforgeVersion by extra("0.8.0") val dataforgeVersion by extra("0.8.0")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.4.0-dev-2" version = "0.4.0-dev-3"
} }
subprojects { subprojects {

View File

@ -1,9 +1,6 @@
package ru.mipt.npm.root package ru.mipt.npm.root
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus import space.kscience.dataforge.names.plus
@ -365,7 +362,7 @@ private fun buildVolume(volume: DGeoVolume, context: RootToSolidContext): Solid?
} }
return if (group.children.isEmpty()) { return if (group.children.isEmpty()) {
null null
} else if (group.items.size == 1 && group.properties.own == null) { } else if (group.items.size == 1 && group.properties.own.isEmpty()) {
group.items.values.first().apply { parent = null } group.items.values.first().apply { parent = null }
} else { } else {
group group

View File

@ -7,7 +7,7 @@ import kotlinx.html.h2
import kotlinx.html.p import kotlinx.html.p
import space.kscience.visionforge.VisionControlEvent import space.kscience.visionforge.VisionControlEvent
import space.kscience.visionforge.html.* import space.kscience.visionforge.html.*
import space.kscience.visionforge.onClick import space.kscience.visionforge.onSubmit
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -59,7 +59,7 @@ fun main() = serve {
vision { vision {
button("Click me") { button("Click me") {
onClick(context) { onSubmit(context) {
pushEvent(this) pushEvent(this)
} }
} }

View File

@ -1,8 +1,6 @@
package space.kscience.visionforge package space.kscience.visionforge
import kotlinx.serialization.SerialName import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
@ -14,16 +12,12 @@ public abstract class AbstractVision : Vision {
override var parent: Vision? = null override var parent: Vision? = null
@SerialName("properties") @SerialName("properties")
protected var propertiesInternal: MutableMeta? = null @OptIn(ExperimentalSerializationApi::class)
@EncodeDefault(EncodeDefault.Mode.NEVER)
protected var propertiesInternal: MutableMeta = MutableMeta()
final override val properties: MutableVisionProperties by lazy { final override val properties: MutableVisionProperties by lazy {
object : AbstractVisionProperties(this) { AbstractVisionProperties(this, propertiesInternal)
override var properties: MutableMeta?
get() = propertiesInternal
set(value) {
propertiesInternal = value
}
}
} }
override val descriptor: MetaDescriptor? get() = null override val descriptor: MetaDescriptor? get() = null

View File

@ -34,11 +34,11 @@ public interface ControlVision : Vision {
/** /**
* @param payload The optional payload associated with the click event. * An event for submitting changes
*/ */
@Serializable @Serializable
@SerialName("control.click") @SerialName("control.submit")
public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() { public class VisionSubmitEvent(override val meta: Meta) : VisionControlEvent() {
public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY
public val name: Name? get() = meta["name"].string?.parseAsName() public val name: Name? get() = meta["name"].string?.parseAsName()
@ -46,28 +46,28 @@ public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
override fun toString(): String = meta.toString() override fun toString(): String = meta.toString()
} }
public fun VisionClickEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionClickEvent = VisionClickEvent( public fun VisionSubmitEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionSubmitEvent = VisionSubmitEvent(
Meta { Meta {
VisionClickEvent::payload.name put payload VisionSubmitEvent::payload.name put payload
VisionClickEvent::name.name put name.toString() VisionSubmitEvent::name.name put name.toString()
} }
) )
public interface ClickControl : ControlVision { public interface DataControl : ControlVision {
/** /**
* Create and dispatch a click event * Create and dispatch submit event
*/ */
public suspend fun click(builder: MutableMeta.() -> Unit = {}) { public suspend fun submit(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder))) dispatchControlEvent(VisionSubmitEvent(Meta(builder)))
} }
} }
/** /**
* Register listener * Register listener
*/ */
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job = public fun DataControl.onSubmit(scope: CoroutineScope, block: suspend VisionSubmitEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope) controlEventFlow.filterIsInstance<VisionSubmitEvent>().onEach(block).launchIn(scope)
@Serializable @Serializable

View File

@ -67,7 +67,7 @@ internal fun Vision.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?)
* List of names of styles applied to this object. Order matters. Not inherited. * List of names of styles applied to this object. Order matters. Not inherited.
*/ */
public var Vision.styles: List<String> public var Vision.styles: List<String>
get() = properties.own?.getValue(Vision.STYLE_KEY)?.stringList ?: emptyList() get() = properties.own[Vision.STYLE_KEY]?.stringList ?: emptyList()
set(value) { set(value) {
properties.setValue(Vision.STYLE_KEY, value.map { it.asValue() }.asValue()) properties.setValue(Vision.STYLE_KEY, value.map { it.asValue() }.asValue())
} }
@ -83,7 +83,7 @@ public val Vision.styleSheet: StyleSheet get() = StyleSheet(this)
* The style with given name does not necessary exist at the moment. * The style with given name does not necessary exist at the moment.
*/ */
public fun Vision.useStyle(name: String, notify: Boolean = true) { public fun Vision.useStyle(name: String, notify: Boolean = true) {
val newStyle = properties.own?.get(Vision.STYLE_KEY)?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue()) val newStyle = properties.own[Vision.STYLE_KEY]?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue())
properties.setValue(Vision.STYLE_KEY, newStyle.asValue(), notify) properties.setValue(Vision.STYLE_KEY, newStyle.asValue(), notify)
} }
@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) {
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision]. * Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
*/ */
public fun Vision.getStyle(name: String): Meta? = public fun Vision.getStyle(name: String): Meta? =
properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name) properties.own[StyleSheet.STYLESHEET_KEY + name] ?: parent?.getStyle(name)
/** /**
* Resolve a property from all styles * Resolve a property from all styles

View File

@ -1,9 +1,5 @@
package space.kscience.visionforge package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn import space.kscience.dataforge.context.warn
import space.kscience.dataforge.meta.asValue import space.kscience.dataforge.meta.asValue
@ -74,13 +70,3 @@ public var Vision.visible: Boolean?
set(value) { set(value) {
properties.setValue(Vision.VISIBLE_KEY, value?.asValue()) properties.setValue(Vision.VISIBLE_KEY, value?.asValue())
} }
/**
* Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled
*/
public fun Vision.onPropertyChange(
scope: CoroutineScope,
callback: suspend (Name) -> Unit,
): Job = properties.changes.onEach {
callback(it)
}.launchIn(scope)

View File

@ -147,8 +147,8 @@ private fun CoroutineScope.collectChange(
) { ) {
//Collect properties change //Collect properties change
source.properties.changes.onEach { propertyName -> source.properties.flowChanges().onEach { propertyName ->
val newItem = source.properties.own?.get(propertyName) val newItem = source.properties.own[propertyName]
collector.propertyChanged(name, propertyName, newItem) collector.propertyChanged(name, propertyName, newItem)
}.launchIn(this) }.launchIn(this)

View File

@ -83,7 +83,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
polymorphic(VisionEvent::class) { polymorphic(VisionEvent::class) {
subclass(VisionChange.serializer()) subclass(VisionChange.serializer())
subclass(VisionMetaEvent.serializer()) subclass(VisionMetaEvent.serializer())
subclass(VisionClickEvent.serializer()) subclass(VisionSubmitEvent.serializer())
subclass(VisionValueChangeEvent.serializer()) subclass(VisionValueChangeEvent.serializer())
subclass(VisionInputEvent.serializer()) subclass(VisionInputEvent.serializer())
} }

View File

@ -1,9 +1,8 @@
package space.kscience.visionforge package space.kscience.visionforge
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
@ -15,7 +14,7 @@ public interface VisionProperties : MetaProvider {
/** /**
* Raw Visions own properties without styles, defaults, etc. * Raw Visions own properties without styles, defaults, etc.
*/ */
public val own: Meta? public val own: Meta
public val descriptor: MetaDescriptor? public val descriptor: MetaDescriptor?
@ -41,7 +40,7 @@ public interface VisionProperties : MetaProvider {
override fun get(name: Name): Meta? = get(name, null, null) override fun get(name: Name): Meta? = get(name, null, null)
public val changes: Flow<Name> public fun flowChanges(): Flow<Name>
/** /**
* Notify all listeners that a property has been changed and should be invalidated. * Notify all listeners that a property has been changed and should be invalidated.
@ -112,7 +111,7 @@ private class VisionPropertiesItem(
override val items: Map<NameToken, MutableMeta> override val items: Map<NameToken, MutableMeta>
get() { get() {
val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet() val metaKeys = properties.own[nodeName]?.items?.keys ?: emptySet()
val descriptorKeys = descriptor?.nodes?.map { NameToken(it.key) } ?: emptySet() val descriptorKeys = descriptor?.nodes?.map { NameToken(it.key) } ?: emptySet()
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet() val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
val inheritFlag = descriptor?.inherited ?: inherit val inheritFlag = descriptor?.inherited ?: inherit
@ -158,25 +157,12 @@ private class VisionPropertiesItem(
/** /**
* A base implementation of [MutableVisionProperties] * A base implementation of [MutableVisionProperties]
*/ */
public abstract class AbstractVisionProperties( public open class AbstractVisionProperties(
public val vision: Vision, public val vision: Vision,
final override val own: MutableMeta,
) : MutableVisionProperties { ) : MutableVisionProperties {
override val descriptor: MetaDescriptor? get() = vision.descriptor override val descriptor: MetaDescriptor? get() = vision.descriptor
protected abstract var properties: MutableMeta?
override val own: Meta? get() = properties
@JvmSynchronized
protected fun getOrCreateProperties(): MutableMeta {
if (properties == null) {
//TODO check performance issues
val newProperties = MutableMeta()
properties = newProperties
}
return properties!!
}
private val descriptorCache = HashMap<Name, MetaDescriptor?>() private val descriptorCache = HashMap<Name, MetaDescriptor?>()
override fun getValue( override fun getValue(
@ -184,7 +170,7 @@ public abstract class AbstractVisionProperties(
inherit: Boolean?, inherit: Boolean?,
includeStyles: Boolean?, includeStyles: Boolean?,
): Value? { ): Value? {
own?.get(name)?.value?.let { return it } own[name]?.value?.let { return it }
val descriptor = descriptor?.let { descriptor -> descriptorCache.getOrPut(name) { descriptor[name] } } val descriptor = descriptor?.let { descriptor -> descriptorCache.getOrPut(name) { descriptor[name] } }
val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true
@ -202,14 +188,26 @@ public abstract class AbstractVisionProperties(
override fun set(name: Name, node: Meta?, notify: Boolean) { override fun set(name: Name, node: Meta?, notify: Boolean) {
//ignore if the value is the same as existing //ignore if the value is the same as existing
if (own?.get(name) == node) return if (own[name] == node) return
if (name.isEmpty()) { if (name.isEmpty()) {
properties = node?.asMutableMeta() if (node == null) {
own.items.keys.forEach {
remove(it.asName())
}
} else {
(own.items.keys - node.items.keys).forEach {
remove(it.asName())
}
node.items.forEach { (token, item) ->
set(token, item)
}
}
} else if (node == null) { } else if (node == null) {
properties?.set(name, node) own[name] = node
} else { } else {
getOrCreateProperties()[name] = node own[name] = node
} }
if (notify) { if (notify) {
invalidate(name) invalidate(name)
@ -218,12 +216,12 @@ public abstract class AbstractVisionProperties(
override fun setValue(name: Name, value: Value?, notify: Boolean) { override fun setValue(name: Name, value: Value?, notify: Boolean) {
//ignore if the value is the same as existing //ignore if the value is the same as existing
if (own?.getValue(name) == value) return if (own.getValue(name) == value) return
if (value == null) { if (value == null) {
properties?.get(name)?.value = null own[name]?.value = null
} else { } else {
getOrCreateProperties().setValue(name, value) own.setValue(name, value)
} }
if (notify) { if (notify) {
invalidate(name) invalidate(name)
@ -231,12 +229,20 @@ public abstract class AbstractVisionProperties(
} }
@Transient @Transient
protected val changesInternal: MutableSharedFlow<Name> = MutableSharedFlow(500, onBufferOverflow = BufferOverflow.DROP_OLDEST) protected val changesInternal: MutableSharedFlow<Name> = MutableSharedFlow()
override val changes: SharedFlow<Name> get() = changesInternal
override fun flowChanges(): Flow<Name> = changesInternal
override fun invalidate(propertyName: Name) { override fun invalidate(propertyName: Name) {
//send update signal //send update signal
changesInternal.tryEmit(propertyName) val manager = vision.manager
if (manager != null) {
manager.context.launch {
changesInternal.emit(propertyName)
}
} else {
changesInternal.tryEmit(propertyName)
}
//notify children if there are any //notify children if there are any
if (vision is VisionGroup) { if (vision is VisionGroup) {

View File

@ -18,7 +18,7 @@ public fun Vision.flowProperty(
): Flow<Meta> = flow { ): Flow<Meta> = flow {
//Pass initial value. //Pass initial value.
emit(properties.get(propertyName, inherit, includeStyles)) emit(properties.get(propertyName, inherit, includeStyles))
properties.changes.collect { name -> properties.flowChanges().collect { name ->
if (name.startsWith(propertyName)) { if (name.startsWith(propertyName)) {
emit(properties.get(propertyName, inherit, includeStyles)) emit(properties.get(propertyName, inherit, includeStyles))
} }
@ -41,7 +41,7 @@ public fun Vision.flowPropertyValue(
): Flow<Value?> = flow { ): Flow<Value?> = flow {
//Pass initial value. //Pass initial value.
emit(properties.getValue(propertyName, inherit, includeStyles)) emit(properties.getValue(propertyName, inherit, includeStyles))
properties.changes.collect { name -> properties.flowChanges().collect { name ->
if (name.startsWith(propertyName)) { if (name.startsWith(propertyName)) {
emit(properties.getValue(propertyName, inherit, includeStyles)) emit(properties.getValue(propertyName, inherit, includeStyles))
} }

View File

@ -8,8 +8,8 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node import space.kscience.dataforge.meta.node
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.visionforge.ClickControl import space.kscience.visionforge.DataControl
import space.kscience.visionforge.onClick import space.kscience.visionforge.onSubmit
/** /**
* @param formId an id of the element in rendered DOM, this form is bound to * @param formId an id of the element in rendered DOM, this form is bound to
@ -18,7 +18,7 @@ import space.kscience.visionforge.onClick
@SerialName("html.form") @SerialName("html.form")
public class VisionOfHtmlForm( public class VisionOfHtmlForm(
public val formId: String, public val formId: String,
) : VisionOfHtmlControl(), ClickControl { ) : VisionOfHtmlControl(), DataControl {
public var values: Meta? by properties.node() public var values: Meta? by properties.node()
} }
@ -40,12 +40,12 @@ public inline fun <T, C : TagConsumer<T>> C.visionOfForm(
} }
public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) } public fun VisionOfHtmlForm.onFormSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onSubmit(scope) { block(payload) }
@Serializable @Serializable
@SerialName("html.button") @SerialName("html.button")
public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl { public class VisionOfHtmlButton : VisionOfHtmlControl(), DataControl {
public var label: String? by properties.string() public var label: String? by properties.string()
} }

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
@ -19,12 +20,12 @@ public fun Vision.useProperty(
propertyName: Name, propertyName: Name,
inherit: Boolean? = null, inherit: Boolean? = null,
includeStyles: Boolean? = null, includeStyles: Boolean? = null,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: (Meta) -> Unit, callback: (Meta) -> Unit,
): Job { ): Job {
//Pass initial value. //Pass initial value.
callback(properties.get(propertyName, inherit, includeStyles)) callback(properties.get(propertyName, inherit, includeStyles))
return properties.changes.onEach { name -> return properties.flowChanges().onEach { name ->
if (name.startsWith(propertyName)) { if (name.startsWith(propertyName)) {
callback(properties.get(propertyName, inherit, includeStyles)) callback(properties.get(propertyName, inherit, includeStyles))
} }
@ -35,33 +36,41 @@ public fun Vision.useProperty(
propertyName: String, propertyName: String,
inherit: Boolean? = null, inherit: Boolean? = null,
includeStyles: Boolean? = null, includeStyles: Boolean? = null,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: (Meta) -> Unit, callback: (Meta) -> Unit,
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback) ): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
callback(property.get(this))
return properties.flowChanges().onEach { name ->
if (name.startsWith(property.name.asName())) {
callback(property.get(this@useProperty))
}
}.launchIn(scope)
}
/**
* Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled
*/
public fun Vision.onPropertyChange(
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend (Name) -> Unit,
): Job = properties.flowChanges().onEach {
callback(it)
}.launchIn(scope)
/** /**
* Observe changes to the specific property without passing the initial value. * Observe changes to the specific property without passing the initial value.
*/ */
public fun <V : Vision, T> V.onPropertyChange( public fun <V : Vision, T> V.onPropertyChange(
property: KProperty1<V, T>, property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"), scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend V.(T) -> Unit, callback: suspend V.(T) -> Unit,
): Job = properties.changes.onEach { name -> ): Job = properties.flowChanges().filter { it.startsWith(property.name.asName()) }.onEach {
if (name.startsWith(property.name.asName())) {
callback(property.get(this))
}
}.launchIn(scope)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
callback(property.get(this)) callback(property.get(this))
return properties.changes.onEach { name -> }.launchIn(scope)
if (name.startsWith(property.name.asName())) {
callback(property.get(this@useProperty))
}
}.launchIn(scope)
}

View File

@ -55,7 +55,7 @@ class HtmlTagTest {
div { div {
h2 { +"Properties" } h2 { +"Properties" }
ul { ul {
vision.properties.own?.items?.forEach { vision.properties.own.items.forEach {
li { li {
a { +it.key.toString() } a { +it.key.toString() }
p { +it.value.toString() } p { +it.value.toString() }

View File

@ -1,11 +1,10 @@
package space.kscience.visionforge.meta package space.kscience.visionforge.meta
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request import space.kscience.dataforge.context.request
@ -92,7 +91,7 @@ internal class VisionPropertyTest {
} }
@Test @Test
fun testChildrenPropertyFlow() = runTest(timeout = 200.milliseconds) { fun testChildrenPropertyFlow() = runTest(timeout = 500.milliseconds) {
val group = Global.request(VisionManager).group { val group = Global.request(VisionManager).group {
properties { properties {
@ -109,17 +108,32 @@ internal class VisionPropertyTest {
val child = group.children["child"]!! val child = group.children["child"]!!
launch { val semaphore = Semaphore(1, 1)
val list = child.flowPropertyValue("test", inherit = true).take(3).map { it?.int }.toList()
assertEquals(22, list.first()) val changesFlow = child.flowPropertyValue("test", inherit = true).map {
//assertEquals(11, list[1]) //a race semaphore.release()
assertEquals(33, list.last()) it!!.int
} }
//wait for subscription to be created val collectedValues = ArrayList<Int>(5)
delay(5)
val collectorJob = changesFlow.onEach {
collectedValues.add(it)
}.launchIn(this)
assertEquals(22, child.properties["test", true].int)
semaphore.acquire()
child.properties.remove("test") child.properties.remove("test")
assertEquals(11, child.properties["test", true].int)
semaphore.acquire()
group.properties["test"] = 33 group.properties["test"] = 33
assertEquals(33, child.properties["test", true].int)
semaphore.acquire()
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
} }
} }

View File

@ -3,17 +3,11 @@ package space.kscience.visionforge
import kotlinx.dom.clear import kotlinx.dom.clear
import kotlinx.html.TagConsumer import kotlinx.html.TagConsumer
import kotlinx.html.dom.append import kotlinx.html.dom.append
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializerOrNull
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.cast import kotlin.reflect.cast
@ -21,7 +15,7 @@ import kotlin.reflect.cast
* A browser renderer for a [Vision]. * A browser renderer for a [Vision].
*/ */
@DfType(ElementVisionRenderer.TYPE) @DfType(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer : Named { public interface ElementVisionRenderer {
/** /**
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer * Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
@ -58,11 +52,6 @@ public class SingleTypeVisionRenderer<T : Vision>(
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit, private val renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
) : ElementVisionRenderer { ) : ElementVisionRenderer {
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
override val name: Name
get() = kClass.serializerOrNull()?.descriptor?.serialName?.parseAsName()
?: kClass.toString().asName()
override fun rateVision(vision: Vision): Int = override fun rateVision(vision: Vision): Int =
if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING

View File

@ -21,6 +21,7 @@ import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.int
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
@ -262,7 +263,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
rangeVisionRenderer, rangeVisionRenderer,
formVisionRenderer, formVisionRenderer,
buttonVisionRenderer buttonVisionRenderer
).associateByName() ).associateBy { it.toString().asName() }
} else super<AbstractPlugin>.content(target) } else super<AbstractPlugin>.content(target)
public companion object : PluginFactory<JsVisionClient> { public companion object : PluginFactory<JsVisionClient> {
@ -321,7 +322,7 @@ public class VisionClientApplication(public val context: Context) : Application
client.renderers.joinToString( client.renderers.joinToString(
prefix = "\n\t", prefix = "\n\t",
separator = "\n\t" separator = "\n\t"
) { it.name.toString() } ) { it.toString() }
}" }"
} }
val element = document.body ?: error("Document does not have a body") val element = document.body ?: error("Document does not have a body")

View File

@ -70,7 +70,7 @@ internal val formVisionRenderer: ElementVisionRenderer =
event.preventDefault() event.preventDefault()
val formData = FormData(form).toMeta() val formData = FormData(form).toMeta()
client.context.launch { client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name, payload = formData)) client.sendEvent(name, VisionSubmitEvent(name = name, payload = formData))
} }
console.info("Sent form data: ${formData.toMap()}") console.info("Sent form data: ${formData.toMap()}")
false false
@ -83,7 +83,7 @@ internal val buttonVisionRenderer: ElementVisionRenderer =
button.subscribeToVision(vision) button.subscribeToVision(vision)
button.onclick = { button.onclick = {
client.context.launch { client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name)) client.sendEvent(name, VisionSubmitEvent(name = name))
} }
} }
vision.useProperty(VisionOfHtmlButton::label) { vision.useProperty(VisionOfHtmlButton::label) {

View File

@ -8,12 +8,16 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.MutableMetaSerializer
import space.kscience.dataforge.meta.ObservableMeta
import space.kscience.dataforge.meta.asObservable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.plotly.Plot import space.kscience.plotly.Plot
import space.kscience.plotly.Plotly import space.kscience.plotly.Plotly
import space.kscience.plotly.PlotlyConfig import space.kscience.plotly.PlotlyConfig
import space.kscience.visionforge.AbstractVisionProperties
import space.kscience.visionforge.MutableVisionProperties import space.kscience.visionforge.MutableVisionProperties
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionBuilder import space.kscience.visionforge.VisionBuilder
@ -32,32 +36,9 @@ public class VisionOfPlotly private constructor(
override var parent: Vision? = null override var parent: Vision? = null
@Transient @Transient
override val properties: MutableVisionProperties = object : MutableVisionProperties { override val properties: MutableVisionProperties = object : AbstractVisionProperties(this, meta) {
override fun set(name: Name, node: Meta?, notify: Boolean) {
meta[name] = node
}
override fun setValue(name: Name, value: Value?, notify: Boolean) { override fun flowChanges(): Flow<Name> = if (meta is ObservableMeta) {
meta.setValue(name, value)
}
override val own: Meta get() = meta
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): MutableMeta = meta[name] ?: MutableMeta()
override fun getValue(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): Value? = meta.getValue(name)
override val changes: Flow<Name> = if (meta is ObservableMeta) {
callbackFlow { callbackFlow {
meta.onChange(this) { meta.onChange(this) {
launch { launch {
@ -78,7 +59,7 @@ public class VisionOfPlotly private constructor(
} }
override val descriptor: MetaDescriptor? = null // TODO add descriptor for Plot override val descriptor: MetaDescriptor = Plot.descriptor
} }
public fun Plot.asVision(): VisionOfPlotly = VisionOfPlotly(this) public fun Plot.asVision(): VisionOfPlotly = VisionOfPlotly(this)

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge.solid
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.MutableVisionContainer import space.kscience.visionforge.MutableVisionContainer
import space.kscience.visionforge.VisionBuilder import space.kscience.visionforge.VisionBuilder
@ -55,7 +56,7 @@ public fun SolidGroup.smartComposite(
@VisionBuilder builder: SolidGroup.() -> Unit, @VisionBuilder builder: SolidGroup.() -> Unit,
): Solid = if (type == CompositeType.GROUP) { ): Solid = if (type == CompositeType.GROUP) {
val group = SolidGroup().apply(builder) val group = SolidGroup().apply(builder)
if (name == null && group.properties.own == null) { if (name == null && group.properties.own.isEmpty()) {
//append directly to group if no properties are defined //append directly to group if no properties are defined
group.items.forEach { (_, value) -> group.items.forEach { (_, value) ->
value.parent = null value.parent = null

View File

@ -177,7 +177,7 @@ internal fun float32Vector(
): ReadWriteProperty<Solid, Float32Vector3D?> = ): ReadWriteProperty<Solid, Float32Vector3D?> =
object : ReadWriteProperty<Solid, Float32Vector3D?> { object : ReadWriteProperty<Solid, Float32Vector3D?> {
override fun getValue(thisRef: Solid, property: KProperty<*>): Float32Vector3D? { override fun getValue(thisRef: Solid, property: KProperty<*>): Float32Vector3D? {
val item = thisRef.properties.own?.get(name) ?: return null val item = thisRef.properties.own[name] ?: return null
//using dynamic property accessor because values could change //using dynamic property accessor because values could change
return object : Float32Vector3D { return object : Float32Vector3D {
override val x: Float get() = item[X_KEY]?.float ?: defaultX override val x: Float get() = item[X_KEY]?.float ?: defaultX

View File

@ -6,9 +6,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.* import space.kscience.dataforge.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
@ -51,21 +49,19 @@ public class SolidReference(
} }
override val descriptor: MetaDescriptor get() = prototype.descriptor override val descriptor: MetaDescriptor get() = prototype.descriptor
@SerialName("properties") @SerialName("properties")
private var propertiesInternal: MutableMeta? = null @OptIn(ExperimentalSerializationApi::class)
@EncodeDefault(EncodeDefault.Mode.NEVER)
private val propertiesInternal: MutableMeta = MutableMeta()
override val properties: MutableVisionProperties by lazy { override val properties: MutableVisionProperties by lazy {
object : AbstractVisionProperties(this) { object : AbstractVisionProperties(this, propertiesInternal) {
override var properties: MutableMeta?
get() = propertiesInternal
set(value) {
propertiesInternal = value
}
override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? { override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? {
if (name == Vision.STYLE_KEY) { if (name == Vision.STYLE_KEY) {
return buildList { return buildList {
properties?.getValue(Vision.STYLE_KEY)?.list?.forEach { own.getValue(Vision.STYLE_KEY)?.list?.forEach {
add(it) add(it)
} }
prototype.styles.forEach { prototype.styles.forEach {
@ -74,18 +70,18 @@ public class SolidReference(
}.distinct().asValue() }.distinct().asValue()
} }
//1. resolve own properties //1. resolve own properties
properties?.getValue(name)?.let { return it } own.getValue(name)?.let { return it }
val descriptor = descriptor?.get(name) val descriptor = descriptor?.get(name)
val inheritFlag = inherit ?: descriptor?.inherited ?: false val inheritFlag = inherit ?: descriptor?.inherited ?: false
val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true
//2. Resolve prototype onw properties //2. Resolve prototype onw properties
prototype.properties.own?.getValue(name)?.let { return it } prototype.properties.own.getValue(name)?.let { return it }
if (stylesFlag) { if (stylesFlag) {
//3. own styles //3. own styles
own?.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName -> own.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName ->
getStyle(styleName.string)?.getValue(name)?.let { return it } getStyle(styleName.string)?.getValue(name)?.let { return it }
} }
//4. prototype styles //4. prototype styles
@ -178,7 +174,8 @@ internal class SolidReferenceChild(
own.setValue(name, value) own.setValue(name, value)
} }
override val changes: Flow<Name> get() = owner.properties.changes.filter { it.startsWith(childToken(childName)) } override fun flowChanges(): Flow<Name> =
owner.properties.flowChanges().filter { it.startsWith(childToken(childName)) }
override fun invalidate(propertyName: Name) { override fun invalidate(propertyName: Name) {
owner.properties.invalidate(childPropertyName(childName, propertyName)) owner.properties.invalidate(childPropertyName(childName, propertyName))

View File

@ -1,18 +1,17 @@
package space.kscience.visionforge.solid package space.kscience.visionforge.solid
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.meta.getValue import space.kscience.dataforge.meta.getValue
import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.set import space.kscience.dataforge.meta.set
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.visionforge.* import space.kscience.visionforge.getValue
import space.kscience.visionforge.styleSheet
import space.kscience.visionforge.styles
import space.kscience.visionforge.useStyle
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
@ -29,25 +28,14 @@ class SolidPropertyTest {
} }
@Test @Test
fun testColorUpdate() = runTest(timeout = 200.milliseconds) { fun testColorUpdate() {
val box = Box(10.0f, 10.0f, 10.0f) val box = Box(10.0f, 10.0f, 10.0f)
val c = CompletableDeferred<String?>()
val subscription = box.onPropertyChange(this) { key ->
if (key == SolidMaterial.MATERIAL_COLOR_KEY) {
c.complete(box.color.string)
}
}
delay(5)
box.material { box.material {
color("pink") color("pink")
} }
assertEquals("pink", c.await()) assertEquals("pink", box.properties[SolidMaterial.MATERIAL_COLOR_KEY].string)
subscription.cancel()
} }
@Test @Test

View File

@ -133,7 +133,7 @@ internal var Material.cached: Boolean
public fun Mesh.setMaterial(vision: Vision) { public fun Mesh.setMaterial(vision: Vision) {
if ( if (
vision.properties.own?.get(SolidMaterial.MATERIAL_KEY) == null vision.properties.own[SolidMaterial.MATERIAL_KEY] == null
&& vision.getStyleNodes(SolidMaterial.MATERIAL_KEY).isEmpty() && vision.getStyleNodes(SolidMaterial.MATERIAL_KEY).isEmpty()
) { ) {
//if this is a reference, use material of the prototype //if this is a reference, use material of the prototype

View File

@ -14,7 +14,6 @@ import space.kscience.visionforge.compose.ComposeVisionRenderer
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.compose.ThreeView import space.kscience.visionforge.solid.three.compose.ThreeView
import space.kscience.visionforge.solid.three.set
import three.core.Object3D import three.core.Object3D
import kotlin.collections.set import kotlin.collections.set
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -85,7 +84,7 @@ public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer {
updatePosition(vision) updatePosition(vision)
//obj.onChildrenChange() //obj.onChildrenChange()
if (observe) { if (observe) {
vision.properties.changes.onEach { name -> vision.properties.flowChanges().onEach { name ->
if ( if (
name.startsWith(Solid.POSITION_KEY) || name.startsWith(Solid.POSITION_KEY) ||
name.startsWith(Solid.ROTATION_KEY) || name.startsWith(Solid.ROTATION_KEY) ||

View File

@ -138,7 +138,7 @@ public fun ThreeView(
scope = context, scope = context,
rootMeta = vision.properties.root(), rootMeta = vision.properties.root(),
getPropertyState = { name -> getPropertyState = { name ->
if (vision.properties.own?.get(name) != null) { if (vision.properties.own[name] != null) {
EditorPropertyState.Defined EditorPropertyState.Defined
} else if (vision.properties.root()[name] != null) { } else if (vision.properties.root()[name] != null) {
// TODO differentiate // TODO differentiate
@ -148,7 +148,7 @@ public fun ThreeView(
} }
}, },
name = Name.EMPTY, name = Name.EMPTY,
updates = vision.properties.changes, updates = vision.properties.flowChanges(),
rootDescriptor = vision.descriptor rootDescriptor = vision.descriptor
) )
vision.styles.takeIf { it.isNotEmpty() }?.let { styles -> vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->