1package soy.iko.opencode.ui.components
3import android.provider.Settings
4import androidx.compose.animation.EnterTransition
5import androidx.compose.animation.ExitTransition
6import androidx.compose.animation.core.AnimationSpec
7import androidx.compose.animation.core.snap
8import androidx.compose.animation.core.tween
9import androidx.compose.animation.expandVertically
10import androidx.compose.animation.fadeIn
11import androidx.compose.animation.fadeOut
12import androidx.compose.animation.shrinkVertically
13import androidx.compose.animation.animateContentSize
14import androidx.compose.foundation.lazy.LazyItemScope
15import androidx.compose.runtime.Composable
16import androidx.compose.runtime.DisposableEffect
17import androidx.compose.runtime.getValue
18import androidx.compose.runtime.mutableIntStateOf
19import androidx.compose.runtime.remember
20import androidx.compose.runtime.setValue
21import androidx.compose.runtime.staticCompositionLocalOf
22import androidx.compose.ui.Modifier
23import androidx.compose.ui.platform.LocalContext
24import androidx.lifecycle.Lifecycle
25import androidx.lifecycle.LifecycleEventObserver
26import androidx.lifecycle.compose.LocalLifecycleOwner
29 * Whether the user has reduced motion enabled at the system level (animator duration scale
30 * set to 0 in Developer Options). When true, transitions should skip animations and content
31 * swaps should be instant, respecting the user's accessibility preference.
33 * Note: Compose's animation system does NOT honor [Settings.Global.ANIMATOR_DURATION_SCALE]
34 * (that setting only gates the View-system ValueAnimator/ObjectAnimator), so each Compose
35 * animation must opt in via [LocalReducedMotion].
37 * Re-reads on each ON_RESUME so toggling "Remove animations" / animator scale in Developer
38 * Options takes effect without a process restart (keying only on `context`, which is stable
39 * for the activity's lifetime, would otherwise cache the first read forever).
42fun isReducedMotion(): Boolean {
43 val context = LocalContext.current
44 var resumeTick by remember { mutableIntStateOf(0) }
45 val lifecycleOwner = LocalLifecycleOwner.current
46 DisposableEffect(lifecycleOwner) {
47 val observer = LifecycleEventObserver { _, event ->
48 if (event == Lifecycle.Event.ON_RESUME) resumeTick++
50 lifecycleOwner.lifecycle.addObserver(observer)
51 onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
53 return remember(context, resumeTick) {
55 Settings.Global.getFloat(
56 context.contentResolver,
57 Settings.Global.ANIMATOR_DURATION_SCALE,
65 * Whether reduced motion is in effect for this composition. Provided once near the app root
66 * (see [soy.iko.opencode.ui.OpencodeApp]) from [isReducedMotion] so individual animation call
67 * sites collapse their motion to instant swaps without each re-reading the system setting.
68 * Defaults to false in previews/tests (no provider).
70val LocalReducedMotion = staticCompositionLocalOf { false }
73 * Enter/exit pair for [androidx.compose.animation.AnimatedVisibility], pre-resolved to honor
74 * [LocalReducedMotion]. Under reduced motion both are [EnterTransition.None] /
75 * [ExitTransition.None] so content appears/disappears instantly; otherwise a standard
76 * expand+fade / shrink+fade. Centralizes the gate so call sites stay one-liners.
78data class VisibilityTransitions(val enter: EnterTransition, val exit: ExitTransition)
81fun rememberVisibilityTransitions(): VisibilityTransitions {
82 val reduced = LocalReducedMotion.current
83 return remember(reduced) {
85 VisibilityTransitions(EnterTransition.None, ExitTransition.None)
87 VisibilityTransitions(expandVertically() + fadeIn(), shrinkVertically() + fadeOut())
93 * A [tween] [AnimationSpec] that collapses to an instant [snap] under [LocalReducedMotion],
94 * for single-value animations (e.g. an image zoom) that want a single spec source.
97fun <T> rememberMotionTween(durationMs: Int): AnimationSpec<T> {
98 val reduced = LocalReducedMotion.current
99 return remember(reduced, durationMs) {
100 if (reduced) snap() else tween(durationMs)
105 * [LazyItemScope.animateItem] gated by [LocalReducedMotion]: under reduced motion the
106 * modifier is a no-op so item insert/remove/reorder is instant, otherwise the standard
107 * placement animation runs. The chat list already applied this pattern inline; this helper
108 * centralizes it so every lazy list (sessions, servers, files, diagnostics) honors the
109 * same accessibility preference without each repeating the branch.
111 * Returns a [Modifier] (not a `Modifier` extension) because `animateItem` is a `Modifier`
112 * extension scoped to [LazyItemScope]; call sites use `modifier = reducedMotionAnimateItem()`
113 * inside an `items { ... }` block where `this` is the `LazyItemScope`.
116fun LazyItemScope.reducedMotionAnimateItem(): Modifier {
117 val reduced = LocalReducedMotion.current
118 return if (reduced) Modifier else Modifier.animateItem()
122 * [Modifier.animateContentSize] gated by [LocalReducedMotion]: under reduced motion the
123 * size change is instant (no animated transition), otherwise the standard spring/tween
124 * size animation runs. Used by collapsible code/diff blocks so an expand/collapse isn't
125 * a jarring jump for sighted users while remaining instant under reduced motion.
128fun Modifier.reducedMotionAnimateContentSize(): Modifier {
129 val reduced = LocalReducedMotion.current
130 return if (reduced) this else this.animateContentSize()
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list