diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt index d9a0fca1..27c4dc90 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/canvas.kt @@ -96,4 +96,20 @@ internal class TextPixel(var value: String) { var style = None constructor(char: Char) : this(char.toString()) + + override fun toString() = buildString { + append("TextPixel(\"") + append(value) + append("\"") + if (background != null) { + append(" bg=") + append(background) + } + if (foreground != null) { + append(" fg=") + append(foreground) + } + // TODO style + append(')') + } } diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Background.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Background.kt new file mode 100644 index 00000000..6cad7da9 --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Background.kt @@ -0,0 +1,25 @@ +package com.jakewharton.mosaic.layout + +import androidx.compose.runtime.Stable +import com.jakewharton.mosaic.modifier.Modifier +import com.jakewharton.mosaic.ui.Color + +@Stable +public fun Modifier.background( + color: Color, +): Modifier = this.then( + BackgroundModifier( + color = color, + ) +) + +private class BackgroundModifier( + private val color: Color, +) : DrawModifier { + override fun ContentDrawScope.draw() { + drawRect(color) + drawContent() + } + + override fun toString() = "Background($color)" +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt index e0924518..07742fa0 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawModifier.kt @@ -20,6 +20,8 @@ import com.jakewharton.mosaic.modifier.Modifier public interface DrawModifier : Modifier.Element { public fun ContentDrawScope.draw() + // Force subclasses to add a debugging implementation. + override fun toString(): String } public fun Modifier.drawBehind( diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt index 301ce2fa..de066386 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/DrawScope.kt @@ -21,7 +21,18 @@ import com.jakewharton.mosaic.ui.Color import com.jakewharton.mosaic.ui.TextStyle public interface DrawScope { - public fun write( + public val width: Int + public val height: Int + + public fun drawRect( + color: Color, + row: Int = 0, + column: Int = 0, + width: Int = this.width, + height: Int = this.height, + ) + + public fun drawText( row: Int, column: Int, string: String, @@ -33,8 +44,24 @@ public interface DrawScope { internal open class TextCanvasDrawScope( private val canvas: TextCanvas, + override val width: Int, + override val height: Int, ): DrawScope { - override fun write( + override fun drawRect( + color: Color, + row: Int, + column: Int, + width: Int, + height: Int, + ) { + for (y in row until row + height) { + for (x in column until column + width) { + canvas[y, x].background = color + } + } + } + + override fun drawText( row: Int, column: Int, string: String, diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt index 84ddfead..3bb157ce 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/LayoutModifier.kt @@ -46,6 +46,9 @@ public interface LayoutModifier : Modifier.Element { public fun MeasureScope.measure( measurable: Measurable, ): MeasureResult + + // Force subclasses to add a debugging implementation. + override fun toString(): String } /** diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt index e0ca36a4..0f7fa3c0 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/layout/Node.kt @@ -70,54 +70,18 @@ internal class MosaicNode( var debugPolicy: DebugPolicy, val onStaticDraw: (() -> Unit)?, ) : Measurable { - private val isStatic = onStaticDraw != null + val isStatic get() = onStaticDraw != null val children = mutableListOf() - private val bottomLayer: MosaicNodeLayer = object : AbstractMosaicNodeLayer(null, isStatic) { - override fun doMeasure(): MeasureResult { - return measurePolicy.run { measure(children) } - } - - override fun drawTo(canvas: TextCanvas) { - for (child in children) { - if (child.width != 0 && child.height != 0) { - child.topLayer.drawTo(canvas) - } - } - } - } - - private var topLayer = bottomLayer + private val bottomLayer: MosaicNodeLayer = BottomLayer(this) + var topLayer: MosaicNodeLayer = bottomLayer var modifiers: Modifier = Modifier set(value) { topLayer = value.foldOut(bottomLayer) { element, lowerLayer -> when (element) { - is LayoutModifier -> { - object : AbstractMosaicNodeLayer(lowerLayer, false) { - override fun doMeasure(): MeasureResult { - return element.run { measure(lowerLayer) } - } - } - } - - is DrawModifier -> { - object : AbstractMosaicNodeLayer(lowerLayer, false) { - override fun drawTo(canvas: TextCanvas) { - canvas.translationX += x - canvas.translationY += y - val scope = object : TextCanvasDrawScope(canvas), ContentDrawScope { - override fun drawContent() { - lowerLayer.drawTo(canvas) - } - } - element.run { scope.draw() } - canvas.translationX -= x - canvas.translationY -= y - } - } - } - + is LayoutModifier -> LayoutLayer(element, lowerLayer) + is DrawModifier -> DrawLayer(element, lowerLayer) else -> lowerLayer } } @@ -162,3 +126,48 @@ internal class MosaicNode( override fun toString() = debugPolicy.run { renderDebug() } } + +private class BottomLayer( + private val node: MosaicNode, +) : AbstractMosaicNodeLayer(null, node.isStatic) { + override fun doMeasure(): MeasureResult { + return node.measurePolicy.run { measure(node.children) } + } + + override fun drawTo(canvas: TextCanvas) { + for (child in node.children) { + if (child.width != 0 && child.height != 0) { + child.topLayer.drawTo(canvas) + } + } + } +} + +private class LayoutLayer( + private val element: LayoutModifier, + private val lowerLayer: MosaicNodeLayer, +) : AbstractMosaicNodeLayer(lowerLayer, false) { + override fun doMeasure(): MeasureResult { + return element.run { measure(lowerLayer) } + } +} + +private class DrawLayer( + private val element: DrawModifier, + private val lowerLayer: MosaicNodeLayer, +) : AbstractMosaicNodeLayer(lowerLayer, false) { + override fun drawTo(canvas: TextCanvas) { + val oldX = canvas.translationX + val oldY = canvas.translationY + canvas.translationX = x + canvas.translationY = y + val scope = object : TextCanvasDrawScope(canvas, width, height), ContentDrawScope { + override fun drawContent() { + lowerLayer.drawTo(canvas) + } + } + element.run { scope.draw() } + canvas.translationX = oldX + canvas.translationY = oldY + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Color.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Color.kt index 8c70417a..23f27765 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Color.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Color.kt @@ -8,6 +8,8 @@ public class Color private constructor( internal val fg: Int, internal val bg: Int, ) { + override fun toString(): String = "Color($fg)" + public companion object { @Stable public val Black: Color = Color(30, 40) diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt index dfe58998..ba9f8c86 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Text.kt @@ -12,6 +12,7 @@ import kotlin.jvm.JvmName @Composable public fun Text( value: String, + modifier: Modifier = Modifier, color: Color? = null, background: Color? = null, style: TextStyle? = null, @@ -27,9 +28,9 @@ public fun Text( layout.measure() layout(layout.width, layout.height) }, - modifiers = Modifier.drawBehind { + modifiers = modifier.drawBehind { layout.lines.forEachIndexed { row, line -> - write(row, 0, line, color, background, style) + drawText(row, 0, line, color, background, style) } }, ) diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt index 76c47479..a81f1161 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/LayoutTest.kt @@ -103,7 +103,7 @@ class LayoutTest { Text("..") Layout(modifiers = Modifier.drawBehind { repeat(4) { row -> - write(row, 0, "XXXX") + drawText(row, 0, "XXXX") } }) { layout(2, 2) diff --git a/samples/jest/src/main/kotlin/example/jest.kt b/samples/jest/src/main/kotlin/example/jest.kt index cededce0..97fdbd00 100644 --- a/samples/jest/src/main/kotlin/example/jest.kt +++ b/samples/jest/src/main/kotlin/example/jest.kt @@ -10,6 +10,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotStateList +import com.jakewharton.mosaic.layout.background +import com.jakewharton.mosaic.layout.padding +import com.jakewharton.mosaic.modifier.Modifier import com.jakewharton.mosaic.runMosaicBlocking import com.jakewharton.mosaic.ui.Color.Companion.Black import com.jakewharton.mosaic.ui.Color.Companion.BrightBlack @@ -96,13 +99,18 @@ fun TestRow(test: Test) { Pass -> Green Fail -> Red } - // TODO use start/end padding of 1 to achieve spacing val state = when (test.state) { - Running -> " RUNS " - Pass -> " PASS " - Fail -> " FAIL " + Running -> "RUNS" + Pass -> "PASS" + Fail -> "FAIL" } - Text(state, color = Black, background = bg) + Text( + state, + modifier = Modifier + .background(bg) + .padding(horizontal = 1), + color = Black + ) val dir = test.path.substringBeforeLast('/') val name = test.path.substringAfterLast('/')