1use crate::{Button, Buttons, Color, ColorError, Colors, Theme};
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum ThemeError {
12 #[error("reading {path}: {source}")]
14 Io {
15 path: String,
17 source: std::io::Error,
19 },
20
21 #[error("parsing the theme: {0}")]
23 Syntax(String),
24
25 #[error("{field}: expected {expected}")]
30 WrongType {
31 field: String,
33 expected: &'static str,
35 },
36
37 #[error("{field}: {source}")]
39 BadColor {
40 field: String,
42 source: ColorError,
44 },
45}
46
47pub fn load_theme_str(text: &str, base: Theme) -> Result<Theme, ThemeError> {
65 let doc: toml::Value = toml::from_str(text)
70 .map_err(|error: toml::de::Error| ThemeError::Syntax(error.to_string()))?;
71
72 let mut theme = base;
73 read_string(&doc, "name", &mut theme.name)?;
74 read_int(&doc, "corner_radius", &mut theme.corner_radius)?;
75 read_float(&doc, "noise_amplitude", &mut theme.noise_amplitude)?;
76 read_int(&doc, "shadow_extent", &mut theme.shadow_extent)?;
77 read_int(
78 &doc,
79 "animation_duration_ms",
80 &mut theme.animation_duration_ms,
81 )?;
82
83 if let Some(bar) = doc.get("title_bar") {
84 read_int(bar, "height", &mut theme.title_bar.height)?;
85 read_string(bar, "font", &mut theme.title_bar.font)?;
86 read_int(bar, "font_size", &mut theme.title_bar.font_size)?;
87 }
88
89 if let Some(colors) = doc.get("colors") {
90 let Colors {
91 panel_top,
92 panel_bottom,
93 rim_focused,
94 rim_blurred,
95 shadow,
96 title_text,
97 title_shadow,
98 } = &mut theme.colors;
99 read_color(colors, "panel_top", panel_top)?;
100 read_color(colors, "panel_bottom", panel_bottom)?;
101 read_color(colors, "rim_focused", rim_focused)?;
102 read_color(colors, "rim_blurred", rim_blurred)?;
103 read_color(colors, "shadow", shadow)?;
104 read_color(colors, "title_text", title_text)?;
105 read_color(colors, "title_shadow", title_shadow)?;
106 }
107
108 if let Some(buttons) = doc.get("buttons") {
109 let Buttons {
110 close,
111 minimize,
112 maximize,
113 } = &mut theme.buttons;
114 read_button(buttons, "close", close)?;
115 read_button(buttons, "minimize", minimize)?;
116 read_button(buttons, "maximize", maximize)?;
117 }
118
119 Ok(theme)
120}
121
122pub fn load_theme_file(path: &str, base: Theme) -> Result<Theme, ThemeError> {
129 let text = std::fs::read_to_string(path).map_err(|source| ThemeError::Io {
130 path: path.to_owned(),
131 source,
132 })?;
133 load_theme_str(&text, base)
134}
135
136fn read_string(table: &toml::Value, field: &str, into: &mut String) -> Result<(), ThemeError> {
138 let Some(value) = table.get(field) else {
139 return Ok(());
140 };
141 let text = value.as_str().ok_or_else(|| ThemeError::WrongType {
142 field: field.to_owned(),
143 expected: "a string",
144 })?;
145 into.clear();
146 into.push_str(text);
147 Ok(())
148}
149
150fn read_int(table: &toml::Value, field: &str, into: &mut i32) -> Result<(), ThemeError> {
152 let Some(value) = table.get(field) else {
153 return Ok(());
154 };
155 let wrong = || ThemeError::WrongType {
156 field: field.to_owned(),
157 expected: "an integer",
158 };
159 *into = i32::try_from(value.as_integer().ok_or_else(wrong)?).map_err(|_| wrong())?;
160 Ok(())
161}
162
163fn read_float(table: &toml::Value, field: &str, into: &mut f64) -> Result<(), ThemeError> {
168 let Some(value) = table.get(field) else {
169 return Ok(());
170 };
171 *into = match value {
172 toml::Value::Float(float) => *float,
173 toml::Value::Integer(int) => {
178 f64::from(i32::try_from(*int).map_err(|_| ThemeError::WrongType {
179 field: field.to_owned(),
180 expected: "a number a theme could use",
181 })?)
182 }
183 _ => {
184 return Err(ThemeError::WrongType {
185 field: field.to_owned(),
186 expected: "a number",
187 });
188 }
189 };
190 Ok(())
191}
192
193fn read_color(table: &toml::Value, field: &str, into: &mut Color) -> Result<(), ThemeError> {
195 read_color_as(table, field, field, into)
196}
197
198fn read_color_as(
206 table: &toml::Value,
207 key: &str,
208 label: &str,
209 into: &mut Color,
210) -> Result<(), ThemeError> {
211 let Some(value) = table.get(key) else {
212 return Ok(());
213 };
214 let text = value.as_str().ok_or_else(|| ThemeError::WrongType {
215 field: label.to_owned(),
216 expected: "a #RRGGBB or #RRGGBBAA string",
217 })?;
218 *into = Color::from_hex(text).map_err(|source| ThemeError::BadColor {
219 field: label.to_owned(),
220 source,
221 })?;
222 Ok(())
223}
224
225fn read_button(table: &toml::Value, field: &str, into: &mut Button) -> Result<(), ThemeError> {
227 let Some(button) = table.get(field) else {
228 return Ok(());
229 };
230 read_color_as(
231 button,
232 "fill",
233 &format!("buttons.{field}.fill"),
234 &mut into.fill,
235 )?;
236 read_color_as(
237 button,
238 "hover",
239 &format!("buttons.{field}.hover"),
240 &mut into.hover,
241 )
242}