From e037324822b283ef81024c393238e29972bb275f Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 27 Apr 2024 19:52:20 +0100 Subject: [PATCH] Experiment --- .../common/attributes/ArithmeticExpression.kt | 22 +-- extensions/build.gradle.kts | 3 +- previews/build.gradle.kts | 21 +++ .../kotlin/splitties/wff/previews/TagTree.kt | 63 +++++++++ .../splitties/wff/previews/TagTreeConsumer.kt | 64 +++++++++ .../splitties/wff/previews/WffPreview.kt | 130 ++++++++++++++++++ sample/build.gradle.kts | 14 ++ .../splitties/wff/sample/SimpleDigital.kt | 69 ++++++++++ .../kotlin/splitties/wff/sample/WffPreview.kt | 22 +++ settings.gradle.kts | 2 + 10 files changed, 398 insertions(+), 12 deletions(-) create mode 100644 previews/build.gradle.kts create mode 100644 previews/src/main/kotlin/splitties/wff/previews/TagTree.kt create mode 100644 previews/src/main/kotlin/splitties/wff/previews/TagTreeConsumer.kt create mode 100644 previews/src/main/kotlin/splitties/wff/previews/WffPreview.kt create mode 100644 sample/build.gradle.kts create mode 100644 sample/src/main/kotlin/splitties/wff/sample/SimpleDigital.kt create mode 100644 sample/src/main/kotlin/splitties/wff/sample/WffPreview.kt diff --git a/core/src/main/kotlin/splitties/wff/common/attributes/ArithmeticExpression.kt b/core/src/main/kotlin/splitties/wff/common/attributes/ArithmeticExpression.kt index 8a765d0..7b93898 100644 --- a/core/src/main/kotlin/splitties/wff/common/attributes/ArithmeticExpression.kt +++ b/core/src/main/kotlin/splitties/wff/common/attributes/ArithmeticExpression.kt @@ -266,17 +266,17 @@ class ArithmeticExpressionScope private constructor() { operator fun Exp.Int.not() = Exp.Boolean { "!$this" } operator fun Exp.Boolean.not() = Exp.Boolean { "!$this" } - @Suppress("DANGEROUS_CHARACTERS") - infix fun Int.`|`(other: Exp.Int) = Exp.Int { "$this | $other" } - - @Suppress("DANGEROUS_CHARACTERS") - infix fun Exp.Int.`|`(other: Int) = Exp.Int { "$this | $other" } - - @Suppress("DANGEROUS_CHARACTERS") - infix fun Exp.Int.`|`(other: Exp.Int) = Exp.Int { "$this | $other" } - - @Suppress("DANGEROUS_CHARACTERS") - infix fun Exp.Boolean.`||`(other: Exp.Boolean) = Exp.Boolean { "$this || $other" } +// @Suppress("DANGEROUS_CHARACTERS") +// infix fun Int.`|`(other: Exp.Int) = Exp.Int { "$this | $other" } +// +// @Suppress("DANGEROUS_CHARACTERS") +// infix fun Exp.Int.`|`(other: Int) = Exp.Int { "$this | $other" } +// +// @Suppress("DANGEROUS_CHARACTERS") +// infix fun Exp.Int.`|`(other: Exp.Int) = Exp.Int { "$this | $other" } +// +// @Suppress("DANGEROUS_CHARACTERS") +// infix fun Exp.Boolean.`||`(other: Exp.Boolean) = Exp.Boolean { "$this || $other" } infix fun Int.`&`(other: Exp.Int) = Exp.Int { "$this & $other" } infix fun Exp.Int.`&`(other: Int) = Exp.Int { "$this & $other" } diff --git a/extensions/build.gradle.kts b/extensions/build.gradle.kts index ff3b99f..22934bd 100644 --- a/extensions/build.gradle.kts +++ b/extensions/build.gradle.kts @@ -1,5 +1,6 @@ plugins { - id("kotlin-jvm-lib") + kotlin("jvm") + id("org.jetbrains.compose") version "1.6.2" } dependencies { diff --git a/previews/build.gradle.kts b/previews/build.gradle.kts new file mode 100644 index 0000000..e2d0109 --- /dev/null +++ b/previews/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + kotlin("jvm") + id("org.jetbrains.compose") version "1.6.2" +} + +dependencies { + api(project(":core")) + + implementation(compose.desktop.currentOs) + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3") +} + +kotlin { + compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/previews/src/main/kotlin/splitties/wff/previews/TagTree.kt b/previews/src/main/kotlin/splitties/wff/previews/TagTree.kt new file mode 100644 index 0000000..02d1e54 --- /dev/null +++ b/previews/src/main/kotlin/splitties/wff/previews/TagTree.kt @@ -0,0 +1,63 @@ +package splitties.wff.previews + +import kotlinx.html.Tag +import splitties.wff.SCENE +import splitties.wff.WATCHFACE +import splitties.wff.XMLTag +import splitties.wff.attr.AttrRef +import splitties.wff.clock.TIMETEXT +import splitties.wff.common.attributes.ArithmeticExpression + +class TagTree { + lateinit var watchface: WATCHFACE + + val childMap = mutableMapOf>() + val tagContent = mutableMapOf() + val stack = mutableListOf() + + fun push(tag: Tag) { + if (tag is WATCHFACE) { + watchface = tag + } else { + childMap.getOrPut(stack.last()) { mutableListOf() }.add(tag) + } + + stack.add(tag) + } + + fun pop(tag: Tag) { + val removed = stack.removeLast() + check(removed == tag) + } + + fun content(content: CharSequence) { + tagContent[stack.last()] = content.toString() + } + + val WATCHFACE.scene: SCENE + get() = childMap.getValue(this).find { it is SCENE } as SCENE + + val Tag.children: List + get() = childMap[this].orEmpty() + + val Tag.content: String? + get() = tagContent[this] +} + +val TIMETEXT.format: String + get() = this.attributes.getValue("format") + +val TIMETEXT.align: String + get() = this.attributes.getValue("format") + +context(XMLTag) +val AttrRef.value: Int + get() { + val value = attributes.getValue(this.name).toString().toIntOrNull() + + if (value == null) { + println("Int value: $this ${this.name}") + } + + return value ?: 0 + } \ No newline at end of file diff --git a/previews/src/main/kotlin/splitties/wff/previews/TagTreeConsumer.kt b/previews/src/main/kotlin/splitties/wff/previews/TagTreeConsumer.kt new file mode 100644 index 0000000..fba35fa --- /dev/null +++ b/previews/src/main/kotlin/splitties/wff/previews/TagTreeConsumer.kt @@ -0,0 +1,64 @@ +package splitties.wff.previews + +import kotlinx.html.Entities +import kotlinx.html.Tag +import kotlinx.html.TagConsumer +import kotlinx.html.Unsafe +import kotlinx.html.org.w3c.dom.events.Event + +class TagTreeConsumer: TagConsumer { + val tree = TagTree() + + override fun finalize(): TagTree { + return tree.also { + println(it) + } + } + + override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { + println("onTagAttributeChange: $tag $attribute=$value") + } + + override fun onTagComment(content: CharSequence) { + println("onTagComment: $content") + } + + override fun onTagContent(content: CharSequence) { + println("onTagContent: $content") + + tree.content(content) + } + + override fun onTagContentEntity(entity: Entities) { + println("onTagContentEntity: $entity") + } + + override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { + println("onTagContentUnsafe") + } + + override fun onTagStart(tag: Tag) { + println("onTagStart: $tag ${tag.attributes.toMap()}") + + tree.push(tag) + } + + override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { + println("onTagEvent: $tag") + } + + override fun onTagEnd(tag: Tag) { + println("onTagEnd: $tag") + + tree.pop(tag) + } +} + +abstract class WffNode() { + abstract val name: String + +} + +class Unknown(override val name: String): WffNode() { + +} diff --git a/previews/src/main/kotlin/splitties/wff/previews/WffPreview.kt b/previews/src/main/kotlin/splitties/wff/previews/WffPreview.kt new file mode 100644 index 0000000..3b73ee4 --- /dev/null +++ b/previews/src/main/kotlin/splitties/wff/previews/WffPreview.kt @@ -0,0 +1,130 @@ +package splitties.wff.previews + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.html.Tag +import splitties.wff.Alignment +import splitties.wff.GROUP +import splitties.wff.SCENE +import splitties.wff.WATCHFACE +import splitties.wff.WatchFaceDsl +import splitties.wff.clock.DIGITALCLOCK +import splitties.wff.clock.TIMETEXT +import splitties.wff.group.part.text.PARTTEXT +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun WffPreview(watchface: WatchFaceDsl.() -> TagTree) { + val tagTree = with(WatchFaceDsl(TagTreeConsumer())) { + watchface() + } + + val dp = with(LocalDensity.current) { + (450.0f).toDp() + } + with(DrawResources(LocalFontFamilyResolver.current, LocalDensity.current)) { + Canvas(modifier = Modifier.size(dp)) { + tagTree.draw() + } + } +} + +context(DrawScope, DrawResources) +private fun TagTree.draw() { + watchface.drawWatchface() +} + +context(DrawScope, TagTree, DrawResources) +private fun WATCHFACE.drawWatchface() { + scene.draw() +} + +context(DrawScope, TagTree, DrawResources) +private fun Tag.draw() { + when (this) { + is SCENE -> drawScene() + is DIGITALCLOCK -> drawDigitalClock() + is TIMETEXT -> drawTimeText() + is GROUP -> drawGroup() + is PARTTEXT -> drawPartText() + else -> println("Can't draw unknown: $this") + } +} + +context(DrawScope, TagTree, DrawResources) +private fun SCENE.drawScene() { + children.forEach { + it.draw() + } +} + +context(DrawScope, TagTree, DrawResources) +private fun GROUP.drawGroup() { + children.forEach { + it.draw() + } +} + +context(DrawScope, TagTree, DrawResources) +private fun DIGITALCLOCK.drawDigitalClock() { + children.forEach { + it.draw() + } +} + +context(DrawScope, TagTree, DrawResources) +private fun TIMETEXT.drawTimeText() { + val text = formatTime(this.format) + + val topLeft = Offset(attrs.x.value.toFloat(), attrs.y.value.toFloat()) + val size = Size(attrs.width.value.toFloat(), attrs.height.value.toFloat()) + val align = Alignment.valueOf(attributes["align"] ?: "START") + + drawText(textMeasurer = textMeasurer, text = text, topLeft = topLeft, size = size, align = align) +} + +context(DrawScope, TagTree, DrawResources) +private fun PARTTEXT.drawPartText() { + val text = content ?: "MISSING!" + + val topLeft = Offset(attrs.x.value.toFloat(), attrs.y.value.toFloat()) + val size = Size(attrs.width.value.toFloat(), attrs.height.value.toFloat()) + val align = Alignment.valueOf(attributes["align"] ?: "START") + + drawText(textMeasurer = textMeasurer, text = text, topLeft = topLeft, size = size, align = align) +} + +context(DrawScope, TagTree, DrawResources) +private fun drawText(text: String, textMeasurer: TextMeasurer, topLeft: Offset, size: Size, align: Alignment) { + val result = textMeasurer.measure(text, maxLines = 1, constraints = Constraints(maxWidth = size.width.toInt(), maxHeight = size.width.toInt())) + + drawText(result, topLeft = topLeft) +} + +class DrawResources(resolver: FontFamily.Resolver, density: Density) { + val time = LocalDateTime.now() + val textMeasurer = TextMeasurer(resolver, density, LayoutDirection.Ltr) + + init { + println("Density: ${density.density}") + } + + fun formatTime(format: String): String { + val formatter = DateTimeFormatter.ofPattern(format) + return formatter.format(time) + } +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..cd4ae75 --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + kotlin("jvm") + id("org.jetbrains.compose") version "1.6.2" +} + +kotlin { + compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") +} + +dependencies { + api(project(":core")) + api(project(":previews")) + implementation(compose.desktop.currentOs) +} diff --git a/sample/src/main/kotlin/splitties/wff/sample/SimpleDigital.kt b/sample/src/main/kotlin/splitties/wff/sample/SimpleDigital.kt new file mode 100644 index 0000000..33df561 --- /dev/null +++ b/sample/src/main/kotlin/splitties/wff/sample/SimpleDigital.kt @@ -0,0 +1,69 @@ +package splitties.wff.sample + +import splitties.wff.* +import splitties.wff.clock.TIMETEXT.HourFormat.SYNC_TO_DEVICE +import splitties.wff.clock.digitalClock +import splitties.wff.clock.timeText +import splitties.wff.common.variant.ambientVariant +import splitties.wff.group.part.text.FONT.Weight.THIN +import splitties.wff.group.part.text.font +import splitties.wff.group.part.text.formatter.template +import splitties.wff.group.part.text.partText +import splitties.wff.group.part.text.text + +/** + * Taken from https://github.com/android/wear-os-samples/blob/main/WatchFaceFormat/SimpleDigital/res/raw/watchface.xml + */ +context(WatchFaceDsl) +internal fun simpleDigital(): T = watchFace(width = 450, height = 450) { + clockType(ClockType.DIGITAL) + metadata("TICK_PER_SECOND", "15") + previewTime("10:08:32") + scene { + digitalClock { + comment(" SYNC_TO_DEVICE specifies to respect the device 12/24h setting ") + comment(" Interactive mode version") + timeText( + format = "hh:mm", + hourFormat = SYNC_TO_DEVICE, + align = Alignment.CENTER, + y = 175, + height = 100 + ) { + ambientVariant(attrs.alpha, 0) + font( + size = 128, + color = Color.white + ) + } + comment(" Ambient mode version - thinner weight ") + timeText( + format = "hh:mm", + align = Alignment.CENTER, + y = 175, + height = 100, + alpha = 0u + ) { + ambientVariant(attrs.alpha, 0xFF) + font( + size = 128, + weight = THIN, + color = Color.white + ) + } + } + group(name = "hello_world") { + partText(y = 285, height = 50) { + ambientVariant(attrs.alpha, 0) + text { + font( + size = 36, + color = Color.white + ) { + template("greeting") + } + } + } + } + } +} diff --git a/sample/src/main/kotlin/splitties/wff/sample/WffPreview.kt b/sample/src/main/kotlin/splitties/wff/sample/WffPreview.kt new file mode 100644 index 0000000..e9caae1 --- /dev/null +++ b/sample/src/main/kotlin/splitties/wff/sample/WffPreview.kt @@ -0,0 +1,22 @@ +package splitties.wff.sample + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.singleWindowApplication +import splitties.wff.previews.WffPreview + +@Preview +@Composable +fun SimpleDigitalPreview() { + WffPreview { simpleDigital() } +} + +fun main() { + val width = (450f / 1.5f).dp + singleWindowApplication(state = WindowState(size = DpSize(width, width))) { + SimpleDigitalPreview() + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 41ce2db..79bf3a6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,10 +29,12 @@ enableFeaturePreview("STABLE_CONFIGURATION_CACHE") include { "core"() "extensions"() + "previews"() "plugins" { "gradle" { "watchface-app"() } } + "sample"() }