1use super::GeneratedCode;
8use crate::ir::style::Color;
9use crate::ir::theme::ThemeDocument;
10
11pub fn generate_theme_code(
55 document: &ThemeDocument,
56 module_name: &str,
57) -> Result<GeneratedCode, String> {
58 if document.themes.is_empty() {
59 return Err("THEME_001: At least one theme must be defined".to_string());
60 }
61
62 let mut code = String::new();
63
64 code.push_str("//! Generated theme code - DO NOT EDIT\n");
65 code.push_str("//! This file is auto-generated by the dampen codegen.\n\n");
66
67 code.push_str("use std::collections::HashMap;\n");
68 code.push_str("use iced::Color;\n");
69 code.push_str("use iced::Theme;\n\n");
70
71 code.push_str("/// Get the theme by name, or the default if not found\n");
72 code.push_str(&format!("pub fn {}_theme() -> Theme {{\n", module_name));
73 code.push_str(&format!(" {}_default_theme()\n", module_name));
74 code.push_str("}\n\n");
75
76 code.push_str("/// Get a specific theme by name\n");
77 code.push_str(&format!(
78 "pub fn {}_theme_named(name: &str) -> Option<Theme> {{\n",
79 module_name
80 ));
81 code.push_str(&format!(" let themes = {}_themes();\n", module_name));
82 code.push_str(" themes.get(name).cloned()\n");
83 code.push_str("}\n\n");
84
85 code.push_str("/// Get all available themes\n");
86 code.push_str(&format!(
87 "pub fn {}_themes() -> HashMap<&'static str, Theme> {{\n",
88 module_name
89 ));
90 code.push_str(" let mut themes = HashMap::new();\n");
91
92 let mut theme_names: Vec<&str> = document.themes.keys().map(|s| s.as_str()).collect();
93 theme_names.sort();
94
95 for theme_name in &theme_names {
96 code.push_str(&format!(
97 " themes.insert(\"{}\", {}_{}());\n",
98 theme_name, module_name, theme_name
99 ));
100 }
101
102 code.push_str(" themes\n");
103 code.push_str("}\n\n");
104
105 code.push_str("/// Get the default theme\n");
106 code.push_str(&format!(
107 "pub fn {}_default_theme() -> Theme {{\n",
108 module_name
109 ));
110
111 let effective_default = document.effective_default(None);
112 code.push_str(&format!(" {}_{}()\n", module_name, effective_default));
113 code.push_str("}\n\n");
114
115 code.push_str("/// Get the default theme name as a string\n");
116 code.push_str(&format!(
117 "pub fn {}_default_theme_name() -> &'static str {{\n",
118 module_name
119 ));
120 code.push_str(&format!(" \"{}\"\n", effective_default));
121 code.push_str("}\n\n");
122
123 code.push_str("/// Get whether the theme follows system preference\n");
124 code.push_str(&format!(
125 "pub fn {}_follows_system() -> bool {{\n",
126 module_name
127 ));
128 code.push_str(&format!(" {}\n", document.follow_system));
129 code.push_str("}\n\n");
130
131 for theme_name in &theme_names {
132 let theme = match document.themes.get(*theme_name) {
133 Some(t) => t,
134 None => continue,
135 };
136
137 let theme_fn_name = format!("{}_{}", module_name, theme_name);
138 code.push_str(&format!("/// Theme: {}\n", theme_name));
139 code.push_str("fn ");
140 code.push_str(&theme_fn_name);
141 code.push_str("() -> Theme {\n");
142
143 let palette = &theme.palette;
144 let primary = color_to_rgb8_tuple(palette.primary.as_ref());
145 let background = color_to_rgb8_tuple(palette.background.as_ref());
146 let text = color_to_rgb8_tuple(palette.text.as_ref());
147 let success = color_to_rgb8_tuple(palette.success.as_ref());
148 let warning = color_to_rgb8_tuple(palette.warning.as_ref());
149 let danger = color_to_rgb8_tuple(palette.danger.as_ref());
150
151 code.push_str(" Theme::custom(\n");
152 code.push_str(&format!(" \"{}\".to_string(),\n", theme_name));
153 code.push_str(" iced::theme::Palette {\n");
154 code.push_str(&format!(
155 " background: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
156 (background.0 * 255.0) as u8,
157 (background.1 * 255.0) as u8,
158 (background.2 * 255.0) as u8
159 ));
160 code.push_str(&format!(
161 " text: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
162 (text.0 * 255.0) as u8,
163 (text.1 * 255.0) as u8,
164 (text.2 * 255.0) as u8
165 ));
166 code.push_str(&format!(
167 " primary: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
168 (primary.0 * 255.0) as u8,
169 (primary.1 * 255.0) as u8,
170 (primary.2 * 255.0) as u8
171 ));
172 code.push_str(&format!(
173 " success: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
174 (success.0 * 255.0) as u8,
175 (success.1 * 255.0) as u8,
176 (success.2 * 255.0) as u8
177 ));
178 code.push_str(&format!(
179 " warning: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
180 (warning.0 * 255.0) as u8,
181 (warning.1 * 255.0) as u8,
182 (warning.2 * 255.0) as u8
183 ));
184 code.push_str(&format!(
185 " danger: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
186 (danger.0 * 255.0) as u8,
187 (danger.1 * 255.0) as u8,
188 (danger.2 * 255.0) as u8
189 ));
190 code.push_str(" }\n");
191 code.push_str(" )\n");
192 code.push_str("}\n\n");
193 }
194
195 let source_file = format!("{}/theme.dampen", module_name);
196 Ok(GeneratedCode::new(
197 code,
198 format!("{}_theme", module_name),
199 std::path::PathBuf::from(source_file),
200 ))
201}
202
203fn color_to_rgb8_tuple(color: Option<&Color>) -> (f32, f32, f32) {
205 match color {
206 Some(c) => (c.r, c.g, c.b),
207 None => (0.0, 0.0, 0.0),
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::ir::style::Color;
215 use crate::ir::theme::{SpacingScale, Theme, ThemePalette, Typography};
216
217 fn create_test_palette_with_hex(hex: &str) -> ThemePalette {
218 ThemePalette {
219 primary: Some(Color::from_hex(hex).unwrap()),
220 secondary: Some(Color::from_hex("#2ecc71").unwrap()),
221 success: Some(Color::from_hex("#27ae60").unwrap()),
222 warning: Some(Color::from_hex("#f39c12").unwrap()),
223 danger: Some(Color::from_hex("#e74c3c").unwrap()),
224 background: Some(Color::from_hex("#ecf0f1").unwrap()),
225 surface: Some(Color::from_hex("#ffffff").unwrap()),
226 text: Some(Color::from_hex("#2c3e50").unwrap()),
227 text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
228 }
229 }
230
231 fn create_test_theme(name: &str, primary_hex: &str) -> Theme {
232 Theme {
233 name: name.to_string(),
234 palette: create_test_palette_with_hex(primary_hex),
235 typography: Typography {
236 font_family: Some("sans-serif".to_string()),
237 font_size_base: Some(16.0),
238 font_size_small: Some(12.0),
239 font_size_large: Some(24.0),
240 font_weight: crate::ir::theme::FontWeight::Normal,
241 line_height: Some(1.5),
242 },
243 spacing: SpacingScale { unit: Some(8.0) },
244 base_styles: std::collections::HashMap::new(),
245 extends: None,
246 }
247 }
248
249 #[test]
250 fn test_generate_theme_code_basic() {
251 let doc = ThemeDocument {
252 themes: std::collections::HashMap::from([(
253 "light".to_string(),
254 create_test_theme("light", "#3498db"),
255 )]),
256 default_theme: Some("light".to_string()),
257 follow_system: false,
258 };
259
260 let result = generate_theme_code(&doc, "test");
261
262 assert!(result.is_ok());
263 let code = result.unwrap().code;
264
265 assert!(code.contains("pub fn test_theme()"));
266 assert!(code.contains("pub fn test_themes()"));
267 assert!(code.contains("pub fn test_default_theme()"));
268 assert!(code.contains("fn test_light()"));
269 assert!(code.contains("Theme::custom"));
270 assert!(code.contains("Color::from_rgb8"));
271 }
272
273 #[test]
274 fn test_generate_theme_code_multiple_themes() {
275 let doc = ThemeDocument {
276 themes: std::collections::HashMap::from([
277 ("light".to_string(), create_test_theme("light", "#3498db")),
278 ("dark".to_string(), create_test_theme("dark", "#5dade2")),
279 ]),
280 default_theme: Some("light".to_string()),
281 follow_system: true,
282 };
283
284 let result = generate_theme_code(&doc, "app");
285
286 assert!(result.is_ok());
287 let code = result.unwrap().code;
288
289 assert!(code.contains("fn app_light()"));
290 assert!(code.contains("fn app_dark()"));
291 assert!(code.contains("themes.insert(\"light\""));
292 assert!(code.contains("themes.insert(\"dark\""));
293 }
294
295 #[test]
296 fn test_generate_theme_code_empty_themes_error() {
297 let doc = ThemeDocument {
298 themes: std::collections::HashMap::new(),
299 default_theme: None,
300 follow_system: true,
301 };
302
303 let result = generate_theme_code(&doc, "app");
304
305 assert!(result.is_err());
306 let err = result.unwrap_err();
307 assert!(err.contains("THEME_001") || err.contains("no themes"));
308 }
309
310 #[test]
311 fn test_generate_theme_code_valid_rust_syntax() {
312 let doc = ThemeDocument {
313 themes: std::collections::HashMap::from([(
314 "test".to_string(),
315 create_test_theme("test", "#ff0000"),
316 )]),
317 default_theme: Some("test".to_string()),
318 follow_system: false,
319 };
320
321 let result = generate_theme_code(&doc, "test");
322
323 assert!(result.is_ok());
324 let code = result.unwrap().code;
325
326 let parsed = syn::parse_file(&code);
327 assert!(
328 parsed.is_ok(),
329 "Generated code should be valid Rust syntax: {:?}",
330 parsed.err()
331 );
332 }
333
334 #[test]
335 fn test_generate_theme_code_contains_color_values() {
336 let doc = ThemeDocument {
337 themes: std::collections::HashMap::from([(
338 "custom".to_string(),
339 create_test_theme("custom", "#AABBCC"),
340 )]),
341 default_theme: Some("custom".to_string()),
342 follow_system: false,
343 };
344
345 let result = generate_theme_code(&doc, "myapp");
346
347 assert!(result.is_ok());
348 let code = result.unwrap().code;
349
350 assert!(
351 code.contains("0xAA") || code.contains("0xBB") || code.contains("0xCC"),
352 "Generated code should contain the color values"
353 );
354 }
355}