mirror of
https://github.com/JakeWharton/mosaic.git
synced 2025-11-01 12:01:22 +08:00
Include node tree in debug output
This commit is contained in:
committed by
Jake Wharton
parent
ac258f9d6f
commit
6de6f035b7
@ -3,11 +3,17 @@ package com.jakewharton.mosaic
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ComposeNode
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
public fun Text(
|
||||
@ -135,9 +141,19 @@ public fun <T> Static(
|
||||
// Keep list of items which have not yet been drawn.
|
||||
val pending = remember { mutableStateListOf<Item>() }
|
||||
|
||||
LaunchedEffect(items) {
|
||||
items.collect {
|
||||
pending.add(Item(it, drawn = false))
|
||||
// We use all this manual scope/job/launch stuff instead of a LaunchedEffect so that
|
||||
// the items collection occurs within the same recomposition as it is created.
|
||||
val scope = rememberCoroutineScope()
|
||||
var job by remember { mutableStateOf<Job?>(null) }
|
||||
var seenItems by remember { mutableStateOf<Flow<T>?>(null) }
|
||||
if (seenItems !== items) {
|
||||
job?.cancel()
|
||||
|
||||
seenItems = items
|
||||
job = scope.launch(start = UNDISPATCHED) {
|
||||
items.collect {
|
||||
pending.add(Item(it, drawn = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import kotlinx.coroutines.yield
|
||||
*/
|
||||
private const val debugOutput = false
|
||||
|
||||
public fun renderMosaic(content: @Composable () -> Unit): String {
|
||||
internal fun mosaicNodes(content: @Composable () -> Unit): MosaicNode {
|
||||
val clock = BroadcastFrameClock()
|
||||
val job = Job()
|
||||
val composeContext = clock + job
|
||||
@ -35,10 +35,12 @@ public fun renderMosaic(content: @Composable () -> Unit): String {
|
||||
job.cancel()
|
||||
composition.dispose()
|
||||
|
||||
val canvas = rootNode.draw()
|
||||
val statics = rootNode.drawStatics()
|
||||
val render = AnsiRendering().render(canvas, statics)
|
||||
return rootNode
|
||||
}
|
||||
|
||||
public fun renderMosaic(content: @Composable () -> Unit): String {
|
||||
val rootNode = mosaicNodes(content)
|
||||
val render = AnsiRendering().render(rootNode)
|
||||
return render.toString()
|
||||
}
|
||||
|
||||
@ -78,9 +80,7 @@ public suspend fun runMosaic(body: suspend MosaicScope.() -> Unit): Unit = corou
|
||||
hasFrameWaiters = false
|
||||
clock.sendFrame(0L) // Frame time value is not used by Compose runtime.
|
||||
|
||||
val canvas = rootNode.draw()
|
||||
val statics = rootNode.drawStatics()
|
||||
val render = rendering.render(canvas, statics)
|
||||
val render = rendering.render(rootNode)
|
||||
platformDisplay(render)
|
||||
|
||||
displaySignal?.complete(Unit)
|
||||
|
||||
@ -106,7 +106,7 @@ internal class RootNode : ContainerNode() {
|
||||
return children.flatMap(MosaicNode::drawStatics)
|
||||
}
|
||||
|
||||
override fun toString() = children.joinToString(prefix = "Box(", postfix = ")")
|
||||
override fun toString() = children.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
internal class LinearNode(var isRow: Boolean = true) : ContainerNode() {
|
||||
@ -192,7 +192,7 @@ internal class LinearNode(var isRow: Boolean = true) : ContainerNode() {
|
||||
|
||||
override fun toString() = buildString {
|
||||
append(if (isRow) "Row" else "Column")
|
||||
children.joinTo(this, prefix = "(", postfix = ")")
|
||||
children.joinTo(this, prefix = "()") { "\n" + it.toString().prependIndent(" ") }
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +236,7 @@ internal class StaticNode(
|
||||
return statics
|
||||
}
|
||||
|
||||
override fun toString() = box.children.joinToString(prefix = "Static(", postfix = ")")
|
||||
override fun toString() = box.children.joinToString(prefix = "Static()") { "\n" + it.toString().prependIndent(" ") }
|
||||
}
|
||||
|
||||
internal class MosaicNodeApplier(root: RootNode) : AbstractApplier<MosaicNode>(root) {
|
||||
|
||||
@ -6,12 +6,12 @@ import kotlin.time.TimeSource
|
||||
|
||||
internal interface Rendering {
|
||||
/**
|
||||
* Render [canvas] and [statics] to a single string for display.
|
||||
* Render [node] to a single string for display.
|
||||
*
|
||||
* Note: The returned [CharSequence] is only valid until the next call to this function,
|
||||
* as implementations are free to reuse buffers across invocations.
|
||||
*/
|
||||
fun render(canvas: TextCanvas, statics: List<TextCanvas> = emptyList()): CharSequence
|
||||
fun render(node: MosaicNode): CharSequence
|
||||
}
|
||||
|
||||
@ExperimentalTime
|
||||
@ -20,7 +20,7 @@ internal class DebugRendering(
|
||||
) : Rendering {
|
||||
private var lastRender: TimeMark? = null
|
||||
|
||||
override fun render(canvas: TextCanvas, statics: List<TextCanvas>): CharSequence {
|
||||
override fun render(node: MosaicNode): CharSequence {
|
||||
return buildString {
|
||||
lastRender?.let { lastRender ->
|
||||
repeat(50) { append('~') }
|
||||
@ -29,11 +29,21 @@ internal class DebugRendering(
|
||||
}
|
||||
lastRender = systemClock.markNow()
|
||||
|
||||
for (static in statics) {
|
||||
appendLine(static.render())
|
||||
}
|
||||
val canvas = node.draw()
|
||||
val statics = node.drawStatics()
|
||||
|
||||
appendLine(canvas.render())
|
||||
appendLine("NODES:")
|
||||
appendLine(node)
|
||||
appendLine()
|
||||
if (statics.isNotEmpty()) {
|
||||
appendLine("STATIC:")
|
||||
for (static in statics) {
|
||||
appendLine(static)
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
appendLine("OUTPUT:")
|
||||
appendLine(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,7 +52,10 @@ internal class AnsiRendering : Rendering {
|
||||
private val stringBuilder = StringBuilder(100)
|
||||
private var lastHeight = 0
|
||||
|
||||
override fun render(canvas: TextCanvas, statics: List<TextCanvas>): CharSequence {
|
||||
override fun render(node: MosaicNode): CharSequence {
|
||||
val canvas = node.draw()
|
||||
val statics = node.drawStatics()
|
||||
|
||||
return stringBuilder.apply {
|
||||
clear()
|
||||
|
||||
|
||||
@ -7,9 +7,11 @@ class AnsiRenderingTest {
|
||||
private val rendering = AnsiRendering()
|
||||
|
||||
@Test fun firstRender() {
|
||||
val helloCanvas = TextSurface(6, 2).apply {
|
||||
write(0, 0, "Hello")
|
||||
write(1, 0, "World!")
|
||||
val hello = mosaicNodes {
|
||||
Column {
|
||||
Text("Hello")
|
||||
Text("World!")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO We should not draw trailing whitespace.
|
||||
@ -18,14 +20,16 @@ class AnsiRenderingTest {
|
||||
|Hello$s
|
||||
|World!
|
||||
|""".trimMargin(),
|
||||
rendering.render(helloCanvas).toString(),
|
||||
rendering.render(hello).toString(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun subsequentLongerRenderClearsRenderedLines() {
|
||||
val firstCanvas = TextSurface(6, 2).apply {
|
||||
write(0, 0, "Hello")
|
||||
write(1, 0, "World!")
|
||||
val first = mosaicNodes {
|
||||
Column {
|
||||
Text("Hello")
|
||||
Text("World!")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
@ -33,14 +37,16 @@ class AnsiRenderingTest {
|
||||
|Hello$s
|
||||
|World!
|
||||
|""".trimMargin(),
|
||||
rendering.render(firstCanvas).toString(),
|
||||
rendering.render(first).toString(),
|
||||
)
|
||||
|
||||
val secondCanvas = TextSurface(3, 4).apply {
|
||||
write(0, 0, "Hel")
|
||||
write(1, 0, "lo")
|
||||
write(2, 0, "Wor")
|
||||
write(3, 0, "ld!")
|
||||
val second = mosaicNodes {
|
||||
Column {
|
||||
Text("Hel")
|
||||
Text("lo")
|
||||
Text("Wor")
|
||||
Text("ld!")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
@ -50,16 +56,18 @@ class AnsiRenderingTest {
|
||||
|Wor
|
||||
|ld!
|
||||
|""".trimMargin(),
|
||||
rendering.render(secondCanvas).toString(),
|
||||
rendering.render(second).toString(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun subsequentShorterRenderClearsRenderedLines() {
|
||||
val firstCanvas = TextSurface(3, 4).apply {
|
||||
write(0, 0, "Hel")
|
||||
write(1, 0, "lo")
|
||||
write(2, 0, "Wor")
|
||||
write(3, 0, "ld!")
|
||||
val first = mosaicNodes {
|
||||
Column {
|
||||
Text("Hel")
|
||||
Text("lo")
|
||||
Text("Wor")
|
||||
Text("ld!")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
@ -69,12 +77,14 @@ class AnsiRenderingTest {
|
||||
|Wor
|
||||
|ld!
|
||||
|""".trimMargin(),
|
||||
rendering.render(firstCanvas).toString(),
|
||||
rendering.render(first).toString(),
|
||||
)
|
||||
|
||||
val secondCanvas = TextSurface(6, 2).apply {
|
||||
write(0, 0, "Hello")
|
||||
write(1, 0, "World!")
|
||||
val second = mosaicNodes {
|
||||
Column {
|
||||
Text("Hello")
|
||||
Text("World!")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
@ -84,7 +94,7 @@ class AnsiRenderingTest {
|
||||
|$clearLine
|
||||
|$clearLine$cursorUp
|
||||
""".trimMargin(),
|
||||
rendering.render(secondCanvas).toString(),
|
||||
rendering.render(second).toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.TestTimeSource
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class DebugRenderingTest {
|
||||
@ -12,38 +13,57 @@ class DebugRenderingTest {
|
||||
private val rendering = DebugRendering(timeSource)
|
||||
|
||||
@Test fun framesIncludeStatics() {
|
||||
val helloCanvas = TextSurface(5, 1)
|
||||
helloCanvas.write(0, 0, "Hello")
|
||||
val staticCanvas = TextSurface(6, 1)
|
||||
staticCanvas.write(0, 0, "Static")
|
||||
val nodes = mosaicNodes {
|
||||
Text("Hello")
|
||||
Static(flowOf("Static")) {
|
||||
Text(it)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
|NODES:
|
||||
|Text("Hello", x=0, y=0, width=5, height=1)
|
||||
|Static()
|
||||
| Text("Static", x=0, y=0, width=6, height=1)
|
||||
|
|
||||
|STATIC:
|
||||
|Static
|
||||
|
|
||||
|OUTPUT:
|
||||
|Hello
|
||||
|""".trimMargin(),
|
||||
rendering.render(helloCanvas, listOf(staticCanvas)),
|
||||
rendering.render(nodes),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun framesAfterFirstHaveTimeHeader() {
|
||||
val helloCanvas = TextSurface(5, 1)
|
||||
helloCanvas.write(0, 0, "Hello")
|
||||
val hello = mosaicNodes {
|
||||
Text("Hello")
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"""
|
||||
|NODES:
|
||||
|Text("Hello", x=0, y=0, width=5, height=1)
|
||||
|
|
||||
|OUTPUT:
|
||||
|Hello
|
||||
|""".trimMargin(),
|
||||
rendering.render(helloCanvas),
|
||||
rendering.render(hello),
|
||||
)
|
||||
|
||||
timeSource += 100.milliseconds
|
||||
assertEquals(
|
||||
"""
|
||||
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +100ms
|
||||
|NODES:
|
||||
|Text("Hello", x=0, y=0, width=5, height=1)
|
||||
|
|
||||
|OUTPUT:
|
||||
|Hello
|
||||
|""".trimMargin(),
|
||||
rendering.render(helloCanvas),
|
||||
rendering.render(hello),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user