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