1#![allow(non_snake_case)]
6
7use std::cell::RefCell;
8
9use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
10use cranpose_macros::composable;
11use cranpose_services::isSystemInDarkTheme;
12use cranpose_ui::text::{FontWeight, SpanStyle, TextStyle, TextUnit};
13use cranpose_ui_graphics::Color;
14
15use crate::appearance::GlassTintAmount;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
19pub enum SchemeMode {
20 #[default]
22 Auto,
23 Light,
24 Dark,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct LiquidColors {
30 pub is_dark: bool,
32 pub label: Color,
34 pub secondary_label: Color,
36 pub tertiary_label: Color,
38 pub separator: Color,
40 pub fill: Color,
42 pub secondary_fill: Color,
44 pub surface_glass: Color,
47 pub background: Color,
49 pub surface: Color,
51 pub surface_pressed: Color,
53 pub accent: Color,
55 pub on_accent: Color,
57 pub destructive: Color,
59 pub success: Color,
61 pub toggle_on: Color,
63 pub toggle_off: Color,
65 pub warning: Color,
67 pub glass_tint: Color,
69 pub glass_stroke: Color,
71}
72
73impl LiquidColors {
74 pub fn light(accent: Color) -> Self {
75 Self {
76 is_dark: false,
77 label: Color::from_rgb_u8(17, 17, 20),
78 secondary_label: Color::from_rgba_u8(60, 60, 67, 153),
79 tertiary_label: Color::from_rgba_u8(60, 60, 67, 76),
80 separator: Color::from_rgba_u8(60, 60, 67, 56),
81 fill: Color::from_rgba_u8(120, 120, 128, 40),
82 secondary_fill: Color::from_rgba_u8(120, 120, 128, 28),
83 surface_glass: Color::from_rgba_u8(255, 255, 255, 191),
84 background: Color::from_rgb_u8(242, 242, 247),
85 surface: Color::WHITE,
86 surface_pressed: Color::from_rgb_u8(226, 226, 231),
87 accent,
88 on_accent: Color::WHITE,
89 destructive: Color::from_rgb_u8(255, 59, 48),
90 success: Color::from_rgb_u8(52, 199, 89),
91 toggle_on: accent,
92 toggle_off: Color::from_rgb_u8(170, 170, 181),
93 warning: Color::from_rgb_u8(255, 149, 0),
94 glass_tint: Color::from_rgba_u8(255, 255, 255, 18),
95 glass_stroke: Color::from_rgba_u8(255, 255, 255, 120),
96 }
97 }
98
99 pub fn dark(accent: Color) -> Self {
100 Self {
101 is_dark: true,
102 label: Color::from_rgb_u8(242, 242, 247),
103 secondary_label: Color::from_rgba_u8(235, 235, 245, 153),
104 tertiary_label: Color::from_rgba_u8(235, 235, 245, 76),
105 separator: Color::from_rgba_u8(84, 84, 88, 130),
106 fill: Color::from_rgba_u8(120, 120, 128, 70),
107 secondary_fill: Color::from_rgba_u8(120, 120, 128, 50),
108 surface_glass: Color::from_rgba_u8(28, 28, 30, 184),
109 background: Color::from_rgb_u8(10, 10, 12),
110 surface: Color::from_rgb_u8(28, 28, 30),
111 surface_pressed: Color::from_rgb_u8(44, 44, 46),
112 accent,
113 on_accent: Color::WHITE,
114 destructive: Color::from_rgb_u8(255, 69, 58),
115 success: Color::from_rgb_u8(48, 209, 88),
116 toggle_on: accent,
117 toggle_off: Color::from_rgb_u8(99, 99, 102),
118 warning: Color::from_rgb_u8(255, 159, 10),
119 glass_tint: Color::from_rgba_u8(20, 20, 24, 40),
120 glass_stroke: Color::from_rgba_u8(255, 255, 255, 46),
121 }
122 }
123}
124
125#[derive(Clone, Debug, PartialEq)]
128pub struct LiquidTypography {
129 pub large_title: TextStyle,
130 pub title1: TextStyle,
131 pub title2: TextStyle,
132 pub title3: TextStyle,
133 pub headline: TextStyle,
134 pub body: TextStyle,
135 pub callout: TextStyle,
136 pub subheadline: TextStyle,
137 pub footnote: TextStyle,
138 pub caption1: TextStyle,
139 pub caption2: TextStyle,
140}
141
142fn ramp_style(size_sp: f32, weight: FontWeight) -> TextStyle {
143 TextStyle {
144 span_style: SpanStyle {
145 font_size: TextUnit::Sp(size_sp),
146 font_weight: Some(weight),
147 ..Default::default()
148 },
149 ..Default::default()
150 }
151}
152
153impl Default for LiquidTypography {
154 fn default() -> Self {
155 Self {
156 large_title: ramp_style(34.0, FontWeight::BOLD),
157 title1: ramp_style(28.0, FontWeight::BOLD),
158 title2: ramp_style(22.0, FontWeight::BOLD),
159 title3: ramp_style(20.0, FontWeight::SEMI_BOLD),
160 headline: ramp_style(17.0, FontWeight::SEMI_BOLD),
161 body: ramp_style(17.0, FontWeight::NORMAL),
162 callout: ramp_style(16.0, FontWeight::NORMAL),
163 subheadline: ramp_style(15.0, FontWeight::NORMAL),
164 footnote: ramp_style(13.0, FontWeight::NORMAL),
165 caption1: ramp_style(12.0, FontWeight::NORMAL),
166 caption2: ramp_style(11.0, FontWeight::NORMAL),
167 }
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
173pub struct LiquidThemeSpec {
174 pub scheme: SchemeMode,
175 pub accent: Color,
177 pub typography: LiquidTypography,
178 pub glass_tint_amount: GlassTintAmount,
180}
181
182impl Default for LiquidThemeSpec {
183 fn default() -> Self {
184 Self {
185 scheme: SchemeMode::Auto,
186 accent: Color::from_rgb_u8(0, 122, 255),
187 typography: LiquidTypography::default(),
188 glass_tint_amount: GlassTintAmount::default(),
189 }
190 }
191}
192
193fn theme_local<T: Clone + PartialEq + 'static>(
194 cell: &RefCell<Option<CompositionLocal<T>>>,
195 default: fn() -> T,
196) -> CompositionLocal<T> {
197 cell.borrow_mut()
198 .get_or_insert_with(|| compositionLocalOf(default))
199 .clone()
200}
201
202fn local_liquid_colors() -> CompositionLocal<LiquidColors> {
203 thread_local! {
204 static LOCAL: RefCell<Option<CompositionLocal<LiquidColors>>> = const { RefCell::new(None) };
205 }
206 LOCAL.with(|cell| {
207 theme_local(cell, || {
208 LiquidColors::light(LiquidThemeSpec::default().accent)
209 })
210 })
211}
212
213fn local_liquid_typography() -> CompositionLocal<LiquidTypography> {
214 thread_local! {
215 static LOCAL: RefCell<Option<CompositionLocal<LiquidTypography>>> = const { RefCell::new(None) };
216 }
217 LOCAL.with(|cell| theme_local(cell, LiquidTypography::default))
218}
219
220fn local_liquid_glass_tint_amount() -> CompositionLocal<GlassTintAmount> {
221 thread_local! {
222 static LOCAL: RefCell<Option<CompositionLocal<GlassTintAmount>>> = const { RefCell::new(None) };
223 }
224 LOCAL.with(|cell| theme_local(cell, GlassTintAmount::default))
225}
226
227#[composable]
229pub fn liquid_glass_tint_amount() -> GlassTintAmount {
230 local_liquid_glass_tint_amount().current()
231}
232
233#[composable]
235pub fn liquid_colors() -> LiquidColors {
236 local_liquid_colors().current()
237}
238
239#[composable]
241pub fn liquid_typography() -> LiquidTypography {
242 local_liquid_typography().current()
243}
244
245#[composable]
249pub fn LiquidTheme(spec: LiquidThemeSpec, content: impl FnOnce()) {
250 let dark = match spec.scheme {
251 SchemeMode::Auto => isSystemInDarkTheme(),
252 SchemeMode::Light => false,
253 SchemeMode::Dark => true,
254 };
255 let colors = if dark {
256 LiquidColors::dark(spec.accent)
257 } else {
258 LiquidColors::light(spec.accent)
259 };
260 CompositionLocalProvider(
261 vec![
262 local_liquid_colors().provides(colors),
263 local_liquid_typography().provides(spec.typography.clone()),
264 local_liquid_glass_tint_amount().provides(spec.glass_tint_amount),
265 ],
266 move || {
267 content();
268 },
269 );
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn palettes_differ_and_share_accent() {
278 let accent = Color::from_rgb_u8(0, 122, 255);
279 let light = LiquidColors::light(accent);
280 let dark = LiquidColors::dark(accent);
281 assert!(!light.is_dark);
282 assert!(dark.is_dark);
283 assert_eq!(light.accent, dark.accent);
284 assert_ne!(light.background, dark.background);
285 assert_ne!(light.glass_tint, dark.glass_tint);
286 assert_eq!(light.toggle_on, accent);
287 assert_eq!(dark.toggle_on, accent);
288 assert_ne!(light.toggle_off, dark.toggle_off);
289 }
290
291 #[test]
292 fn type_ramp_is_descending() {
293 let t = LiquidTypography::default();
294 let sizes = [
295 &t.large_title,
296 &t.title1,
297 &t.title2,
298 &t.title3,
299 &t.body,
300 &t.footnote,
301 &t.caption2,
302 ];
303 let values: Vec<f32> = sizes
304 .iter()
305 .map(|style| style.span_style.font_size.value())
306 .collect();
307 assert!(values.windows(2).all(|pair| pair[0] >= pair[1]));
308 }
309}