Create new static logging system (#777)

`Static` function is now called `StaticEffect` to better indicate that it only renders its content once.

`LocalStaticLogger` composition local provides access to `StaticLogger` which allows logging plain strings at arbitrary points for inclusion in the next frame. This can be used from effects, callback, state classes, etc.
This commit is contained in:
Jake Wharton
2025-03-07 00:56:46 -05:00
committed by GitHub
parent 667921777c
commit c38c09d8bc
16 changed files with 235 additions and 293 deletions

View File

@ -7,10 +7,12 @@ New:
- Add `focused` and `darkTheme` booleans to `Terminal` (available through `LocalTerminal`). These default to true and false, respectively, but will be updated if the terminal supports sending change notifications.
- Bind `Terminal.focused` to a `Lifecycle` and expose into the composition as `LocalLifecycleOwner`. This allows using Compose lifecycle helpers such as `LifecycleResumeEffect` and others.
- Underline styles (single, double, dashed, dotted, curved) and colors can now be specified for text and annotated string spans.
- `LocalStaticLogger` composition local provides access to `StaticLogger` which allows logging plain strings at arbitrary points for inclusion in the next frame. This can be used from effects, callback, state classes, etc.
Changed:
- Switched to our own terminal integration library. Report any issues with keyboard input, incorrect size reporting, or garbled output.
- Only disable the cursor and emit synchronized rendering markers if the terminal reports support for those features.
- `Static` function is now called `StaticEffect` to better indicate that it only renders its content once.
Fixed:
- Prevent final character from being erased when a row writes into the last column of the terminal.

View File

@ -1,11 +1,10 @@
public abstract interface class com/jakewharton/mosaic/Mosaic {
public abstract fun awaitComplete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun cancel ()V
public abstract fun dump ()Ljava/lang/String;
public abstract fun paint ()Lcom/jakewharton/mosaic/TextCanvas;
public fun paintStatics ()Ljava/util/List;
public abstract fun paintStaticsTo (Landroidx/collection/MutableObjectList;)V
public abstract fun draw ()Lcom/jakewharton/mosaic/TextCanvas;
public abstract fun dumpNodes ()Ljava/lang/String;
public abstract fun setContent (Lkotlin/jvm/functions/Function2;)V
public abstract fun static ()Ljava/lang/String;
}
public final class com/jakewharton/mosaic/MosaicKt {
@ -604,8 +603,16 @@ public final class com/jakewharton/mosaic/ui/SpacerKt {
}
public final class com/jakewharton/mosaic/ui/Static {
public static final fun Static (Landroidx/compose/runtime/snapshots/SnapshotStateList;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
public static final fun Static (Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
public static final fun StaticEffect (Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
public static final fun getLocalStaticLogger ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}
public final class com/jakewharton/mosaic/ui/StaticLogger {
public static final field $stable I
public final fun log (Lcom/jakewharton/mosaic/TextCanvas;)V
public final fun log (Ljava/lang/String;)V
public final fun plusAssign (Lcom/jakewharton/mosaic/TextCanvas;)V
public final fun plusAssign (Ljava/lang/String;)V
}
public final class com/jakewharton/mosaic/ui/Text {

View File

@ -198,12 +198,11 @@ abstract interface com.jakewharton.mosaic.ui/RowScope { // com.jakewharton.mosai
abstract interface com.jakewharton.mosaic/Mosaic { // com.jakewharton.mosaic/Mosaic|null[0]
abstract fun cancel() // com.jakewharton.mosaic/Mosaic.cancel|cancel(){}[0]
abstract fun dump(): kotlin/String // com.jakewharton.mosaic/Mosaic.dump|dump(){}[0]
abstract fun paint(): com.jakewharton.mosaic/TextCanvas // com.jakewharton.mosaic/Mosaic.paint|paint(){}[0]
abstract fun paintStaticsTo(androidx.collection/MutableObjectList<com.jakewharton.mosaic/TextCanvas>) // com.jakewharton.mosaic/Mosaic.paintStaticsTo|paintStaticsTo(androidx.collection.MutableObjectList<com.jakewharton.mosaic.TextCanvas>){}[0]
abstract fun draw(): com.jakewharton.mosaic/TextCanvas // com.jakewharton.mosaic/Mosaic.draw|draw(){}[0]
abstract fun dumpNodes(): kotlin/String // com.jakewharton.mosaic/Mosaic.dumpNodes|dumpNodes(){}[0]
abstract fun setContent(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.jakewharton.mosaic/Mosaic.setContent|setContent(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
abstract fun static(): kotlin/String? // com.jakewharton.mosaic/Mosaic.static|static(){}[0]
abstract suspend fun awaitComplete() // com.jakewharton.mosaic/Mosaic.awaitComplete|awaitComplete(){}[0]
open fun paintStatics(): kotlin.collections/List<com.jakewharton.mosaic/TextCanvas> // com.jakewharton.mosaic/Mosaic.paintStatics|paintStatics(){}[0]
}
abstract interface com.jakewharton.mosaic/TextCanvas { // com.jakewharton.mosaic/TextCanvas|null[0]
@ -394,6 +393,13 @@ final class com.jakewharton.mosaic.ui/BiasAlignment : com.jakewharton.mosaic.ui/
}
}
final class com.jakewharton.mosaic.ui/StaticLogger { // com.jakewharton.mosaic.ui/StaticLogger|null[0]
final fun log(com.jakewharton.mosaic/TextCanvas) // com.jakewharton.mosaic.ui/StaticLogger.log|log(com.jakewharton.mosaic.TextCanvas){}[0]
final fun log(kotlin/String) // com.jakewharton.mosaic.ui/StaticLogger.log|log(kotlin.String){}[0]
final inline fun plusAssign(com.jakewharton.mosaic/TextCanvas) // com.jakewharton.mosaic.ui/StaticLogger.plusAssign|plusAssign(com.jakewharton.mosaic.TextCanvas){}[0]
final inline fun plusAssign(kotlin/String) // com.jakewharton.mosaic.ui/StaticLogger.plusAssign|plusAssign(kotlin.String){}[0]
}
final class com.jakewharton.mosaic/Terminal { // com.jakewharton.mosaic/Terminal|null[0]
constructor <init>(kotlin/Boolean, kotlin/Boolean, com.jakewharton.mosaic.ui.unit/IntSize) // com.jakewharton.mosaic/Terminal.<init>|<init>(kotlin.Boolean;kotlin.Boolean;com.jakewharton.mosaic.ui.unit.IntSize){}[0]
@ -689,6 +695,8 @@ final val com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_AnnotatedStrin
final val com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_SpanStyle$stableprop // com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_SpanStyle$stableprop|#static{}com_jakewharton_mosaic_text_SpanStyle$stableprop[0]
final val com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_StringTextLayout$stableprop // com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_StringTextLayout$stableprop|#static{}com_jakewharton_mosaic_text_StringTextLayout$stableprop[0]
final val com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_TextLayout$stableprop // com.jakewharton.mosaic.text/com_jakewharton_mosaic_text_TextLayout$stableprop|#static{}com_jakewharton_mosaic_text_TextLayout$stableprop[0]
final val com.jakewharton.mosaic.ui/LocalStaticLogger // com.jakewharton.mosaic.ui/LocalStaticLogger|{}LocalStaticLogger[0]
final fun <get-LocalStaticLogger>(): androidx.compose.runtime/ProvidableCompositionLocal<com.jakewharton.mosaic.ui/StaticLogger> // com.jakewharton.mosaic.ui/LocalStaticLogger.<get-LocalStaticLogger>|<get-LocalStaticLogger>(){}[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement$stableprop|#static{}com_jakewharton_mosaic_ui_Arrangement$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement_Absolute$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement_Absolute$stableprop|#static{}com_jakewharton_mosaic_ui_Arrangement_Absolute$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement_SpacedAligned$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement_SpacedAligned$stableprop|#static{}com_jakewharton_mosaic_ui_Arrangement_SpacedAligned$stableprop[0]
@ -708,7 +716,7 @@ final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurePo
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop|#static{}com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnParentData$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnParentData$stableprop|#static{}com_jakewharton_mosaic_ui_RowColumnParentData$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowScopeInstance$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowScopeInstance$stableprop|#static{}com_jakewharton_mosaic_ui_RowScopeInstance$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticState$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticState$stableprop|#static{}com_jakewharton_mosaic_ui_StaticState$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticLogger$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticLogger$stableprop|#static{}com_jakewharton_mosaic_ui_StaticLogger$stableprop[0]
final val com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop|#static{}com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop[0]
final val com.jakewharton.mosaic.ui/isEmptyTextStyle // com.jakewharton.mosaic.ui/isEmptyTextStyle|@com.jakewharton.mosaic.ui.TextStyle{}isEmptyTextStyle[0]
final inline fun (com.jakewharton.mosaic.ui/TextStyle).<get-isEmptyTextStyle>(): kotlin/Boolean // com.jakewharton.mosaic.ui/isEmptyTextStyle.<get-isEmptyTextStyle>|<get-isEmptyTextStyle>@com.jakewharton.mosaic.ui.TextStyle(){}[0]
@ -788,7 +796,6 @@ final fun (com.jakewharton.mosaic.ui.unit/Constraints).com.jakewharton.mosaic.ui
final fun (com.jakewharton.mosaic.ui.unit/Constraints).com.jakewharton.mosaic.ui.unit/constrainWidth(kotlin/Int): kotlin/Int // com.jakewharton.mosaic.ui.unit/constrainWidth|constrainWidth@com.jakewharton.mosaic.ui.unit.Constraints(kotlin.Int){}[0]
final fun (com.jakewharton.mosaic.ui.unit/Constraints).com.jakewharton.mosaic.ui.unit/isSatisfiedBy(com.jakewharton.mosaic.ui.unit/IntSize): kotlin/Boolean // com.jakewharton.mosaic.ui.unit/isSatisfiedBy|isSatisfiedBy@com.jakewharton.mosaic.ui.unit.Constraints(com.jakewharton.mosaic.ui.unit.IntSize){}[0]
final fun (com.jakewharton.mosaic.ui.unit/Constraints).com.jakewharton.mosaic.ui.unit/offset(kotlin/Int = ..., kotlin/Int = ...): com.jakewharton.mosaic.ui.unit/Constraints // com.jakewharton.mosaic.ui.unit/offset|offset@com.jakewharton.mosaic.ui.unit.Constraints(kotlin.Int;kotlin.Int){}[0]
final fun <#A: kotlin/Any?> com.jakewharton.mosaic.ui/Static(androidx.compose.runtime.snapshots/SnapshotStateList<#A>, kotlin/Function3<#A, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // com.jakewharton.mosaic.ui/Static|Static(androidx.compose.runtime.snapshots.SnapshotStateList<0:0>;kotlin.Function3<0:0,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){0§<kotlin.Any?>}[0]
final fun com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_DrawStyle_Fill$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_DrawStyle_Fill$stableprop_getter|com_jakewharton_mosaic_layout_DrawStyle_Fill$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_DrawStyle_Stroke$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_DrawStyle_Stroke$stableprop_getter|com_jakewharton_mosaic_layout_DrawStyle_Stroke$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_KeyEvent$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.layout/com_jakewharton_mosaic_layout_KeyEvent$stableprop_getter|com_jakewharton_mosaic_layout_KeyEvent$stableprop_getter(){}[0]
@ -820,7 +827,7 @@ final fun com.jakewharton.mosaic.ui/Filler(kotlin/Int, com.jakewharton.mosaic.mo
final fun com.jakewharton.mosaic.ui/Layout(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, com.jakewharton.mosaic.modifier/Modifier?, kotlin/Function0<kotlin/String>?, com.jakewharton.mosaic.layout/MeasurePolicy, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Layout|Layout(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;com.jakewharton.mosaic.modifier.Modifier?;kotlin.Function0<kotlin.String>?;com.jakewharton.mosaic.layout.MeasurePolicy;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/Row(com.jakewharton.mosaic.modifier/Modifier?, com.jakewharton.mosaic.ui/Arrangement.Horizontal?, com.jakewharton.mosaic.ui/Alignment.Vertical?, kotlin/Function3<com.jakewharton.mosaic.ui/RowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Row|Row(com.jakewharton.mosaic.modifier.Modifier?;com.jakewharton.mosaic.ui.Arrangement.Horizontal?;com.jakewharton.mosaic.ui.Alignment.Vertical?;kotlin.Function3<com.jakewharton.mosaic.ui.RowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/Spacer(com.jakewharton.mosaic.modifier/Modifier?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Spacer|Spacer(com.jakewharton.mosaic.modifier.Modifier?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/Static(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // com.jakewharton.mosaic.ui/Static|Static(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/StaticEffect(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // com.jakewharton.mosaic.ui/StaticEffect|StaticEffect(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/Text(com.jakewharton.mosaic.text/AnnotatedString, com.jakewharton.mosaic.modifier/Modifier?, com.jakewharton.mosaic.ui/Color, com.jakewharton.mosaic.ui/Color, com.jakewharton.mosaic.ui/TextStyle, com.jakewharton.mosaic.ui/UnderlineStyle, com.jakewharton.mosaic.ui/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Text|Text(com.jakewharton.mosaic.text.AnnotatedString;com.jakewharton.mosaic.modifier.Modifier?;com.jakewharton.mosaic.ui.Color;com.jakewharton.mosaic.ui.Color;com.jakewharton.mosaic.ui.TextStyle;com.jakewharton.mosaic.ui.UnderlineStyle;com.jakewharton.mosaic.ui.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/Text(kotlin/String, com.jakewharton.mosaic.modifier/Modifier?, com.jakewharton.mosaic.ui/Color, com.jakewharton.mosaic.ui/Color, com.jakewharton.mosaic.ui/TextStyle, com.jakewharton.mosaic.ui/UnderlineStyle, com.jakewharton.mosaic.ui/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Text|Text(kotlin.String;com.jakewharton.mosaic.modifier.Modifier?;com.jakewharton.mosaic.ui.Color;com.jakewharton.mosaic.ui.Color;com.jakewharton.mosaic.ui.TextStyle;com.jakewharton.mosaic.ui.UnderlineStyle;com.jakewharton.mosaic.ui.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_Arrangement$stableprop_getter|com_jakewharton_mosaic_ui_Arrangement$stableprop_getter(){}[0]
@ -842,7 +849,7 @@ final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurePo
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop_getter|com_jakewharton_mosaic_ui_RowColumnMeasurementHelper$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnParentData$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnParentData$stableprop_getter|com_jakewharton_mosaic_ui_RowColumnParentData$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowScopeInstance$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowScopeInstance$stableprop_getter|com_jakewharton_mosaic_ui_RowScopeInstance$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticState$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticState$stableprop_getter|com_jakewharton_mosaic_ui_StaticState$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticLogger$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_StaticLogger$stableprop_getter|com_jakewharton_mosaic_ui_StaticLogger$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop_getter|com_jakewharton_mosaic_ui_VerticalAlignModifier$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic/Mosaic(kotlin.coroutines/CoroutineContext, kotlin/Function1<com.jakewharton.mosaic/Mosaic, kotlin/Unit>, kotlinx.coroutines.channels/Channel<com.jakewharton.mosaic.layout/KeyEvent>, androidx.compose.runtime/State<com.jakewharton.mosaic/Terminal>): com.jakewharton.mosaic/Mosaic // com.jakewharton.mosaic/Mosaic|Mosaic(kotlin.coroutines.CoroutineContext;kotlin.Function1<com.jakewharton.mosaic.Mosaic,kotlin.Unit>;kotlinx.coroutines.channels.Channel<com.jakewharton.mosaic.layout.KeyEvent>;androidx.compose.runtime.State<com.jakewharton.mosaic.Terminal>){}[0]
final fun com.jakewharton.mosaic/com_jakewharton_mosaic_AnsiRendering$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic/com_jakewharton_mosaic_AnsiRendering$stableprop_getter|com_jakewharton_mosaic_AnsiRendering$stableprop_getter(){}[0]

View File

@ -1,20 +1,17 @@
package com.jakewharton.mosaic.layout
import androidx.collection.MutableObjectList
import com.jakewharton.mosaic.TextCanvas
import com.jakewharton.mosaic.TextSurface
import com.jakewharton.mosaic.layout.Placeable.PlacementScope
import com.jakewharton.mosaic.modifier.Modifier
import com.jakewharton.mosaic.ui.StaticState
import com.jakewharton.mosaic.ui.unit.Constraints
internal fun interface DebugPolicy {
fun MosaicNode.renderDebug(): String
}
internal abstract class MosaicNodeLayer(
private val isStatic: Boolean,
) : Placeable(),
internal abstract class MosaicNodeLayer :
Placeable(),
Measurable,
PlacementScope,
MeasureScope {
@ -49,13 +46,8 @@ internal abstract class MosaicNodeLayer(
private set
final override fun placeAt(x: Int, y: Int) {
// If this layer belongs to a static node, ignore the placement coordinates from the parent.
// We reset the coordinate system to draw at 0,0 since static drawing will be on a canvas
// sized to this node's width and height.
if (!isStatic) {
this.x = x
this.y = y
}
measureResult.placeChildren()
}
@ -96,7 +88,6 @@ internal class MosaicNode(
val isStatic: Boolean,
) : Measurable {
val children = ArrayList<MosaicNode>()
var staticState: StaticState? = null
private val bottomLayer: MosaicNodeLayer = BottomLayer(this)
var topLayer: MosaicNodeLayer = bottomLayer
@ -149,34 +140,12 @@ internal class MosaicNode(
* Draw this node to a [TextSurface].
* A call to [measureAndPlace] must precede calls to this function.
*/
fun paint(): TextCanvas {
fun draw(): TextCanvas {
val surface = TextSurface(width, height)
topLayer.drawTo(surface)
return surface
}
/**
* Append any static [TextSurfaces][TextSurface] to [statics].
* A call to [measureAndPlace] must precede calls to this function.
*/
fun paintStaticsTo(statics: MutableObjectList<TextCanvas>) {
if (!isStatic) {
for (index in children.indices) {
children[index].paintStaticsTo(statics)
}
return
}
staticState?.let { staticState ->
for (index in children.indices) {
val child = children[index]
statics += child.paint()
child.paintStaticsTo(statics)
}
children.clear()
this.staticState = null
}
}
fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
return topLayer.sendKeyEvent(keyEvent)
}
@ -202,7 +171,7 @@ internal class MosaicNode(
private class BottomLayer(
private val node: MosaicNode,
) : MosaicNodeLayer(node.isStatic) {
) : MosaicNodeLayer() {
override val next: MosaicNodeLayer? get() = null
override fun doMeasure(constraints: Constraints): MeasureResult {
@ -246,7 +215,7 @@ private class BottomLayer(
private class LayoutLayer(
private val element: LayoutModifier,
override val next: MosaicNodeLayer,
) : MosaicNodeLayer(false) {
) : MosaicNodeLayer() {
override fun doMeasure(constraints: Constraints): MeasureResult {
return element.run { measure(next, constraints) }
}
@ -271,7 +240,7 @@ private class LayoutLayer(
private class DrawLayer(
private val element: DrawModifier,
override val next: MosaicNodeLayer,
) : MosaicNodeLayer(false) {
) : MosaicNodeLayer() {
override fun drawTo(canvas: TextSurface) {
val oldX = canvas.translationX
val oldY = canvas.translationY
@ -291,7 +260,7 @@ private class DrawLayer(
private class KeyLayer(
private val element: KeyModifier,
override val next: MosaicNodeLayer,
) : MosaicNodeLayer(false) {
) : MosaicNodeLayer() {
override fun sendKeyEvent(keyEvent: KeyEvent) =
element.onPreKeyEvent(keyEvent) ||
next.sendKeyEvent(keyEvent) ||

View File

@ -1,7 +1,5 @@
package com.jakewharton.mosaic
import androidx.collection.MutableObjectList
import androidx.collection.mutableObjectListOf
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.BroadcastFrameClock
@ -37,7 +35,10 @@ import com.jakewharton.mosaic.terminal.event.OperatingStatusResponseEvent
import com.jakewharton.mosaic.terminal.event.PrimaryDeviceAttributesEvent
import com.jakewharton.mosaic.terminal.event.ResizeEvent
import com.jakewharton.mosaic.terminal.event.SystemThemeEvent
import com.jakewharton.mosaic.ui.AnsiLevel
import com.jakewharton.mosaic.ui.BoxMeasurePolicy
import com.jakewharton.mosaic.ui.LocalStaticLogger
import com.jakewharton.mosaic.ui.StaticLogger
import com.jakewharton.mosaic.ui.unit.IntSize
import kotlin.concurrent.Volatile
import kotlin.coroutines.CoroutineContext
@ -285,7 +286,7 @@ public suspend fun runMosaic(content: @Composable () -> Unit) {
AnsiRendering(ansiLevel, supportsSynchronizedRendering, supportsKittyUnderlines)
}
runMosaicComposition(rendering, keyEvents, terminalState, content)
runMosaicComposition(rendering, keyEvents, terminalState, ansiLevel, supportsKittyUnderlines, content)
eventJob.cancel()
},
@ -296,6 +297,8 @@ internal suspend fun runMosaicComposition(
rendering: Rendering,
keyEvents: Channel<KeyEvent>,
terminalState: MutableState<Terminal>,
ansiLevel: AnsiLevel,
supportsKittyUnderlines: Boolean,
content: @Composable () -> Unit,
) {
val clock = BroadcastFrameClock()
@ -306,6 +309,8 @@ internal suspend fun runMosaicComposition(
},
keyEvents = keyEvents,
terminalState = terminalState,
ansiLevel = ansiLevel,
supportsKittyUnderlines = supportsKittyUnderlines,
)
mosaicComposition.setContent(content)
@ -332,15 +337,9 @@ internal inline fun <T> MutableState<T>.update(updater: T.() -> T) {
public interface Mosaic {
public fun setContent(content: @Composable () -> Unit)
public fun paint(): TextCanvas
public fun paintStaticsTo(list: MutableObjectList<TextCanvas>)
public fun paintStatics(): List<TextCanvas> {
return mutableObjectListOf<TextCanvas>()
.apply(::paintStaticsTo)
.asList()
}
public fun dump(): String
public fun draw(): TextCanvas
public fun static(): String?
public fun dumpNodes(): String
public suspend fun awaitComplete()
public fun cancel()
@ -353,7 +352,7 @@ public fun Mosaic(
keyEvents: Channel<KeyEvent>,
terminalState: State<Terminal>,
): Mosaic {
return MosaicComposition(coroutineContext, onDraw, keyEvents, terminalState)
return MosaicComposition(coroutineContext, onDraw, keyEvents, terminalState, AnsiLevel.NONE, false)
}
internal class MosaicComposition(
@ -361,6 +360,9 @@ internal class MosaicComposition(
private val onDraw: (Mosaic) -> Unit,
private val keyEvents: Channel<KeyEvent>,
private val terminalState: State<Terminal>,
// TODO These two don't belong here!
private val ansiLevel: AnsiLevel,
private val supportsKittyUnderlines: Boolean,
) : Mosaic,
LifecycleOwner {
private val externalClock = checkNotNull(coroutineContext[MonotonicFrameClock]) {
@ -377,6 +379,9 @@ internal class MosaicComposition(
private val recomposer = Recomposer(composeContext)
private val composition = Composition(applier, recomposer)
private val staticLogs = Channel<Any>(UNLIMITED)
private val staticLogger = StaticLogger(staticLogs) { needDraw = true }
override val lifecycle = LifecycleRegistry.createUnsafe(this).also { lifecycle ->
scope.launch(start = UNDISPATCHED) {
snapshotFlow { terminalState.value.focused }.collect { focused ->
@ -421,17 +426,41 @@ internal class MosaicComposition(
onDraw(this)
}
override fun paint(): TextCanvas {
override fun draw(): TextCanvas {
return Snapshot.observe(readObserver = drawBlockStateReadObserver) {
rootNode.paint()
rootNode.draw()
}
}
override fun paintStaticsTo(list: MutableObjectList<TextCanvas>) {
rootNode.paintStaticsTo(list)
override fun static(): String? {
var static = staticLogs.tryReceive().getOrNull()
if (static == null) {
return null
}
return buildString {
do {
when (static) {
is String -> {
append(static)
append("\r\n")
}
is TextCanvas -> {
for (row in 0 until static.height) {
static.appendRowTo(this, row, ansiLevel, supportsKittyUnderlines)
append("\r\n")
}
}
}
override fun dump(): String {
static = staticLogs.tryReceive().getOrNull()
} while (static != null)
// Remove trailing "\r\n".
setLength(length - 2)
}
}
override fun dumpNodes(): String {
return rootNode.toString()
}
@ -494,6 +523,7 @@ internal class MosaicComposition(
composition.setContent {
CompositionLocalProvider(
LocalTerminal provides terminalState.value,
LocalStaticLogger provides staticLogger,
LocalLifecycleOwner provides this,
content = content,
)
@ -531,10 +561,9 @@ internal class MosaicComposition(
}
internal class MosaicNodeApplier(
root: MosaicNode? = null,
private val onChanges: () -> Unit = {},
) : AbstractApplier<MosaicNode>(
root = root ?: MosaicNode(
root = MosaicNode(
measurePolicy = BoxMeasurePolicy(),
debugPolicy = { children.joinToString(separator = "\n") },
isStatic = false,

View File

@ -1,6 +1,5 @@
package com.jakewharton.mosaic
import androidx.collection.mutableObjectListOf
import com.jakewharton.mosaic.ui.AnsiLevel
import kotlin.time.TimeMark
import kotlin.time.TimeSource
@ -41,18 +40,14 @@ internal class DebugRendering(
lastRender = systemClock.markNow()
append("NODES:\r\n")
append(mosaic.dump().replace("\n", "\r\n"))
append(mosaic.dumpNodes().replace("\n", "\r\n"))
append("\r\n\r\n")
val statics = mutableObjectListOf<TextCanvas>()
try {
mosaic.paintStaticsTo(statics)
if (statics.isNotEmpty()) {
mosaic.static()?.let { static ->
append("STATIC:\r\n")
statics.forEach { static ->
appendSurface(static)
}
append("\r\n")
append(static)
append("\r\n\r\n")
}
} catch (t: Throwable) {
failed = true
@ -62,7 +57,7 @@ internal class DebugRendering(
append("OUTPUT:\r\n")
try {
appendSurface(mosaic.paint())
appendSurface(mosaic.draw())
} catch (t: Throwable) {
failed = true
append(t.stackTraceToString().replace("\n", "\r\n"))
@ -81,7 +76,6 @@ internal class AnsiRendering(
private val supportsKittyUnderlines: Boolean,
) : Rendering {
private val stringBuilder = StringBuilder(100)
private val staticSurfaces = mutableObjectListOf<TextCanvas>()
private var lastHeight = 0
override fun render(mosaic: Mosaic): CharSequence {
@ -112,17 +106,20 @@ internal class AnsiRendering(
}
}
staticSurfaces.let { staticSurfaces ->
mosaic.paintStaticsTo(staticSurfaces)
if (staticSurfaces.isNotEmpty()) {
staticSurfaces.forEach { staticSurface ->
appendSurface(staticSurface)
}
staticSurfaces.clear()
mosaic.static()?.let { static ->
// We don't know the width or height of static strings, and we don't want to waste time
// parsing them to get an accurate count of each to minimally clear only the lines it will
// write. Instead, just clear everything to ensure we neither leave old cells nor attempt
// a clear to end-of-line when the cursor is on the last column (thus clearing that cell).
if (staleLines > 0) {
append(clearDisplay)
staleLines = 0
}
append(static)
append("\r\n")
}
val surface = mosaic.paint()
val surface = mosaic.draw()
appendSurface(surface)
// If the new output contains fewer lines than the last output, clear those old lines.

View File

@ -1,100 +0,0 @@
@file:JvmName("Static")
package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.snapshots.SnapshotStateList
import com.jakewharton.mosaic.MosaicNodeApplier
import com.jakewharton.mosaic.layout.MosaicNode
import kotlin.jvm.JvmName
/** Render each value emitted by [items] as permanent output above the regular display. */
@Deprecated(
"Loop over items list yourself and call Static on each item",
ReplaceWith("items.forEach { Static { content(it) } }"),
)
@Composable
public fun <T> Static(
items: SnapshotStateList<T>,
content: @Composable (item: T) -> Unit,
) {
items.forEach {
Static {
content(it)
}
}
}
/**
* Render [content] once as permanent output above the regular display.
*
* The [content] function will be recomposed once and then never again.
* Any contained [SideEffect]s or [DisposableEffect]s will run (and be disposed),
* but [LaunchedEffect]s will not launch.
*/
@Composable
public fun Static(
content: @Composable () -> Unit,
) {
val compositionContext = rememberCompositionContext()
val state = remember {
StaticState(compositionContext, content)
}
ComposeNode<MosaicNode, Applier<Any>>(
factory = StaticFactory,
update = {
set(state, BindStateToNode)
},
)
}
private val BindStateToNode: MosaicNode.(StaticState) -> Unit = {
staticState = it
it.setNode(this)
}
private val StaticFactory: () -> MosaicNode = {
MosaicNode(
measurePolicy = { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(0, 0) {
// Despite reporting no size to our parent, we still place each child at
// 0,0 since they will be individually rendered.
placeables.forEach { placeable ->
placeable.place(0, 0)
}
}
},
debugPolicy = {
children.joinToString(prefix = "Static()") { "\n" + it.toString().prependIndent(" ") }
},
isStatic = true,
)
}
@Stable
internal class StaticState(
private val compositionContext: CompositionContext,
private val content: @Composable () -> Unit,
) {
fun setNode(parent: MosaicNode) {
val applier = MosaicNodeApplier(parent)
val composition = Composition(applier, compositionContext)
composition.setContent(content)
composition.dispose()
}
}

View File

@ -0,0 +1,65 @@
@file:JvmName("Static")
package com.jakewharton.mosaic.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.staticCompositionLocalOf
import com.jakewharton.mosaic.MosaicNodeApplier
import com.jakewharton.mosaic.TextCanvas
import kotlin.jvm.JvmName
import kotlinx.coroutines.channels.SendChannel
@Stable
public class StaticLogger internal constructor(
private val logs: SendChannel<Any>,
private val onLog: () -> Unit,
) {
private fun send(value: Any) {
logs.trySend(value).getOrThrow()
onLog()
}
public fun log(string: String): Unit = send(string)
public fun log(canvas: TextCanvas): Unit = send(canvas)
public inline operator fun plusAssign(string: String): Unit = log(string)
public inline operator fun plusAssign(canvas: TextCanvas): Unit = log(canvas)
}
public val LocalStaticLogger: ProvidableCompositionLocal<StaticLogger> =
staticCompositionLocalOf {
throw AssertionError("No static logger provided")
}
/**
* Render [content] once as permanent output above the regular display.
*
* The [content] function will be recomposed once and then never again.
* Any contained [SideEffect]s or [DisposableEffect]s will run (and be disposed),
* but [LaunchedEffect]s will not launch.
*/
@Composable
public fun StaticEffect(
content: @Composable () -> Unit,
) {
val compositionContext = rememberCompositionContext()
val staticLogging = LocalStaticLogger.current
SideEffect {
val applier = MosaicNodeApplier()
val composition = Composition(applier, compositionContext)
composition.setContent(content)
val root = applier.root
root.measureAndPlace()
staticLogging += root.draw()
composition.dispose()
}
}

View File

@ -9,7 +9,7 @@ import com.jakewharton.mosaic.ui.AnsiLevel
import com.jakewharton.mosaic.ui.Color
import com.jakewharton.mosaic.ui.Column
import com.jakewharton.mosaic.ui.Row
import com.jakewharton.mosaic.ui.Static
import com.jakewharton.mosaic.ui.StaticEffect
import com.jakewharton.mosaic.ui.Text
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
@ -120,7 +120,7 @@ class AnsiRenderingTest {
runMosaicTest(RenderingSnapshots(rendering)) {
setContent {
Text("Hello")
Static {
StaticEffect {
Text("World!")
}
}
@ -138,7 +138,7 @@ class AnsiRenderingTest {
@Test fun staticLinesNotErased() = runTest {
runMosaicTest(RenderingSnapshots(rendering)) {
setContent {
Static {
StaticEffect {
Text("One")
}
Text("Two")
@ -153,7 +153,7 @@ class AnsiRenderingTest {
)
setContent {
Static {
StaticEffect {
Text("Three")
}
Text("Four")
@ -161,7 +161,7 @@ class AnsiRenderingTest {
assertThat(awaitSnapshot()).isEqualTo(
"""
|${cursorUp(1)}${clearLine}Three
|${cursorUp(1)}${clearDisplay}Three
|Four
|
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
@ -172,24 +172,24 @@ class AnsiRenderingTest {
@Test fun staticOrderingIsDfs() = runTest {
runMosaicTest(RenderingSnapshots(rendering)) {
setContent {
Static {
StaticEffect {
Text("One")
}
Column {
Static {
StaticEffect {
Text("Two")
}
Row {
Static {
StaticEffect {
Text("Three")
}
Text("Sup")
}
Static {
StaticEffect {
Text("Four")
}
}
Static {
StaticEffect {
Text("Five")
}
}
@ -215,7 +215,7 @@ class AnsiRenderingTest {
Text("TopTopTop")
Row {
Text("LeftLeft")
Static {
StaticEffect {
Text("Static")
}
}

View File

@ -13,7 +13,7 @@ import com.jakewharton.mosaic.testing.runMosaicTest
import com.jakewharton.mosaic.ui.AnsiLevel
import com.jakewharton.mosaic.ui.Layout
import com.jakewharton.mosaic.ui.Row
import com.jakewharton.mosaic.ui.Static
import com.jakewharton.mosaic.ui.StaticEffect
import com.jakewharton.mosaic.ui.Text
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
@ -64,7 +64,7 @@ class DebugRenderingTest {
runMosaicTest(RenderingSnapshots(rendering)) {
setContent {
Text("Hello")
Static {
StaticEffect {
Text("Static")
}
}
@ -73,8 +73,6 @@ class DebugRenderingTest {
"""
|NODES:
|Text("Hello") x=0 y=0 w=5 h=1 DrawBehind
|Static()
| Text("Static") x=0 y=0 w=6 h=1 DrawBehind
|
|STATIC:
|Static

View File

@ -113,6 +113,8 @@ class MosaicTest {
),
keyEvents = Channel(),
terminalState = mutableStateOf(Terminal.Default),
ansiLevel = AnsiLevel.NONE,
supportsKittyUnderlines = false,
) {
LaunchedEffect(Unit) {
withFrameNanos { frameTimeNanos ->

View File

@ -5,7 +5,7 @@ import com.jakewharton.mosaic.testing.SnapshotStrategy
internal object DumpSnapshots : SnapshotStrategy<String> {
override fun create(mosaic: Mosaic): String {
return mosaic.dump()
return mosaic.dumpNodes()
}
}

View File

@ -60,7 +60,7 @@ class BoxTest {
assertThat(positionedChildContainerNode.size).isEqualTo(IntSize(size, size))
assertThat(positionedChildContainerNode.position).isEqualTo(IntOffset.Zero)
assertThat(rootNode.paint().render(AnsiLevel.NONE, false)).isEqualTo(
assertThat(rootNode.draw().render(AnsiLevel.NONE, false)).isEqualTo(
"""
|
|
@ -142,7 +142,7 @@ class BoxTest {
assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size))
assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero)
assertThat(rootNode.paint().render(AnsiLevel.NONE, false)).isEqualTo(
assertThat(rootNode.draw().render(AnsiLevel.NONE, false)).isEqualTo(
"""
|
| $TestChar$TestChar$TestChar$TestChar$TestChar
@ -189,7 +189,7 @@ class BoxTest {
assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size))
assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero)
assertThat(rootNode.paint().render(AnsiLevel.NONE, false)).isEqualTo(
assertThat(rootNode.draw().render(AnsiLevel.NONE, false)).isEqualTo(
"""
|$TestChar$TestChar$TestChar$TestChar$TestChar
|$TestChar$TestChar$TestChar$TestChar$TestChar
@ -236,7 +236,7 @@ class BoxTest {
assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size))
assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero)
assertThat(rootNode.paint().render(AnsiLevel.NONE, false)).isEqualTo(
assertThat(rootNode.draw().render(AnsiLevel.NONE, false)).isEqualTo(
"""
| $TestChar$TestChar$TestChar$TestChar
| $TestChar$TestChar$TestChar$TestChar
@ -283,7 +283,7 @@ class BoxTest {
assertThat(secondChildContainerNode.size).isEqualTo(IntSize(size, size))
assertThat(secondChildContainerNode.position).isEqualTo(IntOffset.Zero)
assertThat(rootNode.paint().render(AnsiLevel.NONE, false)).isEqualTo(
assertThat(rootNode.draw().render(AnsiLevel.NONE, false)).isEqualTo(
"""
|
|$TestChar$TestChar$TestChar$TestChar$TestChar$TestChar

View File

@ -1,6 +1,5 @@
package com.jakewharton.mosaic.ui
import androidx.collection.mutableObjectListOf
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
@ -9,14 +8,10 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import assertk.assertAll
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.hasSize
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import assertk.assertions.isTrue
import com.jakewharton.mosaic.NodeSnapshots
import com.jakewharton.mosaic.assertFailure
import com.jakewharton.mosaic.render
import com.jakewharton.mosaic.testing.MosaicSnapshots
import com.jakewharton.mosaic.testing.runMosaicTest
@ -31,11 +26,11 @@ class StaticTest {
@Test fun renderingDoesNotCauseAnotherFrame() = runTest {
runMosaicTest(MosaicSnapshots) {
setContent {
Static { Text("static") }
StaticEffect { Text("static") }
Text("content")
}
assertThat(awaitSnapshot().paintStatics()).hasSize(1)
assertThat(awaitSnapshot().static()).isEqualTo("static")
assertFailsWith<TimeoutCancellationException> { awaitSnapshot() }
}
}
@ -44,57 +39,19 @@ class StaticTest {
runMosaicTest(MosaicSnapshots) {
var count by mutableIntStateOf(1)
setContent {
Static { Text("static: $count") }
StaticEffect { Text("static: $count") }
Text("content: $count")
}
val one = awaitSnapshot()
assertThat(one.paint().render()).isEqualTo("content: 1")
assertThat(one.paintStatics().render()).containsExactly("static: 1")
assertThat(one.draw().render()).isEqualTo("content: 1")
assertThat(one.static()).isEqualTo("static: 1")
count = 2
val two = awaitSnapshot()
assertThat(two.paint().render()).isEqualTo("content: 2")
assertThat(two.paintStatics().render()).isEmpty()
}
}
@Test fun staticNodesRemovedAfterRenderWithoutRecomposition() = runTest {
runMosaicTest(NodeSnapshots) {
var count by mutableIntStateOf(1)
setContent {
Static { Text("static: $count") }
Text("content: $count")
}
val one = awaitSnapshot()
assertThat(one.toString()).isEqualTo(
"""
|Static()
| Text("static: 1") x=0 y=0 w=9 h=1 DrawBehind
|Text("content: 1") x=0 y=0 w=10 h=1 DrawBehind
""".trimMargin(),
)
one.paintStaticsTo(mutableObjectListOf())
assertThat(one.toString()).isEqualTo(
"""
|Static()
|Text("content: 1") x=0 y=0 w=10 h=1 DrawBehind
""".trimMargin(),
)
assertFailure<TimeoutCancellationException> { awaitSnapshot() }
count = 2
val two = awaitSnapshot()
assertThat(two.toString()).isEqualTo(
"""
|Static()
|Text("content: 2") x=0 y=0 w=10 h=1 DrawBehind
""".trimMargin(),
)
assertThat(two.draw().render()).isEqualTo("content: 2")
assertThat(two.static()).isNull()
}
}
@ -102,7 +59,7 @@ class StaticTest {
runMosaicTest {
var ran = false
setContent {
Static {
StaticEffect {
SideEffect {
ran = true
}
@ -116,7 +73,7 @@ class StaticTest {
runMosaicTest {
var ran = false
setContent {
Static {
StaticEffect {
LaunchedEffect(Unit) {
ran = true
}
@ -135,7 +92,7 @@ class StaticTest {
var effectRan = false
var disposeRan = false
setContent {
Static {
StaticEffect {
DisposableEffect(Unit) {
effectRan = true
onDispose {
@ -157,7 +114,7 @@ class StaticTest {
var normalRecompositions = 0
var count by mutableIntStateOf(0)
setContentAndSnapshot {
Static {
StaticEffect {
staticRecompositions++
Text("count: $count")
}
@ -174,4 +131,22 @@ class StaticTest {
assertThat(normalRecompositions).isEqualTo(2)
}
}
@Test fun loggingCausesFrameWithoutRecomposition() = runTest {
runMosaicTest(MosaicSnapshots) {
var recompositionCount = 0
lateinit var staticLogger: StaticLogger
val initial = setContentAndSnapshot {
staticLogger = LocalStaticLogger.current
Text("Count: ${++recompositionCount}")
}
assertThat(initial.draw().render()).isEqualTo("Count: 1")
assertThat(initial.static()).isNull()
staticLogger += "sup"
val snapshot = awaitSnapshot()
assertThat(snapshot.draw().render()).isEqualTo("Count: 1")
assertThat(snapshot.static()).isEqualTo("sup")
}
}
}

View File

@ -1,13 +1,11 @@
package com.jakewharton.mosaic.testing
import androidx.collection.MutableObjectList
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.jakewharton.mosaic.Mosaic
import com.jakewharton.mosaic.Terminal
import com.jakewharton.mosaic.TextCanvas
import com.jakewharton.mosaic.layout.KeyEvent
import com.jakewharton.mosaic.ui.AnsiLevel
import kotlin.coroutines.CoroutineContext
@ -106,25 +104,16 @@ private class RealTestMosaic<T>(
keyEvents.trySend(keyEvent)
}
override fun paint() = mosaic.paint()
override fun draw() = mosaic.draw()
override fun static() = mosaic.static()
override fun dumpNodes() = mosaic.dumpNodes()
override fun paintStaticsTo(list: MutableObjectList<TextCanvas>) {
mosaic.paintStaticsTo(list)
}
override fun dump() = mosaic.dump()
override suspend fun awaitComplete() {
mosaic.awaitComplete()
}
override fun cancel() {
mosaic.cancel()
}
override suspend fun awaitComplete() = mosaic.awaitComplete()
override fun cancel() = mosaic.cancel()
}
internal object PlainTextSnapshots : SnapshotStrategy<String> {
override fun create(mosaic: Mosaic): String {
return mosaic.paint().render(AnsiLevel.NONE, false)
return mosaic.draw().render(AnsiLevel.NONE, false)
}
}

View File

@ -26,7 +26,7 @@ import com.jakewharton.mosaic.ui.Color.Companion.Yellow
import com.jakewharton.mosaic.ui.Column
import com.jakewharton.mosaic.ui.Row
import com.jakewharton.mosaic.ui.Spacer
import com.jakewharton.mosaic.ui.Static
import com.jakewharton.mosaic.ui.StaticEffect
import com.jakewharton.mosaic.ui.Text
import com.jakewharton.mosaic.ui.TextStyle.Companion.Bold
import example.TestState.Fail
@ -138,7 +138,8 @@ fun TestRow(test: Test) {
@Composable
fun Log(complete: SnapshotStateList<Test>) {
complete.forEach { test ->
Static {
StaticEffect {
Column {
TestRow(test)
if (test.failures.isNotEmpty()) {
for (failure in test.failures) {
@ -148,6 +149,7 @@ fun Log(complete: SnapshotStateList<Test>) {
}
}
}
}
// Separate logs from rest of display by a single line if latest test result is success.
if (complete.lastOrNull()?.state == Pass) {