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