1use std::cell::RefCell;
6
7use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
8use cranpose_macros::composable;
9use cranpose_services::isSystemInDarkTheme;
10use cranpose_ui::text::{FontWeight, SpanStyle, TextStyle, TextUnit};
11use cranpose_ui_graphics::Color;
12
13use crate::appearance::GlassTintAmount;
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::BLACK,
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, 60, 30),
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_rgba_u8(60, 60, 67, 76),
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::WHITE,
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, 128),
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_rgba_u8(235, 235, 245, 76),
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
123impl LiquidColors {
126 pub fn with_more_contrast(mut self) -> Self {
130 self.secondary_label = self.secondary_label.with_alpha(0.9);
131 self.tertiary_label = self.tertiary_label.with_alpha(0.7);
132 self.separator = self.separator.with_alpha(0.6);
133 self.fill = self.fill.with_alpha(0.32);
134 self.secondary_fill = self.secondary_fill.with_alpha(0.24);
135 self.glass_stroke = self.glass_stroke.with_alpha(0.9);
136 self.toggle_off = if self.is_dark {
137 Color::from_rgb_u8(142, 142, 147)
138 } else {
139 Color::from_rgb_u8(99, 99, 108)
140 };
141 self
142 }
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub struct LiquidTypography {
147 pub large_title: TextStyle,
148 pub title1: TextStyle,
149 pub title2: TextStyle,
150 pub title3: TextStyle,
151 pub headline: TextStyle,
152 pub body: TextStyle,
153 pub callout: TextStyle,
154 pub subheadline: TextStyle,
155 pub footnote: TextStyle,
156 pub caption1: TextStyle,
157 pub caption2: TextStyle,
158}
159
160fn ramp_style(size_sp: f32, weight: FontWeight) -> TextStyle {
161 TextStyle {
162 span_style: SpanStyle {
163 font_size: TextUnit::Sp(size_sp),
164 font_weight: Some(weight),
165 ..Default::default()
166 },
167 ..Default::default()
168 }
169}
170
171impl LiquidTypography {
172 pub fn bolder(self) -> Self {
176 Self {
177 large_title: bolder(self.large_title),
178 title1: bolder(self.title1),
179 title2: bolder(self.title2),
180 title3: bolder(self.title3),
181 headline: bolder(self.headline),
182 body: bolder(self.body),
183 callout: bolder(self.callout),
184 subheadline: bolder(self.subheadline),
185 footnote: bolder(self.footnote),
186 caption1: bolder(self.caption1),
187 caption2: bolder(self.caption2),
188 }
189 }
190}
191
192fn bolder(mut style: TextStyle) -> TextStyle {
193 let weight = style.span_style.font_weight.unwrap_or(FontWeight::NORMAL);
194 style.span_style.font_weight = Some(FontWeight((weight.0 + 200).min(900)));
195 style
196}
197
198impl Default for LiquidTypography {
199 fn default() -> Self {
200 Self {
201 large_title: ramp_style(34.0, FontWeight::BOLD),
202 title1: ramp_style(28.0, FontWeight::BOLD),
203 title2: ramp_style(22.0, FontWeight::BOLD),
204 title3: ramp_style(20.0, FontWeight::SEMI_BOLD),
205 headline: ramp_style(17.0, FontWeight::SEMI_BOLD),
206 body: ramp_style(17.0, FontWeight::NORMAL),
207 callout: ramp_style(16.0, FontWeight::NORMAL),
208 subheadline: ramp_style(15.0, FontWeight::NORMAL),
209 footnote: ramp_style(13.0, FontWeight::NORMAL),
210 caption1: ramp_style(12.0, FontWeight::NORMAL),
211 caption2: ramp_style(11.0, FontWeight::NORMAL),
212 }
213 }
214}
215
216#[derive(Clone, Debug, PartialEq)]
218pub struct LiquidThemeSpec {
219 pub scheme: SchemeMode,
220 pub accent: Color,
222 pub typography: LiquidTypography,
223 pub glass_tint_amount: GlassTintAmount,
225}
226
227impl Default for LiquidThemeSpec {
228 fn default() -> Self {
229 Self {
230 scheme: SchemeMode::Auto,
231 accent: Color::from_rgb_u8(0, 122, 255),
232 typography: LiquidTypography::default(),
233 glass_tint_amount: GlassTintAmount::default(),
234 }
235 }
236}
237
238fn theme_local<T: Clone + PartialEq + 'static>(
239 cell: &RefCell<Option<CompositionLocal<T>>>,
240 default: fn() -> T,
241) -> CompositionLocal<T> {
242 cell.borrow_mut()
243 .get_or_insert_with(|| compositionLocalOf(default))
244 .clone()
245}
246
247fn local_liquid_colors() -> CompositionLocal<LiquidColors> {
248 thread_local! {
249 static LOCAL: RefCell<Option<CompositionLocal<LiquidColors>>> = const { RefCell::new(None) };
250 }
251 LOCAL.with(|cell| {
252 theme_local(cell, || {
253 LiquidColors::light(LiquidThemeSpec::default().accent)
254 })
255 })
256}
257
258fn local_liquid_typography() -> CompositionLocal<LiquidTypography> {
259 thread_local! {
260 static LOCAL: RefCell<Option<CompositionLocal<LiquidTypography>>> = const { RefCell::new(None) };
261 }
262 LOCAL.with(|cell| theme_local(cell, LiquidTypography::default))
263}
264
265fn local_liquid_glass_tint_amount() -> CompositionLocal<GlassTintAmount> {
266 thread_local! {
267 static LOCAL: RefCell<Option<CompositionLocal<GlassTintAmount>>> = const { RefCell::new(None) };
268 }
269 LOCAL.with(|cell| theme_local(cell, GlassTintAmount::default))
270}
271
272#[composable]
274pub fn liquid_glass_tint_amount() -> GlassTintAmount {
275 local_liquid_glass_tint_amount().current()
276}
277
278#[composable]
280pub fn liquid_colors() -> LiquidColors {
281 local_liquid_colors().current()
282}
283
284#[composable]
286pub fn liquid_typography() -> LiquidTypography {
287 local_liquid_typography().current()
288}
289
290#[composable]
296pub fn LiquidTheme(spec: LiquidThemeSpec, content: impl FnOnce()) {
297 let options = cranpose_services::local_accessibility_options().current();
298 let dark = match spec.scheme {
299 SchemeMode::Auto => isSystemInDarkTheme(),
300 SchemeMode::Light => false,
301 SchemeMode::Dark => true,
302 } != options.invert_colors;
303 let colors = if dark {
304 LiquidColors::dark(spec.accent)
305 } else {
306 LiquidColors::light(spec.accent)
307 };
308 let colors = if options.increase_contrast {
309 colors.with_more_contrast()
310 } else {
311 colors
312 };
313 let typography = if options.bold_text {
314 spec.typography.clone().bolder()
315 } else {
316 spec.typography.clone()
317 };
318 CompositionLocalProvider(
319 [
320 local_liquid_colors().provides(colors),
321 local_liquid_typography().provides(typography),
322 local_liquid_glass_tint_amount().provides(spec.glass_tint_amount),
323 cranpose_ui::local_on_light_surface().provides(!dark),
324 ],
325 move || {
326 content();
327 },
328 );
329}
330
331#[cfg(test)]
332#[path = "tests/theme_tests.rs"]
333mod tests;
334
335#[cfg(test)]
336#[path = "tests/theme_option_tests.rs"]
337mod option_tests;