1use alpha2_model::Rgba;
4
5use crate::spec::{Theme, ThemeChoice, Transition};
6
7pub const THEMES: [&str; 2] = ["clean-dark", "clean-light"];
9
10#[derive(Clone, Debug, PartialEq)]
12pub(crate) struct Resolved {
13 pub font_family: Option<String>,
14 pub text: Rgba,
15 pub background: Rgba,
16 pub surface: Rgba,
17 pub panel: Rgba,
18 pub accent: Rgba,
19 pub title_size: f32,
20 pub subtitle_size: f32,
21 pub body_size: f32,
22 pub caption_size: f32,
23 pub label_size: f32,
24 pub margin: f32,
25 pub transition: Transition,
26 pub still_seconds: f64,
27 pub text_seconds: f64,
28 pub fade_seconds: f64,
29}
30
31const fn gray(level: f32, alpha: f32) -> Rgba {
32 Rgba::new(level, level, level, alpha)
33}
34
35fn builtin(name: &str) -> Option<Resolved> {
36 let crossfade = Transition {
37 kind: "crossfade".to_owned(),
38 duration: 0.5,
39 easing: None,
40 params: std::collections::BTreeMap::new(),
41 version: None,
42 };
43 let common = Resolved {
44 font_family: None,
45 text: gray(1.0, 1.0),
46 background: gray(0.0, 1.0),
47 surface: Rgba::new(0.067, 0.075, 0.094, 1.0),
48 panel: gray(0.0, 0.6),
49 accent: Rgba::new(0.31, 0.545, 1.0, 1.0),
50 title_size: 0.1,
51 subtitle_size: 0.045,
52 body_size: 0.04,
53 caption_size: 0.06,
54 label_size: 0.034,
55 margin: 0.04,
56 transition: crossfade,
57 still_seconds: 4.0,
58 text_seconds: 3.0,
59 fade_seconds: 0.4,
60 };
61 match name {
62 "clean-dark" => Some(common),
63 "clean-light" => Some(Resolved {
64 text: gray(0.067, 1.0),
65 background: gray(1.0, 1.0),
66 surface: Rgba::new(0.96, 0.96, 0.95, 1.0),
67 panel: gray(1.0, 0.75),
68 accent: Rgba::new(0.0, 0.4, 0.85, 1.0),
69 ..common
70 }),
71 _ => None,
72 }
73}
74
75pub(crate) fn resolve(choice: &ThemeChoice) -> Result<Resolved, String> {
77 let unknown = |name: &str| {
78 format!(
79 "unknown theme \"{name}\"; the built-in themes are {}",
80 THEMES.join(", ")
81 )
82 };
83 let theme: &Theme = match choice {
84 ThemeChoice::Named(name) => return builtin(name).ok_or_else(|| unknown(name)),
85 ThemeChoice::Custom(theme) => theme,
86 };
87 let base_name = theme.base.as_deref().unwrap_or(THEMES[0]);
88 let base = builtin(base_name).ok_or_else(|| unknown(base_name))?;
89 let size = |value: Option<f32>, fallback: f32, field: &str| match value {
90 Some(value) if !(value.is_finite() && value > 0.0 && value <= 1.0) => Err(format!(
91 "{field} is {value}; a theme size is a fraction of the canvas's shorter side, in (0, 1]"
92 )),
93 value => Ok(value.unwrap_or(fallback)),
94 };
95 let seconds = |value: Option<f64>, fallback: f64, field: &str| match value {
96 Some(value) if !(value.is_finite() && value >= 0.0) => {
97 Err(format!("{field} is {value}; it is non-negative seconds"))
98 }
99 value => Ok(value.unwrap_or(fallback)),
100 };
101 Ok(Resolved {
102 font_family: theme.font_family.clone().or(base.font_family),
103 text: theme.text.unwrap_or(base.text),
104 background: theme.background.unwrap_or(base.background),
105 surface: theme.surface.unwrap_or(base.surface),
106 panel: theme.panel.unwrap_or(base.panel),
107 accent: theme.accent.unwrap_or(base.accent),
108 title_size: size(theme.title_size, base.title_size, "title_size")?,
109 subtitle_size: size(theme.subtitle_size, base.subtitle_size, "subtitle_size")?,
110 body_size: size(theme.body_size, base.body_size, "body_size")?,
111 caption_size: size(theme.caption_size, base.caption_size, "caption_size")?,
112 label_size: size(theme.label_size, base.label_size, "label_size")?,
113 margin: size(theme.margin, base.margin, "margin")?,
114 transition: theme.transition.clone().unwrap_or(base.transition),
115 still_seconds: seconds(theme.still_seconds, base.still_seconds, "still_seconds")?,
116 text_seconds: seconds(theme.text_seconds, base.text_seconds, "text_seconds")?,
117 fade_seconds: seconds(theme.fade_seconds, base.fade_seconds, "fade_seconds")?,
118 })
119}