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