diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7e42a3..c9eb6ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/mosaic-runtime/api/mosaic-runtime.api b/mosaic-runtime/api/mosaic-runtime.api index 7e42e9d5..5b0e5f2b 100644 --- a/mosaic-runtime/api/mosaic-runtime.api +++ b/mosaic-runtime/api/mosaic-runtime.api @@ -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 { diff --git a/mosaic-runtime/api/mosaic-runtime.klib.api b/mosaic-runtime/api/mosaic-runtime.klib.api index e1461857..49eefdf5 100644 --- a/mosaic-runtime/api/mosaic-runtime.klib.api +++ b/mosaic-runtime/api/mosaic-runtime.klib.api @@ -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/Mosaic.paintStaticsTo|paintStaticsTo(androidx.collection.MutableObjectList){}[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) // com.jakewharton.mosaic/Mosaic.setContent|setContent(kotlin.Function2){}[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/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 (kotlin/Boolean, kotlin/Boolean, com.jakewharton.mosaic.ui.unit/IntSize) // com.jakewharton.mosaic/Terminal.|(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 (): androidx.compose.runtime/ProvidableCompositionLocal // com.jakewharton.mosaic.ui/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).(): kotlin/Boolean // com.jakewharton.mosaic.ui/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§}[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, com.jakewharton.mosaic.modifier/Modifier?, kotlin/Function0?, com.jakewharton.mosaic.layout/MeasurePolicy, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.jakewharton.mosaic.ui/Layout|Layout(kotlin.Function2;com.jakewharton.mosaic.modifier.Modifier?;kotlin.Function0?;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, 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;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) // com.jakewharton.mosaic.ui/Static|Static(kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int){}[0] +final fun com.jakewharton.mosaic.ui/StaticEffect(kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int) // com.jakewharton.mosaic.ui/StaticEffect|StaticEffect(kotlin.Function2;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, kotlinx.coroutines.channels/Channel, androidx.compose.runtime/State): com.jakewharton.mosaic/Mosaic // com.jakewharton.mosaic/Mosaic|Mosaic(kotlin.coroutines.CoroutineContext;kotlin.Function1;kotlinx.coroutines.channels.Channel;androidx.compose.runtime.State){}[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] 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 17fcf002..4a443b31 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 @@ -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 - } + this.x = x + this.y = y measureResult.placeChildren() } @@ -96,7 +88,6 @@ internal class MosaicNode( val isStatic: Boolean, ) : Measurable { val children = ArrayList() - 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) { - 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) || diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt index cfdd31fb..e3273830 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt @@ -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, terminalState: MutableState, + 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 MutableState.update(updater: T.() -> T) { public interface Mosaic { public fun setContent(content: @Composable () -> Unit) - public fun paint(): TextCanvas - public fun paintStaticsTo(list: MutableObjectList) - public fun paintStatics(): List { - return mutableObjectListOf() - .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, terminalState: State, ): 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, private val terminalState: State, + // 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(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) { - 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") + } + } + } + + static = staticLogs.tryReceive().getOrNull() + } while (static != null) + + // Remove trailing "\r\n". + setLength(length - 2) + } } - override fun dump(): String { + 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( - root = root ?: MosaicNode( + root = MosaicNode( measurePolicy = BoxMeasurePolicy(), debugPolicy = { children.joinToString(separator = "\n") }, isStatic = false, diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt index 3994be31..877675cf 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt @@ -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() 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() 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. diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt deleted file mode 100644 index e77600f4..00000000 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/Static.kt +++ /dev/null @@ -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 Static( - items: SnapshotStateList, - 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>( - 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() - } -} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/StaticEffect.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/StaticEffect.kt new file mode 100644 index 00000000..44889a4e --- /dev/null +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ui/StaticEffect.kt @@ -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, + 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 = + 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() + } +} diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt index d654c503..7c97a0e9 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt @@ -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") } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt index 4083295d..0540398f 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/DebugRenderingTest.kt @@ -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 diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt index 75455ae0..d3bc7aa5 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt @@ -113,6 +113,8 @@ class MosaicTest { ), keyEvents = Channel(), terminalState = mutableStateOf(Terminal.Default), + ansiLevel = AnsiLevel.NONE, + supportsKittyUnderlines = false, ) { LaunchedEffect(Unit) { withFrameNanos { frameTimeNanos -> diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/snapshotStrategies.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/snapshotStrategies.kt index 1edc9e07..91ff1d89 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/snapshotStrategies.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/snapshotStrategies.kt @@ -5,7 +5,7 @@ import com.jakewharton.mosaic.testing.SnapshotStrategy internal object DumpSnapshots : SnapshotStrategy { override fun create(mosaic: Mosaic): String { - return mosaic.dump() + return mosaic.dumpNodes() } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt index 9f9cfc3d..8884aa29 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/BoxTest.kt @@ -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 diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/StaticTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/StaticTest.kt index 76d53488..49698004 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/StaticTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/ui/StaticTest.kt @@ -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 { 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 { 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") + } + } } diff --git a/mosaic-testing/src/commonMain/kotlin/com/jakewharton/mosaic/testing/TestMosaic.kt b/mosaic-testing/src/commonMain/kotlin/com/jakewharton/mosaic/testing/TestMosaic.kt index e44d953b..5368cf45 100644 --- a/mosaic-testing/src/commonMain/kotlin/com/jakewharton/mosaic/testing/TestMosaic.kt +++ b/mosaic-testing/src/commonMain/kotlin/com/jakewharton/mosaic/testing/TestMosaic.kt @@ -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( 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) { - 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 { override fun create(mosaic: Mosaic): String { - return mosaic.paint().render(AnsiLevel.NONE, false) + return mosaic.draw().render(AnsiLevel.NONE, false) } } diff --git a/samples/jest/src/main/kotlin/example/jest.kt b/samples/jest/src/main/kotlin/example/jest.kt index 582b41b2..f67dfabb 100644 --- a/samples/jest/src/main/kotlin/example/jest.kt +++ b/samples/jest/src/main/kotlin/example/jest.kt @@ -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,13 +138,15 @@ fun TestRow(test: Test) { @Composable fun Log(complete: SnapshotStateList) { complete.forEach { test -> - Static { - TestRow(test) - if (test.failures.isNotEmpty()) { - for (failure in test.failures) { - Text(" ‣ $failure") + StaticEffect { + Column { + TestRow(test) + if (test.failures.isNotEmpty()) { + for (failure in test.failures) { + Text(" ‣ $failure") + } + Spacer(Modifier.height(1)) // Blank line } - Spacer(Modifier.height(1)) // Blank line } } }