Skip to content

Commit

Permalink
Expose a public Mosaic type (#616)
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton authored Jan 3, 2025
1 parent 2ffbf43 commit 313db47
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 50 deletions.
29 changes: 29 additions & 0 deletions mosaic-runtime/api/mosaic-runtime.api
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
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 getTerminalState ()Landroidx/compose/runtime/MutableState;
public abstract fun paint ()Lcom/jakewharton/mosaic/TextCanvas;
public abstract fun paintStaticsTo (Landroidx/collection/MutableObjectList;)V
public abstract fun sendKeyEvent (Lcom/jakewharton/mosaic/layout/KeyEvent;)V
public abstract fun setContent (Lkotlin/jvm/functions/Function2;)V
}

public final class com/jakewharton/mosaic/MosaicKt {
public static final fun Mosaic (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)Lcom/jakewharton/mosaic/Mosaic;
public static final fun renderMosaic (Lkotlin/jvm/functions/Function2;)Ljava/lang/String;
public static final fun runMosaic (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun runMosaicBlocking (Lkotlin/jvm/functions/Function2;)V
Expand All @@ -17,6 +29,13 @@ public final class com/jakewharton/mosaic/TerminalKt {
public static final fun getLocalTerminal ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public abstract interface class com/jakewharton/mosaic/TextCanvas {
public abstract fun appendRowTo (Ljava/lang/Appendable;ILcom/jakewharton/mosaic/ui/AnsiLevel;)V
public abstract fun getHeight ()I
public abstract fun getWidth ()I
public abstract fun render (Lcom/jakewharton/mosaic/ui/AnsiLevel;)Ljava/lang/String;
}

public final class com/jakewharton/mosaic/layout/AspectRatioKt {
public static final fun aspectRatio (Lcom/jakewharton/mosaic/modifier/Modifier;FZ)Lcom/jakewharton/mosaic/modifier/Modifier;
public static synthetic fun aspectRatio$default (Lcom/jakewharton/mosaic/modifier/Modifier;FZILjava/lang/Object;)Lcom/jakewharton/mosaic/modifier/Modifier;
Expand Down Expand Up @@ -368,6 +387,16 @@ public abstract interface class com/jakewharton/mosaic/ui/Alignment$Vertical {
public abstract fun align (II)I
}

public final class com/jakewharton/mosaic/ui/AnsiLevel : java/lang/Enum {
public static final field ANSI16 Lcom/jakewharton/mosaic/ui/AnsiLevel;
public static final field ANSI256 Lcom/jakewharton/mosaic/ui/AnsiLevel;
public static final field NONE Lcom/jakewharton/mosaic/ui/AnsiLevel;
public static final field TRUECOLOR Lcom/jakewharton/mosaic/ui/AnsiLevel;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/jakewharton/mosaic/ui/AnsiLevel;
public static fun values ()[Lcom/jakewharton/mosaic/ui/AnsiLevel;
}

public final class com/jakewharton/mosaic/ui/Arrangement {
public static final field $stable I
public static final field INSTANCE Lcom/jakewharton/mosaic/ui/Arrangement;
Expand Down
37 changes: 37 additions & 0 deletions mosaic-runtime/api/mosaic-runtime.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ final enum class com.jakewharton.mosaic.layout/IntrinsicSize : kotlin/Enum<com.j
final fun values(): kotlin/Array<com.jakewharton.mosaic.layout/IntrinsicSize> // com.jakewharton.mosaic.layout/IntrinsicSize.values|values#static(){}[0]
}

final enum class com.jakewharton.mosaic.ui/AnsiLevel : kotlin/Enum<com.jakewharton.mosaic.ui/AnsiLevel> { // com.jakewharton.mosaic.ui/AnsiLevel|null[0]
enum entry ANSI16 // com.jakewharton.mosaic.ui/AnsiLevel.ANSI16|null[0]
enum entry ANSI256 // com.jakewharton.mosaic.ui/AnsiLevel.ANSI256|null[0]
enum entry NONE // com.jakewharton.mosaic.ui/AnsiLevel.NONE|null[0]
enum entry TRUECOLOR // com.jakewharton.mosaic.ui/AnsiLevel.TRUECOLOR|null[0]

final val entries // com.jakewharton.mosaic.ui/AnsiLevel.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<com.jakewharton.mosaic.ui/AnsiLevel> // com.jakewharton.mosaic.ui/AnsiLevel.entries.<get-entries>|<get-entries>#static(){}[0]

final fun valueOf(kotlin/String): com.jakewharton.mosaic.ui/AnsiLevel // com.jakewharton.mosaic.ui/AnsiLevel.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<com.jakewharton.mosaic.ui/AnsiLevel> // com.jakewharton.mosaic.ui/AnsiLevel.values|values#static(){}[0]
}

abstract fun interface com.jakewharton.mosaic.layout/MeasurePolicy { // com.jakewharton.mosaic.layout/MeasurePolicy|null[0]
abstract fun (com.jakewharton.mosaic.layout/MeasureScope).measure(kotlin.collections/List<com.jakewharton.mosaic.layout/Measurable>, com.jakewharton.mosaic.ui.unit/Constraints): com.jakewharton.mosaic.layout/MeasureResult // com.jakewharton.mosaic.layout/MeasurePolicy.measure|[email protected](kotlin.collections.List<com.jakewharton.mosaic.layout.Measurable>;com.jakewharton.mosaic.ui.unit.Constraints){}[0]
open fun maxIntrinsicHeight(kotlin.collections/List<com.jakewharton.mosaic.layout/IntrinsicMeasurable>, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.layout/MeasurePolicy.maxIntrinsicHeight|maxIntrinsicHeight(kotlin.collections.List<com.jakewharton.mosaic.layout.IntrinsicMeasurable>;kotlin.Int){}[0]
Expand Down Expand Up @@ -183,6 +196,29 @@ abstract interface com.jakewharton.mosaic.ui/RowScope { // com.jakewharton.mosai
abstract fun (com.jakewharton.mosaic.modifier/Modifier).weight(kotlin/Float, kotlin/Boolean = ...): com.jakewharton.mosaic.modifier/Modifier // com.jakewharton.mosaic.ui/RowScope.weight|[email protected](kotlin.Float;kotlin.Boolean){}[0]
}

abstract interface com.jakewharton.mosaic/Mosaic { // com.jakewharton.mosaic/Mosaic|null[0]
abstract val terminalState // com.jakewharton.mosaic/Mosaic.terminalState|{}terminalState[0]
abstract fun <get-terminalState>(): androidx.compose.runtime/MutableState<com.jakewharton.mosaic/Terminal> // com.jakewharton.mosaic/Mosaic.terminalState.<get-terminalState>|<get-terminalState>(){}[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 sendKeyEvent(com.jakewharton.mosaic.layout/KeyEvent) // com.jakewharton.mosaic/Mosaic.sendKeyEvent|sendKeyEvent(com.jakewharton.mosaic.layout.KeyEvent){}[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 suspend fun awaitComplete() // com.jakewharton.mosaic/Mosaic.awaitComplete|awaitComplete(){}[0]
}

abstract interface com.jakewharton.mosaic/TextCanvas { // com.jakewharton.mosaic/TextCanvas|null[0]
abstract val height // com.jakewharton.mosaic/TextCanvas.height|{}height[0]
abstract fun <get-height>(): kotlin/Int // com.jakewharton.mosaic/TextCanvas.height.<get-height>|<get-height>(){}[0]
abstract val width // com.jakewharton.mosaic/TextCanvas.width|{}width[0]
abstract fun <get-width>(): kotlin/Int // com.jakewharton.mosaic/TextCanvas.width.<get-width>|<get-width>(){}[0]

abstract fun appendRowTo(kotlin.text/Appendable, kotlin/Int, com.jakewharton.mosaic.ui/AnsiLevel) // com.jakewharton.mosaic/TextCanvas.appendRowTo|appendRowTo(kotlin.text.Appendable;kotlin.Int;com.jakewharton.mosaic.ui.AnsiLevel){}[0]
abstract fun render(com.jakewharton.mosaic.ui/AnsiLevel): kotlin/String // com.jakewharton.mosaic/TextCanvas.render|render(com.jakewharton.mosaic.ui.AnsiLevel){}[0]
}

sealed interface com.jakewharton.mosaic.layout/DrawStyle { // com.jakewharton.mosaic.layout/DrawStyle|null[0]
final class Stroke : com.jakewharton.mosaic.layout/DrawStyle { // com.jakewharton.mosaic.layout/DrawStyle.Stroke|null[0]
constructor <init>(kotlin/Int = ...) // com.jakewharton.mosaic.layout/DrawStyle.Stroke.<init>|<init>(kotlin.Int){}[0]
Expand Down Expand Up @@ -752,6 +788,7 @@ final fun com.jakewharton.mosaic.ui/com_jakewharton_mosaic_ui_RowColumnMeasureme
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_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>): com.jakewharton.mosaic/Mosaic // com.jakewharton.mosaic/Mosaic|Mosaic(kotlin.coroutines.CoroutineContext;kotlin.Function1<com.jakewharton.mosaic.Mosaic,kotlin.Unit>){}[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]
final fun com.jakewharton.mosaic/com_jakewharton_mosaic_DebugRendering$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic/com_jakewharton_mosaic_DebugRendering$stableprop_getter|com_jakewharton_mosaic_DebugRendering$stableprop_getter(){}[0]
final fun com.jakewharton.mosaic/com_jakewharton_mosaic_GlobalSnapshotManager$stableprop_getter(): kotlin/Int // com.jakewharton.mosaic/com_jakewharton_mosaic_GlobalSnapshotManager$stableprop_getter|com_jakewharton_mosaic_GlobalSnapshotManager$stableprop_getter(){}[0]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
Expand Down Expand Up @@ -139,7 +140,7 @@ internal class MosaicNode(
* Draw this node to a [TextSurface].
* A call to [measureAndPlace] must precede calls to this function.
*/
fun paint(): TextSurface {
fun paint(): TextCanvas {
val surface = TextSurface(width, height)
topLayer.drawTo(surface)
return surface
Expand All @@ -149,7 +150,7 @@ internal class MosaicNode(
* Append any static [TextSurfaces][TextSurface] to [statics].
* A call to [measureAndPlace] must precede calls to this function.
*/
fun paintStaticsTo(statics: MutableObjectList<TextSurface>) {
fun paintStaticsTo(statics: MutableObjectList<TextCanvas>) {
for (index in children.indices) {
val child = children[index]
if (isStatic) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.ObserverHandle
Expand Down Expand Up @@ -124,7 +125,7 @@ private fun createRendering(ansiLevel: AnsiLevel = AnsiLevel.TRUECOLOR): Renderi
}
}

private fun CoroutineScope.updateTerminalInfo(terminal: MordantTerminal, mosaic: MosaicComposition) {
private fun CoroutineScope.updateTerminalInfo(terminal: MordantTerminal, mosaic: Mosaic) {
launch(start = UNDISPATCHED) {
while (true) {
val currentTerminalInfo = mosaic.terminalState.value
Expand All @@ -139,7 +140,7 @@ private fun CoroutineScope.updateTerminalInfo(terminal: MordantTerminal, mosaic:
}
}

private fun CoroutineScope.readRawModeKeys(rawMode: RawModeScope, mosaic: MosaicComposition) {
private fun CoroutineScope.readRawModeKeys(rawMode: RawModeScope, mosaic: Mosaic) {
launch(Dispatchers.IO) {
while (isActive) {
val keyboardEvent = rawMode.readKeyOrNull(10.milliseconds) ?: continue
Expand All @@ -154,14 +155,27 @@ private fun CoroutineScope.readRawModeKeys(rawMode: RawModeScope, mosaic: Mosaic
}
}

internal interface Mosaic {
fun paint(): TextSurface
fun paintStaticsTo(list: MutableObjectList<TextSurface>)
fun dump(): String
public interface Mosaic {
public fun setContent(content: @Composable () -> Unit)

public fun sendKeyEvent(keyEvent: KeyEvent)
public val terminalState: MutableState<Terminal>

public fun paint(): TextCanvas
public fun paintStaticsTo(list: MutableObjectList<TextCanvas>)
public fun dump(): String

public suspend fun awaitComplete()
public fun cancel()
}

// https://en.wikipedia.org/wiki/VT52
private val DefaultTestTerminalSize = IntSize(width = 80, height = 24)
// TODO This function signature is all kinds of broken for structured concurrency!
public fun Mosaic(
coroutineContext: CoroutineContext,
onDraw: (Mosaic) -> Unit,
): Mosaic {
return MosaicComposition(coroutineContext, onDraw)
}

internal class MosaicComposition(
coroutineContext: CoroutineContext,
Expand All @@ -177,7 +191,7 @@ internal class MosaicComposition(
val scope = CoroutineScope(composeContext)

private val keyEvents = Channel<KeyEvent>(UNLIMITED)
val terminalState = mutableStateOf(Terminal(DefaultTestTerminalSize))
override val terminalState = mutableStateOf(Terminal(DefaultTestTerminalSize))

private val applier = MosaicNodeApplier { needLayout = true }
val rootNode = applier.root
Expand Down Expand Up @@ -218,13 +232,13 @@ internal class MosaicComposition(
onDraw(this)
}

override fun paint(): TextSurface {
override fun paint(): TextCanvas {
return Snapshot.observe(readObserver = drawBlockStateReadObserver) {
rootNode.paint()
}
}

override fun paintStaticsTo(list: MutableObjectList<TextSurface>) {
override fun paintStaticsTo(list: MutableObjectList<TextCanvas>) {
rootNode.paintStaticsTo(list)
}

Expand Down Expand Up @@ -287,7 +301,7 @@ internal class MosaicComposition(
}
}

fun setContent(content: @Composable () -> Unit) {
override fun setContent(content: @Composable () -> Unit) {
composition.setContent {
CompositionLocalProvider(
LocalTerminal provides terminalState.value,
Expand All @@ -297,11 +311,11 @@ internal class MosaicComposition(
performLayout()
}

fun sendKeyEvent(keyEvent: KeyEvent) {
override fun sendKeyEvent(keyEvent: KeyEvent) {
keyEvents.trySend(keyEvent)
}

suspend fun awaitComplete() {
override suspend fun awaitComplete() {
try {
val effectJob = checkNotNull(recomposer.effectCoroutineContext[Job]) {
"No Job in effectCoroutineContext of recomposer"
Expand All @@ -322,7 +336,7 @@ internal class MosaicComposition(
}
}

fun cancel() {
override fun cancel() {
applyObserverHandle.dispose()
recomposer.cancel()
job.cancel()
Expand All @@ -333,6 +347,9 @@ internal class MosaicComposition(
}
}

// https://en.wikipedia.org/wiki/VT52
private val DefaultTestTerminalSize = IntSize(width = 80, height = 24)

internal class MosaicNodeApplier(
private val onChanges: () -> Unit = {},
) : AbstractApplier<MosaicNode>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal class DebugRendering(
appendLine(mosaic.dump())
appendLine()

val statics = mutableObjectListOf<TextSurface>()
val statics = mutableObjectListOf<TextCanvas>()
try {
mosaic.paintStaticsTo(statics)
if (statics.isNotEmpty()) {
Expand Down Expand Up @@ -69,7 +69,7 @@ internal class AnsiRendering(
private val ansiLevel: AnsiLevel = AnsiLevel.TRUECOLOR,
) : Rendering {
private val stringBuilder = StringBuilder(100)
private val staticSurfaces = mutableObjectListOf<TextSurface>()
private val staticSurfaces = mutableObjectListOf<TextCanvas>()
private var lastHeight = 0

override fun render(mosaic: Mosaic): CharSequence {
Expand All @@ -83,7 +83,7 @@ internal class AnsiRendering(
append(cursorUp)
}

fun appendSurface(canvas: TextSurface) {
fun appendSurface(canvas: TextCanvas) {
for (row in 0 until canvas.height) {
canvas.appendRowTo(this, row, ansiLevel)
if (staleLines-- > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ import de.cketti.codepoints.appendCodePoint

private val blankPixel = TextPixel(' ')

public interface TextCanvas {
public val height: Int
public val width: Int

public fun render(ansiLevel: AnsiLevel): String
public fun appendRowTo(appendable: Appendable, row: Int, ansiLevel: AnsiLevel)
}

internal class TextSurface(
val width: Int,
val height: Int,
) {
override val width: Int,
override val height: Int,
) : TextCanvas {
var translationX = 0
var translationY = 0

Expand All @@ -35,7 +43,7 @@ internal class TextSurface(
return cells[y * width + x]
}

fun appendRowTo(appendable: Appendable, row: Int, ansiLevel: AnsiLevel) {
override fun appendRowTo(appendable: Appendable, row: Int, ansiLevel: AnsiLevel) {
// Reused heap allocation for building ANSI attributes inside the loop.
val attributes = mutableIntListOf()

Expand Down Expand Up @@ -145,7 +153,7 @@ internal class TextSurface(
}
}

fun render(ansiLevel: AnsiLevel): String = buildString {
override fun render(ansiLevel: AnsiLevel): String = buildString {
if (height > 0) {
for (rowIndex in 0 until height) {
appendRowTo(this, rowIndex, ansiLevel)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.jakewharton.mosaic.ui

internal enum class AnsiLevel {
public enum class AnsiLevel {
NONE,
ANSI16,
ANSI256,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.jakewharton.mosaic.ui.Alignment
import com.jakewharton.mosaic.ui.Box
import com.jakewharton.mosaic.ui.Column
import com.jakewharton.mosaic.ui.Text
import com.jakewharton.mosaic.ui.unit.IntSize
import kotlin.test.Test
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
Expand All @@ -29,13 +30,13 @@ class CounterTest {

@Test fun counterInTerminalCenter() = runTest {
runMosaicTest {
setTerminalSize(width = 30, height = 1)
terminalState.value = Terminal(size = IntSize(width = 30, height = 1))
setCounterInTerminalCenter()
for (count in 0..9) {
assertThat(awaitSnapshot()).isEqualTo(" The count is: $count ")
}

setTerminalSize(width = 20, height = 1)
terminalState.value = Terminal(size = IntSize(width = 20, height = 1))

// After changing the terminal size, we wait for the counter to increase before getting a
// new snapshot, otherwise there will be the previous value (9) and a different output size.
Expand Down
Loading

0 comments on commit 313db47

Please sign in to comment.