dampen_core/codegen/
view.rs

1//! View function generation
2//!
3//! This module generates static Rust code for widget trees with inlined bindings.
4
5#![allow(dead_code)]
6
7use crate::DampenDocument;
8use crate::codegen::bindings::generate_expr;
9use crate::ir::layout::{LayoutConstraints, Length as LayoutLength};
10use crate::ir::node::{AttributeValue, InterpolatedPart, WidgetKind};
11use crate::ir::style::{
12    Background, Border, BorderRadius, Color, Gradient, Shadow, StyleProperties,
13};
14use crate::ir::theme::StyleClass;
15use proc_macro2::TokenStream;
16use quote::{format_ident, quote};
17use std::collections::HashMap;
18
19/// Generate the view function body from a Dampen document
20pub fn generate_view(
21    document: &DampenDocument,
22    _model_name: &str,
23    message_name: &str,
24) -> Result<TokenStream, super::CodegenError> {
25    let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
26    let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
27
28    let root_widget = generate_widget(
29        &document.root,
30        &model_ident,
31        &message_ident,
32        &document.style_classes,
33    )?;
34
35    Ok(quote! {
36        #root_widget
37    })
38}
39
40/// Get merged layout constraints from node.layout and style classes
41fn get_merged_layout<'a>(
42    node: &'a crate::WidgetNode,
43    style_classes: &'a HashMap<String, StyleClass>,
44) -> Option<MergedLayout<'a>> {
45    // Priority: node.layout > style_class.layout
46    let node_layout = node.layout.as_ref();
47    let class_layout = node
48        .classes
49        .first()
50        .and_then(|class_name| style_classes.get(class_name))
51        .and_then(|class| class.layout.as_ref());
52
53    if node_layout.is_some() || class_layout.is_some() {
54        Some(MergedLayout {
55            node_layout,
56            class_layout,
57        })
58    } else {
59        None
60    }
61}
62
63/// Helper struct to hold merged layout info from node and style class
64struct MergedLayout<'a> {
65    node_layout: Option<&'a LayoutConstraints>,
66    class_layout: Option<&'a LayoutConstraints>,
67}
68
69impl<'a> MergedLayout<'a> {
70    fn padding(&self) -> Option<f32> {
71        self.node_layout
72            .and_then(|l| l.padding.as_ref())
73            .map(|p| p.top)
74            .or_else(|| {
75                self.class_layout
76                    .and_then(|l| l.padding.as_ref())
77                    .map(|p| p.top)
78            })
79    }
80
81    fn spacing(&self) -> Option<f32> {
82        self.node_layout
83            .and_then(|l| l.spacing)
84            .or_else(|| self.class_layout.and_then(|l| l.spacing))
85    }
86
87    fn width(&self) -> Option<&'a LayoutLength> {
88        self.node_layout
89            .and_then(|l| l.width.as_ref())
90            .or_else(|| self.class_layout.and_then(|l| l.width.as_ref()))
91    }
92
93    fn height(&self) -> Option<&'a LayoutLength> {
94        self.node_layout
95            .and_then(|l| l.height.as_ref())
96            .or_else(|| self.class_layout.and_then(|l| l.height.as_ref()))
97    }
98}
99
100/// Generate code for a widget node
101fn generate_widget(
102    node: &crate::WidgetNode,
103    model_ident: &syn::Ident,
104    message_ident: &syn::Ident,
105    style_classes: &HashMap<String, StyleClass>,
106) -> Result<TokenStream, super::CodegenError> {
107    generate_widget_with_locals(
108        node,
109        model_ident,
110        message_ident,
111        style_classes,
112        &std::collections::HashSet::new(),
113    )
114}
115
116/// Generate code for a widget node with local variable context
117fn generate_widget_with_locals(
118    node: &crate::WidgetNode,
119    model_ident: &syn::Ident,
120    message_ident: &syn::Ident,
121    style_classes: &HashMap<String, StyleClass>,
122    local_vars: &std::collections::HashSet<String>,
123) -> Result<TokenStream, super::CodegenError> {
124    match node.kind {
125        WidgetKind::Text => generate_text_with_locals(node, model_ident, style_classes, local_vars),
126        WidgetKind::Button => {
127            generate_button_with_locals(node, model_ident, message_ident, style_classes, local_vars)
128        }
129        WidgetKind::Column => generate_container_with_locals(
130            node,
131            "column",
132            model_ident,
133            message_ident,
134            style_classes,
135            local_vars,
136        ),
137        WidgetKind::Row => generate_container_with_locals(
138            node,
139            "row",
140            model_ident,
141            message_ident,
142            style_classes,
143            local_vars,
144        ),
145        WidgetKind::Container => generate_container_with_locals(
146            node,
147            "container",
148            model_ident,
149            message_ident,
150            style_classes,
151            local_vars,
152        ),
153        WidgetKind::Scrollable => generate_container_with_locals(
154            node,
155            "scrollable",
156            model_ident,
157            message_ident,
158            style_classes,
159            local_vars,
160        ),
161        WidgetKind::Stack => generate_stack(node, model_ident, message_ident, style_classes),
162        WidgetKind::Space => generate_space(node),
163        WidgetKind::Rule => generate_rule(node),
164        WidgetKind::Checkbox => generate_checkbox_with_locals(
165            node,
166            model_ident,
167            message_ident,
168            style_classes,
169            local_vars,
170        ),
171        WidgetKind::Toggler => generate_toggler(node, model_ident, message_ident, style_classes),
172        WidgetKind::Slider => generate_slider(node, model_ident, message_ident, style_classes),
173        WidgetKind::Radio => generate_radio(node, model_ident, message_ident, style_classes),
174        WidgetKind::ProgressBar => generate_progress_bar(node, model_ident, style_classes),
175        WidgetKind::TextInput => generate_text_input_with_locals(
176            node,
177            model_ident,
178            message_ident,
179            style_classes,
180            local_vars,
181        ),
182        WidgetKind::Image => generate_image(node),
183        WidgetKind::Svg => generate_svg(node),
184        WidgetKind::PickList => generate_pick_list(node, model_ident, message_ident, style_classes),
185        WidgetKind::ComboBox => generate_combo_box(node, model_ident, message_ident, style_classes),
186        WidgetKind::Tooltip => generate_tooltip(node, model_ident, message_ident, style_classes),
187        WidgetKind::Grid => generate_grid(node, model_ident, message_ident, style_classes),
188        WidgetKind::Canvas => generate_canvas(node, model_ident, message_ident, style_classes),
189        WidgetKind::Float => generate_float(node, model_ident, message_ident, style_classes),
190        WidgetKind::For => {
191            generate_for_with_locals(node, model_ident, message_ident, style_classes, local_vars)
192        }
193        WidgetKind::If => {
194            generate_if_with_locals(node, model_ident, message_ident, style_classes, local_vars)
195        }
196        WidgetKind::Custom(ref name) => {
197            generate_custom_widget(node, name, model_ident, message_ident, style_classes)
198        }
199    }
200}
201
202// ============================================================================
203// Style Application Functions
204// ============================================================================
205
206/// Apply inline styles or CSS classes to a widget
207///
208/// Priority order:
209/// 1. Inline styles (node.style) - highest priority
210/// 2. CSS classes (node.classes) - medium priority
211/// 3. Default Iced styles - lowest priority (fallback)
212fn apply_widget_style(
213    widget: TokenStream,
214    node: &crate::WidgetNode,
215    widget_type: &str,
216    style_classes: &HashMap<String, StyleClass>,
217) -> Result<TokenStream, super::CodegenError> {
218    // Check if widget has any styling
219    let has_inline_style = node.style.is_some();
220    let has_classes = !node.classes.is_empty();
221
222    // Check for dynamic class binding (e.g., class="{if filter == 'All' then 'btn_primary' else 'btn_filter'}")
223    let class_binding = node.attributes.get("class").and_then(|attr| match attr {
224        AttributeValue::Binding(expr) => Some(expr),
225        _ => None,
226    });
227    let has_class_binding = class_binding.is_some();
228
229    if !has_inline_style && !has_classes && !has_class_binding {
230        // No styling needed, return widget as-is
231        return Ok(widget);
232    }
233
234    // Get style class if widget has classes
235    let style_class = if let Some(class_name) = node.classes.first() {
236        style_classes.get(class_name)
237    } else {
238        None
239    };
240
241    // Generate style closure based on priority
242    if let Some(ref style_props) = node.style {
243        // Priority 1: Inline styles
244        let style_closure =
245            generate_inline_style_closure(style_props, widget_type, &node.kind, style_class)?;
246        Ok(quote! {
247            #widget.style(#style_closure)
248        })
249    } else if let Some(class_name) = node.classes.first() {
250        // Priority 2: CSS class (use first class for now)
251        // Generate a wrapper closure that matches the widget's expected signature
252        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
253
254        match widget_type {
255            "text_input" => {
256                // text_input.style() expects fn(theme, status) -> text_input::Style
257                // Style class functions return container::Style, so we need to convert
258                Ok(quote! {
259                    #widget.style(|theme: &iced::Theme, _status: iced::widget::text_input::Status| {
260                        let container_style = #style_fn_ident(theme);
261                        iced::widget::text_input::Style {
262                            background: container_style.background.unwrap_or(iced::Background::Color(theme.extended_palette().background.base.color)),
263                            border: container_style.border,
264                            icon: theme.extended_palette().background.base.text,
265                            placeholder: theme.extended_palette().background.weak.text,
266                            value: container_style.text_color.unwrap_or(theme.extended_palette().background.base.text),
267                            selection: theme.extended_palette().primary.weak.color,
268                        }
269                    })
270                })
271            }
272            "checkbox" => {
273                // checkbox.style() expects fn(theme, status) -> checkbox::Style
274                // Check if the style class has state variants (needs 2-arg call with button::Status)
275                let has_state_variants = style_class
276                    .map(|sc| !sc.state_variants.is_empty())
277                    .unwrap_or(false);
278
279                if has_state_variants {
280                    // Style function expects (theme, button::Status), map checkbox status to button status
281                    Ok(quote! {
282                        #widget.style(|theme: &iced::Theme, status: iced::widget::checkbox::Status| {
283                            // Map checkbox status to button status for the style function
284                            let button_status = match status {
285                                iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
286                                iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
287                                iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
288                            };
289                            let button_style = #style_fn_ident(theme, button_status);
290                            iced::widget::checkbox::Style {
291                                background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
292                                icon_color: button_style.text_color,
293                                border: button_style.border,
294                                text_color: None,
295                            }
296                        })
297                    })
298                } else {
299                    // Style function expects only theme (container style)
300                    Ok(quote! {
301                        #widget.style(|theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
302                            let container_style = #style_fn_ident(theme);
303                            iced::widget::checkbox::Style {
304                                background: container_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
305                                icon_color: container_style.text_color,
306                                border: container_style.border,
307                                text_color: None,
308                            }
309                        })
310                    })
311                }
312            }
313            "button" => {
314                // button.style() expects fn(theme, status) -> button::Style
315                // Style class functions for buttons already have the correct signature
316                Ok(quote! {
317                    #widget.style(#style_fn_ident)
318                })
319            }
320            _ => {
321                // Default: container-style widgets (container, row, column, etc.)
322                Ok(quote! {
323                    #widget.style(#style_fn_ident)
324                })
325            }
326        }
327    } else if let Some(binding_expr) = class_binding {
328        // Priority 3: Dynamic class binding
329        generate_dynamic_class_style(widget, binding_expr, widget_type, style_classes)
330    } else {
331        Ok(widget)
332    }
333}
334
335/// Generate style application for dynamic class bindings
336///
337/// Generates code that evaluates the binding at runtime and dispatches
338/// to the appropriate style function based on the class name.
339fn generate_dynamic_class_style(
340    widget: TokenStream,
341    binding_expr: &crate::expr::BindingExpr,
342    widget_type: &str,
343    style_classes: &HashMap<String, StyleClass>,
344) -> Result<TokenStream, super::CodegenError> {
345    // Generate code to evaluate the binding
346    let class_expr = super::bindings::generate_expr(&binding_expr.expr);
347
348    match widget_type {
349        "button" => {
350            // Generate match arms only for button-compatible style classes
351            // (those with state variants, which generate fn(theme, status) -> button::Style)
352            let mut match_arms = Vec::new();
353            for (class_name, style_class) in style_classes.iter() {
354                // Only include classes that have state variants (button styles)
355                if !style_class.state_variants.is_empty() {
356                    let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
357                    let class_lit = proc_macro2::Literal::string(class_name);
358                    match_arms.push(quote! {
359                        #class_lit => #style_fn(_theme, status),
360                    });
361                }
362            }
363
364            Ok(quote! {
365                #widget.style({
366                    let __class_name = #class_expr;
367                    move |_theme: &iced::Theme, status: iced::widget::button::Status| {
368                        match __class_name.as_str() {
369                            #(#match_arms)*
370                            _ => iced::widget::button::Style::default(),
371                        }
372                    }
373                })
374            })
375        }
376        "checkbox" => {
377            // For checkboxes, map checkbox status to button status
378            // Only use style classes with state variants
379            let mut checkbox_match_arms = Vec::new();
380            for (class_name, style_class) in style_classes.iter() {
381                if !style_class.state_variants.is_empty() {
382                    let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
383                    let class_lit = proc_macro2::Literal::string(class_name);
384                    checkbox_match_arms.push(quote! {
385                        #class_lit => {
386                            let button_style = #style_fn(_theme, button_status);
387                            iced::widget::checkbox::Style {
388                                background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
389                                icon_color: button_style.text_color,
390                                border: button_style.border,
391                                text_color: None,
392                            }
393                        }
394                    });
395                }
396            }
397            Ok(quote! {
398                #widget.style({
399                    let __class_name = #class_expr;
400                    move |_theme: &iced::Theme, status: iced::widget::checkbox::Status| {
401                        let button_status = match status {
402                            iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
403                            iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
404                            iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
405                        };
406                        match __class_name.as_str() {
407                            #(#checkbox_match_arms)*
408                            _ => iced::widget::checkbox::Style::default(),
409                        }
410                    }
411                })
412            })
413        }
414        _ => {
415            // For other widgets (container, etc.), use container style functions
416            // Only include classes without state variants (container styles)
417            let mut container_match_arms = Vec::new();
418            for (class_name, style_class) in style_classes.iter() {
419                if style_class.state_variants.is_empty() {
420                    let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
421                    let class_lit = proc_macro2::Literal::string(class_name);
422                    container_match_arms.push(quote! {
423                        #class_lit => #style_fn(_theme),
424                    });
425                }
426            }
427            Ok(quote! {
428                #widget.style({
429                    let __class_name = #class_expr;
430                    move |_theme: &iced::Theme| {
431                        match __class_name.as_str() {
432                            #(#container_match_arms)*
433                            _ => iced::widget::container::Style::default(),
434                        }
435                    }
436                })
437            })
438        }
439    }
440}
441
442/// Generate state-specific style application code
443///
444/// Creates a match expression that applies different styles based on widget state.
445/// Merges base style with state-specific overrides.
446///
447/// # Arguments
448/// * `base_style` - The base style struct (used when state is None)
449/// * `style_class` - The style class containing state variants
450/// * `widget_state_ident` - Identifier for the widget_state variable
451/// * `style_struct_fn` - Function to generate style struct from StyleProperties
452fn generate_state_style_match(
453    base_style: TokenStream,
454    style_class: &StyleClass,
455    widget_state_ident: &syn::Ident,
456    style_struct_fn: fn(&StyleProperties) -> Result<TokenStream, super::CodegenError>,
457) -> Result<TokenStream, super::CodegenError> {
458    use crate::ir::theme::WidgetState;
459
460    // Collect all state variants
461    let mut state_arms = Vec::new();
462
463    for (state, state_props) in &style_class.state_variants {
464        let state_variant = match state {
465            WidgetState::Hover => quote! { dampen_core::ir::WidgetState::Hover },
466            WidgetState::Focus => quote! { dampen_core::ir::WidgetState::Focus },
467            WidgetState::Active => quote! { dampen_core::ir::WidgetState::Active },
468            WidgetState::Disabled => quote! { dampen_core::ir::WidgetState::Disabled },
469        };
470
471        // Generate style struct for this state
472        let state_style = style_struct_fn(state_props)?;
473
474        state_arms.push(quote! {
475            Some(#state_variant) => #state_style
476        });
477    }
478
479    // Generate match expression
480    Ok(quote! {
481        match #widget_state_ident {
482            #(#state_arms,)*
483            None => #base_style
484        }
485    })
486}
487
488/// Generate inline style closure for a widget
489///
490/// Creates a closure like: |_theme: &iced::Theme, status| { ... }
491///
492/// # State-Aware Styling
493///
494/// When a widget has state variants (hover, focus, etc.), this generates
495/// code that maps the status parameter to WidgetState and applies the
496/// appropriate style.
497///
498/// # Arguments
499/// * `style_props` - Base style properties for the widget
500/// * `widget_type` - Type of widget ("button", "text_input", etc.)
501/// * `widget_kind` - The WidgetKind enum (needed for status mapping)
502/// * `style_class` - Optional style class with state variants
503fn generate_inline_style_closure(
504    style_props: &StyleProperties,
505    widget_type: &str,
506    widget_kind: &WidgetKind,
507    style_class: Option<&StyleClass>,
508) -> Result<TokenStream, super::CodegenError> {
509    // Check if we have state-specific styling
510    let has_state_variants = style_class
511        .map(|sc| !sc.state_variants.is_empty())
512        .unwrap_or(false);
513
514    match widget_type {
515        "button" => {
516            let base_style = generate_button_style_struct(style_props)?;
517
518            if has_state_variants {
519                // Generate state-aware closure
520                let status_ident = format_ident!("status");
521                if let Some(status_mapping) =
522                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
523                {
524                    let widget_state_ident = format_ident!("widget_state");
525                    // Safe: has_state_variants guarantees style_class.is_some()
526                    let class = style_class.ok_or_else(|| {
527                        super::CodegenError::InvalidWidget(
528                            "Expected style class with state variants".to_string(),
529                        )
530                    })?;
531                    let style_match = generate_state_style_match(
532                        base_style,
533                        class,
534                        &widget_state_ident,
535                        generate_button_style_struct,
536                    )?;
537
538                    Ok(quote! {
539                        |_theme: &iced::Theme, #status_ident: iced::widget::button::Status| {
540                            // Map Iced status to WidgetState
541                            let #widget_state_ident = #status_mapping;
542
543                            // Apply state-specific styling
544                            #style_match
545                        }
546                    })
547                } else {
548                    // Widget kind doesn't support status mapping, fall back to simple closure
549                    Ok(quote! {
550                        |_theme: &iced::Theme, _status: iced::widget::button::Status| {
551                            #base_style
552                        }
553                    })
554                }
555            } else {
556                // No state variants, use simple closure
557                Ok(quote! {
558                    |_theme: &iced::Theme, _status: iced::widget::button::Status| {
559                        #base_style
560                    }
561                })
562            }
563        }
564        "container" => {
565            let style_struct = generate_container_style_struct(style_props)?;
566            Ok(quote! {
567                |_theme: &iced::Theme| {
568                    #style_struct
569                }
570            })
571        }
572        "text_input" => {
573            let base_style = generate_text_input_style_struct(style_props)?;
574
575            if has_state_variants {
576                // Generate state-aware closure
577                let status_ident = format_ident!("status");
578                if let Some(status_mapping) =
579                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
580                {
581                    let widget_state_ident = format_ident!("widget_state");
582                    let class = style_class.ok_or_else(|| {
583                        super::CodegenError::InvalidWidget(
584                            "Expected style class with state variants".to_string(),
585                        )
586                    })?;
587                    let style_match = generate_state_style_match(
588                        base_style,
589                        class,
590                        &widget_state_ident,
591                        generate_text_input_style_struct,
592                    )?;
593
594                    Ok(quote! {
595                        |_theme: &iced::Theme, #status_ident: iced::widget::text_input::Status| {
596                            // Map Iced status to WidgetState
597                            let #widget_state_ident = #status_mapping;
598
599                            // Apply state-specific styling
600                            #style_match
601                        }
602                    })
603                } else {
604                    // Widget kind doesn't support status mapping, fall back to simple closure
605                    Ok(quote! {
606                        |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
607                            #base_style
608                        }
609                    })
610                }
611            } else {
612                // No state variants, use simple closure
613                Ok(quote! {
614                    |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
615                        #base_style
616                    }
617                })
618            }
619        }
620        "checkbox" => {
621            let base_style = generate_checkbox_style_struct(style_props)?;
622
623            if has_state_variants {
624                let status_ident = format_ident!("status");
625                if let Some(status_mapping) =
626                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
627                {
628                    let widget_state_ident = format_ident!("widget_state");
629                    let class = style_class.ok_or_else(|| {
630                        super::CodegenError::InvalidWidget(
631                            "Expected style class with state variants".to_string(),
632                        )
633                    })?;
634                    let style_match = generate_state_style_match(
635                        base_style,
636                        class,
637                        &widget_state_ident,
638                        generate_checkbox_style_struct,
639                    )?;
640
641                    Ok(quote! {
642                        |_theme: &iced::Theme, #status_ident: iced::widget::checkbox::Status| {
643                            let #widget_state_ident = #status_mapping;
644                            #style_match
645                        }
646                    })
647                } else {
648                    Ok(quote! {
649                        |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
650                            #base_style
651                        }
652                    })
653                }
654            } else {
655                Ok(quote! {
656                    |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
657                        #base_style
658                    }
659                })
660            }
661        }
662        "toggler" => {
663            let base_style = generate_toggler_style_struct(style_props)?;
664
665            if has_state_variants {
666                let status_ident = format_ident!("status");
667                if let Some(status_mapping) =
668                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
669                {
670                    let widget_state_ident = format_ident!("widget_state");
671                    let class = style_class.ok_or_else(|| {
672                        super::CodegenError::InvalidWidget(
673                            "Expected style class with state variants".to_string(),
674                        )
675                    })?;
676                    let style_match = generate_state_style_match(
677                        base_style,
678                        class,
679                        &widget_state_ident,
680                        generate_toggler_style_struct,
681                    )?;
682
683                    Ok(quote! {
684                        |_theme: &iced::Theme, #status_ident: iced::widget::toggler::Status| {
685                            let #widget_state_ident = #status_mapping;
686                            #style_match
687                        }
688                    })
689                } else {
690                    Ok(quote! {
691                        |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
692                            #base_style
693                        }
694                    })
695                }
696            } else {
697                Ok(quote! {
698                    |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
699                        #base_style
700                    }
701                })
702            }
703        }
704        "slider" => {
705            let base_style = generate_slider_style_struct(style_props)?;
706
707            if has_state_variants {
708                let status_ident = format_ident!("status");
709                if let Some(status_mapping) =
710                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
711                {
712                    let widget_state_ident = format_ident!("widget_state");
713                    let class = style_class.ok_or_else(|| {
714                        super::CodegenError::InvalidWidget(
715                            "Expected style class with state variants".to_string(),
716                        )
717                    })?;
718                    let style_match = generate_state_style_match(
719                        base_style,
720                        class,
721                        &widget_state_ident,
722                        generate_slider_style_struct,
723                    )?;
724
725                    Ok(quote! {
726                        |_theme: &iced::Theme, #status_ident: iced::widget::slider::Status| {
727                            let #widget_state_ident = #status_mapping;
728                            #style_match
729                        }
730                    })
731                } else {
732                    Ok(quote! {
733                        |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
734                            #base_style
735                        }
736                    })
737                }
738            } else {
739                Ok(quote! {
740                    |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
741                        #base_style
742                    }
743                })
744            }
745        }
746        _ => {
747            // For unsupported widgets, return a no-op closure
748            Ok(quote! {
749                |_theme: &iced::Theme| iced::widget::container::Style::default()
750            })
751        }
752    }
753}
754
755// ============================================================================
756// Helper Functions (copied from theme.rs for reuse)
757// ============================================================================
758
759/// Generate iced::Color from Color IR
760fn generate_color_expr(color: &Color) -> TokenStream {
761    let r = color.r;
762    let g = color.g;
763    let b = color.b;
764    let a = color.a;
765    quote! {
766        iced::Color::from_rgba(#r, #g, #b, #a)
767    }
768}
769
770/// Generate iced::Background from Background IR
771fn generate_background_expr(bg: &Background) -> TokenStream {
772    match bg {
773        Background::Color(color) => {
774            let color_expr = generate_color_expr(color);
775            quote! { iced::Background::Color(#color_expr) }
776        }
777        Background::Gradient(gradient) => generate_gradient_expr(gradient),
778        Background::Image { .. } => {
779            quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
780        }
781    }
782}
783
784/// Generate iced::Gradient from Gradient IR
785fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
786    match gradient {
787        Gradient::Linear { angle, stops } => {
788            let radians = angle * (std::f32::consts::PI / 180.0);
789            let color_exprs: Vec<_> = stops
790                .iter()
791                .map(|s| generate_color_expr(&s.color))
792                .collect();
793            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
794
795            quote! {
796                iced::Background::Gradient(iced::Gradient::Linear(
797                    iced::gradient::Linear::new(#radians)
798                        #(.add_stop(#offsets, #color_exprs))*
799                ))
800            }
801        }
802        Gradient::Radial { stops, .. } => {
803            // Fallback to linear for radial (Iced limitation)
804            let color_exprs: Vec<_> = stops
805                .iter()
806                .map(|s| generate_color_expr(&s.color))
807                .collect();
808            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
809
810            quote! {
811                iced::Background::Gradient(iced::Gradient::Linear(
812                    iced::gradient::Linear::new(0.0)
813                        #(.add_stop(#offsets, #color_exprs))*
814                ))
815            }
816        }
817    }
818}
819
820/// Generate iced::Border from Border IR
821fn generate_border_expr(border: &Border) -> TokenStream {
822    let width = border.width;
823    let color_expr = generate_color_expr(&border.color);
824    let radius_expr = generate_border_radius_expr(&border.radius);
825
826    quote! {
827        iced::Border {
828            width: #width,
829            color: #color_expr,
830            radius: #radius_expr,
831        }
832    }
833}
834
835/// Generate iced::border::Radius from BorderRadius IR
836fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
837    let tl = radius.top_left;
838    let tr = radius.top_right;
839    let br = radius.bottom_right;
840    let bl = radius.bottom_left;
841
842    quote! {
843        iced::border::Radius::from(#tl).top_right(#tr).bottom_right(#br).bottom_left(#bl)
844    }
845}
846
847/// Generate iced::Shadow from Shadow IR
848fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
849    let offset_x = shadow.offset_x;
850    let offset_y = shadow.offset_y;
851    let blur = shadow.blur_radius;
852    let color_expr = generate_color_expr(&shadow.color);
853
854    quote! {
855        iced::Shadow {
856            offset: iced::Vector::new(#offset_x, #offset_y),
857            blur_radius: #blur,
858            color: #color_expr,
859        }
860    }
861}
862
863/// Generate iced::widget::button::Style struct from StyleProperties
864///
865/// When no explicit color is set, uses theme text color via _theme parameter
866/// that's available in the style closure scope.
867fn generate_button_style_struct(
868    props: &StyleProperties,
869) -> Result<TokenStream, super::CodegenError> {
870    let background_expr = props
871        .background
872        .as_ref()
873        .map(|bg| {
874            let expr = generate_background_expr(bg);
875            quote! { Some(#expr) }
876        })
877        .unwrap_or_else(|| quote! { None });
878
879    // Use theme text color as fallback instead of hardcoded BLACK
880    let text_color_expr = props
881        .color
882        .as_ref()
883        .map(generate_color_expr)
884        .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
885
886    let border_expr = props
887        .border
888        .as_ref()
889        .map(generate_border_expr)
890        .unwrap_or_else(|| quote! { iced::Border::default() });
891
892    let shadow_expr = props
893        .shadow
894        .as_ref()
895        .map(generate_shadow_expr)
896        .unwrap_or_else(|| quote! { iced::Shadow::default() });
897
898    Ok(quote! {
899        iced::widget::button::Style {
900            background: #background_expr,
901            text_color: #text_color_expr,
902            border: #border_expr,
903            shadow: #shadow_expr,
904            snap: false,
905        }
906    })
907}
908
909/// Generate iced::widget::container::Style struct from StyleProperties
910fn generate_container_style_struct(
911    props: &StyleProperties,
912) -> Result<TokenStream, super::CodegenError> {
913    let background_expr = props
914        .background
915        .as_ref()
916        .map(|bg| {
917            let expr = generate_background_expr(bg);
918            quote! { Some(#expr) }
919        })
920        .unwrap_or_else(|| quote! { None });
921
922    let text_color_expr = props
923        .color
924        .as_ref()
925        .map(|color| {
926            let color_expr = generate_color_expr(color);
927            quote! { Some(#color_expr) }
928        })
929        .unwrap_or_else(|| quote! { None });
930
931    let border_expr = props
932        .border
933        .as_ref()
934        .map(generate_border_expr)
935        .unwrap_or_else(|| quote! { iced::Border::default() });
936
937    let shadow_expr = props
938        .shadow
939        .as_ref()
940        .map(generate_shadow_expr)
941        .unwrap_or_else(|| quote! { iced::Shadow::default() });
942
943    Ok(quote! {
944        iced::widget::container::Style {
945            background: #background_expr,
946            text_color: #text_color_expr,
947            border: #border_expr,
948            shadow: #shadow_expr,
949            snap: false,
950        }
951    })
952}
953
954/// Generate iced::widget::text_input::Style struct from StyleProperties
955///
956/// When no explicit colors are set, uses theme text colors via _theme parameter
957/// that's available in the style closure scope.
958fn generate_text_input_style_struct(
959    props: &StyleProperties,
960) -> Result<TokenStream, super::CodegenError> {
961    let background_expr = props
962        .background
963        .as_ref()
964        .map(|bg| {
965            let expr = generate_background_expr(bg);
966            quote! { #expr }
967        })
968        .unwrap_or_else(
969            || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
970        );
971
972    let border_expr = props
973        .border
974        .as_ref()
975        .map(generate_border_expr)
976        .unwrap_or_else(|| quote! { iced::Border::default() });
977
978    // Use theme colors as fallback instead of hardcoded colors
979    let value_color = props
980        .color
981        .as_ref()
982        .map(generate_color_expr)
983        .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
984
985    Ok(quote! {
986        iced::widget::text_input::Style {
987            background: #background_expr,
988            border: #border_expr,
989            icon: _theme.extended_palette().background.base.text,
990            placeholder: _theme.extended_palette().background.weak.text,
991            value: #value_color,
992            selection: _theme.extended_palette().primary.weak.color,
993        }
994    })
995}
996
997/// Generate checkbox style struct from StyleProperties
998///
999/// When no explicit colors are set, uses theme colors via _theme parameter
1000/// that's available in the style closure scope.
1001fn generate_checkbox_style_struct(
1002    props: &StyleProperties,
1003) -> Result<TokenStream, super::CodegenError> {
1004    let background_expr = props
1005        .background
1006        .as_ref()
1007        .map(|bg| {
1008            let expr = generate_background_expr(bg);
1009            quote! { #expr }
1010        })
1011        .unwrap_or_else(
1012            || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
1013        );
1014
1015    let border_expr = props
1016        .border
1017        .as_ref()
1018        .map(generate_border_expr)
1019        .unwrap_or_else(|| quote! { iced::Border::default() });
1020
1021    // Use theme text color as fallback instead of hardcoded BLACK
1022    let text_color = props
1023        .color
1024        .as_ref()
1025        .map(generate_color_expr)
1026        .unwrap_or_else(|| quote! { _theme.extended_palette().primary.base.color });
1027
1028    Ok(quote! {
1029        iced::widget::checkbox::Style {
1030            background: #background_expr,
1031            icon_color: #text_color,
1032            border: #border_expr,
1033            text_color: None,
1034        }
1035    })
1036}
1037
1038/// Generate toggler style struct from StyleProperties
1039fn generate_toggler_style_struct(
1040    props: &StyleProperties,
1041) -> Result<TokenStream, super::CodegenError> {
1042    let background_expr = props
1043        .background
1044        .as_ref()
1045        .map(|bg| {
1046            let expr = generate_background_expr(bg);
1047            quote! { #expr }
1048        })
1049        .unwrap_or_else(
1050            || quote! { iced::Background::Color(iced::Color::from_rgb(0.5, 0.5, 0.5)) },
1051        );
1052
1053    Ok(quote! {
1054        iced::widget::toggler::Style {
1055            background: #background_expr,
1056            background_border_width: 0.0,
1057            background_border_color: iced::Color::TRANSPARENT,
1058            foreground: iced::Background::Color(iced::Color::WHITE),
1059            foreground_border_width: 0.0,
1060            foreground_border_color: iced::Color::TRANSPARENT,
1061        }
1062    })
1063}
1064
1065/// Generate slider style struct from StyleProperties
1066fn generate_slider_style_struct(
1067    props: &StyleProperties,
1068) -> Result<TokenStream, super::CodegenError> {
1069    let border_expr = props
1070        .border
1071        .as_ref()
1072        .map(generate_border_expr)
1073        .unwrap_or_else(|| quote! { iced::Border::default() });
1074
1075    Ok(quote! {
1076        iced::widget::slider::Style {
1077            rail: iced::widget::slider::Rail {
1078                colors: (
1079                    iced::Color::from_rgb(0.6, 0.6, 0.6),
1080                    iced::Color::from_rgb(0.2, 0.6, 1.0),
1081                ),
1082                width: 4.0,
1083                border: #border_expr,
1084            },
1085            handle: iced::widget::slider::Handle {
1086                shape: iced::widget::slider::HandleShape::Circle { radius: 8.0 },
1087                color: iced::Color::WHITE,
1088                border_width: 1.0,
1089                border_color: iced::Color::from_rgb(0.6, 0.6, 0.6),
1090            },
1091        }
1092    })
1093}
1094
1095/// Generate text widget
1096fn generate_text(
1097    node: &crate::WidgetNode,
1098    model_ident: &syn::Ident,
1099    _style_classes: &HashMap<String, StyleClass>,
1100) -> Result<TokenStream, super::CodegenError> {
1101    let value_attr = node.attributes.get("value").ok_or_else(|| {
1102        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
1103    })?;
1104
1105    let value_expr = generate_attribute_value(value_attr, model_ident);
1106
1107    let mut text_widget = quote! {
1108        iced::widget::text(#value_expr)
1109    };
1110
1111    // Apply size attribute
1112    if let Some(size) = node.attributes.get("size").and_then(|attr| {
1113        if let AttributeValue::Static(s) = attr {
1114            s.parse::<f32>().ok()
1115        } else {
1116            None
1117        }
1118    }) {
1119        text_widget = quote! { #text_widget.size(#size) };
1120    }
1121
1122    // Apply weight attribute (bold, normal, etc.)
1123    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
1124        if let AttributeValue::Static(s) = attr {
1125            Some(s.clone())
1126        } else {
1127            None
1128        }
1129    }) {
1130        let weight_expr = match weight.to_lowercase().as_str() {
1131            "bold" => quote! { iced::font::Weight::Bold },
1132            "semibold" => quote! { iced::font::Weight::Semibold },
1133            "medium" => quote! { iced::font::Weight::Medium },
1134            "light" => quote! { iced::font::Weight::Light },
1135            _ => quote! { iced::font::Weight::Normal },
1136        };
1137        text_widget = quote! {
1138            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
1139        };
1140    }
1141
1142    // Apply inline style color if present
1143    if let Some(ref style_props) = node.style
1144        && let Some(ref color) = style_props.color
1145    {
1146        let color_expr = generate_color_expr(color);
1147        text_widget = quote! { #text_widget.color(#color_expr) };
1148    }
1149
1150    // Use helper to wrap in container if layout attributes are present
1151    Ok(maybe_wrap_in_container(text_widget, node))
1152}
1153
1154/// Generate Length expression from string
1155fn generate_length_expr(s: &str) -> TokenStream {
1156    let s = s.trim().to_lowercase();
1157    if s == "fill" {
1158        quote! { iced::Length::Fill }
1159    } else if s == "shrink" {
1160        quote! { iced::Length::Shrink }
1161    } else if let Some(pct) = s.strip_suffix('%') {
1162        if let Ok(p) = pct.parse::<f32>() {
1163            // Iced doesn't have a direct percentage, use FillPortion as approximation
1164            let portion = ((p / 100.0) * 16.0).round() as u16;
1165            let portion = portion.max(1);
1166            quote! { iced::Length::FillPortion(#portion) }
1167        } else {
1168            quote! { iced::Length::Shrink }
1169        }
1170    } else if let Ok(px) = s.parse::<f32>() {
1171        quote! { iced::Length::Fixed(#px) }
1172    } else {
1173        quote! { iced::Length::Shrink }
1174    }
1175}
1176
1177/// Generate Length expression from LayoutLength IR type
1178fn generate_layout_length_expr(length: &LayoutLength) -> TokenStream {
1179    match length {
1180        LayoutLength::Fixed(px) => quote! { iced::Length::Fixed(#px) },
1181        LayoutLength::Fill => quote! { iced::Length::Fill },
1182        LayoutLength::Shrink => quote! { iced::Length::Shrink },
1183        LayoutLength::FillPortion(portion) => {
1184            let p = *portion as u16;
1185            quote! { iced::Length::FillPortion(#p) }
1186        }
1187        LayoutLength::Percentage(pct) => {
1188            // Iced doesn't have direct percentage, approximate with FillPortion
1189            let portion = ((pct / 100.0) * 16.0).round() as u16;
1190            let portion = portion.max(1);
1191            quote! { iced::Length::FillPortion(#portion) }
1192        }
1193    }
1194}
1195
1196/// Generate horizontal alignment expression
1197fn generate_horizontal_alignment_expr(s: &str) -> TokenStream {
1198    match s.trim().to_lowercase().as_str() {
1199        "center" => quote! { iced::alignment::Horizontal::Center },
1200        "end" | "right" => quote! { iced::alignment::Horizontal::Right },
1201        _ => quote! { iced::alignment::Horizontal::Left },
1202    }
1203}
1204
1205/// Generate vertical alignment expression
1206fn generate_vertical_alignment_expr(s: &str) -> TokenStream {
1207    match s.trim().to_lowercase().as_str() {
1208        "center" => quote! { iced::alignment::Vertical::Center },
1209        "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
1210        _ => quote! { iced::alignment::Vertical::Top },
1211    }
1212}
1213
1214/// Wraps a widget in a container if layout attributes are present.
1215///
1216/// This helper provides consistent layout attribute support across all widgets
1217/// by wrapping them in a container when needed.
1218///
1219/// # Arguments
1220///
1221/// * `widget` - The widget expression to potentially wrap
1222/// * `node` - The widget node containing attributes
1223///
1224/// # Returns
1225///
1226/// Returns the widget wrapped in a container if layout attributes are present,
1227/// otherwise returns the original widget.
1228fn maybe_wrap_in_container(widget: TokenStream, node: &crate::WidgetNode) -> TokenStream {
1229    // Check if we need to wrap in container for layout/alignment/classes
1230    let needs_container = node.layout.is_some()
1231        || !node.classes.is_empty()
1232        || node.attributes.contains_key("align_x")
1233        || node.attributes.contains_key("align_y")
1234        || node.attributes.contains_key("width")
1235        || node.attributes.contains_key("height")
1236        || node.attributes.contains_key("padding");
1237
1238    if !needs_container {
1239        return quote! { #widget.into() };
1240    }
1241
1242    let mut container = quote! {
1243        iced::widget::container(#widget)
1244    };
1245
1246    // Apply width
1247    if let Some(width) = node.attributes.get("width").and_then(|attr| {
1248        if let AttributeValue::Static(s) = attr {
1249            Some(s.clone())
1250        } else {
1251            None
1252        }
1253    }) {
1254        let width_expr = generate_length_expr(&width);
1255        container = quote! { #container.width(#width_expr) };
1256    }
1257
1258    // Apply height
1259    if let Some(height) = node.attributes.get("height").and_then(|attr| {
1260        if let AttributeValue::Static(s) = attr {
1261            Some(s.clone())
1262        } else {
1263            None
1264        }
1265    }) {
1266        let height_expr = generate_length_expr(&height);
1267        container = quote! { #container.height(#height_expr) };
1268    }
1269
1270    // Apply padding
1271    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1272        if let AttributeValue::Static(s) = attr {
1273            s.parse::<f32>().ok()
1274        } else {
1275            None
1276        }
1277    }) {
1278        container = quote! { #container.padding(#padding) };
1279    }
1280
1281    // Apply align_x
1282    if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1283        if let AttributeValue::Static(s) = attr {
1284            Some(s.clone())
1285        } else {
1286            None
1287        }
1288    }) {
1289        let align_expr = generate_horizontal_alignment_expr(&align_x);
1290        container = quote! { #container.align_x(#align_expr) };
1291    }
1292
1293    // Apply align_y
1294    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1295        if let AttributeValue::Static(s) = attr {
1296            Some(s.clone())
1297        } else {
1298            None
1299        }
1300    }) {
1301        let align_expr = generate_vertical_alignment_expr(&align_y);
1302        container = quote! { #container.align_y(#align_expr) };
1303    }
1304
1305    // Apply class style if present
1306    if let Some(class_name) = node.classes.first() {
1307        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1308        container = quote! { #container.style(#style_fn_ident) };
1309    }
1310
1311    quote! { #container.into() }
1312}
1313
1314/// Generate button widget
1315fn generate_button(
1316    node: &crate::WidgetNode,
1317    model_ident: &syn::Ident,
1318    message_ident: &syn::Ident,
1319    style_classes: &HashMap<String, StyleClass>,
1320) -> Result<TokenStream, super::CodegenError> {
1321    let label_attr = node.attributes.get("label").ok_or_else(|| {
1322        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
1323    })?;
1324
1325    let label_expr = generate_attribute_value(label_attr, model_ident);
1326
1327    let on_click = node
1328        .events
1329        .iter()
1330        .find(|e| e.event == crate::EventKind::Click);
1331
1332    let mut button = quote! {
1333        iced::widget::button(iced::widget::text(#label_expr))
1334    };
1335
1336    // Handle enabled attribute
1337    let enabled_condition = node.attributes.get("enabled").map(|attr| match attr {
1338        AttributeValue::Static(s) => {
1339            // Static enabled values
1340            match s.to_lowercase().as_str() {
1341                "true" | "1" | "yes" | "on" => quote! { true },
1342                "false" | "0" | "no" | "off" => quote! { false },
1343                _ => quote! { true }, // Default to enabled
1344            }
1345        }
1346        AttributeValue::Binding(binding_expr) => {
1347            // Dynamic binding expression - use generate_bool_expr for native boolean
1348            super::bindings::generate_bool_expr(&binding_expr.expr)
1349        }
1350        AttributeValue::Interpolated(_) => {
1351            // Interpolated strings treated as enabled if non-empty
1352            let expr_tokens = generate_attribute_value(attr, model_ident);
1353            quote! { !#expr_tokens.is_empty() && #expr_tokens != "false" && #expr_tokens != "0" }
1354        }
1355    });
1356
1357    if let Some(event) = on_click {
1358        let variant_name = to_upper_camel_case(&event.handler);
1359        let handler_ident = format_ident!("{}", variant_name);
1360
1361        let param_expr = if let Some(ref param) = event.param {
1362            let param_tokens = generate_expr(&param.expr);
1363            quote! { (#param_tokens) }
1364        } else {
1365            quote! {}
1366        };
1367
1368        // Generate on_press call based on enabled condition
1369        button = match enabled_condition {
1370            None => {
1371                // No enabled attribute - always enabled
1372                quote! {
1373                    #button.on_press(#message_ident::#handler_ident #param_expr)
1374                }
1375            }
1376            Some(condition) => {
1377                // Conditional enabled - use on_press_maybe
1378                quote! {
1379                    #button.on_press_maybe(
1380                        if #condition {
1381                            Some(#message_ident::#handler_ident #param_expr)
1382                        } else {
1383                            None
1384                        }
1385                    )
1386                }
1387            }
1388        };
1389    }
1390
1391    // Apply styles (inline or classes)
1392    button = apply_widget_style(button, node, "button", style_classes)?;
1393
1394    Ok(quote! { #button.into() })
1395}
1396
1397/// Helper function to convert snake_case to UpperCamelCase
1398fn to_upper_camel_case(s: &str) -> String {
1399    let mut result = String::new();
1400    let mut capitalize_next = true;
1401    for c in s.chars() {
1402        if c == '_' {
1403            capitalize_next = true;
1404        } else if capitalize_next {
1405            result.push(c.to_ascii_uppercase());
1406            capitalize_next = false;
1407        } else {
1408            result.push(c);
1409        }
1410    }
1411    result
1412}
1413
1414/// Generate container widget (column, row, container, scrollable)
1415fn generate_container(
1416    node: &crate::WidgetNode,
1417    widget_type: &str,
1418    model_ident: &syn::Ident,
1419    message_ident: &syn::Ident,
1420    style_classes: &HashMap<String, StyleClass>,
1421) -> Result<TokenStream, super::CodegenError> {
1422    let children: Vec<TokenStream> = node
1423        .children
1424        .iter()
1425        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1426        .collect::<Result<_, _>>()?;
1427
1428    let widget_ident = format_ident!("{}", widget_type);
1429
1430    // Get merged layout from node.layout and style classes
1431    let merged_layout = get_merged_layout(node, style_classes);
1432
1433    // Get spacing from attributes or merged layout
1434    let spacing = node
1435        .attributes
1436        .get("spacing")
1437        .and_then(|attr| {
1438            if let AttributeValue::Static(s) = attr {
1439                s.parse::<f32>().ok()
1440            } else {
1441                None
1442            }
1443        })
1444        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
1445
1446    // Get padding from attributes or merged layout
1447    let padding = node
1448        .attributes
1449        .get("padding")
1450        .and_then(|attr| {
1451            if let AttributeValue::Static(s) = attr {
1452                s.parse::<f32>().ok()
1453            } else {
1454                None
1455            }
1456        })
1457        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
1458
1459    let mut widget = if widget_type == "container" {
1460        // Container in Iced can only have one child
1461        // If multiple children provided, wrap them in a column automatically
1462        // Use let binding with explicit type to help inference when child is a column/row
1463        if children.is_empty() {
1464            quote! {
1465                iced::widget::container(iced::widget::Space::new())
1466            }
1467        } else if children.len() == 1 {
1468            let child = &children[0];
1469            quote! {
1470                {
1471                    let content: iced::Element<'_, _, _> = #child;
1472                    iced::widget::container(content)
1473                }
1474            }
1475        } else {
1476            // Multiple children - wrap in a column
1477            quote! {
1478                {
1479                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1480                    iced::widget::container(content)
1481                }
1482            }
1483        }
1484    } else if widget_type == "scrollable" {
1485        // Scrollable in Iced can only have one child
1486        // If multiple children provided, wrap them in a column automatically
1487        // Use let binding with explicit type to help inference when child is a column/row
1488        if children.is_empty() {
1489            quote! {
1490                iced::widget::scrollable(iced::widget::Space::new())
1491            }
1492        } else if children.len() == 1 {
1493            let child = &children[0];
1494            quote! {
1495                {
1496                    let content: iced::Element<'_, _, _> = #child;
1497                    iced::widget::scrollable(content)
1498                }
1499            }
1500        } else {
1501            // Multiple children - wrap in a column
1502            quote! {
1503                {
1504                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1505                    iced::widget::scrollable(content)
1506                }
1507            }
1508        }
1509    } else {
1510        quote! {
1511            iced::widget::#widget_ident(vec![#(#children),*])
1512        }
1513    };
1514
1515    if let Some(s) = spacing {
1516        widget = quote! { #widget.spacing(#s) };
1517    }
1518
1519    if let Some(p) = padding {
1520        widget = quote! { #widget.padding(#p) };
1521    }
1522
1523    // Apply width from attributes or merged layout
1524    let width_from_attr = node.attributes.get("width").and_then(|attr| {
1525        if let AttributeValue::Static(s) = attr {
1526            Some(s.clone())
1527        } else {
1528            None
1529        }
1530    });
1531    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
1532
1533    if let Some(width) = width_from_attr {
1534        let width_expr = generate_length_expr(&width);
1535        widget = quote! { #widget.width(#width_expr) };
1536    } else if let Some(layout_width) = width_from_layout {
1537        let width_expr = generate_layout_length_expr(layout_width);
1538        widget = quote! { #widget.width(#width_expr) };
1539    }
1540
1541    // Apply height from attributes or merged layout
1542    let height_from_attr = node.attributes.get("height").and_then(|attr| {
1543        if let AttributeValue::Static(s) = attr {
1544            Some(s.clone())
1545        } else {
1546            None
1547        }
1548    });
1549    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
1550
1551    if let Some(height) = height_from_attr {
1552        let height_expr = generate_length_expr(&height);
1553        widget = quote! { #widget.height(#height_expr) };
1554    } else if let Some(layout_height) = height_from_layout {
1555        let height_expr = generate_layout_length_expr(layout_height);
1556        widget = quote! { #widget.height(#height_expr) };
1557    }
1558
1559    // Apply align_x attribute (for containers)
1560    if widget_type == "container" {
1561        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1562            if let AttributeValue::Static(s) = attr {
1563                Some(s.clone())
1564            } else {
1565                None
1566            }
1567        }) {
1568            let align_expr = generate_horizontal_alignment_expr(&align_x);
1569            widget = quote! { #widget.align_x(#align_expr) };
1570        }
1571
1572        // Apply align_y attribute
1573        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1574            if let AttributeValue::Static(s) = attr {
1575                Some(s.clone())
1576            } else {
1577                None
1578            }
1579        }) {
1580            let align_expr = generate_vertical_alignment_expr(&align_y);
1581            widget = quote! { #widget.align_y(#align_expr) };
1582        }
1583    }
1584
1585    // Apply align_items for column/row (vertical alignment of children)
1586    if (widget_type == "column" || widget_type == "row")
1587        && let Some(align) = node.attributes.get("align_items").and_then(|attr| {
1588            if let AttributeValue::Static(s) = attr {
1589                Some(s.clone())
1590            } else {
1591                None
1592            }
1593        })
1594    {
1595        let align_expr = match align.to_lowercase().as_str() {
1596            "center" => quote! { iced::Alignment::Center },
1597            "end" => quote! { iced::Alignment::End },
1598            _ => quote! { iced::Alignment::Start },
1599        };
1600        widget = quote! { #widget.align_items(#align_expr) };
1601    }
1602
1603    // Apply styles (only for container, not column/row/scrollable)
1604    if widget_type == "container" {
1605        widget = apply_widget_style(widget, node, "container", style_classes)?;
1606    }
1607
1608    // Check if Column/Row needs to be wrapped in a container for align_x/align_y
1609    // These attributes position the Column/Row itself within its parent,
1610    // which requires an outer container wrapper
1611    if (widget_type == "column" || widget_type == "row")
1612        && (node.attributes.contains_key("align_x") || node.attributes.contains_key("align_y"))
1613    {
1614        let mut container = quote! { iced::widget::container(#widget) };
1615
1616        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1617            if let AttributeValue::Static(s) = attr {
1618                Some(s.clone())
1619            } else {
1620                None
1621            }
1622        }) {
1623            let align_expr = generate_horizontal_alignment_expr(&align_x);
1624            container = quote! { #container.align_x(#align_expr) };
1625        }
1626
1627        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1628            if let AttributeValue::Static(s) = attr {
1629                Some(s.clone())
1630            } else {
1631                None
1632            }
1633        }) {
1634            let align_expr = generate_vertical_alignment_expr(&align_y);
1635            container = quote! { #container.align_y(#align_expr) };
1636        }
1637
1638        // Container needs explicit width/height to enable alignment
1639        container = quote! { #container.width(iced::Length::Fill).height(iced::Length::Fill) };
1640
1641        return Ok(quote! { #container.into() });
1642    }
1643
1644    Ok(quote! { #widget.into() })
1645}
1646
1647/// Generate stack widget
1648fn generate_stack(
1649    node: &crate::WidgetNode,
1650    model_ident: &syn::Ident,
1651    message_ident: &syn::Ident,
1652    style_classes: &HashMap<String, StyleClass>,
1653) -> Result<TokenStream, super::CodegenError> {
1654    let children: Vec<TokenStream> = node
1655        .children
1656        .iter()
1657        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1658        .collect::<Result<_, _>>()?;
1659
1660    Ok(quote! {
1661        iced::widget::stack(vec![#(#children),*]).into()
1662    })
1663}
1664
1665/// Generate space widget
1666fn generate_space(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1667    // Get width attribute
1668    let width = node.attributes.get("width").and_then(|attr| {
1669        if let AttributeValue::Static(s) = attr {
1670            Some(s.clone())
1671        } else {
1672            None
1673        }
1674    });
1675
1676    // Get height attribute
1677    let height = node.attributes.get("height").and_then(|attr| {
1678        if let AttributeValue::Static(s) = attr {
1679            Some(s.clone())
1680        } else {
1681            None
1682        }
1683    });
1684
1685    let mut space = quote! { iced::widget::Space::new() };
1686
1687    // Apply width
1688    if let Some(w) = width {
1689        let width_expr = generate_length_expr(&w);
1690        space = quote! { #space.width(#width_expr) };
1691    }
1692
1693    // Apply height
1694    if let Some(h) = height {
1695        let height_expr = generate_length_expr(&h);
1696        space = quote! { #space.height(#height_expr) };
1697    }
1698
1699    Ok(quote! { #space.into() })
1700}
1701
1702/// Generate rule (horizontal/vertical line) widget
1703fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1704    // Get direction (default to horizontal)
1705    let direction = node
1706        .attributes
1707        .get("direction")
1708        .and_then(|attr| {
1709            if let AttributeValue::Static(s) = attr {
1710                Some(s.clone())
1711            } else {
1712                None
1713            }
1714        })
1715        .unwrap_or_else(|| "horizontal".to_string());
1716
1717    // Get thickness (default to 1)
1718    let thickness = node
1719        .attributes
1720        .get("thickness")
1721        .and_then(|attr| {
1722            if let AttributeValue::Static(s) = attr {
1723                s.parse::<f32>().ok()
1724            } else {
1725                None
1726            }
1727        })
1728        .unwrap_or(1.0);
1729
1730    let rule = if direction.to_lowercase() == "vertical" {
1731        quote! { iced::widget::rule::vertical(#thickness) }
1732    } else {
1733        quote! { iced::widget::rule::horizontal(#thickness) }
1734    };
1735
1736    Ok(quote! { #rule.into() })
1737}
1738
1739/// Generate checkbox widget
1740fn generate_checkbox(
1741    node: &crate::WidgetNode,
1742    model_ident: &syn::Ident,
1743    message_ident: &syn::Ident,
1744    style_classes: &HashMap<String, StyleClass>,
1745) -> Result<TokenStream, super::CodegenError> {
1746    let label = node
1747        .attributes
1748        .get("label")
1749        .and_then(|attr| {
1750            if let AttributeValue::Static(s) = attr {
1751                Some(s.clone())
1752            } else {
1753                None
1754            }
1755        })
1756        .unwrap_or_default();
1757    let label_lit = proc_macro2::Literal::string(&label);
1758    let label_expr = quote! { #label_lit.to_string() };
1759
1760    let checked_attr = node.attributes.get("checked");
1761    let checked_expr = checked_attr
1762        .map(|attr| generate_attribute_value(attr, model_ident))
1763        .unwrap_or(quote! { false });
1764
1765    let on_toggle = node
1766        .events
1767        .iter()
1768        .find(|e| e.event == crate::EventKind::Toggle);
1769
1770    let checkbox = if let Some(event) = on_toggle {
1771        let variant_name = to_upper_camel_case(&event.handler);
1772        let handler_ident = format_ident!("{}", variant_name);
1773        quote! {
1774            iced::widget::checkbox(#label_expr, #checked_expr)
1775                .on_toggle(#message_ident::#handler_ident)
1776        }
1777    } else {
1778        quote! {
1779            iced::widget::checkbox(#label_expr, #checked_expr)
1780        }
1781    };
1782
1783    // Apply styles
1784    let checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
1785
1786    Ok(quote! { #checkbox.into() })
1787}
1788
1789/// Generate toggler widget
1790fn generate_toggler(
1791    node: &crate::WidgetNode,
1792    model_ident: &syn::Ident,
1793    message_ident: &syn::Ident,
1794    style_classes: &HashMap<String, StyleClass>,
1795) -> Result<TokenStream, super::CodegenError> {
1796    let label = node
1797        .attributes
1798        .get("label")
1799        .and_then(|attr| {
1800            if let AttributeValue::Static(s) = attr {
1801                Some(s.clone())
1802            } else {
1803                None
1804            }
1805        })
1806        .unwrap_or_default();
1807    let label_lit = proc_macro2::Literal::string(&label);
1808    let label_expr = quote! { #label_lit.to_string() };
1809
1810    let is_toggled_attr = node.attributes.get("toggled");
1811    let is_toggled_expr = is_toggled_attr
1812        .map(|attr| generate_attribute_value(attr, model_ident))
1813        .unwrap_or(quote! { false });
1814
1815    let on_toggle = node
1816        .events
1817        .iter()
1818        .find(|e| e.event == crate::EventKind::Toggle);
1819
1820    let toggler = if let Some(event) = on_toggle {
1821        let variant_name = to_upper_camel_case(&event.handler);
1822        let handler_ident = format_ident!("{}", variant_name);
1823        quote! {
1824            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1825                .on_toggle(|_| #message_ident::#handler_ident)
1826        }
1827    } else {
1828        quote! {
1829            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1830        }
1831    };
1832
1833    // Apply styles
1834    let toggler = apply_widget_style(toggler, node, "toggler", style_classes)?;
1835
1836    Ok(quote! { #toggler.into() })
1837}
1838
1839/// Generate slider widget
1840fn generate_slider(
1841    node: &crate::WidgetNode,
1842    model_ident: &syn::Ident,
1843    message_ident: &syn::Ident,
1844    style_classes: &HashMap<String, StyleClass>,
1845) -> Result<TokenStream, super::CodegenError> {
1846    let min = node.attributes.get("min").and_then(|attr| {
1847        if let AttributeValue::Static(s) = attr {
1848            s.parse::<f32>().ok()
1849        } else {
1850            None
1851        }
1852    });
1853
1854    let max = node.attributes.get("max").and_then(|attr| {
1855        if let AttributeValue::Static(s) = attr {
1856            s.parse::<f32>().ok()
1857        } else {
1858            None
1859        }
1860    });
1861
1862    let value_attr = node.attributes.get("value").ok_or_else(|| {
1863        super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
1864    })?;
1865    let value_expr = generate_attribute_value(value_attr, model_ident);
1866
1867    let on_change = node
1868        .events
1869        .iter()
1870        .find(|e| e.event == crate::EventKind::Change);
1871
1872    let mut slider = quote! {
1873        iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
1874    };
1875
1876    if let Some(m) = min {
1877        slider = quote! { #slider.min(#m) };
1878    }
1879    if let Some(m) = max {
1880        slider = quote! { #slider.max(#m) };
1881    }
1882
1883    // Apply step attribute (increment size)
1884    let step = node.attributes.get("step").and_then(|attr| {
1885        if let AttributeValue::Static(s) = attr {
1886            s.parse::<f32>().ok()
1887        } else {
1888            None
1889        }
1890    });
1891
1892    if let Some(s) = step {
1893        slider = quote! { #slider.step(#s) };
1894    }
1895
1896    if let Some(event) = on_change {
1897        let variant_name = to_upper_camel_case(&event.handler);
1898        let handler_ident = format_ident!("{}", variant_name);
1899        slider = quote! {
1900            iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
1901        };
1902    }
1903
1904    // Apply styles
1905    slider = apply_widget_style(slider, node, "slider", style_classes)?;
1906
1907    Ok(quote! { #slider.into() })
1908}
1909
1910/// Generate radio widget
1911fn generate_radio(
1912    node: &crate::WidgetNode,
1913    _model_ident: &syn::Ident,
1914    message_ident: &syn::Ident,
1915    _style_classes: &HashMap<String, StyleClass>,
1916) -> Result<TokenStream, super::CodegenError> {
1917    let label = node
1918        .attributes
1919        .get("label")
1920        .and_then(|attr| {
1921            if let AttributeValue::Static(s) = attr {
1922                Some(s.clone())
1923            } else {
1924                None
1925            }
1926        })
1927        .unwrap_or_default();
1928    let label_lit = proc_macro2::Literal::string(&label);
1929    let label_expr = quote! { #label_lit.to_string() };
1930
1931    let value_attr = node.attributes.get("value").ok_or_else(|| {
1932        super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
1933    })?;
1934    let value_expr = match value_attr {
1935        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
1936        _ => quote! { String::new() },
1937    };
1938
1939    let selected_attr = node.attributes.get("selected");
1940    let selected_expr = match selected_attr {
1941        Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
1942        _ => quote! { None },
1943    };
1944
1945    let on_select = node
1946        .events
1947        .iter()
1948        .find(|e| e.event == crate::EventKind::Select);
1949
1950    if let Some(event) = on_select {
1951        let variant_name = to_upper_camel_case(&event.handler);
1952        let handler_ident = format_ident!("{}", variant_name);
1953        Ok(quote! {
1954            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v)).into()
1955        })
1956    } else {
1957        Ok(quote! {
1958            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ()).into()
1959        })
1960    }
1961}
1962
1963/// Generate progress bar widget
1964fn generate_progress_bar(
1965    node: &crate::WidgetNode,
1966    model_ident: &syn::Ident,
1967    _style_classes: &HashMap<String, StyleClass>,
1968) -> Result<TokenStream, super::CodegenError> {
1969    let value_attr = node.attributes.get("value").ok_or_else(|| {
1970        super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
1971    })?;
1972    let value_expr = generate_attribute_value(value_attr, model_ident);
1973
1974    let max_attr = node.attributes.get("max").and_then(|attr| {
1975        if let AttributeValue::Static(s) = attr {
1976            s.parse::<f32>().ok()
1977        } else {
1978            None
1979        }
1980    });
1981
1982    if let Some(max) = max_attr {
1983        Ok(quote! {
1984            iced::widget::progress_bar(0.0..=#max, #value_expr).into()
1985        })
1986    } else {
1987        Ok(quote! {
1988            iced::widget::progress_bar(0.0..=100.0, #value_expr).into()
1989        })
1990    }
1991}
1992
1993/// Generate text input widget
1994fn generate_text_input(
1995    node: &crate::WidgetNode,
1996    model_ident: &syn::Ident,
1997    message_ident: &syn::Ident,
1998    style_classes: &HashMap<String, StyleClass>,
1999) -> Result<TokenStream, super::CodegenError> {
2000    let value_expr = node
2001        .attributes
2002        .get("value")
2003        .map(|attr| generate_attribute_value(attr, model_ident))
2004        .unwrap_or(quote! { String::new() });
2005
2006    let placeholder = node.attributes.get("placeholder").and_then(|attr| {
2007        if let AttributeValue::Static(s) = attr {
2008            Some(s.clone())
2009        } else {
2010            None
2011        }
2012    });
2013
2014    let on_input = node
2015        .events
2016        .iter()
2017        .find(|e| e.event == crate::EventKind::Input);
2018
2019    let on_submit = node
2020        .events
2021        .iter()
2022        .find(|e| e.event == crate::EventKind::Submit);
2023
2024    let mut text_input = match placeholder {
2025        Some(ph) => {
2026            let ph_lit = proc_macro2::Literal::string(&ph);
2027            quote! {
2028                iced::widget::text_input(#ph_lit, &#value_expr)
2029            }
2030        }
2031        None => quote! {
2032            iced::widget::text_input("", &#value_expr)
2033        },
2034    };
2035
2036    if let Some(event) = on_input {
2037        let variant_name = to_upper_camel_case(&event.handler);
2038        let handler_ident = format_ident!("{}", variant_name);
2039        text_input = quote! {
2040            #text_input.on_input(|v| #message_ident::#handler_ident(v))
2041        };
2042    }
2043
2044    if let Some(event) = on_submit {
2045        let variant_name = to_upper_camel_case(&event.handler);
2046        let handler_ident = format_ident!("{}", variant_name);
2047        text_input = quote! {
2048            #text_input.on_submit(#message_ident::#handler_ident)
2049        };
2050    }
2051
2052    // Apply password/secure attribute (masks input)
2053    let is_password = node
2054        .attributes
2055        .get("password")
2056        .or_else(|| node.attributes.get("secure"))
2057        .and_then(|attr| {
2058            if let AttributeValue::Static(s) = attr {
2059                Some(s.to_lowercase() == "true" || s == "1")
2060            } else {
2061                None
2062            }
2063        })
2064        .unwrap_or(false);
2065
2066    if is_password {
2067        text_input = quote! { #text_input.password() };
2068    }
2069
2070    // Apply styles
2071    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
2072
2073    Ok(quote! { #text_input.into() })
2074}
2075
2076/// Generate image widget
2077fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2078    let src_attr = node.attributes.get("src").ok_or_else(|| {
2079        super::CodegenError::InvalidWidget("image requires src attribute".to_string())
2080    })?;
2081
2082    let src = match src_attr {
2083        AttributeValue::Static(s) => s.clone(),
2084        _ => String::new(),
2085    };
2086    let src_lit = proc_macro2::Literal::string(&src);
2087
2088    let width = node.attributes.get("width").and_then(|attr| {
2089        if let AttributeValue::Static(s) = attr {
2090            s.parse::<u32>().ok()
2091        } else {
2092            None
2093        }
2094    });
2095
2096    let height = node.attributes.get("height").and_then(|attr| {
2097        if let AttributeValue::Static(s) = attr {
2098            s.parse::<u32>().ok()
2099        } else {
2100            None
2101        }
2102    });
2103
2104    let mut image = quote! {
2105        iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
2106    };
2107
2108    // Apply native width/height if specified with integer values
2109    if let (Some(w), Some(h)) = (width, height) {
2110        image = quote! { #image.width(#w).height(#h) };
2111    } else if let Some(w) = width {
2112        image = quote! { #image.width(#w) };
2113    } else if let Some(h) = height {
2114        image = quote! { #image.height(#h) };
2115    }
2116
2117    // Check if we need container for NON-native layout attributes
2118    // (padding, alignment, classes - NOT width/height since those are native)
2119    // For Image, only wrap if there are alignment/padding/classes
2120    let needs_container = !node.classes.is_empty()
2121        || node.attributes.contains_key("align_x")
2122        || node.attributes.contains_key("align_y")
2123        || node.attributes.contains_key("padding");
2124
2125    if needs_container {
2126        // Wrap with container for layout attributes, but skip width/height (already applied)
2127        let mut container = quote! { iced::widget::container(#image) };
2128
2129        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2130            if let AttributeValue::Static(s) = attr {
2131                s.parse::<f32>().ok()
2132            } else {
2133                None
2134            }
2135        }) {
2136            container = quote! { #container.padding(#padding) };
2137        }
2138
2139        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2140            if let AttributeValue::Static(s) = attr {
2141                Some(s.clone())
2142            } else {
2143                None
2144            }
2145        }) {
2146            let align_expr = generate_horizontal_alignment_expr(&align_x);
2147            container = quote! { #container.align_x(#align_expr) };
2148        }
2149
2150        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2151            if let AttributeValue::Static(s) = attr {
2152                Some(s.clone())
2153            } else {
2154                None
2155            }
2156        }) {
2157            let align_expr = generate_vertical_alignment_expr(&align_y);
2158            container = quote! { #container.align_y(#align_expr) };
2159        }
2160
2161        if let Some(class_name) = node.classes.first() {
2162            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2163            container = quote! { #container.style(#style_fn_ident) };
2164        }
2165
2166        Ok(quote! { #container.into() })
2167    } else {
2168        Ok(quote! { #image.into() })
2169    }
2170}
2171
2172/// Generate SVG widget
2173fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2174    // Support both "src" (standard) and "path" (legacy) for backward compatibility
2175    let path_attr = node
2176        .attributes
2177        .get("src")
2178        .or_else(|| node.attributes.get("path"))
2179        .ok_or_else(|| {
2180            super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
2181        })?;
2182
2183    let path = match path_attr {
2184        AttributeValue::Static(s) => s.clone(),
2185        _ => String::new(),
2186    };
2187    let path_lit = proc_macro2::Literal::string(&path);
2188
2189    let width = node.attributes.get("width").and_then(|attr| {
2190        if let AttributeValue::Static(s) = attr {
2191            s.parse::<u32>().ok()
2192        } else {
2193            None
2194        }
2195    });
2196
2197    let height = node.attributes.get("height").and_then(|attr| {
2198        if let AttributeValue::Static(s) = attr {
2199            s.parse::<u32>().ok()
2200        } else {
2201            None
2202        }
2203    });
2204
2205    let mut svg = quote! {
2206        iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
2207    };
2208
2209    // Apply native width/height if specified with integer values
2210    if let (Some(w), Some(h)) = (width, height) {
2211        svg = quote! { #svg.width(#w).height(#h) };
2212    } else if let Some(w) = width {
2213        svg = quote! { #svg.width(#w) };
2214    } else if let Some(h) = height {
2215        svg = quote! { #svg.height(#h) };
2216    }
2217
2218    // Check if we need container for NON-native layout attributes
2219    // (padding, alignment, classes - NOT width/height since those are native)
2220    // For SVG, only wrap if there are alignment/padding/classes
2221    let needs_container = !node.classes.is_empty()
2222        || node.attributes.contains_key("align_x")
2223        || node.attributes.contains_key("align_y")
2224        || node.attributes.contains_key("padding");
2225
2226    if needs_container {
2227        // Wrap with container for layout attributes, but skip width/height (already applied)
2228        let mut container = quote! { iced::widget::container(#svg) };
2229
2230        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2231            if let AttributeValue::Static(s) = attr {
2232                s.parse::<f32>().ok()
2233            } else {
2234                None
2235            }
2236        }) {
2237            container = quote! { #container.padding(#padding) };
2238        }
2239
2240        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2241            if let AttributeValue::Static(s) = attr {
2242                Some(s.clone())
2243            } else {
2244                None
2245            }
2246        }) {
2247            let align_expr = generate_horizontal_alignment_expr(&align_x);
2248            container = quote! { #container.align_x(#align_expr) };
2249        }
2250
2251        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2252            if let AttributeValue::Static(s) = attr {
2253                Some(s.clone())
2254            } else {
2255                None
2256            }
2257        }) {
2258            let align_expr = generate_vertical_alignment_expr(&align_y);
2259            container = quote! { #container.align_y(#align_expr) };
2260        }
2261
2262        if let Some(class_name) = node.classes.first() {
2263            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2264            container = quote! { #container.style(#style_fn_ident) };
2265        }
2266
2267        Ok(quote! { #container.into() })
2268    } else {
2269        Ok(quote! { #svg.into() })
2270    }
2271}
2272
2273/// Generate pick list widget
2274fn generate_pick_list(
2275    node: &crate::WidgetNode,
2276    model_ident: &syn::Ident,
2277    message_ident: &syn::Ident,
2278    _style_classes: &HashMap<String, StyleClass>,
2279) -> Result<TokenStream, super::CodegenError> {
2280    let options_attr = node.attributes.get("options").ok_or_else(|| {
2281        super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2282    })?;
2283
2284    let options: Vec<String> = match options_attr {
2285        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2286        _ => Vec::new(),
2287    };
2288    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2289
2290    let selected_attr = node.attributes.get("selected");
2291    let selected_expr = selected_attr
2292        .map(|attr| generate_attribute_value(attr, model_ident))
2293        .unwrap_or(quote! { None });
2294
2295    let on_select = node
2296        .events
2297        .iter()
2298        .find(|e| e.event == crate::EventKind::Select);
2299
2300    if let Some(event) = on_select {
2301        let variant_name = to_upper_camel_case(&event.handler);
2302        let handler_ident = format_ident!("{}", variant_name);
2303        Ok(quote! {
2304            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2305        })
2306    } else {
2307        Ok(quote! {
2308            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2309        })
2310    }
2311}
2312
2313/// Generate combo box widget
2314fn generate_combo_box(
2315    node: &crate::WidgetNode,
2316    model_ident: &syn::Ident,
2317    message_ident: &syn::Ident,
2318    _style_classes: &HashMap<String, StyleClass>,
2319) -> Result<TokenStream, super::CodegenError> {
2320    let options_attr = node.attributes.get("options").ok_or_else(|| {
2321        super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2322    })?;
2323
2324    let options: Vec<String> = match options_attr {
2325        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2326        _ => Vec::new(),
2327    };
2328    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2329
2330    let selected_attr = node.attributes.get("selected");
2331    let selected_expr = selected_attr
2332        .map(|attr| generate_attribute_value(attr, model_ident))
2333        .unwrap_or(quote! { None });
2334
2335    let on_select = node
2336        .events
2337        .iter()
2338        .find(|e| e.event == crate::EventKind::Select);
2339
2340    if let Some(event) = on_select {
2341        let variant_name = to_upper_camel_case(&event.handler);
2342        let handler_ident = format_ident!("{}", variant_name);
2343        Ok(quote! {
2344            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2345        })
2346    } else {
2347        Ok(quote! {
2348            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2349        })
2350    }
2351}
2352
2353/// Generate tooltip widget
2354fn generate_tooltip(
2355    node: &crate::WidgetNode,
2356    model_ident: &syn::Ident,
2357    message_ident: &syn::Ident,
2358    style_classes: &HashMap<String, StyleClass>,
2359) -> Result<TokenStream, super::CodegenError> {
2360    let child = node.children.first().ok_or_else(|| {
2361        super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2362    })?;
2363    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2364
2365    let message_attr = node.attributes.get("message").ok_or_else(|| {
2366        super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2367    })?;
2368    let message_expr = generate_attribute_value(message_attr, model_ident);
2369
2370    Ok(quote! {
2371        iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2372    })
2373}
2374
2375/// Generate grid widget
2376fn generate_grid(
2377    node: &crate::WidgetNode,
2378    model_ident: &syn::Ident,
2379    message_ident: &syn::Ident,
2380    style_classes: &HashMap<String, StyleClass>,
2381) -> Result<TokenStream, super::CodegenError> {
2382    let children: Vec<TokenStream> = node
2383        .children
2384        .iter()
2385        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2386        .collect::<Result<_, _>>()?;
2387
2388    let columns = node
2389        .attributes
2390        .get("columns")
2391        .and_then(|attr| {
2392            if let AttributeValue::Static(s) = attr {
2393                s.parse::<u32>().ok()
2394            } else {
2395                None
2396            }
2397        })
2398        .unwrap_or(1);
2399
2400    let spacing = node.attributes.get("spacing").and_then(|attr| {
2401        if let AttributeValue::Static(s) = attr {
2402            s.parse::<f32>().ok()
2403        } else {
2404            None
2405        }
2406    });
2407
2408    let padding = node.attributes.get("padding").and_then(|attr| {
2409        if let AttributeValue::Static(s) = attr {
2410            s.parse::<f32>().ok()
2411        } else {
2412            None
2413        }
2414    });
2415
2416    let grid = quote! {
2417        iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2418    };
2419
2420    let grid = if let Some(s) = spacing {
2421        quote! { #grid.spacing(#s) }
2422    } else {
2423        grid
2424    };
2425
2426    let grid = if let Some(p) = padding {
2427        quote! { #grid.padding(#p) }
2428    } else {
2429        grid
2430    };
2431
2432    Ok(quote! { #grid.into() })
2433}
2434
2435/// Generate canvas widget
2436fn generate_canvas(
2437    node: &crate::WidgetNode,
2438    _model_ident: &syn::Ident,
2439    _message_ident: &syn::Ident,
2440    _style_classes: &HashMap<String, StyleClass>,
2441) -> Result<TokenStream, super::CodegenError> {
2442    let width = node.attributes.get("width").and_then(|attr| {
2443        if let AttributeValue::Static(s) = attr {
2444            s.parse::<f32>().ok()
2445        } else {
2446            None
2447        }
2448    });
2449
2450    let height = node.attributes.get("height").and_then(|attr| {
2451        if let AttributeValue::Static(s) = attr {
2452            s.parse::<f32>().ok()
2453        } else {
2454            None
2455        }
2456    });
2457
2458    let size = match (width, height) {
2459        (Some(w), Some(h)) => quote! { iced::Size::new(#w, #h) },
2460        (Some(w), None) => quote! { iced::Size::new(#w, 100.0) },
2461        (None, Some(h)) => quote! { iced::Size::new(100.0, #h) },
2462        _ => quote! { iced::Size::new(100.0, 100.0) },
2463    };
2464
2465    Ok(quote! {
2466        iced::widget::canvas(#size).into()
2467    })
2468}
2469
2470/// Generate float widget
2471fn generate_float(
2472    node: &crate::WidgetNode,
2473    model_ident: &syn::Ident,
2474    message_ident: &syn::Ident,
2475    style_classes: &HashMap<String, StyleClass>,
2476) -> Result<TokenStream, super::CodegenError> {
2477    let child = node.children.first().ok_or_else(|| {
2478        super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2479    })?;
2480    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2481
2482    let position = node
2483        .attributes
2484        .get("position")
2485        .and_then(|attr| {
2486            if let AttributeValue::Static(s) = attr {
2487                Some(s.clone())
2488            } else {
2489                None
2490            }
2491        })
2492        .unwrap_or_else(|| "TopRight".to_string());
2493
2494    let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2495        if let AttributeValue::Static(s) = attr {
2496            s.parse::<f32>().ok()
2497        } else {
2498            None
2499        }
2500    });
2501
2502    let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2503        if let AttributeValue::Static(s) = attr {
2504            s.parse::<f32>().ok()
2505        } else {
2506            None
2507        }
2508    });
2509
2510    let float = match position.as_str() {
2511        "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2512        "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2513        "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2514        "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2515        _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2516    };
2517
2518    let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2519        quote! { #float.offset_x(#ox).offset_y(#oy) }
2520    } else if let Some(ox) = offset_x {
2521        quote! { #float.offset_x(#ox) }
2522    } else if let Some(oy) = offset_y {
2523        quote! { #float.offset_y(#oy) }
2524    } else {
2525        float
2526    };
2527
2528    Ok(quote! { #float.into() })
2529}
2530
2531/// Generate for loop widget (iterates over collection)
2532///
2533/// Expects attributes:
2534/// - `each`: variable name for each item (e.g., "task")
2535/// - `in`: binding expression for the collection (e.g., "{filtered_tasks}")
2536fn generate_for(
2537    node: &crate::WidgetNode,
2538    model_ident: &syn::Ident,
2539    message_ident: &syn::Ident,
2540    style_classes: &HashMap<String, StyleClass>,
2541) -> Result<TokenStream, super::CodegenError> {
2542    // Get the 'in' attribute (collection to iterate)
2543    let in_attr = node.attributes.get("in").ok_or_else(|| {
2544        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2545    })?;
2546
2547    // Get the 'each' attribute (loop variable name)
2548    let var_name = node
2549        .attributes
2550        .get("each")
2551        .and_then(|attr| {
2552            if let AttributeValue::Static(s) = attr {
2553                Some(s.clone())
2554            } else {
2555                None
2556            }
2557        })
2558        .unwrap_or_else(|| "item".to_string());
2559
2560    let var_ident = format_ident!("{}", var_name);
2561
2562    // Generate the collection expression (raw, without .to_string())
2563    let collection_expr = generate_attribute_value_raw(in_attr, model_ident);
2564
2565    // Generate children widgets
2566    let children: Vec<TokenStream> = node
2567        .children
2568        .iter()
2569        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2570        .collect::<Result<_, _>>()?;
2571
2572    // Generate the for loop that builds widgets
2573    Ok(quote! {
2574        {
2575            let items: Vec<_> = #collection_expr;
2576            let widgets: Vec<iced::Element<'_, #message_ident>> = items
2577                .iter()
2578                .enumerate()
2579                .flat_map(|(index, #var_ident)| {
2580                    let _ = index; // Suppress unused warning if not used
2581                    vec![#(#children),*]
2582                })
2583                .collect();
2584            iced::widget::column(widgets).into()
2585        }
2586    })
2587}
2588
2589/// Generate if widget
2590fn generate_if(
2591    node: &crate::WidgetNode,
2592    model_ident: &syn::Ident,
2593    message_ident: &syn::Ident,
2594    style_classes: &HashMap<String, StyleClass>,
2595) -> Result<TokenStream, super::CodegenError> {
2596    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
2597        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
2598    })?;
2599
2600    let children: Vec<TokenStream> = node
2601        .children
2602        .iter()
2603        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2604        .collect::<Result<_, _>>()?;
2605
2606    let condition_expr = generate_attribute_value(condition_attr, model_ident);
2607
2608    Ok(quote! {
2609        if #condition_expr.parse::<bool>().unwrap_or(false) {
2610            iced::widget::column(vec![#(#children),*]).into()
2611        } else {
2612            iced::widget::column(vec![]).into()
2613        }
2614    })
2615}
2616
2617/// Generate custom widget
2618fn generate_custom_widget(
2619    node: &crate::WidgetNode,
2620    name: &str,
2621    model_ident: &syn::Ident,
2622    message_ident: &syn::Ident,
2623    style_classes: &HashMap<String, StyleClass>,
2624) -> Result<TokenStream, super::CodegenError> {
2625    let widget_ident = format_ident!("{}", name);
2626    let children: Vec<TokenStream> = node
2627        .children
2628        .iter()
2629        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2630        .collect::<Result<_, _>>()?;
2631
2632    Ok(quote! {
2633        #widget_ident(vec![#(#children),*]).into()
2634    })
2635}
2636
2637/// Generate attribute value expression with inlined bindings
2638fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2639    match attr {
2640        AttributeValue::Static(s) => {
2641            let lit = proc_macro2::Literal::string(s);
2642            quote! { #lit.to_string() }
2643        }
2644        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
2645        AttributeValue::Interpolated(parts) => {
2646            let parts_str: Vec<String> = parts
2647                .iter()
2648                .map(|part| match part {
2649                    InterpolatedPart::Literal(s) => s.clone(),
2650                    InterpolatedPart::Binding(_) => "{}".to_string(),
2651                })
2652                .collect();
2653            let binding_exprs: Vec<TokenStream> = parts
2654                .iter()
2655                .filter_map(|part| {
2656                    if let InterpolatedPart::Binding(expr) = part {
2657                        Some(generate_expr(&expr.expr))
2658                    } else {
2659                        None
2660                    }
2661                })
2662                .collect();
2663
2664            let format_string = parts_str.join("");
2665            let lit = proc_macro2::Literal::string(&format_string);
2666
2667            quote! { format!(#lit, #(#binding_exprs),*) }
2668        }
2669    }
2670}
2671
2672/// Generate attribute value without `.to_string()` conversion
2673/// Used for collections in for loops where we need the raw value
2674fn generate_attribute_value_raw(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2675    match attr {
2676        AttributeValue::Static(s) => {
2677            let lit = proc_macro2::Literal::string(s);
2678            quote! { #lit }
2679        }
2680        AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
2681        AttributeValue::Interpolated(parts) => {
2682            // For interpolated, we still need to generate a string
2683            let parts_str: Vec<String> = parts
2684                .iter()
2685                .map(|part| match part {
2686                    InterpolatedPart::Literal(s) => s.clone(),
2687                    InterpolatedPart::Binding(_) => "{}".to_string(),
2688                })
2689                .collect();
2690            let binding_exprs: Vec<TokenStream> = parts
2691                .iter()
2692                .filter_map(|part| {
2693                    if let InterpolatedPart::Binding(expr) = part {
2694                        Some(generate_expr(&expr.expr))
2695                    } else {
2696                        None
2697                    }
2698                })
2699                .collect();
2700
2701            let format_string = parts_str.join("");
2702            let lit = proc_macro2::Literal::string(&format_string);
2703
2704            quote! { format!(#lit, #(#binding_exprs),*) }
2705        }
2706    }
2707}
2708
2709// ============================================================================
2710// Functions with local variable context support (for loops)
2711// ============================================================================
2712
2713/// Generate text widget with local variable context
2714fn generate_text_with_locals(
2715    node: &crate::WidgetNode,
2716    model_ident: &syn::Ident,
2717    _style_classes: &HashMap<String, StyleClass>,
2718    local_vars: &std::collections::HashSet<String>,
2719) -> Result<TokenStream, super::CodegenError> {
2720    let value_attr = node.attributes.get("value").ok_or_else(|| {
2721        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
2722    })?;
2723
2724    let value_expr = generate_attribute_value_with_locals(value_attr, model_ident, local_vars);
2725
2726    let mut text_widget = quote! {
2727        iced::widget::text(#value_expr)
2728    };
2729
2730    // Apply size attribute
2731    if let Some(size) = node.attributes.get("size").and_then(|attr| {
2732        if let AttributeValue::Static(s) = attr {
2733            s.parse::<f32>().ok()
2734        } else {
2735            None
2736        }
2737    }) {
2738        text_widget = quote! { #text_widget.size(#size) };
2739    }
2740
2741    // Apply weight attribute
2742    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
2743        if let AttributeValue::Static(s) = attr {
2744            Some(s.clone())
2745        } else {
2746            None
2747        }
2748    }) {
2749        let weight_expr = match weight.to_lowercase().as_str() {
2750            "bold" => quote! { iced::font::Weight::Bold },
2751            "semibold" => quote! { iced::font::Weight::Semibold },
2752            "medium" => quote! { iced::font::Weight::Medium },
2753            "light" => quote! { iced::font::Weight::Light },
2754            _ => quote! { iced::font::Weight::Normal },
2755        };
2756        text_widget = quote! {
2757            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
2758        };
2759    }
2760
2761    // Apply inline style color if present
2762    if let Some(ref style_props) = node.style
2763        && let Some(ref color) = style_props.color
2764    {
2765        let color_expr = generate_color_expr(color);
2766        text_widget = quote! { #text_widget.color(#color_expr) };
2767    }
2768
2769    Ok(maybe_wrap_in_container(text_widget, node))
2770}
2771
2772/// Generate button widget with local variable context
2773fn generate_button_with_locals(
2774    node: &crate::WidgetNode,
2775    model_ident: &syn::Ident,
2776    message_ident: &syn::Ident,
2777    style_classes: &HashMap<String, StyleClass>,
2778    local_vars: &std::collections::HashSet<String>,
2779) -> Result<TokenStream, super::CodegenError> {
2780    let label_attr = node.attributes.get("label").ok_or_else(|| {
2781        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
2782    })?;
2783
2784    let label_expr = generate_attribute_value_with_locals(label_attr, model_ident, local_vars);
2785
2786    let on_click = node
2787        .events
2788        .iter()
2789        .find(|e| e.event == crate::EventKind::Click);
2790
2791    let mut button = quote! {
2792        iced::widget::button(iced::widget::text(#label_expr))
2793    };
2794
2795    if let Some(event) = on_click {
2796        let variant_name = to_upper_camel_case(&event.handler);
2797        let handler_ident = format_ident!("{}", variant_name);
2798
2799        let param_expr = if let Some(ref param) = event.param {
2800            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
2801            quote! { (#param_tokens) }
2802        } else {
2803            quote! {}
2804        };
2805
2806        button = quote! {
2807            #button.on_press(#message_ident::#handler_ident #param_expr)
2808        };
2809    }
2810
2811    // Apply styles
2812    button = apply_widget_style(button, node, "button", style_classes)?;
2813
2814    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#button) })
2815}
2816
2817/// Generate container widget with local variable context
2818fn generate_container_with_locals(
2819    node: &crate::WidgetNode,
2820    widget_type: &str,
2821    model_ident: &syn::Ident,
2822    message_ident: &syn::Ident,
2823    style_classes: &HashMap<String, StyleClass>,
2824    local_vars: &std::collections::HashSet<String>,
2825) -> Result<TokenStream, super::CodegenError> {
2826    let children: Vec<TokenStream> = node
2827        .children
2828        .iter()
2829        .map(|child| {
2830            generate_widget_with_locals(
2831                child,
2832                model_ident,
2833                message_ident,
2834                style_classes,
2835                local_vars,
2836            )
2837        })
2838        .collect::<Result<_, _>>()?;
2839
2840    let mut container = match widget_type {
2841        "column" => {
2842            quote! { iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
2843        }
2844        "row" => {
2845            quote! { iced::widget::row({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
2846        }
2847        "scrollable" => {
2848            quote! { iced::widget::scrollable(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
2849        }
2850        _ => {
2851            // container wraps a single child
2852            if children.len() == 1 {
2853                let child = &children[0];
2854                quote! { iced::widget::container(#child) }
2855            } else {
2856                quote! { iced::widget::container(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
2857            }
2858        }
2859    };
2860
2861    // Get merged layout from node.layout and style classes
2862    let merged_layout = get_merged_layout(node, style_classes);
2863
2864    // Get spacing from attributes or merged layout
2865    let spacing = node
2866        .attributes
2867        .get("spacing")
2868        .and_then(|attr| {
2869            if let AttributeValue::Static(s) = attr {
2870                s.parse::<f32>().ok()
2871            } else {
2872                None
2873            }
2874        })
2875        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
2876
2877    // Apply spacing for column/row
2878    if let Some(s) = spacing
2879        && (widget_type == "column" || widget_type == "row")
2880    {
2881        container = quote! { #container.spacing(#s) };
2882    }
2883
2884    // Get padding from attributes or merged layout
2885    let padding = node
2886        .attributes
2887        .get("padding")
2888        .and_then(|attr| {
2889            if let AttributeValue::Static(s) = attr {
2890                s.parse::<f32>().ok()
2891            } else {
2892                None
2893            }
2894        })
2895        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
2896
2897    // Apply padding
2898    if let Some(p) = padding {
2899        container = quote! { #container.padding(#p) };
2900    }
2901
2902    // Apply width from attributes or merged layout
2903    let width_from_attr = node.attributes.get("width").and_then(|attr| {
2904        if let AttributeValue::Static(s) = attr {
2905            Some(s.clone())
2906        } else {
2907            None
2908        }
2909    });
2910    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
2911
2912    if let Some(width) = width_from_attr {
2913        let width_expr = generate_length_expr(&width);
2914        container = quote! { #container.width(#width_expr) };
2915    } else if let Some(layout_width) = width_from_layout {
2916        let width_expr = generate_layout_length_expr(layout_width);
2917        container = quote! { #container.width(#width_expr) };
2918    }
2919
2920    // Apply height from attributes or merged layout
2921    let height_from_attr = node.attributes.get("height").and_then(|attr| {
2922        if let AttributeValue::Static(s) = attr {
2923            Some(s.clone())
2924        } else {
2925            None
2926        }
2927    });
2928    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
2929
2930    if let Some(height) = height_from_attr {
2931        let height_expr = generate_length_expr(&height);
2932        container = quote! { #container.height(#height_expr) };
2933    } else if let Some(layout_height) = height_from_layout {
2934        let height_expr = generate_layout_length_expr(layout_height);
2935        container = quote! { #container.height(#height_expr) };
2936    }
2937
2938    // Apply alignment for row/column
2939    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2940        if let AttributeValue::Static(s) = attr {
2941            Some(s.clone())
2942        } else {
2943            None
2944        }
2945    }) && widget_type == "row"
2946    {
2947        let alignment_expr = match align_y.to_lowercase().as_str() {
2948            "top" | "start" => quote! { iced::alignment::Vertical::Top },
2949            "bottom" | "end" => quote! { iced::alignment::Vertical::Bottom },
2950            _ => quote! { iced::alignment::Vertical::Center },
2951        };
2952        container = quote! { #container.align_y(#alignment_expr) };
2953    }
2954
2955    // Apply styles
2956    if widget_type == "container" {
2957        container = apply_widget_style(container, node, "container", style_classes)?;
2958    }
2959
2960    // Use explicit into() conversion to help type inference with nested containers
2961    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#container) })
2962}
2963
2964/// Generate for loop widget with local variable context
2965fn generate_for_with_locals(
2966    node: &crate::WidgetNode,
2967    model_ident: &syn::Ident,
2968    message_ident: &syn::Ident,
2969    style_classes: &HashMap<String, StyleClass>,
2970    local_vars: &std::collections::HashSet<String>,
2971) -> Result<TokenStream, super::CodegenError> {
2972    // Get the 'in' attribute (collection to iterate)
2973    let in_attr = node.attributes.get("in").ok_or_else(|| {
2974        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2975    })?;
2976
2977    // Get the 'each' attribute (loop variable name)
2978    let var_name = node
2979        .attributes
2980        .get("each")
2981        .and_then(|attr| {
2982            if let AttributeValue::Static(s) = attr {
2983                Some(s.clone())
2984            } else {
2985                None
2986            }
2987        })
2988        .unwrap_or_else(|| "item".to_string());
2989
2990    let var_ident = format_ident!("{}", var_name);
2991
2992    // Generate the collection expression (raw, without .to_string())
2993    let collection_expr =
2994        generate_attribute_value_raw_with_locals(in_attr, model_ident, local_vars);
2995
2996    // Create new local vars set including the loop variable
2997    let mut new_local_vars = local_vars.clone();
2998    new_local_vars.insert(var_name.clone());
2999    new_local_vars.insert("index".to_string());
3000
3001    // Generate children widgets with the new local context
3002    let children: Vec<TokenStream> = node
3003        .children
3004        .iter()
3005        .map(|child| {
3006            generate_widget_with_locals(
3007                child,
3008                model_ident,
3009                message_ident,
3010                style_classes,
3011                &new_local_vars,
3012            )
3013        })
3014        .collect::<Result<_, _>>()?;
3015
3016    // Generate the for loop that builds widgets
3017    // Use explicit type annotations to help Rust's type inference
3018    Ok(quote! {
3019        {
3020            let mut widgets: Vec<Element<'_, #message_ident>> = Vec::new();
3021            for (index, #var_ident) in (#collection_expr).iter().enumerate() {
3022                let _ = index;
3023                #(
3024                    let child_widget: Element<'_, #message_ident> = #children;
3025                    widgets.push(child_widget);
3026                )*
3027            }
3028            Into::<Element<'_, #message_ident>>::into(iced::widget::column(widgets))
3029        }
3030    })
3031}
3032
3033/// Generate if widget with local variable context
3034fn generate_if_with_locals(
3035    node: &crate::WidgetNode,
3036    model_ident: &syn::Ident,
3037    message_ident: &syn::Ident,
3038    style_classes: &HashMap<String, StyleClass>,
3039    local_vars: &std::collections::HashSet<String>,
3040) -> Result<TokenStream, super::CodegenError> {
3041    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3042        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3043    })?;
3044
3045    let children: Vec<TokenStream> = node
3046        .children
3047        .iter()
3048        .map(|child| {
3049            generate_widget_with_locals(
3050                child,
3051                model_ident,
3052                message_ident,
3053                style_classes,
3054                local_vars,
3055            )
3056        })
3057        .collect::<Result<_, _>>()?;
3058
3059    let condition_expr =
3060        generate_attribute_value_with_locals(condition_attr, model_ident, local_vars);
3061
3062    Ok(quote! {
3063        if #condition_expr.parse::<bool>().unwrap_or(false) {
3064            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }))
3065        } else {
3066            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![]; children }))
3067        }
3068    })
3069}
3070
3071/// Generate checkbox widget with local variable context
3072fn generate_checkbox_with_locals(
3073    node: &crate::WidgetNode,
3074    model_ident: &syn::Ident,
3075    message_ident: &syn::Ident,
3076    style_classes: &HashMap<String, StyleClass>,
3077    local_vars: &std::collections::HashSet<String>,
3078) -> Result<TokenStream, super::CodegenError> {
3079    // Get checked attribute
3080    let checked_attr = node.attributes.get("checked");
3081    let checked_expr = if let Some(attr) = checked_attr {
3082        generate_attribute_value_raw_with_locals(attr, model_ident, local_vars)
3083    } else {
3084        quote! { false }
3085    };
3086
3087    // Get on_change event
3088    let on_change = node
3089        .events
3090        .iter()
3091        .find(|e| e.event == crate::EventKind::Change);
3092
3093    let mut checkbox = quote! {
3094        iced::widget::checkbox(#checked_expr)
3095    };
3096
3097    if let Some(event) = on_change {
3098        let variant_name = to_upper_camel_case(&event.handler);
3099        let handler_ident = format_ident!("{}", variant_name);
3100
3101        let param_expr = if let Some(ref param) = event.param {
3102            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
3103            quote! { (#param_tokens) }
3104        } else {
3105            quote! {}
3106        };
3107
3108        checkbox = quote! {
3109            #checkbox.on_toggle(move |_| #message_ident::#handler_ident #param_expr)
3110        };
3111    }
3112
3113    // Apply size
3114    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3115        if let AttributeValue::Static(s) = attr {
3116            s.parse::<f32>().ok()
3117        } else {
3118            None
3119        }
3120    }) {
3121        checkbox = quote! { #checkbox.size(#size) };
3122    }
3123
3124    // Apply styles
3125    checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
3126
3127    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#checkbox) })
3128}
3129
3130/// Generate text_input widget with local variable context
3131fn generate_text_input_with_locals(
3132    node: &crate::WidgetNode,
3133    model_ident: &syn::Ident,
3134    message_ident: &syn::Ident,
3135    style_classes: &HashMap<String, StyleClass>,
3136    local_vars: &std::collections::HashSet<String>,
3137) -> Result<TokenStream, super::CodegenError> {
3138    // Get placeholder
3139    let placeholder = node
3140        .attributes
3141        .get("placeholder")
3142        .and_then(|attr| {
3143            if let AttributeValue::Static(s) = attr {
3144                Some(s.clone())
3145            } else {
3146                None
3147            }
3148        })
3149        .unwrap_or_default();
3150    let placeholder_lit = proc_macro2::Literal::string(&placeholder);
3151
3152    // Get value attribute
3153    let value_attr = node.attributes.get("value");
3154    let value_expr = if let Some(attr) = value_attr {
3155        generate_attribute_value_with_locals(attr, model_ident, local_vars)
3156    } else {
3157        quote! { String::new() }
3158    };
3159
3160    let on_input = node
3161        .events
3162        .iter()
3163        .find(|e| e.event == crate::EventKind::Input);
3164
3165    let on_submit = node
3166        .events
3167        .iter()
3168        .find(|e| e.event == crate::EventKind::Submit);
3169
3170    let mut text_input = quote! {
3171        iced::widget::text_input(#placeholder_lit, &#value_expr)
3172    };
3173
3174    // Apply on_input
3175    if let Some(event) = on_input {
3176        let variant_name = to_upper_camel_case(&event.handler);
3177        let handler_ident = format_ident!("{}", variant_name);
3178        text_input = quote! { #text_input.on_input(|v| #message_ident::#handler_ident(v)) };
3179    }
3180
3181    // Apply on_submit
3182    if let Some(event) = on_submit {
3183        let variant_name = to_upper_camel_case(&event.handler);
3184        let handler_ident = format_ident!("{}", variant_name);
3185        text_input = quote! { #text_input.on_submit(#message_ident::#handler_ident) };
3186    }
3187
3188    // Apply size
3189    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3190        if let AttributeValue::Static(s) = attr {
3191            s.parse::<f32>().ok()
3192        } else {
3193            None
3194        }
3195    }) {
3196        text_input = quote! { #text_input.size(#size) };
3197    }
3198
3199    // Apply padding
3200    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
3201        if let AttributeValue::Static(s) = attr {
3202            s.parse::<f32>().ok()
3203        } else {
3204            None
3205        }
3206    }) {
3207        text_input = quote! { #text_input.padding(#padding) };
3208    }
3209
3210    // Apply width
3211    if let Some(width) = node.attributes.get("width").and_then(|attr| {
3212        if let AttributeValue::Static(s) = attr {
3213            Some(generate_length_expr(s))
3214        } else {
3215            None
3216        }
3217    }) {
3218        text_input = quote! { #text_input.width(#width) };
3219    }
3220
3221    // Apply styles
3222    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
3223
3224    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#text_input) })
3225}
3226
3227/// Generate attribute value expression with local variable context
3228fn generate_attribute_value_with_locals(
3229    attr: &AttributeValue,
3230    _model_ident: &syn::Ident,
3231    local_vars: &std::collections::HashSet<String>,
3232) -> TokenStream {
3233    match attr {
3234        AttributeValue::Static(s) => {
3235            let lit = proc_macro2::Literal::string(s);
3236            quote! { #lit.to_string() }
3237        }
3238        AttributeValue::Binding(expr) => {
3239            super::bindings::generate_expr_with_locals(&expr.expr, local_vars)
3240        }
3241        AttributeValue::Interpolated(parts) => {
3242            let parts_str: Vec<String> = parts
3243                .iter()
3244                .map(|part| match part {
3245                    InterpolatedPart::Literal(s) => s.clone(),
3246                    InterpolatedPart::Binding(_) => "{}".to_string(),
3247                })
3248                .collect();
3249            let binding_exprs: Vec<TokenStream> = parts
3250                .iter()
3251                .filter_map(|part| {
3252                    if let InterpolatedPart::Binding(expr) = part {
3253                        Some(super::bindings::generate_expr_with_locals(
3254                            &expr.expr, local_vars,
3255                        ))
3256                    } else {
3257                        None
3258                    }
3259                })
3260                .collect();
3261
3262            let format_string = parts_str.join("");
3263            let lit = proc_macro2::Literal::string(&format_string);
3264
3265            quote! { format!(#lit, #(#binding_exprs),*) }
3266        }
3267    }
3268}
3269
3270/// Generate attribute value without `.to_string()` conversion with local variable context
3271fn generate_attribute_value_raw_with_locals(
3272    attr: &AttributeValue,
3273    _model_ident: &syn::Ident,
3274    local_vars: &std::collections::HashSet<String>,
3275) -> TokenStream {
3276    match attr {
3277        AttributeValue::Static(s) => {
3278            let lit = proc_macro2::Literal::string(s);
3279            quote! { #lit }
3280        }
3281        AttributeValue::Binding(expr) => {
3282            super::bindings::generate_bool_expr_with_locals(&expr.expr, local_vars)
3283        }
3284        AttributeValue::Interpolated(parts) => {
3285            let parts_str: Vec<String> = parts
3286                .iter()
3287                .map(|part| match part {
3288                    InterpolatedPart::Literal(s) => s.clone(),
3289                    InterpolatedPart::Binding(_) => "{}".to_string(),
3290                })
3291                .collect();
3292            let binding_exprs: Vec<TokenStream> = parts
3293                .iter()
3294                .filter_map(|part| {
3295                    if let InterpolatedPart::Binding(expr) = part {
3296                        Some(super::bindings::generate_expr_with_locals(
3297                            &expr.expr, local_vars,
3298                        ))
3299                    } else {
3300                        None
3301                    }
3302                })
3303                .collect();
3304
3305            let format_string = parts_str.join("");
3306            let lit = proc_macro2::Literal::string(&format_string);
3307
3308            quote! { format!(#lit, #(#binding_exprs),*) }
3309        }
3310    }
3311}
3312
3313#[cfg(test)]
3314mod tests {
3315    use super::*;
3316    use crate::parse;
3317
3318    #[test]
3319    fn test_view_generation() {
3320        let xml = r#"<column><text value="Hello" /></column>"#;
3321        let doc = parse(xml).unwrap();
3322
3323        let result = generate_view(&doc, "Model", "Message").unwrap();
3324        let code = result.to_string();
3325
3326        assert!(code.contains("text"));
3327        assert!(code.contains("column"));
3328    }
3329
3330    #[test]
3331    fn test_view_generation_with_binding() {
3332        let xml = r#"<column><text value="{name}" /></column>"#;
3333        let doc = parse(xml).unwrap();
3334
3335        let result = generate_view(&doc, "Model", "Message").unwrap();
3336        let code = result.to_string();
3337
3338        assert!(code.contains("name"));
3339        assert!(code.contains("to_string"));
3340    }
3341
3342    #[test]
3343    fn test_button_with_handler() {
3344        let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
3345        let doc = parse(xml).unwrap();
3346
3347        let result = generate_view(&doc, "Model", "Message").unwrap();
3348        let code = result.to_string();
3349
3350        assert!(code.contains("button"));
3351        assert!(code.contains("HandleClick"));
3352    }
3353
3354    #[test]
3355    fn test_container_with_children() {
3356        let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
3357        let doc = parse(xml).unwrap();
3358
3359        let result = generate_view(&doc, "Model", "Message").unwrap();
3360        let code = result.to_string();
3361
3362        assert!(code.contains("column"));
3363        assert!(code.contains("spacing"));
3364    }
3365
3366    #[test]
3367    fn test_button_with_inline_style() {
3368        use crate::ir::node::WidgetNode;
3369        use crate::ir::style::{Background, Color, StyleProperties};
3370        use std::collections::HashMap;
3371
3372        // Manually construct a button with inline style
3373        let button_node = WidgetNode {
3374            kind: WidgetKind::Button,
3375            id: None,
3376            attributes: {
3377                let mut attrs = HashMap::new();
3378                attrs.insert(
3379                    "label".to_string(),
3380                    AttributeValue::Static("Test".to_string()),
3381                );
3382                attrs
3383            },
3384            events: vec![],
3385            children: vec![],
3386            span: Default::default(),
3387            style: Some(StyleProperties {
3388                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
3389                color: Some(Color::from_rgb8(255, 255, 255)),
3390                border: None,
3391                shadow: None,
3392                opacity: None,
3393                transform: None,
3394            }),
3395            layout: None,
3396            theme_ref: None,
3397            classes: vec![],
3398            breakpoint_attributes: HashMap::new(),
3399            inline_state_variants: HashMap::new(),
3400        };
3401
3402        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3403        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3404        let style_classes = HashMap::new();
3405
3406        let result =
3407            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
3408        let code = result.to_string();
3409
3410        // Should contain style closure (note: quote! adds spaces)
3411        assert!(code.contains("style"));
3412        assert!(code.contains("button :: Status"));
3413        assert!(code.contains("button :: Style"));
3414        assert!(code.contains("background"));
3415        assert!(code.contains("text_color"));
3416    }
3417
3418    #[test]
3419    fn test_button_with_css_class() {
3420        use crate::ir::node::WidgetNode;
3421        use crate::ir::theme::StyleClass;
3422        use std::collections::HashMap;
3423
3424        // Manually construct a button with CSS class
3425        let button_node = WidgetNode {
3426            kind: WidgetKind::Button,
3427            id: None,
3428            attributes: {
3429                let mut attrs = HashMap::new();
3430                attrs.insert(
3431                    "label".to_string(),
3432                    AttributeValue::Static("Test".to_string()),
3433                );
3434                attrs
3435            },
3436            events: vec![],
3437            children: vec![],
3438            span: Default::default(),
3439            style: None,
3440            layout: None,
3441            theme_ref: None,
3442            classes: vec!["primary-button".to_string()],
3443            breakpoint_attributes: HashMap::new(),
3444            inline_state_variants: HashMap::new(),
3445        };
3446
3447        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3448        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3449        let style_classes: HashMap<String, StyleClass> = HashMap::new();
3450
3451        let result =
3452            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
3453        let code = result.to_string();
3454
3455        // Should call style function (note: quote! adds spaces)
3456        assert!(code.contains("style"));
3457        assert!(code.contains("style_primary_button"));
3458    }
3459
3460    #[test]
3461    fn test_container_with_inline_style() {
3462        use crate::ir::node::WidgetNode;
3463        use crate::ir::style::{
3464            Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
3465        };
3466        use crate::ir::theme::StyleClass;
3467        use std::collections::HashMap;
3468
3469        let container_node = WidgetNode {
3470            kind: WidgetKind::Container,
3471            id: None,
3472            attributes: HashMap::new(),
3473            events: vec![],
3474            children: vec![],
3475            span: Default::default(),
3476            style: Some(StyleProperties {
3477                background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
3478                color: None,
3479                border: Some(Border {
3480                    width: 2.0,
3481                    color: Color::from_rgb8(200, 200, 200),
3482                    radius: BorderRadius {
3483                        top_left: 8.0,
3484                        top_right: 8.0,
3485                        bottom_right: 8.0,
3486                        bottom_left: 8.0,
3487                    },
3488                    style: BorderStyle::Solid,
3489                }),
3490                shadow: None,
3491                opacity: None,
3492                transform: None,
3493            }),
3494            layout: None,
3495            theme_ref: None,
3496            classes: vec![],
3497            breakpoint_attributes: HashMap::new(),
3498            inline_state_variants: HashMap::new(),
3499        };
3500
3501        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3502        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3503        let style_classes: HashMap<String, StyleClass> = HashMap::new();
3504
3505        let result = generate_container(
3506            &container_node,
3507            "container",
3508            &model_ident,
3509            &message_ident,
3510            &style_classes,
3511        )
3512        .unwrap();
3513        let code = result.to_string();
3514
3515        // Should contain style closure (note: quote! adds spaces)
3516        assert!(code.contains("style"));
3517        assert!(code.contains("container :: Style"));
3518        assert!(code.contains("background"));
3519        assert!(code.contains("border"));
3520    }
3521}