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> {
60 let doc: toml::Value = toml::from_str(text)
65 .map_err(|error: toml::de::Error| ThemeError::Syntax(error.to_string()))?;
66
67 let mut theme = base;
68 read_string(&doc, "name", &mut theme.name)?;
69 read_int(&doc, "corner_radius", &mut theme.corner_radius)?;
70 read_float(&doc, "noise_amplitude", &mut theme.noise_amplitude)?;
71 read_int(&doc, "shadow_extent", &mut theme.shadow_extent)?;
72 read_int(
73 &doc,
74 "animation_duration_ms",
75 &mut theme.animation_duration_ms,
76 )?;
77
78 if let Some(bar) = doc.get("title_bar") {
79 read_int(bar, "height", &mut theme.title_bar.height)?;
80 read_string(bar, "font", &mut theme.title_bar.font)?;
81 read_int(bar, "font_size", &mut theme.title_bar.font_size)?;
82 }
83
84 if let Some(colors) = doc.get("colors") {
85 let Colors {
86 panel_top,
87 panel_bottom,
88 rim_focused,
89 rim_blurred,
90 shadow,
91 title_text,
92 title_shadow,
93 } = &mut theme.colors;
94 read_color(colors, "panel_top", panel_top)?;
95 read_color(colors, "panel_bottom", panel_bottom)?;
96 read_color(colors, "rim_focused", rim_focused)?;
97 read_color(colors, "rim_blurred", rim_blurred)?;
98 read_color(colors, "shadow", shadow)?;
99 read_color(colors, "title_text", title_text)?;
100 read_color(colors, "title_shadow", title_shadow)?;
101 }
102
103 if let Some(buttons) = doc.get("buttons") {
104 let Buttons {
105 close,
106 minimize,
107 maximize,
108 } = &mut theme.buttons;
109 read_button(buttons, "close", close)?;
110 read_button(buttons, "minimize", minimize)?;
111 read_button(buttons, "maximize", maximize)?;
112 }
113
114 Ok(theme)
115}
116
117pub fn load_theme_file(path: &str, base: Theme) -> Result<Theme, ThemeError> {
124 let text = std::fs::read_to_string(path).map_err(|source| ThemeError::Io {
125 path: path.to_owned(),
126 source,
127 })?;
128 load_theme_str(&text, base)
129}
130
131fn read_string(table: &toml::Value, field: &str, into: &mut String) -> Result<(), ThemeError> {
133 let Some(value) = table.get(field) else {
134 return Ok(());
135 };
136 let text = value.as_str().ok_or_else(|| ThemeError::WrongType {
137 field: field.to_owned(),
138 expected: "a string",
139 })?;
140 into.clear();
141 into.push_str(text);
142 Ok(())
143}
144
145fn read_int(table: &toml::Value, field: &str, into: &mut i32) -> Result<(), ThemeError> {
147 let Some(value) = table.get(field) else {
148 return Ok(());
149 };
150 let wrong = || ThemeError::WrongType {
151 field: field.to_owned(),
152 expected: "an integer",
153 };
154 *into = i32::try_from(value.as_integer().ok_or_else(wrong)?).map_err(|_| wrong())?;
155 Ok(())
156}
157
158fn read_float(table: &toml::Value, field: &str, into: &mut f64) -> Result<(), ThemeError> {
163 let Some(value) = table.get(field) else {
164 return Ok(());
165 };
166 *into = match value {
167 toml::Value::Float(float) => *float,
168 toml::Value::Integer(int) => {
173 f64::from(i32::try_from(*int).map_err(|_| ThemeError::WrongType {
174 field: field.to_owned(),
175 expected: "a number a theme could use",
176 })?)
177 }
178 _ => {
179 return Err(ThemeError::WrongType {
180 field: field.to_owned(),
181 expected: "a number",
182 });
183 }
184 };
185 Ok(())
186}
187
188fn read_color(table: &toml::Value, field: &str, into: &mut Color) -> Result<(), ThemeError> {
190 read_color_as(table, field, field, into)
191}
192
193fn read_color_as(
201 table: &toml::Value,
202 key: &str,
203 label: &str,
204 into: &mut Color,
205) -> Result<(), ThemeError> {
206 let Some(value) = table.get(key) else {
207 return Ok(());
208 };
209 let text = value.as_str().ok_or_else(|| ThemeError::WrongType {
210 field: label.to_owned(),
211 expected: "a #RRGGBB or #RRGGBBAA string",
212 })?;
213 *into = Color::from_hex(text).map_err(|source| ThemeError::BadColor {
214 field: label.to_owned(),
215 source,
216 })?;
217 Ok(())
218}
219
220fn read_button(table: &toml::Value, field: &str, into: &mut Button) -> Result<(), ThemeError> {
222 let Some(button) = table.get(field) else {
223 return Ok(());
224 };
225 read_color_as(
226 button,
227 "fill",
228 &format!("buttons.{field}.fill"),
229 &mut into.fill,
230 )?;
231 read_color_as(
232 button,
233 "hover",
234 &format!("buttons.{field}.hover"),
235 &mut into.hover,
236 )
237}