1use frus_core::{Color, FontWeight, TextDecoration, TextStyle};
12
13use crate::media::{Brightness, MediaQuery};
14use crate::{CupertinoColors, CupertinoDynamicColor, Theme};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq)]
27pub struct CupertinoTextThemeData {
28 pub primary_color: Option<CupertinoDynamicColor>,
31 pub text_style: Option<TextStyle>,
33 pub action_text_style: Option<TextStyle>,
35 pub action_small_text_style: Option<TextStyle>,
37 pub tab_label_text_style: Option<TextStyle>,
39 pub nav_title_text_style: Option<TextStyle>,
41 pub nav_large_title_text_style: Option<TextStyle>,
43 pub nav_action_text_style: Option<TextStyle>,
45 pub picker_text_style: Option<TextStyle>,
47 pub date_time_picker_text_style: Option<TextStyle>,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct CupertinoTextTheme {
55 pub text_style: TextStyle,
57 pub action_text_style: TextStyle,
59 pub action_small_text_style: TextStyle,
61 pub tab_label_text_style: TextStyle,
63 pub nav_title_text_style: TextStyle,
65 pub nav_large_title_text_style: TextStyle,
67 pub nav_action_text_style: TextStyle,
69 pub picker_text_style: TextStyle,
71 pub date_time_picker_text_style: TextStyle,
73}
74
75const TEXT: TextStyle = TextStyle::new(17.0)
77 .letter_spacing(-0.41)
78 .decoration(TextDecoration::NONE);
79const ACTION: TextStyle = TEXT;
81const ACTION_SMALL: TextStyle = TextStyle::new(15.0)
83 .letter_spacing(-0.23)
84 .decoration(TextDecoration::NONE);
85const TAB_LABEL: TextStyle = TextStyle::new(10.0)
87 .weight(FontWeight::Medium)
88 .letter_spacing(-0.24);
89const NAV_TITLE: TextStyle = TextStyle::new(17.0)
91 .weight(FontWeight::SemiBold)
92 .letter_spacing(-0.41);
93const NAV_LARGE_TITLE: TextStyle = TextStyle::new(34.0)
95 .weight(FontWeight::Bold)
96 .letter_spacing(0.38);
97const PICKER: TextStyle = TextStyle::new(21.0)
99 .weight(FontWeight::Regular)
100 .letter_spacing(-0.6);
101const DATE_TIME_PICKER: TextStyle = TextStyle::new(21.0)
103 .weight(FontWeight::Regular)
104 .letter_spacing(0.4);
105
106impl CupertinoTextThemeData {
107 pub const NONE: Self = Self {
109 primary_color: None,
110 text_style: None,
111 action_text_style: None,
112 action_small_text_style: None,
113 tab_label_text_style: None,
114 nav_title_text_style: None,
115 nav_large_title_text_style: None,
116 nav_action_text_style: None,
117 picker_text_style: None,
118 date_time_picker_text_style: None,
119 };
120
121 #[must_use]
123 pub fn primary_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
124 self.primary_color = Some(color.into());
125 self
126 }
127
128 #[must_use]
130 pub const fn text_style(mut self, style: TextStyle) -> Self {
131 self.text_style = Some(style);
132 self
133 }
134
135 #[must_use]
137 pub const fn action_text_style(mut self, style: TextStyle) -> Self {
138 self.action_text_style = Some(style);
139 self
140 }
141
142 #[must_use]
144 pub const fn nav_title_text_style(mut self, style: TextStyle) -> Self {
145 self.nav_title_text_style = Some(style);
146 self
147 }
148
149 #[must_use]
151 pub const fn nav_large_title_text_style(mut self, style: TextStyle) -> Self {
152 self.nav_large_title_text_style = Some(style);
153 self
154 }
155
156 pub fn resolve(&self, theme: &Theme) -> CupertinoTextTheme {
159 let label = CupertinoColors::LABEL.resolve(theme);
160 let inactive = CupertinoColors::INACTIVE_GRAY.resolve(theme);
161 let primary = self
162 .primary_color
163 .unwrap_or(CupertinoColors::SYSTEM_BLUE)
164 .resolve(theme);
165 let action = self.action_text_style.unwrap_or(ACTION.color(primary));
166 CupertinoTextTheme {
167 text_style: self.text_style.unwrap_or(TEXT.color(label)),
168 action_text_style: action,
169 action_small_text_style: self
170 .action_small_text_style
171 .unwrap_or(ACTION_SMALL.color(primary)),
172 tab_label_text_style: self
173 .tab_label_text_style
174 .unwrap_or(TAB_LABEL.color(inactive)),
175 nav_title_text_style: self.nav_title_text_style.unwrap_or(NAV_TITLE.color(label)),
176 nav_large_title_text_style: self
177 .nav_large_title_text_style
178 .unwrap_or(NAV_LARGE_TITLE.color(label)),
179 nav_action_text_style: self.nav_action_text_style.unwrap_or(ACTION.color(primary)),
182 picker_text_style: self.picker_text_style.unwrap_or(PICKER.color(label)),
183 date_time_picker_text_style: self
184 .date_time_picker_text_style
185 .unwrap_or(DATE_TIME_PICKER.color(label)),
186 }
187 }
188}
189
190#[derive(Clone, Debug, Default, PartialEq)]
202pub struct CupertinoThemeData {
203 pub brightness: Option<Brightness>,
205 pub primary_color: Option<CupertinoDynamicColor>,
207 pub primary_contrasting_color: Option<CupertinoDynamicColor>,
209 pub text_theme: Option<Box<CupertinoTextThemeData>>,
211 pub bar_background_color: Option<CupertinoDynamicColor>,
213 pub scaffold_background_color: Option<CupertinoDynamicColor>,
215 pub selection_handle_color: Option<CupertinoDynamicColor>,
217 pub apply_theme_to_all: Option<bool>,
220}
221
222impl CupertinoThemeData {
223 pub fn new() -> Self {
225 Self::default()
226 }
227
228 #[must_use]
230 pub fn brightness(mut self, brightness: Brightness) -> Self {
231 self.brightness = Some(brightness);
232 self
233 }
234
235 #[must_use]
237 pub fn primary_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
238 self.primary_color = Some(color.into());
239 self
240 }
241
242 #[must_use]
244 pub fn primary_contrasting_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
245 self.primary_contrasting_color = Some(color.into());
246 self
247 }
248
249 #[must_use]
251 pub fn text_theme(mut self, text_theme: CupertinoTextThemeData) -> Self {
252 self.text_theme = Some(Box::new(text_theme));
253 self
254 }
255
256 #[must_use]
258 pub fn bar_background_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
259 self.bar_background_color = Some(color.into());
260 self
261 }
262
263 #[must_use]
265 pub fn scaffold_background_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
266 self.scaffold_background_color = Some(color.into());
267 self
268 }
269
270 #[must_use]
272 pub fn selection_handle_color(mut self, color: impl Into<CupertinoDynamicColor>) -> Self {
273 self.selection_handle_color = Some(color.into());
274 self
275 }
276
277 #[must_use]
279 pub fn apply_theme_to_all(mut self, all: bool) -> Self {
280 self.apply_theme_to_all = Some(all);
281 self
282 }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq)]
288pub struct CupertinoTheme {
289 pub brightness: Brightness,
291 pub primary_color: Color,
293 pub primary_contrasting_color: Color,
295 pub bar_background_color: Color,
297 pub scaffold_background_color: Color,
299 pub selection_handle_color: Color,
301 pub apply_theme_to_all: bool,
303 pub text_theme: CupertinoTextTheme,
305}
306
307impl CupertinoTheme {
308 pub const DEFAULT_BAR_BACKGROUND: CupertinoDynamicColor =
312 CupertinoDynamicColor::with_brightness(
313 Color::from_argb_u32(0xF0F9F9F9),
314 Color::from_argb_u32(0xF01D1D1D),
315 );
316
317 pub fn of(theme: &Theme) -> Self {
323 let own = theme.cupertino_theme.as_deref();
324 let data = own
325 .or(theme.cupertino_override_theme.as_deref())
326 .cloned()
327 .unwrap_or_default();
328 let main = own.is_none();
329 let resolve = |c: CupertinoDynamicColor| c.resolve(theme);
330 let primary = data.primary_color.unwrap_or(if main {
331 theme.scheme.primary.into()
332 } else {
333 CupertinoColors::SYSTEM_BLUE
334 });
335 let text_theme = match data.text_theme.as_deref() {
336 Some(asked) => asked.resolve(theme),
337 None => CupertinoTextThemeData::NONE
338 .primary_color(primary)
339 .resolve(theme),
340 };
341 let main_handles = if main {
343 theme.widgets.text_selection.handle_color.map(Into::into)
344 } else {
345 None
346 };
347 Self {
348 brightness: Self::brightness_of(theme),
349 primary_color: resolve(primary),
350 primary_contrasting_color: resolve(data.primary_contrasting_color.unwrap_or(if main {
351 theme.scheme.on_primary.into()
352 } else {
353 CupertinoColors::WHITE.into()
354 })),
355 bar_background_color: resolve(
356 data.bar_background_color
357 .unwrap_or(Self::DEFAULT_BAR_BACKGROUND),
358 ),
359 scaffold_background_color: resolve(data.scaffold_background_color.unwrap_or(if main {
360 theme.background.into()
361 } else {
362 CupertinoColors::SYSTEM_BACKGROUND
363 })),
364 selection_handle_color: resolve(
365 data.selection_handle_color
366 .or(main_handles)
367 .unwrap_or(CupertinoColors::SYSTEM_BLUE),
368 ),
369 apply_theme_to_all: data.apply_theme_to_all.unwrap_or(false),
370 text_theme,
371 }
372 }
373
374 pub fn brightness_of(theme: &Theme) -> Brightness {
380 match theme.cupertino_theme.as_deref() {
381 Some(own) => own
382 .brightness
383 .unwrap_or_else(|| MediaQuery::of().platform_brightness),
384 None => theme
385 .cupertino_override_theme
386 .as_deref()
387 .and_then(|o| o.brightness)
388 .unwrap_or_else(|| theme.brightness()),
389 }
390 }
391
392 pub fn around<Msg: 'static>(
396 data: CupertinoThemeData,
397 child: impl crate::Widget<Msg> + 'static,
398 ) -> crate::Themed<Msg> {
399 crate::Themed::tweak(
400 move |t| {
401 t.cupertino_theme = Some(Box::new(data.clone()));
402 t.widgets.icon.color = Some(CupertinoTheme::of(t).primary_color);
403 },
404 child,
405 )
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::{CupertinoTextThemeData, CupertinoTheme, CupertinoThemeData};
412 use crate::media::{Brightness, MediaQuery};
413 use crate::{
414 build_ui, Container, CupertinoColors as C, Flex, Runtime, Theme, ThemeBuilder, Widget,
415 };
416 use frus_core::{Color, FontWeight, Primitive, Size, TextStyle};
417
418 #[derive(Clone, Debug, PartialEq)]
419 enum Msg {}
420
421 #[test]
424 fn under_the_main_theme_its_colours_are_the_defaults() {
425 let theme = Theme::dark();
426 let c = CupertinoTheme::of(&theme);
427 assert_eq!(c.brightness, Brightness::Dark);
428 assert_eq!(c.primary_color, theme.scheme.primary);
429 assert_eq!(c.primary_contrasting_color, theme.scheme.on_primary);
430 assert_eq!(c.scaffold_background_color, theme.background);
431 assert_eq!(
432 c.bar_background_color,
433 CupertinoTheme::DEFAULT_BAR_BACKGROUND.dark_color
434 );
435 assert_eq!(c.selection_handle_color, C::SYSTEM_BLUE.dark_color);
436 assert!(!c.apply_theme_to_all);
437 assert_eq!(
439 c.text_theme.action_text_style.color,
440 Some(theme.scheme.primary)
441 );
442 assert_eq!(c.text_theme.text_style.color, Some(C::LABEL.dark_color));
443 let mut named = Theme::light();
445 named.widgets.text_selection.handle_color = Some(Color::rgb8(1, 2, 3));
446 assert_eq!(
447 CupertinoTheme::of(&named).selection_handle_color,
448 Color::rgb8(1, 2, 3)
449 );
450 }
451
452 #[test]
455 fn the_override_replaces_what_it_sets() {
456 let theme = Theme::light().with_cupertino_override_theme(
457 CupertinoThemeData::new()
458 .brightness(Brightness::Dark)
459 .primary_color(C::SYSTEM_GREEN)
460 .scaffold_background_color(C::SYSTEM_BACKGROUND),
461 );
462 let c = CupertinoTheme::of(&theme);
463 assert_eq!(c.brightness, Brightness::Dark);
464 assert_eq!(c.primary_color, C::SYSTEM_GREEN.dark_color);
465 assert_eq!(c.scaffold_background_color, C::SYSTEM_BACKGROUND.dark_color);
466 assert_eq!(c.primary_contrasting_color, theme.scheme.on_primary);
468 assert_eq!(C::LABEL.resolve(&theme), C::LABEL.dark_color);
469 assert_eq!(
470 c.text_theme.action_text_style.color,
471 Some(C::SYSTEM_GREEN.dark_color)
472 );
473 }
474
475 #[test]
478 fn a_subtree_s_own_theme_has_apple_s_defaults() {
479 let mut theme = Theme::light();
480 theme.cupertino_theme = Some(Box::new(CupertinoThemeData::new()));
481 let surface = MediaQuery {
482 platform_brightness: Brightness::Dark,
483 ..MediaQuery::new(Size::new(100.0, 100.0))
484 };
485 surface.scope(|| {
486 let c = CupertinoTheme::of(&theme);
487 assert_eq!(c.brightness, Brightness::Dark);
488 assert_eq!(c.primary_color, C::SYSTEM_BLUE.dark_color);
489 assert_eq!(c.primary_contrasting_color, C::WHITE);
490 assert_eq!(c.scaffold_background_color, C::SYSTEM_BACKGROUND.dark_color);
491 });
492 theme.cupertino_theme = Some(Box::new(
494 CupertinoThemeData::new().brightness(Brightness::Light),
495 ));
496 surface.scope(|| {
497 assert_eq!(
498 CupertinoTheme::of(&theme).scaffold_background_color,
499 C::SYSTEM_BACKGROUND.color
500 )
501 });
502 }
503
504 #[test]
508 fn the_default_type_is_the_reference_s() {
509 let t = CupertinoTheme::of(&Theme::light()).text_theme;
510 let spec = |s: TextStyle| (s.size, s.weight, s.letter_spacing);
511 assert_eq!(spec(t.text_style), (Some(17.0), None, Some(-0.41)));
512 assert_eq!(spec(t.action_text_style), (Some(17.0), None, Some(-0.41)));
513 assert_eq!(
514 spec(t.action_small_text_style),
515 (Some(15.0), None, Some(-0.23))
516 );
517 assert_eq!(
518 spec(t.tab_label_text_style),
519 (Some(10.0), Some(FontWeight::Medium), Some(-0.24))
520 );
521 assert_eq!(
522 spec(t.nav_title_text_style),
523 (Some(17.0), Some(FontWeight::SemiBold), Some(-0.41))
524 );
525 assert_eq!(
526 spec(t.nav_large_title_text_style),
527 (Some(34.0), Some(FontWeight::Bold), Some(0.38))
528 );
529 assert_eq!(
530 spec(t.picker_text_style),
531 (Some(21.0), Some(FontWeight::Regular), Some(-0.6))
532 );
533 assert_eq!(
534 spec(t.date_time_picker_text_style),
535 (Some(21.0), Some(FontWeight::Regular), Some(0.4))
536 );
537 assert_eq!(t.tab_label_text_style.color, Some(C::INACTIVE_GRAY.color));
538 assert_eq!(t.nav_title_text_style.color, Some(C::LABEL.color));
539
540 let mine = TextStyle::new(20.0);
541 let theme = Theme::light().with_cupertino_override_theme(
542 CupertinoThemeData::new()
543 .text_theme(CupertinoTextThemeData::NONE.nav_title_text_style(mine)),
544 );
545 let t = CupertinoTheme::of(&theme).text_theme;
546 assert_eq!(t.nav_title_text_style, mine);
547 assert_eq!(t.action_text_style.color, Some(C::SYSTEM_BLUE.color));
548 }
549
550 #[test]
553 fn a_subtree_takes_its_own_theme() {
554 let green = Theme::light().with_cupertino_override_theme(
555 CupertinoThemeData::new().primary_color(C::SYSTEM_GREEN),
556 );
557 let plain = Theme::light();
558 assert_eq!(
559 plain.lerp(&green, 0.7).cupertino_override_theme,
560 green.cupertino_override_theme
561 );
562 assert_eq!(plain.lerp(&green, 0.3).cupertino_override_theme, None);
563
564 let swatch = || {
565 ThemeBuilder::new(|t: &Theme| {
566 Container::new()
567 .width(50.0)
568 .height(50.0)
569 .color(CupertinoTheme::of(t).primary_color)
570 })
571 };
572 let icon = || {
573 ThemeBuilder::new(|t: &Theme| {
574 Container::new()
575 .width(50.0)
576 .height(50.0)
577 .color(t.widgets.icon.color.unwrap_or(Color::TRANSPARENT))
578 })
579 };
580 let tree = Flex::<Msg>::column()
581 .width(200.0)
582 .height(200.0)
583 .child(swatch())
584 .child(CupertinoTheme::around(CupertinoThemeData::new(), swatch()))
585 .child(CupertinoTheme::around(
586 CupertinoThemeData::new().primary_color(C::SYSTEM_PINK),
587 icon(),
588 ));
589 let painted: Vec<Color> = build_ui(
590 &tree as &dyn Widget<Msg>,
591 Size::new(200.0, 200.0),
592 &Runtime::default(),
593 &Theme::light(),
594 )
595 .scene()
596 .primitives()
597 .iter()
598 .filter_map(|p| match p {
599 Primitive::Rect { color, blur, .. } if *blur == 0.0 => Some(*color),
600 _ => None,
601 })
602 .collect();
603 assert_eq!(
604 painted,
605 vec![
606 Theme::light().scheme.primary,
607 C::SYSTEM_BLUE.color,
608 C::SYSTEM_PINK.color
609 ]
610 );
611 }
612}