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