From 1b6861f2906377434c6373c701d7d8c4d885a673 Mon Sep 17 00:00:00 2001 From: EpicDima Date: Wed, 20 Dec 2023 01:39:08 +0300 Subject: [PATCH] Change runMosaic to support effects --- .../kotlin/com/jakewharton/mosaic/mosaic.kt | 127 +++++++----------- .../kotlin/com/jakewharton/mosaic/platform.kt | 7 + .../kotlin/com/jakewharton/mosaic/blocking.kt | 5 +- .../kotlin/com/jakewharton/mosaic/platform.kt | 13 ++ .../kotlin/com/jakewharton/mosaic/platform.kt | 14 ++ .../src/commonMain/kotlin/example/counter.kt | 22 +-- .../counter/src/jsMain/kotlin/example/main.kt | 2 +- .../src/jvmMain/kotlin/example/main.kt | 2 +- .../src/nativeMain/kotlin/example/main.kt | 2 +- samples/demo/src/main/kotlin/example/demo.kt | 49 +++---- samples/jest/src/main/kotlin/example/jest.kt | 119 ++++++++-------- .../robot/src/main/kotlin/example/robot.kt | 60 +++++---- 12 files changed, 217 insertions(+), 205 deletions(-) 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 eb72a7de9..3104858db 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/mosaic.kt @@ -11,15 +11,16 @@ import androidx.compose.runtime.snapshots.Snapshot import com.github.ajalt.mordant.terminal.Terminal as MordantTerminal import com.jakewharton.mosaic.layout.MosaicNode import com.jakewharton.mosaic.ui.BoxMeasurePolicy +import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.yield /** * True for a debug-like output that renders each "frame" on its own with a timestamp delta. @@ -50,11 +51,7 @@ public fun renderMosaic(content: @Composable () -> Unit): String { return render.toString() } -public interface MosaicScope : CoroutineScope { - public fun setContent(content: @Composable () -> Unit) -} - -public suspend fun runMosaic(body: suspend MosaicScope.() -> Unit): Unit = coroutineScope { +public suspend fun runMosaic(content: @Composable () -> Unit): Unit = coroutineScope { val rendering = if (debugOutput) { @OptIn(ExperimentalTime::class) // Not used in production. DebugRendering() @@ -62,39 +59,24 @@ public suspend fun runMosaic(body: suspend MosaicScope.() -> Unit): Unit = corou AnsiRendering() } - var hasFrameWaiters = false - val clock = BroadcastFrameClock { - hasFrameWaiters = true - } - + val clock = BroadcastFrameClock() val job = Job(coroutineContext[Job]) val composeContext = coroutineContext + clock + job - val rootNode = createRootNode() - var displaySignal: CompletableDeferred? = null - val applier = MosaicNodeApplier(rootNode) { - val render = rendering.render(rootNode) - platformDisplay(render) + GlobalSnapshotManager.ensureStarted(composeContext) - displaySignal?.complete(Unit) - hasFrameWaiters = false + launch(composeContext) { + while (true) { + clock.sendFrame(0L) // Frame time value is not used by Compose runtime. + delay(50L) + } } val recomposer = Recomposer(composeContext) - val composition = Composition(applier, recomposer) - - // Start undispatched to ensure we can use suspending things inside the content. - launch(start = UNDISPATCHED, context = composeContext) { + launch(composeContext, start = CoroutineStart.UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } - launch(context = composeContext) { - while (true) { - clock.sendFrame(0L) // Frame time value is not used by Compose runtime. - delay(50) - } - } - val terminal = MordantTerminal() val terminalInfo = mutableStateOf( Terminal( @@ -102,7 +84,7 @@ public suspend fun runMosaic(body: suspend MosaicScope.() -> Unit): Unit = corou ), ) - launch(context = composeContext) { + launch(composeContext) { while (true) { val currentTerminalInfo = terminalInfo.value if (terminal.info.updateTerminalSize() && @@ -115,59 +97,32 @@ public suspend fun runMosaic(body: suspend MosaicScope.() -> Unit): Unit = corou size = Terminal.Size(terminal.info.width, terminal.info.height), ) } - delay(50) + delay(50L) } } - coroutineScope { - val scope = object : MosaicScope, CoroutineScope by this { - override fun setContent(content: @Composable () -> Unit) { - composition.setContent { - CompositionLocalProvider(LocalTerminal provides terminalInfo.value) { - content() - } - } - } - } + val rootNode = createRootNode() + val applier = MosaicNodeApplier(rootNode) { + val render = rendering.render(rootNode) + platformDisplay(render) + } - var snapshotNotificationsPending = false - val observer: (state: Any) -> Unit = { - if (!snapshotNotificationsPending) { - snapshotNotificationsPending = true - launch { - snapshotNotificationsPending = false - Snapshot.sendApplyNotifications() - } - } - } - val snapshotObserverHandle = Snapshot.registerGlobalWriteObserver(observer) - try { - scope.body() - } finally { - snapshotObserverHandle.dispose() + Composition(applier, recomposer).setContent { + CompositionLocalProvider(LocalTerminal provides terminalInfo.value) { + content() } } - // Ensure the final state modification is discovered. We need to ensure that the coroutine - // which is running the recomposition loop wakes up, notices the changes, and waits for the - // next frame. If you are using snapshots this only requires a single yield. If you are not - // then it requires two yields. THIS IS NOT GREAT! But at least it's implementation detail... - // TODO https://issuetracker.google.com/issues/169425431 - yield() - yield() - Snapshot.sendApplyNotifications() - yield() - yield() - - if (hasFrameWaiters) { - CompletableDeferred().also { - displaySignal = it - it.await() - } + val effectJob = checkNotNull(recomposer.effectCoroutineContext[Job]) { + "No Job in effectCoroutineContext of recomposer" } + effectJob.children.forEach { it.join() } + recomposer.awaitIdle() + + recomposer.close() + recomposer.join() job.cancel() - composition.dispose() } internal fun createRootNode(): MosaicNode { @@ -204,3 +159,25 @@ internal class MosaicNodeApplier( override fun onClear() {} } + +internal object GlobalSnapshotManager { + private val started = AtomicBoolean(false) + private val sent = AtomicBoolean(false) + + fun ensureStarted(coroutineContext: CoroutineContext) { + if (started.compareAndSet(expect = false, update = true)) { + val channel = Channel(1) + CoroutineScope(coroutineContext).launch { + channel.consumeEach { + sent.set(false) + Snapshot.sendApplyNotifications() + } + } + Snapshot.registerGlobalWriteObserver { + if (sent.compareAndSet(expect = false, update = true)) { + channel.trySend(Unit) + } + } + } + } +} diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/platform.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/platform.kt index 18006aa43..fb5004ee0 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/platform.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/platform.kt @@ -1,3 +1,10 @@ package com.jakewharton.mosaic internal expect fun platformDisplay(chars: CharSequence) + +internal expect class AtomicBoolean(initialValue: Boolean) { + + fun set(value: Boolean) + + fun compareAndSet(expect: Boolean, update: Boolean): Boolean +} diff --git a/mosaic-runtime/src/concurrentMain/kotlin/com/jakewharton/mosaic/blocking.kt b/mosaic-runtime/src/concurrentMain/kotlin/com/jakewharton/mosaic/blocking.kt index cfd45abd4..556a297d8 100644 --- a/mosaic-runtime/src/concurrentMain/kotlin/com/jakewharton/mosaic/blocking.kt +++ b/mosaic-runtime/src/concurrentMain/kotlin/com/jakewharton/mosaic/blocking.kt @@ -1,9 +1,10 @@ package com.jakewharton.mosaic +import androidx.compose.runtime.Composable import kotlinx.coroutines.runBlocking -public fun runMosaicBlocking(body: suspend MosaicScope.() -> Unit) { +public fun runMosaicBlocking(content: @Composable () -> Unit) { runBlocking { - runMosaic(body) + runMosaic(content) } } diff --git a/mosaic-runtime/src/jvmMain/kotlin/com/jakewharton/mosaic/platform.kt b/mosaic-runtime/src/jvmMain/kotlin/com/jakewharton/mosaic/platform.kt index b577998f6..c01f6a20c 100644 --- a/mosaic-runtime/src/jvmMain/kotlin/com/jakewharton/mosaic/platform.kt +++ b/mosaic-runtime/src/jvmMain/kotlin/com/jakewharton/mosaic/platform.kt @@ -2,6 +2,7 @@ package com.jakewharton.mosaic import java.nio.CharBuffer import java.nio.charset.StandardCharsets.UTF_8 +import java.util.concurrent.atomic.AtomicBoolean as JavaAtomicBoolean import org.fusesource.jansi.AnsiConsole private val out = AnsiConsole.out()!!.also { AnsiConsole.systemInstall() } @@ -20,3 +21,15 @@ internal actual fun platformDisplay(chars: CharSequence) { // buffered and not processed until the next frame, or not at all on the final frame. out.flush() } + +internal actual class AtomicBoolean actual constructor(initialValue: Boolean) { + private val delegate = JavaAtomicBoolean(initialValue) + + actual fun set(value: Boolean) { + delegate.set(value) + } + + actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean { + return delegate.compareAndSet(expect, update) + } +} diff --git a/mosaic-runtime/src/nonJvmMain/kotlin/com/jakewharton/mosaic/platform.kt b/mosaic-runtime/src/nonJvmMain/kotlin/com/jakewharton/mosaic/platform.kt index e4ef50fda..520f04e12 100644 --- a/mosaic-runtime/src/nonJvmMain/kotlin/com/jakewharton/mosaic/platform.kt +++ b/mosaic-runtime/src/nonJvmMain/kotlin/com/jakewharton/mosaic/platform.kt @@ -3,3 +3,17 @@ package com.jakewharton.mosaic internal actual fun platformDisplay(chars: CharSequence) { print(chars.toString()) } + +internal actual class AtomicBoolean actual constructor(initialValue: Boolean) { + private var value: Boolean = initialValue + + actual fun set(value: Boolean) { + this.value = value + } + + actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean { + if (value != expect) return false + value = update + return true + } +} diff --git a/samples/counter/src/commonMain/kotlin/example/counter.kt b/samples/counter/src/commonMain/kotlin/example/counter.kt index 6f6fbb5dc..29b0eeec8 100644 --- a/samples/counter/src/commonMain/kotlin/example/counter.kt +++ b/samples/counter/src/commonMain/kotlin/example/counter.kt @@ -1,22 +1,24 @@ package example +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.jakewharton.mosaic.MosaicScope import com.jakewharton.mosaic.ui.Text import kotlinx.coroutines.delay -suspend fun MosaicScope.runCounter() { - // TODO https://github.com/JakeWharton/mosaic/issues/3 - var count by mutableStateOf(0) +@Composable +fun Counter() { + var count by remember { mutableStateOf(0) } - setContent { - Text("The count is: $count") - } + Text("The count is: $count") - for (i in 1..20) { - delay(250) - count = i + LaunchedEffect(Unit) { + for (i in 1..20) { + delay(250) + count = i + } } } diff --git a/samples/counter/src/jsMain/kotlin/example/main.kt b/samples/counter/src/jsMain/kotlin/example/main.kt index 7ee5b9b36..cef45e3bf 100644 --- a/samples/counter/src/jsMain/kotlin/example/main.kt +++ b/samples/counter/src/jsMain/kotlin/example/main.kt @@ -3,5 +3,5 @@ package example import com.jakewharton.mosaic.runMosaic suspend fun main() = runMosaic { - runCounter() + Counter() } diff --git a/samples/counter/src/jvmMain/kotlin/example/main.kt b/samples/counter/src/jvmMain/kotlin/example/main.kt index e17a20ada..f0bd4ea1e 100644 --- a/samples/counter/src/jvmMain/kotlin/example/main.kt +++ b/samples/counter/src/jvmMain/kotlin/example/main.kt @@ -5,5 +5,5 @@ package example import com.jakewharton.mosaic.runMosaic suspend fun main() = runMosaic { - runCounter() + Counter() } diff --git a/samples/counter/src/nativeMain/kotlin/example/main.kt b/samples/counter/src/nativeMain/kotlin/example/main.kt index 3d13ba24e..89a00177d 100644 --- a/samples/counter/src/nativeMain/kotlin/example/main.kt +++ b/samples/counter/src/nativeMain/kotlin/example/main.kt @@ -3,5 +3,5 @@ package example import com.jakewharton.mosaic.runMosaicBlocking fun main() = runMosaicBlocking { - runCounter() + Counter() } diff --git a/samples/demo/src/main/kotlin/example/demo.kt b/samples/demo/src/main/kotlin/example/demo.kt index 1ba09ce85..398940443 100644 --- a/samples/demo/src/main/kotlin/example/demo.kt +++ b/samples/demo/src/main/kotlin/example/demo.kt @@ -1,5 +1,6 @@ package example +import androidx.compose.runtime.LaunchedEffect import com.jakewharton.mosaic.LocalTerminal import com.jakewharton.mosaic.runMosaicBlocking import com.jakewharton.mosaic.text.SpanStyle @@ -10,29 +11,29 @@ import com.jakewharton.mosaic.ui.Text import kotlinx.coroutines.suspendCancellableCoroutine fun main() = runMosaicBlocking { - setContent { - val terminal = LocalTerminal.current - Text( - buildAnnotatedString { - append("Terminal(") - withStyle(SpanStyle(color = Color.BrightGreen)) { - append("width=") - } - withStyle(SpanStyle(color = Color.BrightBlue)) { - append(terminal.size.width.toString()) - } - append(", ") - withStyle(SpanStyle(color = Color.BrightGreen)) { - append("height=") - } - withStyle(SpanStyle(color = Color.BrightBlue)) { - append(terminal.size.height.toString()) - } - append(")") - }, - ) - } + val terminal = LocalTerminal.current + Text( + buildAnnotatedString { + append("Terminal(") + withStyle(SpanStyle(color = Color.BrightGreen)) { + append("width=") + } + withStyle(SpanStyle(color = Color.BrightBlue)) { + append(terminal.size.width.toString()) + } + append(", ") + withStyle(SpanStyle(color = Color.BrightGreen)) { + append("height=") + } + withStyle(SpanStyle(color = Color.BrightBlue)) { + append(terminal.size.height.toString()) + } + append(")") + }, + ) - // Run forever! - suspendCancellableCoroutine { } + LaunchedEffect(Unit) { + // Run forever! + suspendCancellableCoroutine {} + } } diff --git a/samples/jest/src/main/kotlin/example/jest.kt b/samples/jest/src/main/kotlin/example/jest.kt index d3fc9ee6c..5d1fca1f9 100644 --- a/samples/jest/src/main/kotlin/example/jest.kt +++ b/samples/jest/src/main/kotlin/example/jest.kt @@ -7,15 +7,17 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotStateList import com.jakewharton.mosaic.layout.background +import com.jakewharton.mosaic.layout.height import com.jakewharton.mosaic.layout.padding +import com.jakewharton.mosaic.layout.size import com.jakewharton.mosaic.modifier.Modifier import com.jakewharton.mosaic.runMosaicBlocking import com.jakewharton.mosaic.text.SpanStyle import com.jakewharton.mosaic.text.buildAnnotatedString import com.jakewharton.mosaic.text.withStyle +import com.jakewharton.mosaic.ui.Color import com.jakewharton.mosaic.ui.Color.Companion.Black import com.jakewharton.mosaic.ui.Color.Companion.BrightBlack import com.jakewharton.mosaic.ui.Color.Companion.Green @@ -23,6 +25,7 @@ import com.jakewharton.mosaic.ui.Color.Companion.Red 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.Text import com.jakewharton.mosaic.ui.TextStyle.Companion.Bold @@ -51,45 +54,46 @@ fun main() = runMosaicBlocking { ) val totalTests = paths.size + var exit by remember { mutableStateOf(false) } val complete = mutableStateListOf() val tests = mutableStateListOf() - // TODO https://github.com/JakeWharton/mosaic/issues/3 - repeat(4) { // Number of test workers. - launch(start = UNDISPATCHED) { - while (true) { - val path = paths.removeFirstOrNull() ?: break - val index = Snapshot.withMutableSnapshot { - val nextIndex = tests.size - tests += Test(path, Running) - nextIndex - } - delay(random.nextLong(2_500L, 4_000L)) + LaunchedEffect(Unit) { + val job = launch { + repeat(4) { // Number of test workers. + launch(start = UNDISPATCHED) { + while (true) { + val path = paths.removeFirstOrNull() ?: break + val nextIndex = tests.size + tests += Test(path, Running) + delay(random.nextLong(2_500L, 4_000L)) - // Flip a coin biased 60% to pass to produce the final state of the test. - tests[index] = when { - random.nextFloat() < .7f -> tests[index].copy(state = Pass) - else -> { - val test = tests[index] - val failures = buildList { - repeat(1 + random.nextInt(2)) { - add("Failure on line ${random.nextInt(50)} in ${test.path}") + // Flip a coin biased 60% to pass to produce the final state of the test. + tests[nextIndex] = when { + random.nextFloat() < .7f -> tests[nextIndex].copy(state = Pass) + else -> { + val test = tests[nextIndex] + val failures = buildList { + repeat(1 + random.nextInt(2)) { + add("Failure on line ${random.nextInt(50)} in ${test.path}") + } + } + test.copy(state = Fail, failures = failures) } } - test.copy(state = Fail, failures = failures) + complete += tests[nextIndex] } } - complete += tests[index] } } + job.join() + exit = true } - setContent { - Column { - Log(complete) - Status(tests) - Summary(totalTests, tests) - } + Column { + Log(complete) + Status(tests) + Summary(totalTests, tests, exit) } } @@ -137,14 +141,14 @@ fun Log(complete: SnapshotStateList) { for (failure in test.failures) { Text(" ‣ $failure") } - Text("") // Blank line + Spacer(Modifier.height(1)) // Blank line } } } // Separate logs from rest of display by a single line if latest test result is success. if (complete.lastOrNull()?.state == Pass) { - Text("") // Blank line + Spacer(Modifier.height(1)) // Blank line } } @@ -156,24 +160,22 @@ fun Status(tests: SnapshotStateList) { TestRow(test) } - Text("") // Blank line + Spacer(Modifier.height(1)) // Blank line } } @Composable -private fun Summary(totalTests: Int, tests: SnapshotStateList) { +private fun Summary(totalTests: Int, tests: SnapshotStateList, exit: Boolean) { val counts = tests.groupingBy { it.state }.eachCount() val failed = counts[Fail] ?: 0 val passed = counts[Pass] ?: 0 val running = counts[Running] ?: 0 var elapsed by remember { mutableStateOf(0) } - LaunchedEffect(Unit) { - while (true) { - delay(1_000) - Snapshot.withMutableSnapshot { - elapsed++ - } + LaunchedEffect(exit) { + while (!exit) { + delay(1_000L) + elapsed++ } } @@ -210,20 +212,18 @@ private fun Summary(totalTests: Int, tests: SnapshotStateList) { Text("Time: ${elapsed}s") if (running > 0) { - TestProgress(totalTests, passed, failed, running) + TestProgress(totalTests, passed, failed, running, exit) } } } @Composable -fun TestProgress(totalTests: Int, passed: Int, failed: Int, running: Int) { +fun TestProgress(totalTests: Int, passed: Int, failed: Int, running: Int, exit: Boolean) { var showRunning by remember { mutableStateOf(true) } - LaunchedEffect(Unit) { - while (true) { - delay(500) - Snapshot.withMutableSnapshot { - showRunning = !showRunning - } + LaunchedEffect(exit) { + while (!exit) { + delay(500L) + showRunning = !showRunning } } @@ -232,22 +232,17 @@ fun TestProgress(totalTests: Int, passed: Int, failed: Int, running: Int) { val passedWidth = (passed.toDouble() * totalWidth / totalTests).toInt() val runningWidth = if (showRunning) (running.toDouble() * totalWidth / totalTests).toInt() else 0 - Text( - buildAnnotatedString { - withStyle(SpanStyle(background = Red)) { - append(" ".repeat(failedWidth)) - } - withStyle(SpanStyle(background = Green)) { - append(" ".repeat(passedWidth)) - } - withStyle(SpanStyle(background = Yellow)) { - append(" ".repeat(runningWidth)) - } - withStyle(SpanStyle(background = BrightBlack)) { - append(" ".repeat(totalWidth - failedWidth - passedWidth - runningWidth)) - } - }, - ) + Row { + TestProgressPart(Red, failedWidth) + TestProgressPart(Green, passedWidth) + TestProgressPart(Yellow, runningWidth) + TestProgressPart(BrightBlack, totalWidth - failedWidth - passedWidth - runningWidth) + } +} + +@Composable +fun TestProgressPart(color: Color, width: Int) { + Spacer(Modifier.background(color).size(width, 1)) } data class Test( diff --git a/samples/robot/src/main/kotlin/example/robot.kt b/samples/robot/src/main/kotlin/example/robot.kt index e6d585202..d5f7eda44 100644 --- a/samples/robot/src/main/kotlin/example/robot.kt +++ b/samples/robot/src/main/kotlin/example/robot.kt @@ -1,7 +1,9 @@ package example +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.jakewharton.mosaic.layout.height import com.jakewharton.mosaic.layout.offset @@ -13,7 +15,8 @@ import com.jakewharton.mosaic.ui.Column import com.jakewharton.mosaic.ui.Spacer import com.jakewharton.mosaic.ui.Text import com.jakewharton.mosaic.ui.unit.IntOffset -import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.jline.terminal.TerminalBuilder @@ -24,39 +27,38 @@ private const val robotWidth = 3 private const val robotHeight = 1 fun main() = runMosaicBlocking { - // TODO https://github.com/JakeWharton/mosaic/issues/3 - var x by mutableStateOf(0) - var y by mutableStateOf(0) + var x by remember { mutableStateOf(0) } + var y by remember { mutableStateOf(0) } - setContent { - Column { - Text("Use arrow keys to move the face. Press “q” to exit.") - Text("Position: $x, $y Robot: $robotWidth, $robotHeight World: $width, $height") - Spacer(Modifier.height(1)) - // TODO https://github.com/JakeWharton/mosaic/issues/11 - Box(modifier = Modifier.size(width, height).offset { IntOffset(x, y) }) { - Text("^_^") - } + Column { + Text("Use arrow keys to move the face. Press “q” to exit.") + Text("Position: $x, $y Robot: $robotWidth, $robotHeight World: $width, $height") + Spacer(Modifier.height(1)) + // TODO https://github.com/JakeWharton/mosaic/issues/11 + Box(modifier = Modifier.size(width, height).offset { IntOffset(x, y) }) { + Text("^_^") } } - withContext(IO) { - val terminal = TerminalBuilder.terminal() - terminal.enterRawMode() - val reader = terminal.reader() + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + val terminal = TerminalBuilder.terminal() + terminal.enterRawMode() + val reader = terminal.reader() - while (true) { - // TODO https://github.com/JakeWharton/mosaic/issues/10 - when (reader.read()) { - 'q'.code -> break - 27 -> { - when (reader.read()) { - 91, 79 -> { - when (reader.read()) { - 65 -> y = (y - 1).coerceAtLeast(0) - 66 -> y = (y + 1).coerceAtMost(height - robotHeight) - 67 -> x = (x + 1).coerceAtMost(width - robotWidth) - 68 -> x = (x - 1).coerceAtLeast(0) + while (isActive) { + // TODO https://github.com/JakeWharton/mosaic/issues/10 + when (reader.read()) { + 'q'.code -> break + 27 -> { + when (reader.read()) { + 91, 79 -> { + when (reader.read()) { + 65 -> y = (y - 1).coerceAtLeast(0) + 66 -> y = (y + 1).coerceAtMost(height - robotHeight) + 67 -> x = (x + 1).coerceAtMost(width - robotWidth) + 68 -> x = (x - 1).coerceAtLeast(0) + } } } }