mirror of
				https://github.com/JakeWharton/mosaic.git
				synced 2025-10-31 18:58:37 +08:00 
			
		
		
		
	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:
		| @ -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. | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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] | ||||
|  | ||||
| @ -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) || | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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() | ||||
| 	} | ||||
| } | ||||
| @ -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() | ||||
| 	} | ||||
| } | ||||
| @ -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") | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -113,6 +113,8 @@ class MosaicTest { | ||||
| 			), | ||||
| 			keyEvents = Channel(), | ||||
| 			terminalState = mutableStateOf(Terminal.Default), | ||||
| 			ansiLevel = AnsiLevel.NONE, | ||||
| 			supportsKittyUnderlines = false, | ||||
| 		) { | ||||
| 			LaunchedEffect(Unit) { | ||||
| 				withFrameNanos { frameTimeNanos -> | ||||
|  | ||||
| @ -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() | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jake Wharton
					Jake Wharton