Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7623efdb authored by Adam Powell's avatar Adam Powell Committed by Dan Sandler
Browse files

Telescope!

Add a composable for managing a simulation listener.
Since it's a tool for observing the universe, it's a Telescope.

Rename VisibleUniverse composable to UniverseCanvas for clarity.

Fix telemetry not updating by using a Telescope to invalidate the
top-level recompose scope for now; this can be narrowed later.

Bug: 373855388
Test: adb shell am start -n com.android.egg/.landroid.MainActivity
Flag: com.android.egg.flags.flag_flag
Change-Id: I68b85652eb3ced269b56ee543c045710e875ff6b
parent cc665404
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@ class DreamUniverse : DreamService() {
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()

        val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
        val universe = Universe(namer = Namer(resources), randomSeed = randomSeed())

        isInteractive = false

+17 −14
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.withInfiniteAnimationFrameNanos
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
@@ -45,6 +44,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -64,7 +64,6 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -75,6 +74,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.lang.Float.max
import java.lang.Float.min
import java.util.Calendar
@@ -83,9 +85,6 @@ import kotlin.math.absoluteValue
import kotlin.math.floor
import kotlin.math.sqrt
import kotlin.random.Random
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

enum class RandomSeedType {
    Fixed,
@@ -139,7 +138,6 @@ fun getDessertCode(): String =
        else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "")
    }


val DEBUG_TEXT = mutableStateOf("Hello Universe")
const val SHOW_DEBUG_TEXT = false

@@ -158,7 +156,7 @@ fun DebugText(text: MutableState<String>) {
}

@Composable
fun Telemetry(universe: VisibleUniverse) {
fun Telemetry(universe: Universe) {
    var topVisible by remember { mutableStateOf(false) }
    var bottomVisible by remember { mutableStateOf(false) }

@@ -180,10 +178,15 @@ fun Telemetry(universe: VisibleUniverse) {
        topVisible = true
    }

    universe.triggerDraw.value // recompose on every frame

    val explored = universe.planets.filter { it.explored }

    // TODO: Narrow the scope of invalidation here to the specific data needed;
    // the behavior below mimics the previous implementation of a snapshot ticker value
    val recomposeScope = currentRecomposeScope
    Telescope(universe) {
        recomposeScope.invalidate()
    }

    BoxWithConstraints(
        modifier =
            Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
@@ -299,7 +302,7 @@ class MainActivity : ComponentActivity() {

        enableEdgeToEdge()

        val universe = VisibleUniverse(namer = Namer(resources), randomSeed = randomSeed())
        val universe = Universe(namer = Namer(resources), randomSeed = randomSeed())

        if (TEST_UNIVERSE) {
            universe.initTest()
@@ -373,7 +376,7 @@ class MainActivity : ComponentActivity() {
@Preview(name = "tablet", device = Devices.TABLET)
@Composable
fun MainActivityPreview() {
    val universe = VisibleUniverse(namer = Namer(Resources.getSystem()), randomSeed = randomSeed())
    val universe = Universe(namer = Namer(Resources.getSystem()), randomSeed = randomSeed())

    universe.initTest()

@@ -458,12 +461,12 @@ fun FlightStick(
@Composable
fun Spaaaace(
    modifier: Modifier,
    u: VisibleUniverse,
    u: Universe,
    foldState: MutableState<FoldingFeature?> = mutableStateOf(null)
) {
    LaunchedEffect(u) {
        while (true) withInfiniteAnimationFrameNanos { frameTimeNanos ->
            u.simulateAndDrawFrame(frameTimeNanos)
            u.step(frameTimeNanos)
        }
    }

@@ -492,7 +495,7 @@ fun Spaaaace(
    val centerFracY: Float by
        animateFloatAsState(if (halfFolded && horizontalFold) 0.25f else 0.5f, label = "centerY")

    Canvas(modifier = canvasModifier) {
    UniverseCanvas(u, canvasModifier) { u ->
        drawRect(Colors.Eigengrau, Offset.Zero, size)

        val closest = u.closestPlanet()
+24 −0
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@
package com.android.egg.landroid

import android.util.ArraySet
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.DisposableHandle
import kotlin.random.Random

// artificially speed up or slow down the simulation
@@ -127,6 +129,7 @@ open class Simulator(val randomSeed: Long) {
    val rng = Random(randomSeed)
    val entities = ArraySet<Entity>(1000)
    val constraints = ArraySet<Constraint>(100)
    private val simStepListeners = mutableListOf<() -> Unit>()

    fun add(e: Entity) = entities.add(e)
    fun remove(e: Entity) = entities.remove(e)
@@ -169,5 +172,26 @@ open class Simulator(val randomSeed: Long) {

        // 3. compute new velocities from updated positions and saved positions
        postUpdateAll(dt, localEntities)

        // 4. notify listeners that step is complete
        simStepListeners.fastForEach { it.invoke() }
    }

    /**
     * Register [listener] to be invoked every time the [Simulator] completes one [step].
     * Use this to enqueue drawing.
     *
     * Instead of the usual register()/unregister() pattern, we're going to borrow
     * [kotlinx.coroutines.DisposableHandle] here. Call [DisposableHandle.dispose] on the return
     * value to unregister.
     */
    fun addSimulationStepListener(listener: () -> Unit): DisposableHandle {
        // add to listener list
        simStepListeners += listener

        return DisposableHandle {
            // on dispose, remove from listener list
            simStepListeners -= listener
        }
    }
}
+108 −10
Original line number Diff line number Diff line
@@ -16,19 +16,31 @@

package com.android.egg.landroid

import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotateRad
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.util.lerp
import androidx.core.math.MathUtils.clamp
import com.android.egg.flags.Flags.flagFlag
import kotlinx.coroutines.DisposableHandle
import java.lang.Float.max
import kotlin.math.exp
import kotlin.math.sqrt
@@ -55,22 +67,108 @@ fun DrawScope.zoom(zoom: Float, block: ZoomedDrawScope.() -> Unit) {
    ds.scale(zoom) { block(ds) }
}

class VisibleUniverse(namer: Namer, randomSeed: Long) : Universe(namer, randomSeed) {
    // Magic variable. Every time we update it, Compose will notice and redraw the universe.
    val triggerDraw = mutableStateOf(0L)
/**
 * A device for observing changes to a [Simulator] such as a [Universe].
 * [observer] will be invoked each time a [Simulator.step] has completed.
 */
@Composable
fun <S : Simulator> Telescope(
    subject: S,
    observer: (S) -> Unit
) {
    remember(subject) {
        object : RememberObserver {
            lateinit var registration: DisposableHandle
            var currentObserver by mutableStateOf(observer)

    fun simulateAndDrawFrame(nanos: Long) {
        // By writing this value, Compose will look for functions that read it (like drawZoomed).
        triggerDraw.value = nanos
            override fun onRemembered() {
                registration = subject.addSimulationStepListener { currentObserver(subject) }
            }

        step(nanos)
            override fun onForgotten() {
                registration.dispose()
            }

            override fun onAbandoned() {}
        }
    }.currentObserver = observer
}

fun ZoomedDrawScope.drawUniverse(universe: VisibleUniverse) {
    with(universe) {
        triggerDraw.value // Please recompose when this value changes.
fun Modifier.drawUniverse(
    universe: Universe,
    draw: DrawScope.(Universe) -> Unit
): Modifier = this then UniverseElement(universe, draw)

@Composable
fun UniverseCanvas(
    universe: Universe,
    modifier: Modifier = Modifier,
    draw: DrawScope.(Universe) -> Unit
) {
    Spacer(modifier.drawUniverse(universe, draw))
}

private class UniverseElement(
    val universe: Universe,
    val draw: DrawScope.(Universe) -> Unit
) : ModifierNodeElement<UniverseModifierNode>() {
    override fun create(): UniverseModifierNode = UniverseModifierNode(universe, draw)

    // Called when a modifier is applied to a Layout whose inputs have changed
    override fun update(node: UniverseModifierNode) {
        node.universe = universe
        node.draw = draw
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as UniverseElement

        if (universe != other.universe) return false
        if (draw != other.draw) return false

        return true
    }

    override fun hashCode(): Int {
        var result = universe.hashCode()
        result = 31 * result + draw.hashCode()
        return result
    }
}

private class UniverseModifierNode(
    universe: Universe,
    draw: DrawScope.(Universe) -> Unit,
) : Modifier.Node(), DrawModifierNode {
    private val universeListener: () -> Unit = { invalidateDraw() }
    private var removeUniverseListener: DisposableHandle? =
        universe.addSimulationStepListener(universeListener)

    var universe: Universe = universe
        set(value) {
            if (field === value) return
            removeUniverseListener?.dispose()
            field = value
            removeUniverseListener = value.addSimulationStepListener(universeListener)
        }

    var draw: ContentDrawScope.(Universe) -> Unit = draw
        set(value) {
            if (field === value) return
            field = value
            invalidateDraw()
        }

    override fun ContentDrawScope.draw() {
        draw(universe)
    }
}

fun ZoomedDrawScope.drawUniverse(universe: Universe) {
    with(universe) {
        constraints.forEach {
            when (it) {
                is Landing -> drawLanding(it)