dampen_core/codegen/
theme.rs

1//! Theme code generation for production builds
2//!
3//! This module generates static Rust code from theme definitions.
4//! The generated code allows themes to be compiled directly into the binary
5//! with zero runtime parsing overhead.
6
7use super::GeneratedCode;
8use crate::ir::style::{
9    Background, Border, BorderRadius, Color, Gradient, Shadow, StyleProperties,
10};
11use crate::ir::theme::{StyleClass, ThemeDocument, WidgetState};
12use proc_macro2::TokenStream;
13use quote::quote;
14use std::collections::HashMap;
15
16// ============================================================================
17// Helper Functions for Style Code Generation
18// ============================================================================
19
20/// Generate TokenStream for a Color IR type to iced::Color
21fn generate_color_expr(color: &Color) -> TokenStream {
22    let r = color.r;
23    let g = color.g;
24    let b = color.b;
25    let a = color.a;
26    quote! {
27        iced::Color::from_rgba(#r, #g, #b, #a)
28    }
29}
30
31/// Generate TokenStream for a Background IR type to iced::Background
32fn generate_background_expr(bg: &Background) -> TokenStream {
33    match bg {
34        Background::Color(color) => {
35            let color_expr = generate_color_expr(color);
36            quote! { iced::Background::Color(#color_expr) }
37        }
38        Background::Gradient(gradient) => generate_gradient_expr(gradient),
39        Background::Image { .. } => {
40            // Iced doesn't support image backgrounds directly
41            quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
42        }
43    }
44}
45
46/// Generate TokenStream for a Gradient IR type to iced::Gradient
47fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
48    match gradient {
49        Gradient::Linear { angle, stops } => {
50            let radians = angle * (std::f32::consts::PI / 180.0);
51
52            let stop_exprs: Vec<TokenStream> = stops
53                .iter()
54                .take(8)
55                .map(|stop| {
56                    let offset = stop.offset;
57                    let color_expr = generate_color_expr(&stop.color);
58                    quote! { .add_stop(#offset, #color_expr) }
59                })
60                .collect();
61
62            quote! {
63                iced::Background::Gradient(
64                    iced::Gradient::Linear(
65                        iced::gradient::Linear::new(iced::Radians(#radians))
66                            #(#stop_exprs)*
67                    )
68                )
69            }
70        }
71        Gradient::Radial { stops, .. } => {
72            // Iced 0.14 only supports linear gradients, convert to linear as fallback
73            let radians = 0.0f32;
74            let stop_exprs: Vec<TokenStream> = stops
75                .iter()
76                .take(8)
77                .map(|stop| {
78                    let offset = stop.offset;
79                    let color_expr = generate_color_expr(&stop.color);
80                    quote! { .add_stop(#offset, #color_expr) }
81                })
82                .collect();
83
84            quote! {
85                iced::Background::Gradient(
86                    iced::Gradient::Linear(
87                        iced::gradient::Linear::new(iced::Radians(#radians))
88                            #(#stop_exprs)*
89                    )
90                )
91            }
92        }
93    }
94}
95
96/// Generate TokenStream for a Border IR type to iced::Border
97fn generate_border_expr(border: &Border) -> TokenStream {
98    let width = border.width;
99    let color_expr = generate_color_expr(&border.color);
100    let radius_expr = generate_border_radius_expr(&border.radius);
101
102    quote! {
103        iced::Border {
104            width: #width,
105            color: #color_expr,
106            radius: #radius_expr,
107        }
108    }
109}
110
111/// Generate TokenStream for a BorderRadius IR type to iced::border::Radius
112fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
113    let tl = radius.top_left;
114    let tr = radius.top_right;
115    let br = radius.bottom_right;
116    let bl = radius.bottom_left;
117
118    quote! {
119        iced::border::Radius::from(#tl)
120            .top_right(#tr)
121            .bottom_right(#br)
122            .bottom_left(#bl)
123    }
124}
125
126/// Generate TokenStream for a Shadow IR type to iced::Shadow
127fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
128    let offset_x = shadow.offset_x;
129    let offset_y = shadow.offset_y;
130    let blur = shadow.blur_radius;
131    let color_expr = generate_color_expr(&shadow.color);
132
133    quote! {
134        iced::Shadow {
135            color: #color_expr,
136            offset: iced::Vector::new(#offset_x, #offset_y),
137            blur_radius: #blur,
138        }
139    }
140}
141
142/// Generate a button::Style struct from StyleProperties
143fn generate_button_style_struct(style: &StyleProperties) -> TokenStream {
144    let background_expr = if let Some(ref bg) = style.background {
145        let bg_expr = generate_background_expr(bg);
146        quote! { Some(#bg_expr) }
147    } else {
148        quote! { None }
149    };
150
151    let text_color_expr = if let Some(ref color) = style.color {
152        generate_color_expr(color)
153    } else {
154        quote! { iced::Color::BLACK }
155    };
156
157    let border_expr = if let Some(ref border) = style.border {
158        generate_border_expr(border)
159    } else {
160        quote! { iced::Border::default() }
161    };
162
163    let shadow_expr = if let Some(ref shadow) = style.shadow {
164        generate_shadow_expr(shadow)
165    } else {
166        quote! { iced::Shadow::default() }
167    };
168
169    quote! {
170        iced::widget::button::Style {
171            background: #background_expr,
172            text_color: #text_color_expr,
173            border: #border_expr,
174            shadow: #shadow_expr,
175            snap: false,
176        }
177    }
178}
179
180/// Generate a container::Style struct from StyleProperties
181fn generate_container_style_struct(style: &StyleProperties) -> TokenStream {
182    let background_expr = if let Some(ref bg) = style.background {
183        let bg_expr = generate_background_expr(bg);
184        quote! { Some(#bg_expr) }
185    } else {
186        quote! { None }
187    };
188
189    let text_color_expr = if let Some(ref color) = style.color {
190        let color_expr = generate_color_expr(color);
191        quote! { Some(#color_expr) }
192    } else {
193        quote! { None }
194    };
195
196    let border_expr = if let Some(ref border) = style.border {
197        generate_border_expr(border)
198    } else {
199        quote! { iced::Border::default() }
200    };
201
202    let shadow_expr = if let Some(ref shadow) = style.shadow {
203        generate_shadow_expr(shadow)
204    } else {
205        quote! { iced::Shadow::default() }
206    };
207
208    quote! {
209        iced::widget::container::Style {
210            background: #background_expr,
211            text_color: #text_color_expr,
212            border: #border_expr,
213            shadow: #shadow_expr,
214            snap: false,
215        }
216    }
217}
218
219/// Merge base style with state override
220fn merge_style_properties(
221    base: &StyleProperties,
222    override_props: &StyleProperties,
223) -> StyleProperties {
224    StyleProperties {
225        background: override_props
226            .background
227            .clone()
228            .or_else(|| base.background.clone()),
229        color: override_props.color.or(base.color),
230        border: override_props
231            .border
232            .clone()
233            .or_else(|| base.border.clone()),
234        shadow: override_props.shadow.or(base.shadow),
235        opacity: override_props.opacity.or(base.opacity),
236        transform: override_props
237            .transform
238            .clone()
239            .or_else(|| base.transform.clone()),
240    }
241}
242
243/// Determine which Iced widget type this style class targets
244fn infer_widget_type_from_class(style_class: &StyleClass) -> &'static str {
245    // For now, default to button if we have state variants (interactive)
246    // Otherwise default to container (static styling)
247    if !style_class.state_variants.is_empty() {
248        "button"
249    } else {
250        "container"
251    }
252}
253
254/// Generate a match block for state-aware styling
255fn generate_state_match_for_button(style_class: &StyleClass) -> TokenStream {
256    let mut match_arms = Vec::new();
257
258    // Base state (Active)
259    let base_style_expr = generate_button_style_struct(&style_class.style);
260    match_arms.push(quote! {
261        iced::widget::button::Status::Active => #base_style_expr
262    });
263
264    // State overrides
265    for (state, override_style) in &style_class.state_variants {
266        let merged_style = merge_style_properties(&style_class.style, override_style);
267        let style_expr = generate_button_style_struct(&merged_style);
268
269        let status_variant = match state {
270            WidgetState::Hover => quote! { iced::widget::button::Status::Hovered },
271            WidgetState::Active => quote! { iced::widget::button::Status::Pressed },
272            WidgetState::Disabled => quote! { iced::widget::button::Status::Disabled },
273            WidgetState::Focus => {
274                // Button doesn't have Focus in Iced, skip or map to Active
275                continue;
276            }
277        };
278
279        match_arms.push(quote! {
280            #status_variant => #style_expr
281        });
282    }
283
284    // Add wildcard arm to catch any other states (use base style)
285    let fallback_style = generate_button_style_struct(&style_class.style);
286    match_arms.push(quote! {
287        _ => #fallback_style
288    });
289
290    quote! {
291        match status {
292            #(#match_arms),*
293        }
294    }
295}
296
297/// Generate a style class function (String output for easier concatenation)
298fn generate_style_class_function(
299    class_name: &str,
300    style_class: &StyleClass,
301) -> Result<String, String> {
302    let fn_name = format!("style_{}", class_name.replace("-", "_").replace(":", "_"));
303    let widget_type = infer_widget_type_from_class(style_class);
304
305    let mut code = String::new();
306    code.push_str(&format!("/// Style function for class '{}'\n", class_name));
307
308    if widget_type == "button" && !style_class.state_variants.is_empty() {
309        // State-aware button style
310        code.push_str(&format!(
311            "pub fn {}(_theme: &iced::Theme, status: iced::widget::button::Status) -> iced::widget::button::Style {{\n",
312            fn_name
313        ));
314
315        let match_expr = generate_state_match_for_button(style_class);
316        let match_str = match_expr.to_string();
317        code.push_str("    ");
318        code.push_str(&match_str);
319        code.push('\n');
320    } else {
321        // Static container style
322        code.push_str(&format!(
323            "pub fn {}(_theme: &iced::Theme) -> iced::widget::container::Style {{\n",
324            fn_name
325        ));
326
327        let style_expr = generate_container_style_struct(&style_class.style);
328        let style_str = style_expr.to_string();
329        code.push_str("    ");
330        code.push_str(&style_str);
331        code.push('\n');
332    }
333
334    code.push_str("}\n\n");
335
336    Ok(code)
337}
338
339// ============================================================================
340// Main Theme Code Generation
341// ============================================================================
342
343/// Generate Rust code for a theme document
344///
345/// This function generates a Rust module containing functions to access
346/// themes at runtime without any parsing overhead. It also generates style
347/// class functions for widget styling.
348///
349/// # Arguments
350///
351/// * `document` - The parsed theme document
352/// * `style_classes` - Style class definitions from the Dampen document
353/// * `module_name` - Name for the generated module (e.g., "app" → app_theme module)
354///
355/// # Returns
356///
357/// Ok(GeneratedCode) with the generated Rust code, or an error if validation fails
358///
359/// # Example Output
360///
361/// ```rust,ignore
362/// // Generated theme code
363/// pub fn app_theme() -> iced::Theme {
364///     app_default_theme()
365/// }
366///
367/// pub fn app_themes() -> HashMap<&'static str, iced::Theme> {
368///     let mut themes = HashMap::new();
369///     themes.insert("light", app_theme_light());
370///     themes.insert("dark", app_theme_dark());
371///     themes
372/// }
373///
374/// fn app_theme_light() -> iced::Theme {
375///     iced::Theme::custom(
376///         "light".to_string(),
377///         iced::theme::Palette {
378///             background: iced::Color::from_rgb8(0xEC, 0xF0, 0xF1),
379///             text: iced::Color::from_rgb8(0x2C, 0x3E, 0x50),
380///             primary: iced::Color::from_rgb8(0x34, 0x98, 0xDB),
381///             success: iced::Color::from_rgb8(0x27, 0xAE, 0x60),
382///             warning: iced::Color::from_rgb8(0xF3, 0x9C, 0x12),
383///             danger: iced::Color::from_rgb8(0xE7, 0x4C, 0x3C),
384///         }
385///     )
386/// }
387///
388/// // Style class functions
389/// pub fn style_primary_button(_theme: &iced::Theme, status: iced::widget::button::Status) -> iced::widget::button::Style {
390///     match status {
391///         iced::widget::button::Status::Active => { /* ... */ }
392///         iced::widget::button::Status::Hovered => { /* ... */ }
393///         _ => iced::widget::button::Style::default()
394///     }
395/// }
396/// ```
397pub fn generate_theme_code(
398    document: &ThemeDocument,
399    style_classes: &HashMap<String, StyleClass>,
400    module_name: &str,
401) -> Result<GeneratedCode, String> {
402    if document.themes.is_empty() {
403        return Err("THEME_001: At least one theme must be defined".to_string());
404    }
405
406    let mut code = String::new();
407
408    code.push_str("// Generated theme code - DO NOT EDIT\n");
409    code.push_str("// This file is auto-generated by the dampen codegen.\n\n");
410
411    // Add thread-local storage for current theme name
412    code.push_str("use std::cell::RefCell;\n\n");
413    code.push_str("thread_local! {\n");
414    code.push_str(
415        "    static CURRENT_THEME: RefCell<Option<String>> = const { RefCell::new(None) };\n",
416    );
417    code.push_str("}\n\n");
418
419    code.push_str("/// Set the current theme by name\n");
420    code.push_str(&format!(
421        "pub fn {}_set_current_theme(name: &str) {{\n",
422        module_name
423    ));
424    code.push_str("    CURRENT_THEME.with(|t| {\n");
425    code.push_str("        *t.borrow_mut() = Some(name.to_string());\n");
426    code.push_str("    });\n");
427    code.push_str("}\n\n");
428
429    code.push_str("/// Get the current theme name\n");
430    code.push_str(&format!(
431        "pub fn {}_current_theme_name() -> String {{\n",
432        module_name
433    ));
434    code.push_str("    CURRENT_THEME.with(|t| {\n");
435    code.push_str("        t.borrow().clone().unwrap_or_else(|| {\n");
436    let effective_default = document.effective_default(None);
437    code.push_str(&format!(
438        "            \"{}\".to_string()\n",
439        effective_default
440    ));
441    code.push_str("        })\n");
442    code.push_str("    })\n");
443    code.push_str("}\n\n");
444
445    code.push_str(
446        "/// Get the current theme (respects system preference when follow_system is enabled)\n",
447    );
448    code.push_str(&format!("pub fn {}_theme() -> Theme {{\n", module_name));
449    code.push_str(&format!(
450        "    let name = {}_current_theme_name();\n",
451        module_name
452    ));
453    code.push_str(&format!(
454        "    {}_theme_named(&name).unwrap_or_else(|| {}_default_theme())\n",
455        module_name, module_name
456    ));
457    code.push_str("}\n\n");
458
459    code.push_str("/// Get a specific theme by name\n");
460    code.push_str(&format!(
461        "pub fn {}_theme_named(name: &str) -> Option<Theme> {{\n",
462        module_name
463    ));
464    code.push_str(&format!("    let themes = {}_themes();\n", module_name));
465    code.push_str("    themes.get(name).cloned()\n");
466    code.push_str("}\n\n");
467
468    code.push_str("/// Get all available themes\n");
469    code.push_str(&format!(
470        "pub fn {}_themes() -> HashMap<&'static str, Theme> {{\n",
471        module_name
472    ));
473    code.push_str("    let mut themes = HashMap::new();\n");
474
475    let mut theme_names: Vec<&str> = document.themes.keys().map(|s| s.as_str()).collect();
476    theme_names.sort();
477
478    for theme_name in &theme_names {
479        code.push_str(&format!(
480            "    themes.insert(\"{}\", {}_{}());\n",
481            theme_name, module_name, theme_name
482        ));
483    }
484
485    code.push_str("    themes\n");
486    code.push_str("}\n\n");
487
488    code.push_str("/// Get the default theme\n");
489    code.push_str(&format!(
490        "pub fn {}_default_theme() -> Theme {{\n",
491        module_name
492    ));
493    code.push_str(&format!("    {}_{}()\n", module_name, effective_default));
494    code.push_str("}\n\n");
495
496    code.push_str("/// Get the default theme name as a string\n");
497    code.push_str(&format!(
498        "pub fn {}_default_theme_name() -> &'static str {{\n",
499        module_name
500    ));
501    code.push_str(&format!("    \"{}\"\n", effective_default));
502    code.push_str("}\n\n");
503
504    code.push_str("/// Get whether the theme follows system preference\n");
505    code.push_str(&format!(
506        "pub fn {}_follows_system() -> bool {{\n",
507        module_name
508    ));
509    code.push_str(&format!("    {}\n", document.follow_system));
510    code.push_str("}\n\n");
511
512    for theme_name in &theme_names {
513        let theme = match document.themes.get(*theme_name) {
514            Some(t) => t,
515            None => continue,
516        };
517
518        let theme_fn_name = format!("{}_{}", module_name, theme_name);
519        code.push_str(&format!("/// Theme: {}\n", theme_name));
520        code.push_str("fn ");
521        code.push_str(&theme_fn_name);
522        code.push_str("() -> Theme {\n");
523
524        let palette = &theme.palette;
525        let primary = color_to_rgb8_tuple(palette.primary.as_ref());
526        let background = color_to_rgb8_tuple(palette.background.as_ref());
527        let text = color_to_rgb8_tuple(palette.text.as_ref());
528        let success = color_to_rgb8_tuple(palette.success.as_ref());
529        let warning = color_to_rgb8_tuple(palette.warning.as_ref());
530        let danger = color_to_rgb8_tuple(palette.danger.as_ref());
531
532        code.push_str("    Theme::custom(\n");
533        code.push_str(&format!("        \"{}\".to_string(),\n", theme_name));
534        code.push_str("        iced::theme::Palette {\n");
535        code.push_str(&format!(
536            "            background: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
537            (background.0 * 255.0) as u8,
538            (background.1 * 255.0) as u8,
539            (background.2 * 255.0) as u8
540        ));
541        code.push_str(&format!(
542            "            text: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
543            (text.0 * 255.0) as u8,
544            (text.1 * 255.0) as u8,
545            (text.2 * 255.0) as u8
546        ));
547        code.push_str(&format!(
548            "            primary: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
549            (primary.0 * 255.0) as u8,
550            (primary.1 * 255.0) as u8,
551            (primary.2 * 255.0) as u8
552        ));
553        code.push_str(&format!(
554            "            success: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
555            (success.0 * 255.0) as u8,
556            (success.1 * 255.0) as u8,
557            (success.2 * 255.0) as u8
558        ));
559        code.push_str(&format!(
560            "            warning: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
561            (warning.0 * 255.0) as u8,
562            (warning.1 * 255.0) as u8,
563            (warning.2 * 255.0) as u8
564        ));
565        code.push_str(&format!(
566            "            danger: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
567            (danger.0 * 255.0) as u8,
568            (danger.1 * 255.0) as u8,
569            (danger.2 * 255.0) as u8
570        ));
571        code.push_str("        }\n");
572        code.push_str("    )\n");
573        code.push_str("}\n\n");
574    }
575
576    // ========================================
577    // Style Class Functions
578    // ========================================
579    if !style_classes.is_empty() {
580        code.push_str("// ========================================\n");
581        code.push_str("// Style Class Functions\n");
582        code.push_str("// ========================================\n\n");
583
584        let mut class_names: Vec<&str> = style_classes.keys().map(|s| s.as_str()).collect();
585        class_names.sort();
586
587        for class_name in class_names {
588            if let Some(style_class) = style_classes.get(class_name) {
589                let class_fn_code = generate_style_class_function(class_name, style_class)?;
590                code.push_str(&class_fn_code);
591            }
592        }
593    }
594
595    let source_file = format!("{}/theme.dampen", module_name);
596    Ok(GeneratedCode::new(
597        code,
598        format!("{}_theme", module_name),
599        std::path::PathBuf::from(source_file),
600    ))
601}
602
603/// Convert a color to RGB tuple (0.0-1.0 range)
604fn color_to_rgb8_tuple(color: Option<&Color>) -> (f32, f32, f32) {
605    match color {
606        Some(c) => (c.r, c.g, c.b),
607        None => (0.0, 0.0, 0.0),
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614    use crate::ir::style::Color;
615    use crate::ir::theme::{SpacingScale, Theme, ThemePalette, Typography};
616
617    fn create_test_palette_with_hex(hex: &str) -> ThemePalette {
618        ThemePalette {
619            primary: Some(Color::from_hex(hex).unwrap()),
620            secondary: Some(Color::from_hex("#2ecc71").unwrap()),
621            success: Some(Color::from_hex("#27ae60").unwrap()),
622            warning: Some(Color::from_hex("#f39c12").unwrap()),
623            danger: Some(Color::from_hex("#e74c3c").unwrap()),
624            background: Some(Color::from_hex("#ecf0f1").unwrap()),
625            surface: Some(Color::from_hex("#ffffff").unwrap()),
626            text: Some(Color::from_hex("#2c3e50").unwrap()),
627            text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
628        }
629    }
630
631    fn create_test_theme(name: &str, primary_hex: &str) -> Theme {
632        Theme {
633            name: name.to_string(),
634            palette: create_test_palette_with_hex(primary_hex),
635            typography: Typography {
636                font_family: Some("sans-serif".to_string()),
637                font_size_base: Some(16.0),
638                font_size_small: Some(12.0),
639                font_size_large: Some(24.0),
640                font_weight: crate::ir::theme::FontWeight::Normal,
641                line_height: Some(1.5),
642            },
643            spacing: SpacingScale { unit: Some(8.0) },
644            base_styles: std::collections::HashMap::new(),
645            extends: None,
646        }
647    }
648
649    #[test]
650    fn test_generate_theme_code_basic() {
651        let doc = ThemeDocument {
652            themes: std::collections::HashMap::from([(
653                "light".to_string(),
654                create_test_theme("light", "#3498db"),
655            )]),
656            default_theme: Some("light".to_string()),
657            follow_system: false,
658        };
659
660        let style_classes = HashMap::new();
661        let result = generate_theme_code(&doc, &style_classes, "test");
662
663        assert!(result.is_ok());
664        let code = result.unwrap().code;
665
666        assert!(code.contains("pub fn test_theme()"));
667        assert!(code.contains("pub fn test_themes()"));
668        assert!(code.contains("pub fn test_default_theme()"));
669        assert!(code.contains("fn test_light()"));
670        assert!(code.contains("Theme::custom"));
671        assert!(code.contains("Color::from_rgb8"));
672    }
673
674    #[test]
675    fn test_generate_theme_code_multiple_themes() {
676        let doc = ThemeDocument {
677            themes: std::collections::HashMap::from([
678                ("light".to_string(), create_test_theme("light", "#3498db")),
679                ("dark".to_string(), create_test_theme("dark", "#5dade2")),
680            ]),
681            default_theme: Some("light".to_string()),
682            follow_system: true,
683        };
684
685        let style_classes = HashMap::new();
686        let result = generate_theme_code(&doc, &style_classes, "app");
687
688        assert!(result.is_ok());
689        let code = result.unwrap().code;
690
691        assert!(code.contains("fn app_light()"));
692        assert!(code.contains("fn app_dark()"));
693        assert!(code.contains("themes.insert(\"light\""));
694        assert!(code.contains("themes.insert(\"dark\""));
695    }
696
697    #[test]
698    fn test_generate_theme_code_empty_themes_error() {
699        let doc = ThemeDocument {
700            themes: std::collections::HashMap::new(),
701            default_theme: None,
702            follow_system: true,
703        };
704
705        let style_classes = HashMap::new();
706        let result = generate_theme_code(&doc, &style_classes, "app");
707
708        assert!(result.is_err());
709        let err = result.unwrap_err();
710        assert!(err.contains("THEME_001") || err.contains("no themes"));
711    }
712
713    #[test]
714    fn test_generate_theme_code_valid_rust_syntax() {
715        let doc = ThemeDocument {
716            themes: std::collections::HashMap::from([(
717                "test".to_string(),
718                create_test_theme("test", "#ff0000"),
719            )]),
720            default_theme: Some("test".to_string()),
721            follow_system: false,
722        };
723
724        let style_classes = HashMap::new();
725        let result = generate_theme_code(&doc, &style_classes, "test");
726
727        assert!(result.is_ok());
728        let code = result.unwrap().code;
729
730        let parsed = syn::parse_file(&code);
731        assert!(
732            parsed.is_ok(),
733            "Generated code should be valid Rust syntax: {:?}",
734            parsed.err()
735        );
736    }
737
738    #[test]
739    fn test_generate_theme_code_contains_color_values() {
740        let doc = ThemeDocument {
741            themes: std::collections::HashMap::from([(
742                "custom".to_string(),
743                create_test_theme("custom", "#AABBCC"),
744            )]),
745            default_theme: Some("custom".to_string()),
746            follow_system: false,
747        };
748
749        let style_classes = HashMap::new();
750        let result = generate_theme_code(&doc, &style_classes, "myapp");
751
752        assert!(result.is_ok());
753        let code = result.unwrap().code;
754
755        assert!(
756            code.contains("0xAA") || code.contains("0xBB") || code.contains("0xCC"),
757            "Generated code should contain the color values"
758        );
759    }
760
761    #[test]
762    fn test_generate_style_class_simple() {
763        let style_class = StyleClass {
764            name: "primary-button".to_string(),
765            style: StyleProperties {
766                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
767                color: Some(Color::from_rgb8(255, 255, 255)),
768                border: None,
769                shadow: None,
770                opacity: None,
771                transform: None,
772            },
773            layout: None,
774            extends: vec![],
775            state_variants: HashMap::new(),
776            combined_state_variants: HashMap::new(),
777        };
778
779        let mut style_classes = HashMap::new();
780        style_classes.insert("primary-button".to_string(), style_class);
781
782        let theme_doc = ThemeDocument {
783            themes: HashMap::from([("light".to_string(), create_test_theme("light", "#3498db"))]),
784            default_theme: Some("light".to_string()),
785            follow_system: false,
786        };
787
788        let result = generate_theme_code(&theme_doc, &style_classes, "test");
789        assert!(result.is_ok());
790
791        let code = result.unwrap().code;
792        assert!(code.contains("pub fn style_primary_button"));
793        assert!(code.contains("Style Class Functions"));
794    }
795
796    #[test]
797    fn test_generate_style_with_hover() {
798        let mut state_variants = HashMap::new();
799        state_variants.insert(
800            WidgetState::Hover,
801            StyleProperties {
802                background: Some(Background::Color(Color::from_rgb8(74, 172, 239))),
803                color: None,
804                border: None,
805                shadow: None,
806                opacity: None,
807                transform: None,
808            },
809        );
810
811        let style_class = StyleClass {
812            name: "hover-button".to_string(),
813            style: StyleProperties {
814                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
815                color: Some(Color::from_rgb8(255, 255, 255)),
816                border: None,
817                shadow: None,
818                opacity: None,
819                transform: None,
820            },
821            layout: None,
822            extends: vec![],
823            state_variants,
824            combined_state_variants: HashMap::new(),
825        };
826
827        let mut style_classes = HashMap::new();
828        style_classes.insert("hover-button".to_string(), style_class);
829
830        let theme_doc = ThemeDocument {
831            themes: HashMap::from([("light".to_string(), create_test_theme("light", "#3498db"))]),
832            default_theme: Some("light".to_string()),
833            follow_system: false,
834        };
835
836        let result = generate_theme_code(&theme_doc, &style_classes, "test");
837        assert!(result.is_ok());
838
839        let code = result.unwrap().code;
840        assert!(code.contains("style_hover_button"));
841        assert!(code.contains("Status :: Active"));
842        assert!(code.contains("Status :: Hovered"));
843    }
844}