Krolyxon
Commit 9dea828

Branches

Refs →

budctlandroidappsrcmainkotliniogithubkrolyxonbudctlLook.kt

157 lines · 6.0 KB · Kotlin

1package io.github.krolyxon.budctl
2
3import android.content.Context
4import android.content.res.AssetManager
5import android.os.Build
6import androidx.compose.animation.core.AnimationSpec
7import androidx.compose.animation.core.CubicBezierEasing
8import androidx.compose.animation.core.snap
9import androidx.compose.animation.core.tween
10import androidx.compose.runtime.Immutable
11import androidx.compose.runtime.staticCompositionLocalOf
12import androidx.compose.ui.graphics.Color
13import androidx.compose.ui.text.font.Font
14import androidx.compose.ui.text.font.FontFamily
15import androidx.compose.ui.text.font.FontWeight
16
17/**
18 * The desktop app's look (CLAUDE.md, GUI): Nothing OS's shapes and type in Material You colors.
19 * The colors come by role, as in crates/budctl-gui/src/theme.rs: a ladder of surfaces, and the
20 * "on" states on top.
21 */
22@Immutable
23data class Palette(
24 /** The page (`surfaceLowest`). */
25 val bg: Color,
26 /** Cards (`surfaceLow`). */
27 val s1: Color,
28 /** Icon circles, tracks, the switches' "off" fill (`surfaceHigh`). */
29 val s2: Color,
30 /** Unlit dots, pressed rows (`surfaceHighest`). */
31 val s3: Color,
32 /** Inactive things: the pages not open, an off switch's knob (`outlineVariant`). */
33 val mute: Color,
34 /** Caps labels (`outline`). */
35 val grey: Color,
36 /** Text (`onSurface`). */
37 val fg: Color,
38 /** Secondary text (`onSurfaceVariant`). */
39 val fgDim: Color,
40 /** What is on or chosen: switches, the chosen segment, lit dots (`primary`). */
41 val fill: Color,
42 /** Text and icons on [fill] (`onPrimary`). */
43 val fillFg: Color,
44 /** Live things, one at a time: the open page's dot, "Saving…", charging (`tertiary`). */
45 val live: Color,
46 val liveFg: Color,
47 val err: Color,
48 val errFg: Color,
49)
50
51/** Near-black surfaces, white "on" states and a red for live things: the desktop's own. */
52val BUILT_IN = Palette(
53 bg = Color(0xFF000000),
54 s1 = Color(0xFF111112),
55 s2 = Color(0xFF1C1C1E),
56 s3 = Color(0xFF2A2A2D),
57 mute = Color(0xFF454549),
58 grey = Color(0xFF8E8E93),
59 fg = Color(0xFFF2F2F2),
60 fgDim = Color(0xFFB8B8BC),
61 fill = Color(0xFFF2F2F2),
62 fillFg = Color(0xFF000000),
63 live = Color(0xFFD71921),
64 liveFg = Color(0xFFFFFFFF),
65 err = Color(0xFFFFB4AB),
66 errFg = Color(0xFF690005),
67)
68
69/**
70 * The phone's Material You colors (from the wallpaper), dark, in the same roles as the desktop
71 * maps Aurora's palette. Android 14 names every role; before it, the tonal palettes stand in.
72 */
73fun materialYou(context: Context): Palette {
74 fun c(id: Int) = Color(context.getColor(id))
75 return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
76 Palette(
77 bg = c(android.R.color.system_surface_container_lowest_dark),
78 s1 = c(android.R.color.system_surface_container_low_dark),
79 s2 = c(android.R.color.system_surface_container_high_dark),
80 s3 = c(android.R.color.system_surface_container_highest_dark),
81 mute = c(android.R.color.system_outline_variant_dark),
82 grey = c(android.R.color.system_outline_dark),
83 fg = c(android.R.color.system_on_surface_dark),
84 fgDim = c(android.R.color.system_on_surface_variant_dark),
85 fill = c(android.R.color.system_primary_dark),
86 fillFg = c(android.R.color.system_on_primary_dark),
87 live = c(android.R.color.system_tertiary_dark),
88 liveFg = c(android.R.color.system_on_tertiary_dark),
89 err = c(android.R.color.system_error_dark),
90 errFg = c(android.R.color.system_on_error_dark),
91 )
92 } else {
93 Palette(
94 bg = c(android.R.color.system_neutral1_1000),
95 s1 = c(android.R.color.system_neutral1_900),
96 s2 = c(android.R.color.system_neutral1_800),
97 s3 = c(android.R.color.system_neutral1_700),
98 mute = c(android.R.color.system_neutral2_700),
99 grey = c(android.R.color.system_neutral2_400),
100 fg = c(android.R.color.system_neutral1_100),
101 fgDim = c(android.R.color.system_neutral2_200),
102 fill = c(android.R.color.system_accent1_200),
103 fillFg = c(android.R.color.system_accent1_800),
104 live = c(android.R.color.system_accent3_200),
105 liveFg = c(android.R.color.system_accent3_800),
106 err = BUILT_IN.err,
107 errFg = BUILT_IN.errFg,
108 )
109 }
110}
111
112/**
113 * The desktop app's fonts, read from crates/budctl-gui/fonts (OFL 1.1), which Gradle adds to the
114 * app's assets: Space Grotesk for text, Space Mono for caps labels, and Doto Rounded for numbers
115 * and page words. Doto is never set under 26 sp: smaller, the dots blur.
116 */
117class Fonts(assets: AssetManager) {
118 val ui = FontFamily(
119 Font("SpaceGrotesk-Regular.otf", assets, FontWeight.Normal),
120 Font("SpaceGrotesk-Medium.otf", assets, FontWeight.Medium),
121 Font("SpaceGrotesk-SemiBold.otf", assets, FontWeight.SemiBold),
122 )
123 val mono = FontFamily(Font("SpaceMono-Regular.ttf", assets))
124 val dot = FontFamily(Font("DotoRounded-Black.ttf", assets, FontWeight.Black))
125}
126
127/** What the screen draws with: the colors, the fonts, and whether things move. */
128@Immutable
129data class Look(val p: Palette, val fonts: Fonts, val motion: Boolean)
130
131val LocalLook = staticCompositionLocalOf<Look> { error("no Look provided") }
132
133/**
134 * Nothing's motion (crates/budctl-gui/src/motion.rs): 150-250 ms, eased out on
135 * `cubic-bezier(0.25, 0.1, 0.25, 1)`, no bounce; things fade rather than slide.
136 */
137object Motion {
138 val EASE = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1f)
139
140 /** A press and a small change of state. */
141 const val HOVER = 150
142
143 /** A switch turning, a choice changing. */
144 const val STATE = 200
145
146 /** A page fading in. */
147 const val PAGE = 240
148
149 /** The battery rings lighting up, dot after dot. */
150 const val SWEEP = 600
151
152 /** A blinking light: lit, then dark, this long each. */
153 const val BLINK = 500
154
155 fun <T> eased(millis: Int, motion: Boolean): AnimationSpec<T> =
156 if (motion) tween(millis, easing = EASE) else snap()
157}