Add background modifier, fix multimodifier positioning (#180)

This commit is contained in:
Jake Wharton
2023-05-16 23:38:59 -04:00
committed by GitHub
parent 92783cf020
commit f18e140e7e
10 changed files with 144 additions and 51 deletions

View File

@ -96,4 +96,20 @@ internal class TextPixel(var value: String) {
var style = None var style = None
constructor(char: Char) : this(char.toString()) 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(')')
}
} }

View File

@ -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)"
}

View File

@ -20,6 +20,8 @@ import com.jakewharton.mosaic.modifier.Modifier
public interface DrawModifier : Modifier.Element { public interface DrawModifier : Modifier.Element {
public fun ContentDrawScope.draw() public fun ContentDrawScope.draw()
// Force subclasses to add a debugging implementation.
override fun toString(): String
} }
public fun Modifier.drawBehind( public fun Modifier.drawBehind(

View File

@ -21,7 +21,18 @@ import com.jakewharton.mosaic.ui.Color
import com.jakewharton.mosaic.ui.TextStyle import com.jakewharton.mosaic.ui.TextStyle
public interface DrawScope { 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, row: Int,
column: Int, column: Int,
string: String, string: String,
@ -33,8 +44,24 @@ public interface DrawScope {
internal open class TextCanvasDrawScope( internal open class TextCanvasDrawScope(
private val canvas: TextCanvas, private val canvas: TextCanvas,
override val width: Int,
override val height: Int,
): DrawScope { ): 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, row: Int,
column: Int, column: Int,
string: String, string: String,

View File

@ -46,6 +46,9 @@ public interface LayoutModifier : Modifier.Element {
public fun MeasureScope.measure( public fun MeasureScope.measure(
measurable: Measurable, measurable: Measurable,
): MeasureResult ): MeasureResult
// Force subclasses to add a debugging implementation.
override fun toString(): String
} }
/** /**

View File

@ -70,54 +70,18 @@ internal class MosaicNode(
var debugPolicy: DebugPolicy, var debugPolicy: DebugPolicy,
val onStaticDraw: (() -> Unit)?, val onStaticDraw: (() -> Unit)?,
) : Measurable { ) : Measurable {
private val isStatic = onStaticDraw != null val isStatic get() = onStaticDraw != null
val children = mutableListOf<MosaicNode>() val children = mutableListOf<MosaicNode>()
private val bottomLayer: MosaicNodeLayer = object : AbstractMosaicNodeLayer(null, isStatic) { private val bottomLayer: MosaicNodeLayer = BottomLayer(this)
override fun doMeasure(): MeasureResult { var topLayer: MosaicNodeLayer = bottomLayer
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
var modifiers: Modifier = Modifier var modifiers: Modifier = Modifier
set(value) { set(value) {
topLayer = value.foldOut(bottomLayer) { element, lowerLayer -> topLayer = value.foldOut(bottomLayer) { element, lowerLayer ->
when (element) { when (element) {
is LayoutModifier -> { is LayoutModifier -> LayoutLayer(element, lowerLayer)
object : AbstractMosaicNodeLayer(lowerLayer, false) { is DrawModifier -> DrawLayer(element, lowerLayer)
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
}
}
}
else -> lowerLayer else -> lowerLayer
} }
} }
@ -162,3 +126,48 @@ internal class MosaicNode(
override fun toString() = debugPolicy.run { renderDebug() } 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
}
}

View File

@ -8,6 +8,8 @@ public class Color private constructor(
internal val fg: Int, internal val fg: Int,
internal val bg: Int, internal val bg: Int,
) { ) {
override fun toString(): String = "Color($fg)"
public companion object { public companion object {
@Stable @Stable
public val Black: Color = Color(30, 40) public val Black: Color = Color(30, 40)

View File

@ -12,6 +12,7 @@ import kotlin.jvm.JvmName
@Composable @Composable
public fun Text( public fun Text(
value: String, value: String,
modifier: Modifier = Modifier,
color: Color? = null, color: Color? = null,
background: Color? = null, background: Color? = null,
style: TextStyle? = null, style: TextStyle? = null,
@ -27,9 +28,9 @@ public fun Text(
layout.measure() layout.measure()
layout(layout.width, layout.height) layout(layout.width, layout.height)
}, },
modifiers = Modifier.drawBehind { modifiers = modifier.drawBehind {
layout.lines.forEachIndexed { row, line -> layout.lines.forEachIndexed { row, line ->
write(row, 0, line, color, background, style) drawText(row, 0, line, color, background, style)
} }
}, },
) )

View File

@ -103,7 +103,7 @@ class LayoutTest {
Text("..") Text("..")
Layout(modifiers = Modifier.drawBehind { Layout(modifiers = Modifier.drawBehind {
repeat(4) { row -> repeat(4) { row ->
write(row, 0, "XXXX") drawText(row, 0, "XXXX")
} }
}) { }) {
layout(2, 2) layout(2, 2)

View File

@ -10,6 +10,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateList 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.runMosaicBlocking
import com.jakewharton.mosaic.ui.Color.Companion.Black import com.jakewharton.mosaic.ui.Color.Companion.Black
import com.jakewharton.mosaic.ui.Color.Companion.BrightBlack import com.jakewharton.mosaic.ui.Color.Companion.BrightBlack
@ -96,13 +99,18 @@ fun TestRow(test: Test) {
Pass -> Green Pass -> Green
Fail -> Red Fail -> Red
} }
// TODO use start/end padding of 1 to achieve spacing
val state = when (test.state) { val state = when (test.state) {
Running -> " RUNS " Running -> "RUNS"
Pass -> " PASS " Pass -> "PASS"
Fail -> " FAIL " 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 dir = test.path.substringBeforeLast('/')
val name = test.path.substringAfterLast('/') val name = test.path.substringAfterLast('/')