Skip to main content

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        WidgetKind::DatePicker => {
200            generate_date_picker(node, model_ident, message_ident, style_classes)
201        }
202        WidgetKind::TimePicker => {
203            generate_time_picker(node, model_ident, message_ident, style_classes)
204        }
205        WidgetKind::ColorPicker => {
206            generate_color_picker(node, model_ident, message_ident, style_classes)
207        }
208        WidgetKind::Menu => generate_menu(node, model_ident, message_ident, style_classes),
209        WidgetKind::MenuItem | WidgetKind::MenuSeparator => {
210            // These are handled by generate_menu and shouldn't appear as top-level widgets
211            Err(super::CodegenError::InvalidWidget(format!(
212                "{:?} must be inside a <menu>",
213                node.kind
214            )))
215        }
216        WidgetKind::ContextMenu => {
217            generate_context_menu(node, model_ident, message_ident, style_classes, local_vars)
218        }
219        WidgetKind::DataTable => {
220            generate_data_table(node, model_ident, message_ident, style_classes)
221        }
222        WidgetKind::DataColumn => {
223            // These are handled by generate_data_table logic, shouldn't appear as top-level widgets
224            Err(super::CodegenError::InvalidWidget(format!(
225                "{:?} must be inside a <data_table>",
226                node.kind
227            )))
228        }
229        WidgetKind::TreeView => {
230            generate_tree_view(node, model_ident, message_ident, style_classes, local_vars)
231        }
232        WidgetKind::TreeNode => {
233            // These are handled by generate_tree_view logic, shouldn't appear as top-level widgets
234            Err(super::CodegenError::InvalidWidget(format!(
235                "{:?} must be inside a <tree_view>",
236                node.kind
237            )))
238        }
239        WidgetKind::CanvasRect
240        | WidgetKind::CanvasCircle
241        | WidgetKind::CanvasLine
242        | WidgetKind::CanvasText
243        | WidgetKind::CanvasGroup => {
244            // These are handled by generate_canvas logic, shouldn't appear as top-level widgets
245            Err(super::CodegenError::InvalidWidget(format!(
246                "{:?} is not a top-level widget and must be inside a <canvas>",
247                node.kind
248            )))
249        }
250        WidgetKind::TabBar => generate_tab_bar_with_locals(
251            node,
252            model_ident,
253            message_ident,
254            style_classes,
255            local_vars,
256        ),
257        WidgetKind::Tab => {
258            // Tab must be inside TabBar, handled by generate_tab_bar
259            Err(super::CodegenError::InvalidWidget(
260                "Tab must be inside TabBar".to_string(),
261            ))
262        }
263    }
264}
265
266// ============================================================================
267// Style Application Functions
268// ============================================================================
269
270/// Apply inline styles or CSS classes to a widget
271///
272/// Priority order:
273/// 1. Inline styles (node.style) - highest priority
274/// 2. CSS classes (node.classes) - medium priority
275/// 3. Default Iced styles - lowest priority (fallback)
276fn apply_widget_style(
277    widget: TokenStream,
278    node: &crate::WidgetNode,
279    widget_type: &str,
280    style_classes: &HashMap<String, StyleClass>,
281) -> Result<TokenStream, super::CodegenError> {
282    // Check if widget has any styling
283    let has_inline_style = node.style.is_some();
284    let has_classes = !node.classes.is_empty();
285
286    // Check for dynamic class binding (e.g., class="{if filter == 'All' then 'btn_primary' else 'btn_filter'}")
287    let class_binding = node.attributes.get("class").and_then(|attr| match attr {
288        AttributeValue::Binding(expr) => Some(expr),
289        _ => None,
290    });
291    let has_class_binding = class_binding.is_some();
292
293    if !has_inline_style && !has_classes && !has_class_binding {
294        // No styling needed, return widget as-is
295        return Ok(widget);
296    }
297
298    // Get style class if widget has classes
299    let style_class = if let Some(class_name) = node.classes.first() {
300        style_classes.get(class_name)
301    } else {
302        None
303    };
304
305    // Generate style closure based on priority
306    if let Some(ref style_props) = node.style {
307        // Priority 1: Inline styles
308        let style_closure =
309            generate_inline_style_closure(style_props, widget_type, &node.kind, style_class)?;
310        Ok(quote! {
311            #widget.style(#style_closure)
312        })
313    } else if let Some(class_name) = node.classes.first() {
314        // Priority 2: CSS class (use first class for now)
315        // Generate a wrapper closure that matches the widget's expected signature
316        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
317
318        match widget_type {
319            "text_input" => {
320                // text_input.style() expects fn(theme, status) -> text_input::Style
321                // Style class functions return container::Style, so we need to convert
322                Ok(quote! {
323                    #widget.style(|theme: &iced::Theme, _status: iced::widget::text_input::Status| {
324                        let container_style = #style_fn_ident(theme);
325                        iced::widget::text_input::Style {
326                            background: container_style.background.unwrap_or(iced::Background::Color(theme.extended_palette().background.base.color)),
327                            border: container_style.border,
328                            icon: theme.extended_palette().background.base.text,
329                            placeholder: theme.extended_palette().background.weak.text,
330                            value: container_style.text_color.unwrap_or(theme.extended_palette().background.base.text),
331                            selection: theme.extended_palette().primary.weak.color,
332                        }
333                    })
334                })
335            }
336            "checkbox" => {
337                // checkbox.style() expects fn(theme, status) -> checkbox::Style
338                // Check if the style class has state variants (needs 2-arg call with button::Status)
339                let has_state_variants = style_class
340                    .map(|sc| !sc.state_variants.is_empty())
341                    .unwrap_or(false);
342
343                if has_state_variants {
344                    // Style function expects (theme, button::Status), map checkbox status to button status
345                    Ok(quote! {
346                        #widget.style(|theme: &iced::Theme, status: iced::widget::checkbox::Status| {
347                            // Map checkbox status to button status for the style function
348                            let button_status = match status {
349                                iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
350                                iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
351                                iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
352                            };
353                            let button_style = #style_fn_ident(theme, button_status);
354                            iced::widget::checkbox::Style {
355                                background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
356                                icon_color: button_style.text_color,
357                                border: button_style.border,
358                                text_color: None,
359                            }
360                        })
361                    })
362                } else {
363                    // Style function expects only theme (container style)
364                    Ok(quote! {
365                        #widget.style(|theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
366                            let container_style = #style_fn_ident(theme);
367                            iced::widget::checkbox::Style {
368                                background: container_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
369                                icon_color: container_style.text_color,
370                                border: container_style.border,
371                                text_color: None,
372                            }
373                        })
374                    })
375                }
376            }
377            "button" => {
378                // button.style() expects fn(theme, status) -> button::Style
379                // Style class functions for buttons already have the correct signature
380                Ok(quote! {
381                    #widget.style(#style_fn_ident)
382                })
383            }
384            _ => {
385                // Default: container-style widgets (container, row, column, etc.)
386                Ok(quote! {
387                    #widget.style(#style_fn_ident)
388                })
389            }
390        }
391    } else if let Some(binding_expr) = class_binding {
392        // Priority 3: Dynamic class binding
393        generate_dynamic_class_style(widget, binding_expr, widget_type, style_classes)
394    } else {
395        Ok(widget)
396    }
397}
398
399/// Generate style application for dynamic class bindings
400///
401/// Generates code that evaluates the binding at runtime and dispatches
402/// to the appropriate style function based on the class name.
403fn generate_dynamic_class_style(
404    widget: TokenStream,
405    binding_expr: &crate::expr::BindingExpr,
406    widget_type: &str,
407    style_classes: &HashMap<String, StyleClass>,
408) -> Result<TokenStream, super::CodegenError> {
409    // Generate code to evaluate the binding
410    let class_expr = super::bindings::generate_expr(&binding_expr.expr);
411
412    match widget_type {
413        "button" => {
414            // Generate match arms only for button-compatible style classes
415            // (those with state variants, which generate fn(theme, status) -> button::Style)
416            let mut match_arms = Vec::new();
417            for (class_name, style_class) in style_classes.iter() {
418                // Only include classes that have state variants (button styles)
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                    match_arms.push(quote! {
423                        #class_lit => #style_fn(_theme, status),
424                    });
425                }
426            }
427
428            Ok(quote! {
429                #widget.style({
430                    let __class_name = #class_expr;
431                    move |_theme: &iced::Theme, status: iced::widget::button::Status| {
432                        match __class_name.as_str() {
433                            #(#match_arms)*
434                            _ => iced::widget::button::Style::default(),
435                        }
436                    }
437                })
438            })
439        }
440        "checkbox" => {
441            // For checkboxes, map checkbox status to button status
442            // Only use style classes with state variants
443            let mut checkbox_match_arms = Vec::new();
444            for (class_name, style_class) in style_classes.iter() {
445                if !style_class.state_variants.is_empty() {
446                    let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
447                    let class_lit = proc_macro2::Literal::string(class_name);
448                    checkbox_match_arms.push(quote! {
449                        #class_lit => {
450                            let button_style = #style_fn(_theme, button_status);
451                            iced::widget::checkbox::Style {
452                                background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
453                                icon_color: button_style.text_color,
454                                border: button_style.border,
455                                text_color: None,
456                            }
457                        }
458                    });
459                }
460            }
461            Ok(quote! {
462                #widget.style({
463                    let __class_name = #class_expr;
464                    move |_theme: &iced::Theme, status: iced::widget::checkbox::Status| {
465                        let button_status = match status {
466                            iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
467                            iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
468                            iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
469                        };
470                        match __class_name.as_str() {
471                            #(#checkbox_match_arms)*
472                            _ => iced::widget::checkbox::Style::default(),
473                        }
474                    }
475                })
476            })
477        }
478        _ => {
479            // For other widgets (container, etc.), use container style functions
480            // Only include classes without state variants (container styles)
481            let mut container_match_arms = Vec::new();
482            for (class_name, style_class) in style_classes.iter() {
483                if style_class.state_variants.is_empty() {
484                    let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
485                    let class_lit = proc_macro2::Literal::string(class_name);
486                    container_match_arms.push(quote! {
487                        #class_lit => #style_fn(_theme),
488                    });
489                }
490            }
491            Ok(quote! {
492                #widget.style({
493                    let __class_name = #class_expr;
494                    move |_theme: &iced::Theme| {
495                        match __class_name.as_str() {
496                            #(#container_match_arms)*
497                            _ => iced::widget::container::Style::default(),
498                        }
499                    }
500                })
501            })
502        }
503    }
504}
505
506/// Generate state-specific style application code
507///
508/// Creates a match expression that applies different styles based on widget state.
509/// Merges base style with state-specific overrides.
510///
511/// # Arguments
512/// * `base_style` - The base style struct (used when state is None)
513/// * `style_class` - The style class containing state variants
514/// * `widget_state_ident` - Identifier for the widget_state variable
515/// * `style_struct_fn` - Function to generate style struct from StyleProperties
516fn generate_state_style_match(
517    base_style: TokenStream,
518    style_class: &StyleClass,
519    widget_state_ident: &syn::Ident,
520    style_struct_fn: fn(&StyleProperties) -> Result<TokenStream, super::CodegenError>,
521) -> Result<TokenStream, super::CodegenError> {
522    use crate::ir::theme::WidgetState;
523
524    // Collect all state variants
525    let mut state_arms = Vec::new();
526
527    for (state, state_props) in &style_class.state_variants {
528        let state_variant = match state {
529            WidgetState::Hover => quote! { dampen_core::ir::WidgetState::Hover },
530            WidgetState::Focus => quote! { dampen_core::ir::WidgetState::Focus },
531            WidgetState::Active => quote! { dampen_core::ir::WidgetState::Active },
532            WidgetState::Disabled => quote! { dampen_core::ir::WidgetState::Disabled },
533        };
534
535        // Generate style struct for this state
536        let state_style = style_struct_fn(state_props)?;
537
538        state_arms.push(quote! {
539            Some(#state_variant) => #state_style
540        });
541    }
542
543    // Generate match expression
544    Ok(quote! {
545        match #widget_state_ident {
546            #(#state_arms,)*
547            None => #base_style
548        }
549    })
550}
551
552/// Generate inline style closure for a widget
553///
554/// Creates a closure like: |_theme: &iced::Theme, status| { ... }
555///
556/// # State-Aware Styling
557///
558/// When a widget has state variants (hover, focus, etc.), this generates
559/// code that maps the status parameter to WidgetState and applies the
560/// appropriate style.
561///
562/// # Arguments
563/// * `style_props` - Base style properties for the widget
564/// * `widget_type` - Type of widget ("button", "text_input", etc.)
565/// * `widget_kind` - The WidgetKind enum (needed for status mapping)
566/// * `style_class` - Optional style class with state variants
567fn generate_inline_style_closure(
568    style_props: &StyleProperties,
569    widget_type: &str,
570    widget_kind: &WidgetKind,
571    style_class: Option<&StyleClass>,
572) -> Result<TokenStream, super::CodegenError> {
573    // Check if we have state-specific styling
574    let has_state_variants = style_class
575        .map(|sc| !sc.state_variants.is_empty())
576        .unwrap_or(false);
577
578    match widget_type {
579        "button" => {
580            let base_style = generate_button_style_struct(style_props)?;
581
582            if has_state_variants {
583                // Generate state-aware closure
584                let status_ident = format_ident!("status");
585                if let Some(status_mapping) =
586                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
587                {
588                    let widget_state_ident = format_ident!("widget_state");
589                    // Safe: has_state_variants guarantees style_class.is_some()
590                    let class = style_class.ok_or_else(|| {
591                        super::CodegenError::InvalidWidget(
592                            "Expected style class with state variants".to_string(),
593                        )
594                    })?;
595                    let style_match = generate_state_style_match(
596                        base_style,
597                        class,
598                        &widget_state_ident,
599                        generate_button_style_struct,
600                    )?;
601
602                    Ok(quote! {
603                        |_theme: &iced::Theme, #status_ident: iced::widget::button::Status| {
604                            // Map Iced status to WidgetState
605                            let #widget_state_ident = #status_mapping;
606
607                            // Apply state-specific styling
608                            #style_match
609                        }
610                    })
611                } else {
612                    // Widget kind doesn't support status mapping, fall back to simple closure
613                    Ok(quote! {
614                        |_theme: &iced::Theme, _status: iced::widget::button::Status| {
615                            #base_style
616                        }
617                    })
618                }
619            } else {
620                // No state variants, use simple closure
621                Ok(quote! {
622                    |_theme: &iced::Theme, _status: iced::widget::button::Status| {
623                        #base_style
624                    }
625                })
626            }
627        }
628        "container" => {
629            let style_struct = generate_container_style_struct(style_props)?;
630            Ok(quote! {
631                |_theme: &iced::Theme| {
632                    #style_struct
633                }
634            })
635        }
636        "text_input" => {
637            let base_style = generate_text_input_style_struct(style_props)?;
638
639            if has_state_variants {
640                // Generate state-aware closure
641                let status_ident = format_ident!("status");
642                if let Some(status_mapping) =
643                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
644                {
645                    let widget_state_ident = format_ident!("widget_state");
646                    let class = style_class.ok_or_else(|| {
647                        super::CodegenError::InvalidWidget(
648                            "Expected style class with state variants".to_string(),
649                        )
650                    })?;
651                    let style_match = generate_state_style_match(
652                        base_style,
653                        class,
654                        &widget_state_ident,
655                        generate_text_input_style_struct,
656                    )?;
657
658                    Ok(quote! {
659                        |_theme: &iced::Theme, #status_ident: iced::widget::text_input::Status| {
660                            // Map Iced status to WidgetState
661                            let #widget_state_ident = #status_mapping;
662
663                            // Apply state-specific styling
664                            #style_match
665                        }
666                    })
667                } else {
668                    // Widget kind doesn't support status mapping, fall back to simple closure
669                    Ok(quote! {
670                        |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
671                            #base_style
672                        }
673                    })
674                }
675            } else {
676                // No state variants, use simple closure
677                Ok(quote! {
678                    |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
679                        #base_style
680                    }
681                })
682            }
683        }
684        "checkbox" => {
685            let base_style = generate_checkbox_style_struct(style_props)?;
686
687            if has_state_variants {
688                let status_ident = format_ident!("status");
689                if let Some(status_mapping) =
690                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
691                {
692                    let widget_state_ident = format_ident!("widget_state");
693                    let class = style_class.ok_or_else(|| {
694                        super::CodegenError::InvalidWidget(
695                            "Expected style class with state variants".to_string(),
696                        )
697                    })?;
698                    let style_match = generate_state_style_match(
699                        base_style,
700                        class,
701                        &widget_state_ident,
702                        generate_checkbox_style_struct,
703                    )?;
704
705                    Ok(quote! {
706                        |_theme: &iced::Theme, #status_ident: iced::widget::checkbox::Status| {
707                            let #widget_state_ident = #status_mapping;
708                            #style_match
709                        }
710                    })
711                } else {
712                    Ok(quote! {
713                        |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
714                            #base_style
715                        }
716                    })
717                }
718            } else {
719                Ok(quote! {
720                    |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
721                        #base_style
722                    }
723                })
724            }
725        }
726        "toggler" => {
727            let base_style = generate_toggler_style_struct(style_props)?;
728
729            if has_state_variants {
730                let status_ident = format_ident!("status");
731                if let Some(status_mapping) =
732                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
733                {
734                    let widget_state_ident = format_ident!("widget_state");
735                    let class = style_class.ok_or_else(|| {
736                        super::CodegenError::InvalidWidget(
737                            "Expected style class with state variants".to_string(),
738                        )
739                    })?;
740                    let style_match = generate_state_style_match(
741                        base_style,
742                        class,
743                        &widget_state_ident,
744                        generate_toggler_style_struct,
745                    )?;
746
747                    Ok(quote! {
748                        |_theme: &iced::Theme, #status_ident: iced::widget::toggler::Status| {
749                            let #widget_state_ident = #status_mapping;
750                            #style_match
751                        }
752                    })
753                } else {
754                    Ok(quote! {
755                        |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
756                            #base_style
757                        }
758                    })
759                }
760            } else {
761                Ok(quote! {
762                    |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
763                        #base_style
764                    }
765                })
766            }
767        }
768        "slider" => {
769            let base_style = generate_slider_style_struct(style_props)?;
770
771            if has_state_variants {
772                let status_ident = format_ident!("status");
773                if let Some(status_mapping) =
774                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
775                {
776                    let widget_state_ident = format_ident!("widget_state");
777                    let class = style_class.ok_or_else(|| {
778                        super::CodegenError::InvalidWidget(
779                            "Expected style class with state variants".to_string(),
780                        )
781                    })?;
782                    let style_match = generate_state_style_match(
783                        base_style,
784                        class,
785                        &widget_state_ident,
786                        generate_slider_style_struct,
787                    )?;
788
789                    Ok(quote! {
790                        |_theme: &iced::Theme, #status_ident: iced::widget::slider::Status| {
791                            let #widget_state_ident = #status_mapping;
792                            #style_match
793                        }
794                    })
795                } else {
796                    Ok(quote! {
797                        |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
798                            #base_style
799                        }
800                    })
801                }
802            } else {
803                Ok(quote! {
804                    |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
805                        #base_style
806                    }
807                })
808            }
809        }
810        _ => {
811            // For unsupported widgets, return a no-op closure
812            Ok(quote! {
813                |_theme: &iced::Theme| iced::widget::container::Style::default()
814            })
815        }
816    }
817}
818
819// ============================================================================
820// Helper Functions (copied from theme.rs for reuse)
821// ============================================================================
822
823/// Generate iced::Color from Color IR
824fn generate_color_expr(color: &Color) -> TokenStream {
825    let r = color.r;
826    let g = color.g;
827    let b = color.b;
828    let a = color.a;
829    quote! {
830        iced::Color::from_rgba(#r, #g, #b, #a)
831    }
832}
833
834/// Generate iced::Background from Background IR
835fn generate_background_expr(bg: &Background) -> TokenStream {
836    match bg {
837        Background::Color(color) => {
838            let color_expr = generate_color_expr(color);
839            quote! { iced::Background::Color(#color_expr) }
840        }
841        Background::Gradient(gradient) => generate_gradient_expr(gradient),
842        Background::Image { .. } => {
843            quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
844        }
845    }
846}
847
848/// Generate iced::Gradient from Gradient IR
849fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
850    match gradient {
851        Gradient::Linear { angle, stops } => {
852            let radians = angle * (std::f32::consts::PI / 180.0);
853            let color_exprs: Vec<_> = stops
854                .iter()
855                .map(|s| generate_color_expr(&s.color))
856                .collect();
857            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
858
859            quote! {
860                iced::Background::Gradient(iced::Gradient::Linear(
861                    iced::gradient::Linear::new(#radians)
862                        #(.add_stop(#offsets, #color_exprs))*
863                ))
864            }
865        }
866        Gradient::Radial { stops, .. } => {
867            // Fallback to linear for radial (Iced limitation)
868            let color_exprs: Vec<_> = stops
869                .iter()
870                .map(|s| generate_color_expr(&s.color))
871                .collect();
872            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
873
874            quote! {
875                iced::Background::Gradient(iced::Gradient::Linear(
876                    iced::gradient::Linear::new(0.0)
877                        #(.add_stop(#offsets, #color_exprs))*
878                ))
879            }
880        }
881    }
882}
883
884/// Generate iced::Border from Border IR
885fn generate_border_expr(border: &Border) -> TokenStream {
886    let width = border.width;
887    let color_expr = generate_color_expr(&border.color);
888    let radius_expr = generate_border_radius_expr(&border.radius);
889
890    quote! {
891        iced::Border {
892            width: #width,
893            color: #color_expr,
894            radius: #radius_expr,
895        }
896    }
897}
898
899/// Generate iced::border::Radius from BorderRadius IR
900fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
901    let tl = radius.top_left;
902    let tr = radius.top_right;
903    let br = radius.bottom_right;
904    let bl = radius.bottom_left;
905
906    quote! {
907        iced::border::Radius::from(#tl).top_right(#tr).bottom_right(#br).bottom_left(#bl)
908    }
909}
910
911/// Generate iced::Shadow from Shadow IR
912fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
913    let offset_x = shadow.offset_x;
914    let offset_y = shadow.offset_y;
915    let blur = shadow.blur_radius;
916    let color_expr = generate_color_expr(&shadow.color);
917
918    quote! {
919        iced::Shadow {
920            offset: iced::Vector::new(#offset_x, #offset_y),
921            blur_radius: #blur,
922            color: #color_expr,
923        }
924    }
925}
926
927/// Generate iced::widget::button::Style struct from StyleProperties
928///
929/// When no explicit color is set, uses theme text color via _theme parameter
930/// that's available in the style closure scope.
931fn generate_button_style_struct(
932    props: &StyleProperties,
933) -> Result<TokenStream, super::CodegenError> {
934    let background_expr = props
935        .background
936        .as_ref()
937        .map(|bg| {
938            let expr = generate_background_expr(bg);
939            quote! { Some(#expr) }
940        })
941        .unwrap_or_else(|| quote! { None });
942
943    // Use theme text color as fallback instead of hardcoded BLACK
944    let text_color_expr = props
945        .color
946        .as_ref()
947        .map(generate_color_expr)
948        .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
949
950    let border_expr = props
951        .border
952        .as_ref()
953        .map(generate_border_expr)
954        .unwrap_or_else(|| quote! { iced::Border::default() });
955
956    let shadow_expr = props
957        .shadow
958        .as_ref()
959        .map(generate_shadow_expr)
960        .unwrap_or_else(|| quote! { iced::Shadow::default() });
961
962    Ok(quote! {
963        iced::widget::button::Style {
964            background: #background_expr,
965            text_color: #text_color_expr,
966            border: #border_expr,
967            shadow: #shadow_expr,
968            snap: false,
969        }
970    })
971}
972
973/// Generate iced::widget::container::Style struct from StyleProperties
974fn generate_container_style_struct(
975    props: &StyleProperties,
976) -> Result<TokenStream, super::CodegenError> {
977    let background_expr = props
978        .background
979        .as_ref()
980        .map(|bg| {
981            let expr = generate_background_expr(bg);
982            quote! { Some(#expr) }
983        })
984        .unwrap_or_else(|| quote! { None });
985
986    let text_color_expr = props
987        .color
988        .as_ref()
989        .map(|color| {
990            let color_expr = generate_color_expr(color);
991            quote! { Some(#color_expr) }
992        })
993        .unwrap_or_else(|| quote! { None });
994
995    let border_expr = props
996        .border
997        .as_ref()
998        .map(generate_border_expr)
999        .unwrap_or_else(|| quote! { iced::Border::default() });
1000
1001    let shadow_expr = props
1002        .shadow
1003        .as_ref()
1004        .map(generate_shadow_expr)
1005        .unwrap_or_else(|| quote! { iced::Shadow::default() });
1006
1007    Ok(quote! {
1008        iced::widget::container::Style {
1009            background: #background_expr,
1010            text_color: #text_color_expr,
1011            border: #border_expr,
1012            shadow: #shadow_expr,
1013            snap: false,
1014        }
1015    })
1016}
1017
1018/// Generate iced::widget::text_input::Style struct from StyleProperties
1019///
1020/// When no explicit colors are set, uses theme text colors via _theme parameter
1021/// that's available in the style closure scope.
1022fn generate_text_input_style_struct(
1023    props: &StyleProperties,
1024) -> Result<TokenStream, super::CodegenError> {
1025    let background_expr = props
1026        .background
1027        .as_ref()
1028        .map(|bg| {
1029            let expr = generate_background_expr(bg);
1030            quote! { #expr }
1031        })
1032        .unwrap_or_else(
1033            || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
1034        );
1035
1036    let border_expr = props
1037        .border
1038        .as_ref()
1039        .map(generate_border_expr)
1040        .unwrap_or_else(|| quote! { iced::Border::default() });
1041
1042    // Use theme colors as fallback instead of hardcoded colors
1043    let value_color = props
1044        .color
1045        .as_ref()
1046        .map(generate_color_expr)
1047        .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
1048
1049    Ok(quote! {
1050        iced::widget::text_input::Style {
1051            background: #background_expr,
1052            border: #border_expr,
1053            icon: _theme.extended_palette().background.base.text,
1054            placeholder: _theme.extended_palette().background.weak.text,
1055            value: #value_color,
1056            selection: _theme.extended_palette().primary.weak.color,
1057        }
1058    })
1059}
1060
1061/// Generate checkbox style struct from StyleProperties
1062///
1063/// When no explicit colors are set, uses theme colors via _theme parameter
1064/// that's available in the style closure scope.
1065fn generate_checkbox_style_struct(
1066    props: &StyleProperties,
1067) -> Result<TokenStream, super::CodegenError> {
1068    let background_expr = props
1069        .background
1070        .as_ref()
1071        .map(|bg| {
1072            let expr = generate_background_expr(bg);
1073            quote! { #expr }
1074        })
1075        .unwrap_or_else(
1076            || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
1077        );
1078
1079    let border_expr = props
1080        .border
1081        .as_ref()
1082        .map(generate_border_expr)
1083        .unwrap_or_else(|| quote! { iced::Border::default() });
1084
1085    // Use theme text color as fallback instead of hardcoded BLACK
1086    let text_color = props
1087        .color
1088        .as_ref()
1089        .map(generate_color_expr)
1090        .unwrap_or_else(|| quote! { _theme.extended_palette().primary.base.color });
1091
1092    Ok(quote! {
1093        iced::widget::checkbox::Style {
1094            background: #background_expr,
1095            icon_color: #text_color,
1096            border: #border_expr,
1097            text_color: None,
1098        }
1099    })
1100}
1101
1102/// Generate toggler style struct from StyleProperties
1103fn generate_toggler_style_struct(
1104    props: &StyleProperties,
1105) -> Result<TokenStream, super::CodegenError> {
1106    let background_expr = props
1107        .background
1108        .as_ref()
1109        .map(|bg| {
1110            let expr = generate_background_expr(bg);
1111            quote! { #expr }
1112        })
1113        .unwrap_or_else(
1114            || quote! { iced::Background::Color(iced::Color::from_rgb(0.5, 0.5, 0.5)) },
1115        );
1116
1117    Ok(quote! {
1118        iced::widget::toggler::Style {
1119            background: #background_expr,
1120            background_border_width: 0.0,
1121            background_border_color: iced::Color::TRANSPARENT,
1122            foreground: iced::Background::Color(iced::Color::WHITE),
1123            foreground_border_width: 0.0,
1124            foreground_border_color: iced::Color::TRANSPARENT,
1125        }
1126    })
1127}
1128
1129/// Generate slider style struct from StyleProperties
1130fn generate_slider_style_struct(
1131    props: &StyleProperties,
1132) -> Result<TokenStream, super::CodegenError> {
1133    let border_expr = props
1134        .border
1135        .as_ref()
1136        .map(generate_border_expr)
1137        .unwrap_or_else(|| quote! { iced::Border::default() });
1138
1139    Ok(quote! {
1140        iced::widget::slider::Style {
1141            rail: iced::widget::slider::Rail {
1142                colors: (
1143                    iced::Color::from_rgb(0.6, 0.6, 0.6),
1144                    iced::Color::from_rgb(0.2, 0.6, 1.0),
1145                ),
1146                width: 4.0,
1147                border: #border_expr,
1148            },
1149            handle: iced::widget::slider::Handle {
1150                shape: iced::widget::slider::HandleShape::Circle { radius: 8.0 },
1151                color: iced::Color::WHITE,
1152                border_width: 1.0,
1153                border_color: iced::Color::from_rgb(0.6, 0.6, 0.6),
1154            },
1155        }
1156    })
1157}
1158
1159/// Generate text widget
1160fn generate_text(
1161    node: &crate::WidgetNode,
1162    model_ident: &syn::Ident,
1163    _style_classes: &HashMap<String, StyleClass>,
1164) -> Result<TokenStream, super::CodegenError> {
1165    let value_attr = node.attributes.get("value").ok_or_else(|| {
1166        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
1167    })?;
1168
1169    let value_expr = generate_attribute_value(value_attr, model_ident);
1170
1171    let mut text_widget = quote! {
1172        iced::widget::text(#value_expr)
1173    };
1174
1175    // Apply size attribute
1176    if let Some(size) = node.attributes.get("size").and_then(|attr| {
1177        if let AttributeValue::Static(s) = attr {
1178            s.parse::<f32>().ok()
1179        } else {
1180            None
1181        }
1182    }) {
1183        text_widget = quote! { #text_widget.size(#size) };
1184    }
1185
1186    // Apply weight attribute (bold, normal, etc.)
1187    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
1188        if let AttributeValue::Static(s) = attr {
1189            Some(s.clone())
1190        } else {
1191            None
1192        }
1193    }) {
1194        let weight_expr = match weight.to_lowercase().as_str() {
1195            "bold" => quote! { iced::font::Weight::Bold },
1196            "semibold" => quote! { iced::font::Weight::Semibold },
1197            "medium" => quote! { iced::font::Weight::Medium },
1198            "light" => quote! { iced::font::Weight::Light },
1199            _ => quote! { iced::font::Weight::Normal },
1200        };
1201        text_widget = quote! {
1202            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
1203        };
1204    }
1205
1206    // Apply inline style color if present
1207    if let Some(ref style_props) = node.style
1208        && let Some(ref color) = style_props.color
1209    {
1210        let color_expr = generate_color_expr(color);
1211        text_widget = quote! { #text_widget.color(#color_expr) };
1212    }
1213
1214    // Use helper to wrap in container if layout attributes are present
1215    Ok(maybe_wrap_in_container(text_widget, node))
1216}
1217
1218/// Generate Length expression from string
1219fn generate_length_expr(s: &str) -> TokenStream {
1220    let s = s.trim().to_lowercase();
1221    if s == "fill" {
1222        quote! { iced::Length::Fill }
1223    } else if s == "shrink" {
1224        quote! { iced::Length::Shrink }
1225    } else if let Some(pct) = s.strip_suffix('%') {
1226        if let Ok(p) = pct.parse::<f32>() {
1227            // Iced doesn't have a direct percentage, use FillPortion as approximation
1228            let portion = ((p / 100.0) * 16.0).round() as u16;
1229            let portion = portion.max(1);
1230            quote! { iced::Length::FillPortion(#portion) }
1231        } else {
1232            quote! { iced::Length::Shrink }
1233        }
1234    } else if let Ok(px) = s.parse::<f32>() {
1235        quote! { iced::Length::Fixed(#px) }
1236    } else {
1237        quote! { iced::Length::Shrink }
1238    }
1239}
1240
1241/// Generate Length expression from LayoutLength IR type
1242fn generate_layout_length_expr(length: &LayoutLength) -> TokenStream {
1243    match length {
1244        LayoutLength::Fixed(px) => quote! { iced::Length::Fixed(#px) },
1245        LayoutLength::Fill => quote! { iced::Length::Fill },
1246        LayoutLength::Shrink => quote! { iced::Length::Shrink },
1247        LayoutLength::FillPortion(portion) => {
1248            let p = *portion as u16;
1249            quote! { iced::Length::FillPortion(#p) }
1250        }
1251        LayoutLength::Percentage(pct) => {
1252            // Iced doesn't have direct percentage, approximate with FillPortion
1253            let portion = ((pct / 100.0) * 16.0).round() as u16;
1254            let portion = portion.max(1);
1255            quote! { iced::Length::FillPortion(#portion) }
1256        }
1257    }
1258}
1259
1260/// Generate horizontal alignment expression
1261fn generate_horizontal_alignment_expr(s: &str) -> TokenStream {
1262    match s.trim().to_lowercase().as_str() {
1263        "center" => quote! { iced::alignment::Horizontal::Center },
1264        "end" | "right" => quote! { iced::alignment::Horizontal::Right },
1265        _ => quote! { iced::alignment::Horizontal::Left },
1266    }
1267}
1268
1269/// Generate vertical alignment expression
1270fn generate_vertical_alignment_expr(s: &str) -> TokenStream {
1271    match s.trim().to_lowercase().as_str() {
1272        "center" => quote! { iced::alignment::Vertical::Center },
1273        "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
1274        _ => quote! { iced::alignment::Vertical::Top },
1275    }
1276}
1277
1278/// Wraps a widget in a container if layout attributes are present.
1279///
1280/// This helper provides consistent layout attribute support across all widgets
1281/// by wrapping them in a container when needed.
1282///
1283/// # Arguments
1284///
1285/// * `widget` - The widget expression to potentially wrap
1286/// * `node` - The widget node containing attributes
1287///
1288/// # Returns
1289///
1290/// Returns the widget wrapped in a container if layout attributes are present,
1291/// otherwise returns the original widget.
1292fn maybe_wrap_in_container(widget: TokenStream, node: &crate::WidgetNode) -> TokenStream {
1293    // Check if we need to wrap in container for layout/alignment/classes
1294    let needs_container = node.layout.is_some()
1295        || !node.classes.is_empty()
1296        || node.attributes.contains_key("align_x")
1297        || node.attributes.contains_key("align_y")
1298        || node.attributes.contains_key("width")
1299        || node.attributes.contains_key("height")
1300        || node.attributes.contains_key("padding");
1301
1302    if !needs_container {
1303        return quote! { #widget.into() };
1304    }
1305
1306    let mut container = quote! {
1307        iced::widget::container(#widget)
1308    };
1309
1310    // Apply width
1311    if let Some(width) = node.attributes.get("width").and_then(|attr| {
1312        if let AttributeValue::Static(s) = attr {
1313            Some(s.clone())
1314        } else {
1315            None
1316        }
1317    }) {
1318        let width_expr = generate_length_expr(&width);
1319        container = quote! { #container.width(#width_expr) };
1320    }
1321
1322    // Apply height
1323    if let Some(height) = node.attributes.get("height").and_then(|attr| {
1324        if let AttributeValue::Static(s) = attr {
1325            Some(s.clone())
1326        } else {
1327            None
1328        }
1329    }) {
1330        let height_expr = generate_length_expr(&height);
1331        container = quote! { #container.height(#height_expr) };
1332    }
1333
1334    // Apply padding
1335    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1336        if let AttributeValue::Static(s) = attr {
1337            s.parse::<f32>().ok()
1338        } else {
1339            None
1340        }
1341    }) {
1342        container = quote! { #container.padding(#padding) };
1343    }
1344
1345    // Apply align_x
1346    if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1347        if let AttributeValue::Static(s) = attr {
1348            Some(s.clone())
1349        } else {
1350            None
1351        }
1352    }) {
1353        let align_expr = generate_horizontal_alignment_expr(&align_x);
1354        container = quote! { #container.align_x(#align_expr) };
1355    }
1356
1357    // Apply align_y
1358    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1359        if let AttributeValue::Static(s) = attr {
1360            Some(s.clone())
1361        } else {
1362            None
1363        }
1364    }) {
1365        let align_expr = generate_vertical_alignment_expr(&align_y);
1366        container = quote! { #container.align_y(#align_expr) };
1367    }
1368
1369    // Apply class style if present
1370    if let Some(class_name) = node.classes.first() {
1371        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1372        container = quote! { #container.style(#style_fn_ident) };
1373    }
1374
1375    quote! { #container.into() }
1376}
1377
1378/// Generate button widget
1379fn generate_button(
1380    node: &crate::WidgetNode,
1381    model_ident: &syn::Ident,
1382    message_ident: &syn::Ident,
1383    style_classes: &HashMap<String, StyleClass>,
1384) -> Result<TokenStream, super::CodegenError> {
1385    let label_attr = node.attributes.get("label").ok_or_else(|| {
1386        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
1387    })?;
1388
1389    let label_expr = generate_attribute_value(label_attr, model_ident);
1390
1391    let on_click = node
1392        .events
1393        .iter()
1394        .find(|e| e.event == crate::EventKind::Click);
1395
1396    let mut button = quote! {
1397        iced::widget::button(iced::widget::text(#label_expr))
1398    };
1399
1400    // Handle enabled attribute
1401    let enabled_condition = node.attributes.get("enabled").map(|attr| match attr {
1402        AttributeValue::Static(s) => {
1403            // Static enabled values
1404            match s.to_lowercase().as_str() {
1405                "true" | "1" | "yes" | "on" => quote! { true },
1406                "false" | "0" | "no" | "off" => quote! { false },
1407                _ => quote! { true }, // Default to enabled
1408            }
1409        }
1410        AttributeValue::Binding(binding_expr) => {
1411            // Dynamic binding expression - use generate_bool_expr for native boolean
1412            super::bindings::generate_bool_expr(&binding_expr.expr)
1413        }
1414        AttributeValue::Interpolated(_) => {
1415            // Interpolated strings treated as enabled if non-empty
1416            let expr_tokens = generate_attribute_value(attr, model_ident);
1417            quote! { !#expr_tokens.is_empty() && #expr_tokens != "false" && #expr_tokens != "0" }
1418        }
1419    });
1420
1421    if let Some(event) = on_click {
1422        let variant_name = to_upper_camel_case(&event.handler);
1423        let handler_ident = format_ident!("{}", variant_name);
1424
1425        let param_expr = if let Some(ref param) = event.param {
1426            let param_tokens = generate_expr(&param.expr);
1427            quote! { (#param_tokens) }
1428        } else {
1429            quote! {}
1430        };
1431
1432        // Generate on_press call based on enabled condition
1433        button = match enabled_condition {
1434            None => {
1435                // No enabled attribute - always enabled
1436                quote! {
1437                    #button.on_press(#message_ident::#handler_ident #param_expr)
1438                }
1439            }
1440            Some(condition) => {
1441                // Conditional enabled - use on_press_maybe
1442                quote! {
1443                    #button.on_press_maybe(
1444                        if #condition {
1445                            Some(#message_ident::#handler_ident #param_expr)
1446                        } else {
1447                            None
1448                        }
1449                    )
1450                }
1451            }
1452        };
1453    }
1454
1455    // Apply styles (inline or classes)
1456    button = apply_widget_style(button, node, "button", style_classes)?;
1457
1458    Ok(quote! { #button.into() })
1459}
1460
1461/// Helper function to convert snake_case to UpperCamelCase
1462fn to_upper_camel_case(s: &str) -> String {
1463    let mut result = String::new();
1464    let mut capitalize_next = true;
1465    for c in s.chars() {
1466        if c == '_' {
1467            capitalize_next = true;
1468        } else if capitalize_next {
1469            result.push(c.to_ascii_uppercase());
1470            capitalize_next = false;
1471        } else {
1472            result.push(c);
1473        }
1474    }
1475    result
1476}
1477
1478/// Generate container widget (column, row, container, scrollable)
1479fn generate_container(
1480    node: &crate::WidgetNode,
1481    widget_type: &str,
1482    model_ident: &syn::Ident,
1483    message_ident: &syn::Ident,
1484    style_classes: &HashMap<String, StyleClass>,
1485) -> Result<TokenStream, super::CodegenError> {
1486    let children: Vec<TokenStream> = node
1487        .children
1488        .iter()
1489        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1490        .collect::<Result<_, _>>()?;
1491
1492    let widget_ident = format_ident!("{}", widget_type);
1493
1494    // Get merged layout from node.layout and style classes
1495    let merged_layout = get_merged_layout(node, style_classes);
1496
1497    // Get spacing from attributes or merged layout
1498    let spacing = node
1499        .attributes
1500        .get("spacing")
1501        .and_then(|attr| {
1502            if let AttributeValue::Static(s) = attr {
1503                s.parse::<f32>().ok()
1504            } else {
1505                None
1506            }
1507        })
1508        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
1509
1510    // Get padding from attributes or merged layout
1511    let padding = node
1512        .attributes
1513        .get("padding")
1514        .and_then(|attr| {
1515            if let AttributeValue::Static(s) = attr {
1516                s.parse::<f32>().ok()
1517            } else {
1518                None
1519            }
1520        })
1521        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
1522
1523    let mut widget = if widget_type == "container" {
1524        // Container in Iced can only have one child
1525        // If multiple children provided, wrap them in a column automatically
1526        // Use let binding with explicit type to help inference when child is a column/row
1527        if children.is_empty() {
1528            quote! {
1529                iced::widget::container(iced::widget::Space::new())
1530            }
1531        } else if children.len() == 1 {
1532            let child = &children[0];
1533            quote! {
1534                {
1535                    let content: iced::Element<'_, _, _> = #child;
1536                    iced::widget::container(content)
1537                }
1538            }
1539        } else {
1540            // Multiple children - wrap in a column
1541            quote! {
1542                {
1543                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1544                    iced::widget::container(content)
1545                }
1546            }
1547        }
1548    } else if widget_type == "scrollable" {
1549        // Scrollable in Iced can only have one child
1550        // If multiple children provided, wrap them in a column automatically
1551        // Use let binding with explicit type to help inference when child is a column/row
1552        if children.is_empty() {
1553            quote! {
1554                iced::widget::scrollable(iced::widget::Space::new())
1555            }
1556        } else if children.len() == 1 {
1557            let child = &children[0];
1558            quote! {
1559                {
1560                    let content: iced::Element<'_, _, _> = #child;
1561                    iced::widget::scrollable(content)
1562                }
1563            }
1564        } else {
1565            // Multiple children - wrap in a column
1566            quote! {
1567                {
1568                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1569                    iced::widget::scrollable(content)
1570                }
1571            }
1572        }
1573    } else {
1574        quote! {
1575            iced::widget::#widget_ident(vec![#(#children),*])
1576        }
1577    };
1578
1579    if let Some(s) = spacing {
1580        widget = quote! { #widget.spacing(#s) };
1581    }
1582
1583    if let Some(p) = padding {
1584        widget = quote! { #widget.padding(#p) };
1585    }
1586
1587    // Apply width from attributes or merged layout
1588    let width_from_attr = node.attributes.get("width").and_then(|attr| {
1589        if let AttributeValue::Static(s) = attr {
1590            Some(s.clone())
1591        } else {
1592            None
1593        }
1594    });
1595    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
1596
1597    if let Some(width) = width_from_attr {
1598        let width_expr = generate_length_expr(&width);
1599        widget = quote! { #widget.width(#width_expr) };
1600    } else if let Some(layout_width) = width_from_layout {
1601        let width_expr = generate_layout_length_expr(layout_width);
1602        widget = quote! { #widget.width(#width_expr) };
1603    }
1604
1605    // Apply height from attributes or merged layout
1606    let height_from_attr = node.attributes.get("height").and_then(|attr| {
1607        if let AttributeValue::Static(s) = attr {
1608            Some(s.clone())
1609        } else {
1610            None
1611        }
1612    });
1613    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
1614
1615    if let Some(height) = height_from_attr {
1616        let height_expr = generate_length_expr(&height);
1617        widget = quote! { #widget.height(#height_expr) };
1618    } else if let Some(layout_height) = height_from_layout {
1619        let height_expr = generate_layout_length_expr(layout_height);
1620        widget = quote! { #widget.height(#height_expr) };
1621    }
1622
1623    // Apply align_x attribute (for containers)
1624    if widget_type == "container" {
1625        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1626            if let AttributeValue::Static(s) = attr {
1627                Some(s.clone())
1628            } else {
1629                None
1630            }
1631        }) {
1632            let align_expr = generate_horizontal_alignment_expr(&align_x);
1633            widget = quote! { #widget.align_x(#align_expr) };
1634        }
1635
1636        // Apply align_y attribute
1637        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1638            if let AttributeValue::Static(s) = attr {
1639                Some(s.clone())
1640            } else {
1641                None
1642            }
1643        }) {
1644            let align_expr = generate_vertical_alignment_expr(&align_y);
1645            widget = quote! { #widget.align_y(#align_expr) };
1646        }
1647    }
1648
1649    // Apply align_items for column/row (vertical alignment of children)
1650    if (widget_type == "column" || widget_type == "row")
1651        && let Some(align) = node.attributes.get("align_items").and_then(|attr| {
1652            if let AttributeValue::Static(s) = attr {
1653                Some(s.clone())
1654            } else {
1655                None
1656            }
1657        })
1658    {
1659        let align_expr = match align.to_lowercase().as_str() {
1660            "center" => quote! { iced::Alignment::Center },
1661            "end" => quote! { iced::Alignment::End },
1662            _ => quote! { iced::Alignment::Start },
1663        };
1664        widget = quote! { #widget.align_items(#align_expr) };
1665    }
1666
1667    // Apply styles (only for container, not column/row/scrollable)
1668    if widget_type == "container" {
1669        widget = apply_widget_style(widget, node, "container", style_classes)?;
1670    }
1671
1672    // Check if Column/Row needs to be wrapped in a container for align_x/align_y
1673    // These attributes position the Column/Row itself within its parent,
1674    // which requires an outer container wrapper
1675    if (widget_type == "column" || widget_type == "row")
1676        && (node.attributes.contains_key("align_x") || node.attributes.contains_key("align_y"))
1677    {
1678        let mut container = quote! { iced::widget::container(#widget) };
1679
1680        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1681            if let AttributeValue::Static(s) = attr {
1682                Some(s.clone())
1683            } else {
1684                None
1685            }
1686        }) {
1687            let align_expr = generate_horizontal_alignment_expr(&align_x);
1688            container = quote! { #container.align_x(#align_expr) };
1689        }
1690
1691        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1692            if let AttributeValue::Static(s) = attr {
1693                Some(s.clone())
1694            } else {
1695                None
1696            }
1697        }) {
1698            let align_expr = generate_vertical_alignment_expr(&align_y);
1699            container = quote! { #container.align_y(#align_expr) };
1700        }
1701
1702        // Container needs explicit width/height to enable alignment
1703        container = quote! { #container.width(iced::Length::Fill).height(iced::Length::Fill) };
1704
1705        return Ok(quote! { #container.into() });
1706    }
1707
1708    Ok(quote! { #widget.into() })
1709}
1710
1711/// Generate stack widget
1712fn generate_stack(
1713    node: &crate::WidgetNode,
1714    model_ident: &syn::Ident,
1715    message_ident: &syn::Ident,
1716    style_classes: &HashMap<String, StyleClass>,
1717) -> Result<TokenStream, super::CodegenError> {
1718    let children: Vec<TokenStream> = node
1719        .children
1720        .iter()
1721        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1722        .collect::<Result<_, _>>()?;
1723
1724    Ok(quote! {
1725        iced::widget::stack(vec![#(#children),*]).into()
1726    })
1727}
1728
1729/// Generate space widget
1730fn generate_space(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1731    // Get width attribute
1732    let width = node.attributes.get("width").and_then(|attr| {
1733        if let AttributeValue::Static(s) = attr {
1734            Some(s.clone())
1735        } else {
1736            None
1737        }
1738    });
1739
1740    // Get height attribute
1741    let height = node.attributes.get("height").and_then(|attr| {
1742        if let AttributeValue::Static(s) = attr {
1743            Some(s.clone())
1744        } else {
1745            None
1746        }
1747    });
1748
1749    let mut space = quote! { iced::widget::Space::new() };
1750
1751    // Apply width
1752    if let Some(w) = width {
1753        let width_expr = generate_length_expr(&w);
1754        space = quote! { #space.width(#width_expr) };
1755    }
1756
1757    // Apply height
1758    if let Some(h) = height {
1759        let height_expr = generate_length_expr(&h);
1760        space = quote! { #space.height(#height_expr) };
1761    }
1762
1763    Ok(quote! { #space.into() })
1764}
1765
1766/// Generate rule (horizontal/vertical line) widget
1767fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1768    // Get direction (default to horizontal)
1769    let direction = node
1770        .attributes
1771        .get("direction")
1772        .and_then(|attr| {
1773            if let AttributeValue::Static(s) = attr {
1774                Some(s.clone())
1775            } else {
1776                None
1777            }
1778        })
1779        .unwrap_or_else(|| "horizontal".to_string());
1780
1781    // Get thickness (default to 1)
1782    let thickness = node
1783        .attributes
1784        .get("thickness")
1785        .and_then(|attr| {
1786            if let AttributeValue::Static(s) = attr {
1787                s.parse::<f32>().ok()
1788            } else {
1789                None
1790            }
1791        })
1792        .unwrap_or(1.0);
1793
1794    let rule = if direction.to_lowercase() == "vertical" {
1795        quote! { iced::widget::rule::vertical(#thickness) }
1796    } else {
1797        quote! { iced::widget::rule::horizontal(#thickness) }
1798    };
1799
1800    Ok(quote! { #rule.into() })
1801}
1802
1803/// Generate checkbox widget
1804fn generate_checkbox(
1805    node: &crate::WidgetNode,
1806    model_ident: &syn::Ident,
1807    message_ident: &syn::Ident,
1808    style_classes: &HashMap<String, StyleClass>,
1809) -> Result<TokenStream, super::CodegenError> {
1810    let label = node
1811        .attributes
1812        .get("label")
1813        .and_then(|attr| {
1814            if let AttributeValue::Static(s) = attr {
1815                Some(s.clone())
1816            } else {
1817                None
1818            }
1819        })
1820        .unwrap_or_default();
1821    let label_lit = proc_macro2::Literal::string(&label);
1822    let label_expr = quote! { #label_lit.to_string() };
1823
1824    let checked_attr = node.attributes.get("checked");
1825    let checked_expr = checked_attr
1826        .map(|attr| generate_attribute_value(attr, model_ident))
1827        .unwrap_or(quote! { false });
1828
1829    let on_toggle = node
1830        .events
1831        .iter()
1832        .find(|e| e.event == crate::EventKind::Toggle);
1833
1834    let checkbox = if let Some(event) = on_toggle {
1835        let variant_name = to_upper_camel_case(&event.handler);
1836        let handler_ident = format_ident!("{}", variant_name);
1837        quote! {
1838            iced::widget::checkbox(#label_expr, #checked_expr)
1839                .on_toggle(#message_ident::#handler_ident)
1840        }
1841    } else {
1842        quote! {
1843            iced::widget::checkbox(#label_expr, #checked_expr)
1844        }
1845    };
1846
1847    // Apply styles
1848    let checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
1849
1850    Ok(quote! { #checkbox.into() })
1851}
1852
1853/// Generate toggler widget
1854fn generate_toggler(
1855    node: &crate::WidgetNode,
1856    model_ident: &syn::Ident,
1857    message_ident: &syn::Ident,
1858    style_classes: &HashMap<String, StyleClass>,
1859) -> Result<TokenStream, super::CodegenError> {
1860    let label = node
1861        .attributes
1862        .get("label")
1863        .and_then(|attr| {
1864            if let AttributeValue::Static(s) = attr {
1865                Some(s.clone())
1866            } else {
1867                None
1868            }
1869        })
1870        .unwrap_or_default();
1871    let label_lit = proc_macro2::Literal::string(&label);
1872    let label_expr = quote! { #label_lit.to_string() };
1873
1874    let is_toggled_attr = node.attributes.get("toggled");
1875    let is_toggled_expr = is_toggled_attr
1876        .map(|attr| generate_attribute_value(attr, model_ident))
1877        .unwrap_or(quote! { false });
1878
1879    let on_toggle = node
1880        .events
1881        .iter()
1882        .find(|e| e.event == crate::EventKind::Toggle);
1883
1884    let toggler = if let Some(event) = on_toggle {
1885        let variant_name = to_upper_camel_case(&event.handler);
1886        let handler_ident = format_ident!("{}", variant_name);
1887        quote! {
1888            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1889                .on_toggle(|_| #message_ident::#handler_ident)
1890        }
1891    } else {
1892        quote! {
1893            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1894        }
1895    };
1896
1897    // Apply styles
1898    let toggler = apply_widget_style(toggler, node, "toggler", style_classes)?;
1899
1900    Ok(quote! { #toggler.into() })
1901}
1902
1903/// Generate slider widget
1904fn generate_slider(
1905    node: &crate::WidgetNode,
1906    model_ident: &syn::Ident,
1907    message_ident: &syn::Ident,
1908    style_classes: &HashMap<String, StyleClass>,
1909) -> Result<TokenStream, super::CodegenError> {
1910    let min = node.attributes.get("min").and_then(|attr| {
1911        if let AttributeValue::Static(s) = attr {
1912            s.parse::<f32>().ok()
1913        } else {
1914            None
1915        }
1916    });
1917
1918    let max = node.attributes.get("max").and_then(|attr| {
1919        if let AttributeValue::Static(s) = attr {
1920            s.parse::<f32>().ok()
1921        } else {
1922            None
1923        }
1924    });
1925
1926    let value_attr = node.attributes.get("value").ok_or_else(|| {
1927        super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
1928    })?;
1929    let value_expr = generate_attribute_value(value_attr, model_ident);
1930
1931    let on_change = node
1932        .events
1933        .iter()
1934        .find(|e| e.event == crate::EventKind::Change);
1935
1936    let mut slider = quote! {
1937        iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
1938    };
1939
1940    if let Some(m) = min {
1941        slider = quote! { #slider.min(#m) };
1942    }
1943    if let Some(m) = max {
1944        slider = quote! { #slider.max(#m) };
1945    }
1946
1947    // Apply step attribute (increment size)
1948    let step = node.attributes.get("step").and_then(|attr| {
1949        if let AttributeValue::Static(s) = attr {
1950            s.parse::<f32>().ok()
1951        } else {
1952            None
1953        }
1954    });
1955
1956    if let Some(s) = step {
1957        slider = quote! { #slider.step(#s) };
1958    }
1959
1960    if let Some(event) = on_change {
1961        let variant_name = to_upper_camel_case(&event.handler);
1962        let handler_ident = format_ident!("{}", variant_name);
1963        slider = quote! {
1964            iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
1965        };
1966    }
1967
1968    // Apply styles
1969    slider = apply_widget_style(slider, node, "slider", style_classes)?;
1970
1971    Ok(quote! { #slider.into() })
1972}
1973
1974/// Generate radio widget
1975fn generate_radio(
1976    node: &crate::WidgetNode,
1977    _model_ident: &syn::Ident,
1978    message_ident: &syn::Ident,
1979    _style_classes: &HashMap<String, StyleClass>,
1980) -> Result<TokenStream, super::CodegenError> {
1981    let label = node
1982        .attributes
1983        .get("label")
1984        .and_then(|attr| {
1985            if let AttributeValue::Static(s) = attr {
1986                Some(s.clone())
1987            } else {
1988                None
1989            }
1990        })
1991        .unwrap_or_default();
1992    let label_lit = proc_macro2::Literal::string(&label);
1993    let label_expr = quote! { #label_lit.to_string() };
1994
1995    let value_attr = node.attributes.get("value").ok_or_else(|| {
1996        super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
1997    })?;
1998    let value_expr = match value_attr {
1999        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
2000        _ => quote! { String::new() },
2001    };
2002
2003    let selected_attr = node.attributes.get("selected");
2004    let selected_expr = match selected_attr {
2005        Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
2006        _ => quote! { None },
2007    };
2008
2009    let on_select = node
2010        .events
2011        .iter()
2012        .find(|e| e.event == crate::EventKind::Select);
2013
2014    if let Some(event) = on_select {
2015        let variant_name = to_upper_camel_case(&event.handler);
2016        let handler_ident = format_ident!("{}", variant_name);
2017        Ok(quote! {
2018            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2019        })
2020    } else {
2021        Ok(quote! {
2022            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ()).into()
2023        })
2024    }
2025}
2026
2027/// Generate progress bar widget
2028fn generate_progress_bar(
2029    node: &crate::WidgetNode,
2030    model_ident: &syn::Ident,
2031    _style_classes: &HashMap<String, StyleClass>,
2032) -> Result<TokenStream, super::CodegenError> {
2033    let value_attr = node.attributes.get("value").ok_or_else(|| {
2034        super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
2035    })?;
2036    let value_expr = generate_attribute_value(value_attr, model_ident);
2037
2038    let max_attr = node.attributes.get("max").and_then(|attr| {
2039        if let AttributeValue::Static(s) = attr {
2040            s.parse::<f32>().ok()
2041        } else {
2042            None
2043        }
2044    });
2045
2046    // Parse style attribute (default to "primary")
2047    let style_str = node
2048        .attributes
2049        .get("style")
2050        .and_then(|attr| {
2051            if let AttributeValue::Static(s) = attr {
2052                Some(s.clone())
2053            } else {
2054                None
2055            }
2056        })
2057        .unwrap_or_else(|| "primary".to_string());
2058
2059    // Parse custom colors
2060    let bar_color = node.attributes.get("bar_color").and_then(|attr| {
2061        if let AttributeValue::Static(s) = attr {
2062            parse_color_to_tokens(s)
2063        } else {
2064            None
2065        }
2066    });
2067
2068    let background_color = node.attributes.get("background_color").and_then(|attr| {
2069        if let AttributeValue::Static(s) = attr {
2070            parse_color_to_tokens(s)
2071        } else {
2072            None
2073        }
2074    });
2075
2076    // Parse border radius
2077    let border_radius = node.attributes.get("border_radius").and_then(|attr| {
2078        if let AttributeValue::Static(s) = attr {
2079            s.parse::<f32>().ok()
2080        } else {
2081            None
2082        }
2083    });
2084
2085    // Parse height (girth)
2086    let height = node.attributes.get("height").and_then(|attr| {
2087        if let AttributeValue::Static(s) = attr {
2088            s.parse::<f32>().ok()
2089        } else {
2090            None
2091        }
2092    });
2093
2094    // Generate style closure based on style attribute
2095    let bar_color_expr = if let Some(color_tokens) = bar_color {
2096        quote! { #color_tokens }
2097    } else {
2098        match style_str.as_str() {
2099            "success" => quote! { palette.success.base.color },
2100            "warning" => quote! { palette.warning.base.color },
2101            "danger" => quote! { palette.danger.base.color },
2102            "secondary" => quote! { palette.secondary.base.color },
2103            _ => quote! { palette.primary.base.color }, // default to primary
2104        }
2105    };
2106
2107    // Generate background color expression
2108    let background_color_expr = if let Some(color_tokens) = background_color {
2109        quote! { #color_tokens }
2110    } else {
2111        quote! { palette.background.weak.color }
2112    };
2113
2114    // Generate border expression
2115    let border_expr = if let Some(radius) = border_radius {
2116        quote! { iced::Border::default().rounded(#radius) }
2117    } else {
2118        quote! { iced::Border::default() }
2119    };
2120
2121    // Generate height/girth expression
2122    let girth_expr = if let Some(h) = height {
2123        quote! { .girth(#h) }
2124    } else {
2125        quote! {}
2126    };
2127
2128    if let Some(max) = max_attr {
2129        Ok(quote! {
2130            iced::widget::progress_bar(0.0..=#max, #value_expr)
2131                #girth_expr
2132                .style(|theme: &iced::Theme| {
2133                    let palette = theme.extended_palette();
2134                    iced::widget::progress_bar::Style {
2135                        background: iced::Background::Color(#background_color_expr),
2136                        bar: iced::Background::Color(#bar_color_expr),
2137                        border: #border_expr,
2138                    }
2139                })
2140                .into()
2141        })
2142    } else {
2143        Ok(quote! {
2144            iced::widget::progress_bar(0.0..=100.0, #value_expr)
2145                #girth_expr
2146                .style(|theme: &iced::Theme| {
2147                    let palette = theme.extended_palette();
2148                    iced::widget::progress_bar::Style {
2149                        background: iced::Background::Color(#background_color_expr),
2150                        bar: iced::Background::Color(#bar_color_expr),
2151                        border: #border_expr,
2152                    }
2153                })
2154                .into()
2155        })
2156    }
2157}
2158
2159/// Parse a color string into TokenStream for code generation
2160fn parse_color_to_tokens(color_str: &str) -> Option<TokenStream> {
2161    // Try hex color (#RRGGBB or #RRGGBBAA)
2162    if let Some(hex) = color_str.strip_prefix('#') {
2163        if hex.len() == 6 {
2164            if let (Ok(r), Ok(g), Ok(b)) = (
2165                u8::from_str_radix(&hex[0..2], 16),
2166                u8::from_str_radix(&hex[2..4], 16),
2167                u8::from_str_radix(&hex[4..6], 16),
2168            ) {
2169                let rf = r as f32 / 255.0;
2170                let gf = g as f32 / 255.0;
2171                let bf = b as f32 / 255.0;
2172                return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2173            }
2174        } else if hex.len() == 8
2175            && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2176                u8::from_str_radix(&hex[0..2], 16),
2177                u8::from_str_radix(&hex[2..4], 16),
2178                u8::from_str_radix(&hex[4..6], 16),
2179                u8::from_str_radix(&hex[6..8], 16),
2180            )
2181        {
2182            let rf = r as f32 / 255.0;
2183            let gf = g as f32 / 255.0;
2184            let bf = b as f32 / 255.0;
2185            let af = a as f32 / 255.0;
2186            return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #af) });
2187        }
2188    }
2189
2190    // Try RGB format: rgb(r,g,b)
2191    if color_str.starts_with("rgb(") && color_str.ends_with(')') {
2192        let inner = &color_str[4..color_str.len() - 1];
2193        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2194        if parts.len() == 3
2195            && let (Ok(r), Ok(g), Ok(b)) = (
2196                parts[0].parse::<u8>(),
2197                parts[1].parse::<u8>(),
2198                parts[2].parse::<u8>(),
2199            )
2200        {
2201            let rf = r as f32 / 255.0;
2202            let gf = g as f32 / 255.0;
2203            let bf = b as f32 / 255.0;
2204            return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2205        }
2206    }
2207
2208    // Try RGBA format: rgba(r,g,b,a)
2209    if color_str.starts_with("rgba(") && color_str.ends_with(')') {
2210        let inner = &color_str[5..color_str.len() - 1];
2211        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2212        if parts.len() == 4
2213            && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2214                parts[0].parse::<u8>(),
2215                parts[1].parse::<u8>(),
2216                parts[2].parse::<u8>(),
2217                parts[3].parse::<f32>(),
2218            )
2219        {
2220            let rf = r as f32 / 255.0;
2221            let gf = g as f32 / 255.0;
2222            let bf = b as f32 / 255.0;
2223            return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #a) });
2224        }
2225    }
2226
2227    None
2228}
2229
2230/// Generate text input widget
2231fn generate_text_input(
2232    node: &crate::WidgetNode,
2233    model_ident: &syn::Ident,
2234    message_ident: &syn::Ident,
2235    style_classes: &HashMap<String, StyleClass>,
2236) -> Result<TokenStream, super::CodegenError> {
2237    let value_expr = node
2238        .attributes
2239        .get("value")
2240        .map(|attr| generate_attribute_value(attr, model_ident))
2241        .unwrap_or(quote! { String::new() });
2242
2243    let placeholder = node.attributes.get("placeholder").and_then(|attr| {
2244        if let AttributeValue::Static(s) = attr {
2245            Some(s.clone())
2246        } else {
2247            None
2248        }
2249    });
2250
2251    let on_input = node
2252        .events
2253        .iter()
2254        .find(|e| e.event == crate::EventKind::Input);
2255
2256    let on_submit = node
2257        .events
2258        .iter()
2259        .find(|e| e.event == crate::EventKind::Submit);
2260
2261    let mut text_input = match placeholder {
2262        Some(ph) => {
2263            let ph_lit = proc_macro2::Literal::string(&ph);
2264            quote! {
2265                iced::widget::text_input(#ph_lit, &#value_expr)
2266            }
2267        }
2268        None => quote! {
2269            iced::widget::text_input("", &#value_expr)
2270        },
2271    };
2272
2273    if let Some(event) = on_input {
2274        let variant_name = to_upper_camel_case(&event.handler);
2275        let handler_ident = format_ident!("{}", variant_name);
2276        text_input = quote! {
2277            #text_input.on_input(|v| #message_ident::#handler_ident(v))
2278        };
2279    }
2280
2281    if let Some(event) = on_submit {
2282        let variant_name = to_upper_camel_case(&event.handler);
2283        let handler_ident = format_ident!("{}", variant_name);
2284        text_input = quote! {
2285            #text_input.on_submit(#message_ident::#handler_ident)
2286        };
2287    }
2288
2289    // Apply password/secure attribute (masks input)
2290    let is_password = node
2291        .attributes
2292        .get("password")
2293        .or_else(|| node.attributes.get("secure"))
2294        .and_then(|attr| {
2295            if let AttributeValue::Static(s) = attr {
2296                Some(s.to_lowercase() == "true" || s == "1")
2297            } else {
2298                None
2299            }
2300        })
2301        .unwrap_or(false);
2302
2303    if is_password {
2304        text_input = quote! { #text_input.password() };
2305    }
2306
2307    // Apply styles
2308    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
2309
2310    Ok(quote! { #text_input.into() })
2311}
2312
2313/// Generate image widget
2314fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2315    let src_attr = node.attributes.get("src").ok_or_else(|| {
2316        super::CodegenError::InvalidWidget("image requires src attribute".to_string())
2317    })?;
2318
2319    let src = match src_attr {
2320        AttributeValue::Static(s) => s.clone(),
2321        _ => String::new(),
2322    };
2323    let src_lit = proc_macro2::Literal::string(&src);
2324
2325    let width = node.attributes.get("width").and_then(|attr| {
2326        if let AttributeValue::Static(s) = attr {
2327            s.parse::<u32>().ok()
2328        } else {
2329            None
2330        }
2331    });
2332
2333    let height = node.attributes.get("height").and_then(|attr| {
2334        if let AttributeValue::Static(s) = attr {
2335            s.parse::<u32>().ok()
2336        } else {
2337            None
2338        }
2339    });
2340
2341    let mut image = quote! {
2342        iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
2343    };
2344
2345    // Apply native width/height if specified with integer values
2346    if let (Some(w), Some(h)) = (width, height) {
2347        image = quote! { #image.width(#w).height(#h) };
2348    } else if let Some(w) = width {
2349        image = quote! { #image.width(#w) };
2350    } else if let Some(h) = height {
2351        image = quote! { #image.height(#h) };
2352    }
2353
2354    // Check if we need container for NON-native layout attributes
2355    // (padding, alignment, classes - NOT width/height since those are native)
2356    // For Image, only wrap if there are alignment/padding/classes
2357    let needs_container = !node.classes.is_empty()
2358        || node.attributes.contains_key("align_x")
2359        || node.attributes.contains_key("align_y")
2360        || node.attributes.contains_key("padding");
2361
2362    if needs_container {
2363        // Wrap with container for layout attributes, but skip width/height (already applied)
2364        let mut container = quote! { iced::widget::container(#image) };
2365
2366        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2367            if let AttributeValue::Static(s) = attr {
2368                s.parse::<f32>().ok()
2369            } else {
2370                None
2371            }
2372        }) {
2373            container = quote! { #container.padding(#padding) };
2374        }
2375
2376        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2377            if let AttributeValue::Static(s) = attr {
2378                Some(s.clone())
2379            } else {
2380                None
2381            }
2382        }) {
2383            let align_expr = generate_horizontal_alignment_expr(&align_x);
2384            container = quote! { #container.align_x(#align_expr) };
2385        }
2386
2387        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2388            if let AttributeValue::Static(s) = attr {
2389                Some(s.clone())
2390            } else {
2391                None
2392            }
2393        }) {
2394            let align_expr = generate_vertical_alignment_expr(&align_y);
2395            container = quote! { #container.align_y(#align_expr) };
2396        }
2397
2398        if let Some(class_name) = node.classes.first() {
2399            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2400            container = quote! { #container.style(#style_fn_ident) };
2401        }
2402
2403        Ok(quote! { #container.into() })
2404    } else {
2405        Ok(quote! { #image.into() })
2406    }
2407}
2408
2409/// Generate SVG widget
2410fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2411    // Support both "src" (standard) and "path" (legacy) for backward compatibility
2412    let path_attr = node
2413        .attributes
2414        .get("src")
2415        .or_else(|| node.attributes.get("path"))
2416        .ok_or_else(|| {
2417            super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
2418        })?;
2419
2420    let path = match path_attr {
2421        AttributeValue::Static(s) => s.clone(),
2422        _ => String::new(),
2423    };
2424    let path_lit = proc_macro2::Literal::string(&path);
2425
2426    let width = node.attributes.get("width").and_then(|attr| {
2427        if let AttributeValue::Static(s) = attr {
2428            s.parse::<u32>().ok()
2429        } else {
2430            None
2431        }
2432    });
2433
2434    let height = node.attributes.get("height").and_then(|attr| {
2435        if let AttributeValue::Static(s) = attr {
2436            s.parse::<u32>().ok()
2437        } else {
2438            None
2439        }
2440    });
2441
2442    let mut svg = quote! {
2443        iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
2444    };
2445
2446    // Apply native width/height if specified with integer values
2447    if let (Some(w), Some(h)) = (width, height) {
2448        svg = quote! { #svg.width(#w).height(#h) };
2449    } else if let Some(w) = width {
2450        svg = quote! { #svg.width(#w) };
2451    } else if let Some(h) = height {
2452        svg = quote! { #svg.height(#h) };
2453    }
2454
2455    // Check if we need container for NON-native layout attributes
2456    // (padding, alignment, classes - NOT width/height since those are native)
2457    // For SVG, only wrap if there are alignment/padding/classes
2458    let needs_container = !node.classes.is_empty()
2459        || node.attributes.contains_key("align_x")
2460        || node.attributes.contains_key("align_y")
2461        || node.attributes.contains_key("padding");
2462
2463    if needs_container {
2464        // Wrap with container for layout attributes, but skip width/height (already applied)
2465        let mut container = quote! { iced::widget::container(#svg) };
2466
2467        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2468            if let AttributeValue::Static(s) = attr {
2469                s.parse::<f32>().ok()
2470            } else {
2471                None
2472            }
2473        }) {
2474            container = quote! { #container.padding(#padding) };
2475        }
2476
2477        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2478            if let AttributeValue::Static(s) = attr {
2479                Some(s.clone())
2480            } else {
2481                None
2482            }
2483        }) {
2484            let align_expr = generate_horizontal_alignment_expr(&align_x);
2485            container = quote! { #container.align_x(#align_expr) };
2486        }
2487
2488        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2489            if let AttributeValue::Static(s) = attr {
2490                Some(s.clone())
2491            } else {
2492                None
2493            }
2494        }) {
2495            let align_expr = generate_vertical_alignment_expr(&align_y);
2496            container = quote! { #container.align_y(#align_expr) };
2497        }
2498
2499        if let Some(class_name) = node.classes.first() {
2500            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2501            container = quote! { #container.style(#style_fn_ident) };
2502        }
2503
2504        Ok(quote! { #container.into() })
2505    } else {
2506        Ok(quote! { #svg.into() })
2507    }
2508}
2509
2510/// Generate pick list widget
2511fn generate_pick_list(
2512    node: &crate::WidgetNode,
2513    model_ident: &syn::Ident,
2514    message_ident: &syn::Ident,
2515    _style_classes: &HashMap<String, StyleClass>,
2516) -> Result<TokenStream, super::CodegenError> {
2517    let options_attr = node.attributes.get("options").ok_or_else(|| {
2518        super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2519    })?;
2520
2521    let options: Vec<String> = match options_attr {
2522        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2523        _ => Vec::new(),
2524    };
2525    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2526
2527    let selected_attr = node.attributes.get("selected");
2528    let selected_expr = selected_attr
2529        .map(|attr| generate_attribute_value(attr, model_ident))
2530        .unwrap_or(quote! { None });
2531
2532    let on_select = node
2533        .events
2534        .iter()
2535        .find(|e| e.event == crate::EventKind::Select);
2536
2537    if let Some(event) = on_select {
2538        let variant_name = to_upper_camel_case(&event.handler);
2539        let handler_ident = format_ident!("{}", variant_name);
2540        Ok(quote! {
2541            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2542        })
2543    } else {
2544        Ok(quote! {
2545            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2546        })
2547    }
2548}
2549
2550/// Generate combo box widget
2551fn generate_combo_box(
2552    node: &crate::WidgetNode,
2553    model_ident: &syn::Ident,
2554    message_ident: &syn::Ident,
2555    _style_classes: &HashMap<String, StyleClass>,
2556) -> Result<TokenStream, super::CodegenError> {
2557    let options_attr = node.attributes.get("options").ok_or_else(|| {
2558        super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2559    })?;
2560
2561    let options: Vec<String> = match options_attr {
2562        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2563        _ => Vec::new(),
2564    };
2565    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2566
2567    let selected_attr = node.attributes.get("selected");
2568    let selected_expr = selected_attr
2569        .map(|attr| generate_attribute_value(attr, model_ident))
2570        .unwrap_or(quote! { None });
2571
2572    let on_select = node
2573        .events
2574        .iter()
2575        .find(|e| e.event == crate::EventKind::Select);
2576
2577    if let Some(event) = on_select {
2578        let variant_name = to_upper_camel_case(&event.handler);
2579        let handler_ident = format_ident!("{}", variant_name);
2580        Ok(quote! {
2581            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2582        })
2583    } else {
2584        Ok(quote! {
2585            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2586        })
2587    }
2588}
2589
2590/// Generate tooltip widget
2591fn generate_tooltip(
2592    node: &crate::WidgetNode,
2593    model_ident: &syn::Ident,
2594    message_ident: &syn::Ident,
2595    style_classes: &HashMap<String, StyleClass>,
2596) -> Result<TokenStream, super::CodegenError> {
2597    let child = node.children.first().ok_or_else(|| {
2598        super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2599    })?;
2600    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2601
2602    let message_attr = node.attributes.get("message").ok_or_else(|| {
2603        super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2604    })?;
2605    let message_expr = generate_attribute_value(message_attr, model_ident);
2606
2607    Ok(quote! {
2608        iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2609    })
2610}
2611
2612/// Generate grid widget
2613fn generate_grid(
2614    node: &crate::WidgetNode,
2615    model_ident: &syn::Ident,
2616    message_ident: &syn::Ident,
2617    style_classes: &HashMap<String, StyleClass>,
2618) -> Result<TokenStream, super::CodegenError> {
2619    let children: Vec<TokenStream> = node
2620        .children
2621        .iter()
2622        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2623        .collect::<Result<_, _>>()?;
2624
2625    let columns = node
2626        .attributes
2627        .get("columns")
2628        .and_then(|attr| {
2629            if let AttributeValue::Static(s) = attr {
2630                s.parse::<u32>().ok()
2631            } else {
2632                None
2633            }
2634        })
2635        .unwrap_or(1);
2636
2637    let spacing = node.attributes.get("spacing").and_then(|attr| {
2638        if let AttributeValue::Static(s) = attr {
2639            s.parse::<f32>().ok()
2640        } else {
2641            None
2642        }
2643    });
2644
2645    let padding = node.attributes.get("padding").and_then(|attr| {
2646        if let AttributeValue::Static(s) = attr {
2647            s.parse::<f32>().ok()
2648        } else {
2649            None
2650        }
2651    });
2652
2653    let grid = quote! {
2654        iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2655    };
2656
2657    let grid = if let Some(s) = spacing {
2658        quote! { #grid.spacing(#s) }
2659    } else {
2660        grid
2661    };
2662
2663    let grid = if let Some(p) = padding {
2664        quote! { #grid.padding(#p) }
2665    } else {
2666        grid
2667    };
2668
2669    Ok(quote! { #grid.into() })
2670}
2671
2672/// Generate canvas widget
2673fn generate_canvas(
2674    node: &crate::WidgetNode,
2675    model_ident: &syn::Ident,
2676    message_ident: &syn::Ident,
2677    _style_classes: &HashMap<String, StyleClass>,
2678) -> Result<TokenStream, super::CodegenError> {
2679    let width = node.attributes.get("width").and_then(|attr| {
2680        if let AttributeValue::Static(s) = attr {
2681            s.parse::<f32>().ok()
2682        } else {
2683            None
2684        }
2685    });
2686
2687    let height = node.attributes.get("height").and_then(|attr| {
2688        if let AttributeValue::Static(s) = attr {
2689            s.parse::<f32>().ok()
2690        } else {
2691            None
2692        }
2693    });
2694
2695    let width_expr = match width {
2696        Some(w) => quote! { iced::Length::Fixed(#w) },
2697        None => quote! { iced::Length::Fixed(400.0) },
2698    };
2699
2700    let height_expr = match height {
2701        Some(h) => quote! { iced::Length::Fixed(#h) },
2702        None => quote! { iced::Length::Fixed(300.0) },
2703    };
2704
2705    // Check for custom program binding
2706    let content_expr = if let Some(program_attr) = node.attributes.get("program") {
2707        let program_binding = match program_attr {
2708            AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
2709            _ => quote! { None },
2710        };
2711
2712        // Generate declarative canvas for the 'else' case
2713        let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2714        let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2715        let prog_init = quote! {
2716            dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2717        };
2718        let prog_with_handlers = if let Some(handlers) = handlers_expr {
2719            quote! { #prog_init.with_handlers(#handlers) }
2720        } else {
2721            prog_init
2722        };
2723
2724        quote! {
2725            if let Some(container) = &#program_binding {
2726                 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2727                     dampen_iced::canvas::CanvasContent::Custom(container.0.clone())
2728                 ))
2729                 .width(#width_expr)
2730                 .height(#height_expr);
2731
2732                 iced::Element::from(canvas).map(|()| unreachable!("Custom program action not supported in codegen"))
2733            } else {
2734                 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2735                     dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2736                 ))
2737                 .width(#width_expr)
2738                 .height(#height_expr);
2739
2740                 iced::Element::from(canvas)
2741            }
2742        }
2743    } else {
2744        // Generate declarative canvas
2745        let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2746
2747        // Parse event handlers
2748        let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2749
2750        let prog_init = quote! {
2751            dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2752        };
2753
2754        let prog_with_handlers = if let Some(handlers) = handlers_expr {
2755            quote! { #prog_init.with_handlers(#handlers) }
2756        } else {
2757            prog_init
2758        };
2759
2760        quote! {
2761            iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2762                dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2763            ))
2764            .width(#width_expr)
2765            .height(#height_expr)
2766            .into()
2767        }
2768    };
2769
2770    Ok(content_expr)
2771}
2772
2773/// Generate float widget
2774fn generate_float(
2775    node: &crate::WidgetNode,
2776    model_ident: &syn::Ident,
2777    message_ident: &syn::Ident,
2778    style_classes: &HashMap<String, StyleClass>,
2779) -> Result<TokenStream, super::CodegenError> {
2780    let child = node.children.first().ok_or_else(|| {
2781        super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2782    })?;
2783    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2784
2785    let position = node
2786        .attributes
2787        .get("position")
2788        .and_then(|attr| {
2789            if let AttributeValue::Static(s) = attr {
2790                Some(s.clone())
2791            } else {
2792                None
2793            }
2794        })
2795        .unwrap_or_else(|| "TopRight".to_string());
2796
2797    let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2798        if let AttributeValue::Static(s) = attr {
2799            s.parse::<f32>().ok()
2800        } else {
2801            None
2802        }
2803    });
2804
2805    let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2806        if let AttributeValue::Static(s) = attr {
2807            s.parse::<f32>().ok()
2808        } else {
2809            None
2810        }
2811    });
2812
2813    let float = match position.as_str() {
2814        "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2815        "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2816        "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2817        "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2818        _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2819    };
2820
2821    let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2822        quote! { #float.offset_x(#ox).offset_y(#oy) }
2823    } else if let Some(ox) = offset_x {
2824        quote! { #float.offset_x(#ox) }
2825    } else if let Some(oy) = offset_y {
2826        quote! { #float.offset_y(#oy) }
2827    } else {
2828        float
2829    };
2830
2831    Ok(quote! { #float.into() })
2832}
2833
2834/// Generate for loop widget (iterates over collection)
2835///
2836/// Expects attributes:
2837/// - `each`: variable name for each item (e.g., "task")
2838/// - `in`: binding expression for the collection (e.g., "{filtered_tasks}")
2839fn generate_for(
2840    node: &crate::WidgetNode,
2841    model_ident: &syn::Ident,
2842    message_ident: &syn::Ident,
2843    style_classes: &HashMap<String, StyleClass>,
2844) -> Result<TokenStream, super::CodegenError> {
2845    // Get the 'in' attribute (collection to iterate)
2846    let in_attr = node.attributes.get("in").ok_or_else(|| {
2847        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2848    })?;
2849
2850    // Get the 'each' attribute (loop variable name)
2851    let var_name = node
2852        .attributes
2853        .get("each")
2854        .and_then(|attr| {
2855            if let AttributeValue::Static(s) = attr {
2856                Some(s.clone())
2857            } else {
2858                None
2859            }
2860        })
2861        .unwrap_or_else(|| "item".to_string());
2862
2863    let var_ident = format_ident!("{}", var_name);
2864
2865    // Generate the collection expression (raw, without .to_string())
2866    let collection_expr = generate_attribute_value_raw(in_attr, model_ident);
2867
2868    // Generate children widgets
2869    let children: Vec<TokenStream> = node
2870        .children
2871        .iter()
2872        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2873        .collect::<Result<_, _>>()?;
2874
2875    // Generate the for loop that builds widgets
2876    Ok(quote! {
2877        {
2878            let items: Vec<_> = #collection_expr;
2879            let widgets: Vec<iced::Element<'_, #message_ident>> = items
2880                .iter()
2881                .enumerate()
2882                .flat_map(|(index, #var_ident)| {
2883                    let _ = index; // Suppress unused warning if not used
2884                    vec![#(#children),*]
2885                })
2886                .collect();
2887            iced::widget::column(widgets).into()
2888        }
2889    })
2890}
2891
2892/// Generate if widget
2893fn generate_if(
2894    node: &crate::WidgetNode,
2895    model_ident: &syn::Ident,
2896    message_ident: &syn::Ident,
2897    style_classes: &HashMap<String, StyleClass>,
2898) -> Result<TokenStream, super::CodegenError> {
2899    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
2900        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
2901    })?;
2902
2903    let children: Vec<TokenStream> = node
2904        .children
2905        .iter()
2906        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2907        .collect::<Result<_, _>>()?;
2908
2909    let condition_expr = generate_attribute_value(condition_attr, model_ident);
2910
2911    Ok(quote! {
2912        if #condition_expr.parse::<bool>().unwrap_or(false) {
2913            iced::widget::column(vec![#(#children),*]).into()
2914        } else {
2915            iced::widget::column(vec![]).into()
2916        }
2917    })
2918}
2919
2920/// Generate custom widget
2921/// Generate DatePicker widget
2922fn generate_date_picker(
2923    node: &crate::WidgetNode,
2924    model_ident: &syn::Ident,
2925    message_ident: &syn::Ident,
2926    style_classes: &HashMap<String, StyleClass>,
2927) -> Result<TokenStream, super::CodegenError> {
2928    let show = node
2929        .attributes
2930        .get("show")
2931        .map(|attr| match attr {
2932            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
2933            AttributeValue::Static(s) => {
2934                let v = s == "true";
2935                quote! { #v }
2936            }
2937            _ => quote! { false },
2938        })
2939        .unwrap_or(quote! { false });
2940
2941    let date = if let Some(attr) = node.attributes.get("value") {
2942        match attr {
2943            AttributeValue::Binding(b) => {
2944                let expr = super::bindings::generate_bool_expr(&b.expr);
2945                quote! { iced_aw::date_picker::Date::from(#expr) }
2946            }
2947            AttributeValue::Static(s) => {
2948                let format = node
2949                    .attributes
2950                    .get("format")
2951                    .map(|f| match f {
2952                        AttributeValue::Static(fs) => fs.as_str(),
2953                        _ => "%Y-%m-%d",
2954                    })
2955                    .unwrap_or("%Y-%m-%d");
2956                quote! {
2957                    iced_aw::date_picker::Date::from(
2958                        chrono::NaiveDate::parse_from_str(#s, #format).unwrap_or_default()
2959                    )
2960                }
2961            }
2962            _ => quote! { iced_aw::date_picker::Date::today() },
2963        }
2964    } else {
2965        quote! { iced_aw::date_picker::Date::today() }
2966    };
2967
2968    let on_cancel = if let Some(h) = node
2969        .events
2970        .iter()
2971        .find(|e| e.event == crate::EventKind::Cancel)
2972    {
2973        let msg = format_ident!("{}", h.handler);
2974        quote! { #message_ident::#msg }
2975    } else {
2976        quote! { #message_ident::None }
2977    };
2978
2979    let on_submit = if let Some(h) = node
2980        .events
2981        .iter()
2982        .find(|e| e.event == crate::EventKind::Submit)
2983    {
2984        let msg = format_ident!("{}", h.handler);
2985        quote! {
2986            |date| {
2987                let s = chrono::NaiveDate::from(date).format("%Y-%m-%d").to_string();
2988                #message_ident::#msg(s)
2989            }
2990        }
2991    } else {
2992        quote! { |_| #message_ident::None }
2993    };
2994
2995    let underlay = if let Some(child) = node.children.first() {
2996        generate_widget(child, model_ident, message_ident, style_classes)?
2997    } else {
2998        quote! { iced::widget::text("Missing child") }
2999    };
3000
3001    Ok(quote! {
3002        iced_aw::widgets::date_picker::DatePicker::new(
3003            #show,
3004            #date,
3005            #underlay,
3006            #on_cancel,
3007            #on_submit
3008        )
3009    })
3010}
3011
3012/// Generate ColorPicker widget
3013fn generate_color_picker(
3014    node: &crate::WidgetNode,
3015    model_ident: &syn::Ident,
3016    message_ident: &syn::Ident,
3017    style_classes: &HashMap<String, StyleClass>,
3018) -> Result<TokenStream, super::CodegenError> {
3019    let show = node
3020        .attributes
3021        .get("show")
3022        .map(|attr| match attr {
3023            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3024            AttributeValue::Static(s) => {
3025                let v = s == "true";
3026                quote! { #v }
3027            }
3028            _ => quote! { false },
3029        })
3030        .unwrap_or(quote! { false });
3031
3032    let color = if let Some(attr) = node.attributes.get("value") {
3033        match attr {
3034            AttributeValue::Binding(b) => {
3035                let expr = super::bindings::generate_expr(&b.expr);
3036                quote! { iced::Color::from_hex(&#expr.to_string()).unwrap_or(iced::Color::BLACK) }
3037            }
3038            AttributeValue::Static(s) => {
3039                quote! { iced::Color::from_hex(#s).unwrap_or(iced::Color::BLACK) }
3040            }
3041            _ => quote! { iced::Color::BLACK },
3042        }
3043    } else {
3044        quote! { iced::Color::BLACK }
3045    };
3046
3047    let on_cancel = if let Some(h) = node
3048        .events
3049        .iter()
3050        .find(|e| e.event == crate::EventKind::Cancel)
3051    {
3052        let msg = format_ident!("{}", h.handler);
3053        quote! { #message_ident::#msg }
3054    } else {
3055        quote! { #message_ident::None }
3056    };
3057
3058    let on_submit = if let Some(h) = node
3059        .events
3060        .iter()
3061        .find(|e| e.event == crate::EventKind::Submit)
3062    {
3063        let msg = format_ident!("{}", h.handler);
3064        quote! {
3065            |color| {
3066                let s = iced::color!(color).to_string();
3067                #message_ident::#msg(s)
3068            }
3069        }
3070    } else {
3071        quote! { |_| #message_ident::None }
3072    };
3073
3074    let underlay = if let Some(child) = node.children.first() {
3075        generate_widget(child, model_ident, message_ident, style_classes)?
3076    } else {
3077        quote! { iced::widget::text("Missing child") }
3078    };
3079
3080    Ok(quote! {
3081        iced_aw::widgets::color_picker::ColorPicker::new(
3082            #show,
3083            #color,
3084            #underlay,
3085            #on_cancel,
3086            #on_submit
3087        )
3088    })
3089}
3090
3091/// Generate TimePicker widget
3092fn generate_time_picker(
3093    node: &crate::WidgetNode,
3094    model_ident: &syn::Ident,
3095    message_ident: &syn::Ident,
3096    style_classes: &HashMap<String, StyleClass>,
3097) -> Result<TokenStream, super::CodegenError> {
3098    let show = node
3099        .attributes
3100        .get("show")
3101        .map(|attr| match attr {
3102            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3103            AttributeValue::Static(s) => {
3104                let v = s == "true";
3105                quote! { #v }
3106            }
3107            _ => quote! { false },
3108        })
3109        .unwrap_or(quote! { false });
3110
3111    let time = if let Some(attr) = node.attributes.get("value") {
3112        match attr {
3113            AttributeValue::Binding(b) => {
3114                let expr = super::bindings::generate_bool_expr(&b.expr);
3115                quote! { iced_aw::time_picker::Time::from(#expr) }
3116            }
3117            AttributeValue::Static(s) => {
3118                let format = node
3119                    .attributes
3120                    .get("format")
3121                    .map(|f| match f {
3122                        AttributeValue::Static(fs) => fs.as_str(),
3123                        _ => "%H:%M:%S",
3124                    })
3125                    .unwrap_or("%H:%M:%S");
3126                quote! {
3127                    iced_aw::time_picker::Time::from(
3128                        chrono::NaiveTime::parse_from_str(#s, #format).unwrap_or_default()
3129                    )
3130                }
3131            }
3132            _ => {
3133                quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3134            }
3135        }
3136    } else {
3137        quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3138    };
3139
3140    let use_24h = node.attributes.get("use_24h").map(|attr| match attr {
3141        AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3142        AttributeValue::Static(s) => {
3143            let v = s == "true";
3144            quote! { #v }
3145        }
3146        _ => quote! { false },
3147    });
3148
3149    let show_seconds = node.attributes.get("show_seconds").map(|attr| match attr {
3150        AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3151        AttributeValue::Static(s) => {
3152            let v = s == "true";
3153            quote! { #v }
3154        }
3155        _ => quote! { false },
3156    });
3157
3158    let on_cancel = if let Some(h) = node
3159        .events
3160        .iter()
3161        .find(|e| e.event == crate::EventKind::Cancel)
3162    {
3163        let msg = format_ident!("{}", h.handler);
3164        quote! { #message_ident::#msg }
3165    } else {
3166        quote! { #message_ident::None }
3167    };
3168
3169    let on_submit = if let Some(h) = node
3170        .events
3171        .iter()
3172        .find(|e| e.event == crate::EventKind::Submit)
3173    {
3174        let msg = format_ident!("{}", h.handler);
3175        quote! {
3176            |time| {
3177                let s = chrono::NaiveTime::from(time).format("%H:%M:%S").to_string();
3178                #message_ident::#msg(s)
3179            }
3180        }
3181    } else {
3182        quote! { |_| #message_ident::None }
3183    };
3184
3185    let underlay = if let Some(child) = node.children.first() {
3186        generate_widget(child, model_ident, message_ident, style_classes)?
3187    } else {
3188        quote! { iced::widget::text("Missing child") }
3189    };
3190
3191    let mut picker_setup = quote! {
3192        let mut picker = iced_aw::widgets::time_picker::TimePicker::new(
3193            #show,
3194            #time,
3195            #underlay,
3196            #on_cancel,
3197            #on_submit
3198        );
3199    };
3200
3201    if let Some(use_24h_expr) = use_24h {
3202        picker_setup.extend(quote! {
3203            if #use_24h_expr {
3204                picker = picker.use_24h();
3205            }
3206        });
3207    }
3208
3209    if let Some(show_seconds_expr) = show_seconds {
3210        picker_setup.extend(quote! {
3211            if #show_seconds_expr {
3212                picker = picker.show_seconds();
3213            }
3214        });
3215    }
3216
3217    Ok(quote! {
3218        {
3219            #picker_setup
3220            picker
3221        }
3222    })
3223}
3224
3225fn generate_custom_widget(
3226    node: &crate::WidgetNode,
3227    name: &str,
3228    model_ident: &syn::Ident,
3229    message_ident: &syn::Ident,
3230    style_classes: &HashMap<String, StyleClass>,
3231) -> Result<TokenStream, super::CodegenError> {
3232    let widget_ident = format_ident!("{}", name);
3233    let children: Vec<TokenStream> = node
3234        .children
3235        .iter()
3236        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
3237        .collect::<Result<_, _>>()?;
3238
3239    Ok(quote! {
3240        #widget_ident(vec![#(#children),*]).into()
3241    })
3242}
3243
3244/// Generate attribute value expression with inlined bindings
3245fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3246    match attr {
3247        AttributeValue::Static(s) => {
3248            let lit = proc_macro2::Literal::string(s);
3249            quote! { #lit.to_string() }
3250        }
3251        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
3252        AttributeValue::Interpolated(parts) => {
3253            let parts_str: Vec<String> = parts
3254                .iter()
3255                .map(|part| match part {
3256                    InterpolatedPart::Literal(s) => s.clone(),
3257                    InterpolatedPart::Binding(_) => "{}".to_string(),
3258                })
3259                .collect();
3260            let binding_exprs: Vec<TokenStream> = parts
3261                .iter()
3262                .filter_map(|part| {
3263                    if let InterpolatedPart::Binding(expr) = part {
3264                        Some(generate_expr(&expr.expr))
3265                    } else {
3266                        None
3267                    }
3268                })
3269                .collect();
3270
3271            let format_string = parts_str.join("");
3272            let lit = proc_macro2::Literal::string(&format_string);
3273
3274            quote! { format!(#lit, #(#binding_exprs),*) }
3275        }
3276    }
3277}
3278
3279/// Generate attribute value without `.to_string()` conversion
3280/// Used for collections in for loops where we need the raw value
3281fn generate_attribute_value_raw(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3282    match attr {
3283        AttributeValue::Static(s) => {
3284            let lit = proc_macro2::Literal::string(s);
3285            quote! { #lit }
3286        }
3287        AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
3288        AttributeValue::Interpolated(parts) => {
3289            // For interpolated, we still need to generate a string
3290            let parts_str: Vec<String> = parts
3291                .iter()
3292                .map(|part| match part {
3293                    InterpolatedPart::Literal(s) => s.clone(),
3294                    InterpolatedPart::Binding(_) => "{}".to_string(),
3295                })
3296                .collect();
3297            let binding_exprs: Vec<TokenStream> = parts
3298                .iter()
3299                .filter_map(|part| {
3300                    if let InterpolatedPart::Binding(expr) = part {
3301                        Some(generate_expr(&expr.expr))
3302                    } else {
3303                        None
3304                    }
3305                })
3306                .collect();
3307
3308            let format_string = parts_str.join("");
3309            let lit = proc_macro2::Literal::string(&format_string);
3310
3311            quote! { format!(#lit, #(#binding_exprs),*) }
3312        }
3313    }
3314}
3315
3316// ============================================================================
3317// Functions with local variable context support (for loops)
3318// ============================================================================
3319
3320/// Generate text widget with local variable context
3321fn generate_text_with_locals(
3322    node: &crate::WidgetNode,
3323    model_ident: &syn::Ident,
3324    _style_classes: &HashMap<String, StyleClass>,
3325    local_vars: &std::collections::HashSet<String>,
3326) -> Result<TokenStream, super::CodegenError> {
3327    let value_attr = node.attributes.get("value").ok_or_else(|| {
3328        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
3329    })?;
3330
3331    let value_expr = generate_attribute_value_with_locals(value_attr, model_ident, local_vars);
3332
3333    let mut text_widget = quote! {
3334        iced::widget::text(#value_expr)
3335    };
3336
3337    // Apply size attribute
3338    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3339        if let AttributeValue::Static(s) = attr {
3340            s.parse::<f32>().ok()
3341        } else {
3342            None
3343        }
3344    }) {
3345        text_widget = quote! { #text_widget.size(#size) };
3346    }
3347
3348    // Apply weight attribute
3349    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
3350        if let AttributeValue::Static(s) = attr {
3351            Some(s.clone())
3352        } else {
3353            None
3354        }
3355    }) {
3356        let weight_expr = match weight.to_lowercase().as_str() {
3357            "bold" => quote! { iced::font::Weight::Bold },
3358            "semibold" => quote! { iced::font::Weight::Semibold },
3359            "medium" => quote! { iced::font::Weight::Medium },
3360            "light" => quote! { iced::font::Weight::Light },
3361            _ => quote! { iced::font::Weight::Normal },
3362        };
3363        text_widget = quote! {
3364            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
3365        };
3366    }
3367
3368    // Apply inline style color if present
3369    if let Some(ref style_props) = node.style
3370        && let Some(ref color) = style_props.color
3371    {
3372        let color_expr = generate_color_expr(color);
3373        text_widget = quote! { #text_widget.color(#color_expr) };
3374    }
3375
3376    Ok(maybe_wrap_in_container(text_widget, node))
3377}
3378
3379/// Generate button widget with local variable context
3380fn generate_button_with_locals(
3381    node: &crate::WidgetNode,
3382    model_ident: &syn::Ident,
3383    message_ident: &syn::Ident,
3384    style_classes: &HashMap<String, StyleClass>,
3385    local_vars: &std::collections::HashSet<String>,
3386) -> Result<TokenStream, super::CodegenError> {
3387    let label_attr = node.attributes.get("label").ok_or_else(|| {
3388        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
3389    })?;
3390
3391    let label_expr = generate_attribute_value_with_locals(label_attr, model_ident, local_vars);
3392
3393    let on_click = node
3394        .events
3395        .iter()
3396        .find(|e| e.event == crate::EventKind::Click);
3397
3398    let mut button = quote! {
3399        iced::widget::button(iced::widget::text(#label_expr))
3400    };
3401
3402    if let Some(event) = on_click {
3403        let variant_name = to_upper_camel_case(&event.handler);
3404        let handler_ident = format_ident!("{}", variant_name);
3405
3406        let param_expr = if let Some(ref param) = event.param {
3407            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
3408            quote! { (#param_tokens) }
3409        } else {
3410            quote! {}
3411        };
3412
3413        button = quote! {
3414            #button.on_press(#message_ident::#handler_ident #param_expr)
3415        };
3416    }
3417
3418    // Apply styles
3419    button = apply_widget_style(button, node, "button", style_classes)?;
3420
3421    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#button) })
3422}
3423
3424/// Generate container widget with local variable context
3425fn generate_container_with_locals(
3426    node: &crate::WidgetNode,
3427    widget_type: &str,
3428    model_ident: &syn::Ident,
3429    message_ident: &syn::Ident,
3430    style_classes: &HashMap<String, StyleClass>,
3431    local_vars: &std::collections::HashSet<String>,
3432) -> Result<TokenStream, super::CodegenError> {
3433    let children: Vec<TokenStream> = node
3434        .children
3435        .iter()
3436        .map(|child| {
3437            generate_widget_with_locals(
3438                child,
3439                model_ident,
3440                message_ident,
3441                style_classes,
3442                local_vars,
3443            )
3444        })
3445        .collect::<Result<_, _>>()?;
3446
3447    let mut container = match widget_type {
3448        "column" => {
3449            quote! { iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3450        }
3451        "row" => {
3452            quote! { iced::widget::row({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3453        }
3454        "scrollable" => {
3455            quote! { iced::widget::scrollable(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3456        }
3457        _ => {
3458            // container wraps a single child
3459            if children.len() == 1 {
3460                let child = &children[0];
3461                quote! { iced::widget::container(#child) }
3462            } else {
3463                quote! { iced::widget::container(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3464            }
3465        }
3466    };
3467
3468    // Get merged layout from node.layout and style classes
3469    let merged_layout = get_merged_layout(node, style_classes);
3470
3471    // Get spacing from attributes or merged layout
3472    let spacing = node
3473        .attributes
3474        .get("spacing")
3475        .and_then(|attr| {
3476            if let AttributeValue::Static(s) = attr {
3477                s.parse::<f32>().ok()
3478            } else {
3479                None
3480            }
3481        })
3482        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
3483
3484    // Apply spacing for column/row
3485    if let Some(s) = spacing
3486        && (widget_type == "column" || widget_type == "row")
3487    {
3488        container = quote! { #container.spacing(#s) };
3489    }
3490
3491    // Get padding from attributes or merged layout
3492    let padding = node
3493        .attributes
3494        .get("padding")
3495        .and_then(|attr| {
3496            if let AttributeValue::Static(s) = attr {
3497                s.parse::<f32>().ok()
3498            } else {
3499                None
3500            }
3501        })
3502        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
3503
3504    // Apply padding
3505    if let Some(p) = padding {
3506        container = quote! { #container.padding(#p) };
3507    }
3508
3509    // Apply width from attributes or merged layout
3510    let width_from_attr = node.attributes.get("width").and_then(|attr| {
3511        if let AttributeValue::Static(s) = attr {
3512            Some(s.clone())
3513        } else {
3514            None
3515        }
3516    });
3517    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
3518
3519    if let Some(width) = width_from_attr {
3520        let width_expr = generate_length_expr(&width);
3521        container = quote! { #container.width(#width_expr) };
3522    } else if let Some(layout_width) = width_from_layout {
3523        let width_expr = generate_layout_length_expr(layout_width);
3524        container = quote! { #container.width(#width_expr) };
3525    }
3526
3527    // Apply height from attributes or merged layout
3528    let height_from_attr = node.attributes.get("height").and_then(|attr| {
3529        if let AttributeValue::Static(s) = attr {
3530            Some(s.clone())
3531        } else {
3532            None
3533        }
3534    });
3535    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
3536
3537    if let Some(height) = height_from_attr {
3538        let height_expr = generate_length_expr(&height);
3539        container = quote! { #container.height(#height_expr) };
3540    } else if let Some(layout_height) = height_from_layout {
3541        let height_expr = generate_layout_length_expr(layout_height);
3542        container = quote! { #container.height(#height_expr) };
3543    }
3544
3545    // Apply alignment for row/column
3546    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
3547        if let AttributeValue::Static(s) = attr {
3548            Some(s.clone())
3549        } else {
3550            None
3551        }
3552    }) && widget_type == "row"
3553    {
3554        let alignment_expr = match align_y.to_lowercase().as_str() {
3555            "top" | "start" => quote! { iced::alignment::Vertical::Top },
3556            "bottom" | "end" => quote! { iced::alignment::Vertical::Bottom },
3557            _ => quote! { iced::alignment::Vertical::Center },
3558        };
3559        container = quote! { #container.align_y(#alignment_expr) };
3560    }
3561
3562    // Apply styles
3563    if widget_type == "container" {
3564        container = apply_widget_style(container, node, "container", style_classes)?;
3565    }
3566
3567    // Use explicit into() conversion to help type inference with nested containers
3568    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#container) })
3569}
3570
3571/// Generate for loop widget with local variable context
3572fn generate_for_with_locals(
3573    node: &crate::WidgetNode,
3574    model_ident: &syn::Ident,
3575    message_ident: &syn::Ident,
3576    style_classes: &HashMap<String, StyleClass>,
3577    local_vars: &std::collections::HashSet<String>,
3578) -> Result<TokenStream, super::CodegenError> {
3579    // Get the 'in' attribute (collection to iterate)
3580    let in_attr = node.attributes.get("in").ok_or_else(|| {
3581        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
3582    })?;
3583
3584    // Get the 'each' attribute (loop variable name)
3585    let var_name = node
3586        .attributes
3587        .get("each")
3588        .and_then(|attr| {
3589            if let AttributeValue::Static(s) = attr {
3590                Some(s.clone())
3591            } else {
3592                None
3593            }
3594        })
3595        .unwrap_or_else(|| "item".to_string());
3596
3597    let var_ident = format_ident!("{}", var_name);
3598
3599    // Generate the collection expression (raw, without .to_string())
3600    let collection_expr =
3601        generate_attribute_value_raw_with_locals(in_attr, model_ident, local_vars);
3602
3603    // Create new local vars set including the loop variable
3604    let mut new_local_vars = local_vars.clone();
3605    new_local_vars.insert(var_name.clone());
3606    new_local_vars.insert("index".to_string());
3607
3608    // Generate children widgets with the new local context
3609    let children: Vec<TokenStream> = node
3610        .children
3611        .iter()
3612        .map(|child| {
3613            generate_widget_with_locals(
3614                child,
3615                model_ident,
3616                message_ident,
3617                style_classes,
3618                &new_local_vars,
3619            )
3620        })
3621        .collect::<Result<_, _>>()?;
3622
3623    // Generate the for loop that builds widgets
3624    // Use explicit type annotations to help Rust's type inference
3625    Ok(quote! {
3626        {
3627            let mut widgets: Vec<Element<'_, #message_ident>> = Vec::new();
3628            for (index, #var_ident) in (#collection_expr).iter().enumerate() {
3629                let _ = index;
3630                #(
3631                    let child_widget: Element<'_, #message_ident> = #children;
3632                    widgets.push(child_widget);
3633                )*
3634            }
3635            Into::<Element<'_, #message_ident>>::into(iced::widget::column(widgets))
3636        }
3637    })
3638}
3639
3640/// Generate if widget with local variable context
3641fn generate_if_with_locals(
3642    node: &crate::WidgetNode,
3643    model_ident: &syn::Ident,
3644    message_ident: &syn::Ident,
3645    style_classes: &HashMap<String, StyleClass>,
3646    local_vars: &std::collections::HashSet<String>,
3647) -> Result<TokenStream, super::CodegenError> {
3648    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3649        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3650    })?;
3651
3652    let children: Vec<TokenStream> = node
3653        .children
3654        .iter()
3655        .map(|child| {
3656            generate_widget_with_locals(
3657                child,
3658                model_ident,
3659                message_ident,
3660                style_classes,
3661                local_vars,
3662            )
3663        })
3664        .collect::<Result<_, _>>()?;
3665
3666    let condition_expr =
3667        generate_attribute_value_with_locals(condition_attr, model_ident, local_vars);
3668
3669    Ok(quote! {
3670        if #condition_expr.parse::<bool>().unwrap_or(false) {
3671            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }))
3672        } else {
3673            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![]; children }))
3674        }
3675    })
3676}
3677
3678/// Generate checkbox widget with local variable context
3679fn generate_checkbox_with_locals(
3680    node: &crate::WidgetNode,
3681    model_ident: &syn::Ident,
3682    message_ident: &syn::Ident,
3683    style_classes: &HashMap<String, StyleClass>,
3684    local_vars: &std::collections::HashSet<String>,
3685) -> Result<TokenStream, super::CodegenError> {
3686    // Get checked attribute
3687    let checked_attr = node.attributes.get("checked");
3688    let checked_expr = if let Some(attr) = checked_attr {
3689        generate_attribute_value_raw_with_locals(attr, model_ident, local_vars)
3690    } else {
3691        quote! { false }
3692    };
3693
3694    // Get on_change event
3695    let on_change = node
3696        .events
3697        .iter()
3698        .find(|e| e.event == crate::EventKind::Change);
3699
3700    let mut checkbox = quote! {
3701        iced::widget::checkbox(#checked_expr)
3702    };
3703
3704    if let Some(event) = on_change {
3705        let variant_name = to_upper_camel_case(&event.handler);
3706        let handler_ident = format_ident!("{}", variant_name);
3707
3708        let param_expr = if let Some(ref param) = event.param {
3709            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
3710            quote! { (#param_tokens) }
3711        } else {
3712            quote! {}
3713        };
3714
3715        checkbox = quote! {
3716            #checkbox.on_toggle(move |_| #message_ident::#handler_ident #param_expr)
3717        };
3718    }
3719
3720    // Apply size
3721    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3722        if let AttributeValue::Static(s) = attr {
3723            s.parse::<f32>().ok()
3724        } else {
3725            None
3726        }
3727    }) {
3728        checkbox = quote! { #checkbox.size(#size) };
3729    }
3730
3731    // Apply styles
3732    checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
3733
3734    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#checkbox) })
3735}
3736
3737/// Generate text_input widget with local variable context
3738fn generate_text_input_with_locals(
3739    node: &crate::WidgetNode,
3740    model_ident: &syn::Ident,
3741    message_ident: &syn::Ident,
3742    style_classes: &HashMap<String, StyleClass>,
3743    local_vars: &std::collections::HashSet<String>,
3744) -> Result<TokenStream, super::CodegenError> {
3745    // Get placeholder
3746    let placeholder = node
3747        .attributes
3748        .get("placeholder")
3749        .and_then(|attr| {
3750            if let AttributeValue::Static(s) = attr {
3751                Some(s.clone())
3752            } else {
3753                None
3754            }
3755        })
3756        .unwrap_or_default();
3757    let placeholder_lit = proc_macro2::Literal::string(&placeholder);
3758
3759    // Get value attribute
3760    let value_attr = node.attributes.get("value");
3761    let value_expr = if let Some(attr) = value_attr {
3762        generate_attribute_value_with_locals(attr, model_ident, local_vars)
3763    } else {
3764        quote! { String::new() }
3765    };
3766
3767    let on_input = node
3768        .events
3769        .iter()
3770        .find(|e| e.event == crate::EventKind::Input);
3771
3772    let on_submit = node
3773        .events
3774        .iter()
3775        .find(|e| e.event == crate::EventKind::Submit);
3776
3777    let mut text_input = quote! {
3778        iced::widget::text_input(#placeholder_lit, &#value_expr)
3779    };
3780
3781    // Apply on_input
3782    if let Some(event) = on_input {
3783        let variant_name = to_upper_camel_case(&event.handler);
3784        let handler_ident = format_ident!("{}", variant_name);
3785        text_input = quote! { #text_input.on_input(|v| #message_ident::#handler_ident(v)) };
3786    }
3787
3788    // Apply on_submit
3789    if let Some(event) = on_submit {
3790        let variant_name = to_upper_camel_case(&event.handler);
3791        let handler_ident = format_ident!("{}", variant_name);
3792        text_input = quote! { #text_input.on_submit(#message_ident::#handler_ident) };
3793    }
3794
3795    // Apply size
3796    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3797        if let AttributeValue::Static(s) = attr {
3798            s.parse::<f32>().ok()
3799        } else {
3800            None
3801        }
3802    }) {
3803        text_input = quote! { #text_input.size(#size) };
3804    }
3805
3806    // Apply padding
3807    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
3808        if let AttributeValue::Static(s) = attr {
3809            s.parse::<f32>().ok()
3810        } else {
3811            None
3812        }
3813    }) {
3814        text_input = quote! { #text_input.padding(#padding) };
3815    }
3816
3817    // Apply width
3818    if let Some(width) = node.attributes.get("width").and_then(|attr| {
3819        if let AttributeValue::Static(s) = attr {
3820            Some(generate_length_expr(s))
3821        } else {
3822            None
3823        }
3824    }) {
3825        text_input = quote! { #text_input.width(#width) };
3826    }
3827
3828    // Apply styles
3829    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
3830
3831    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#text_input) })
3832}
3833
3834/// Generate attribute value expression with local variable context
3835fn generate_attribute_value_with_locals(
3836    attr: &AttributeValue,
3837    _model_ident: &syn::Ident,
3838    local_vars: &std::collections::HashSet<String>,
3839) -> TokenStream {
3840    match attr {
3841        AttributeValue::Static(s) => {
3842            let lit = proc_macro2::Literal::string(s);
3843            quote! { #lit.to_string() }
3844        }
3845        AttributeValue::Binding(expr) => {
3846            super::bindings::generate_expr_with_locals(&expr.expr, local_vars)
3847        }
3848        AttributeValue::Interpolated(parts) => {
3849            let parts_str: Vec<String> = parts
3850                .iter()
3851                .map(|part| match part {
3852                    InterpolatedPart::Literal(s) => s.clone(),
3853                    InterpolatedPart::Binding(_) => "{}".to_string(),
3854                })
3855                .collect();
3856            let binding_exprs: Vec<TokenStream> = parts
3857                .iter()
3858                .filter_map(|part| {
3859                    if let InterpolatedPart::Binding(expr) = part {
3860                        Some(super::bindings::generate_expr_with_locals(
3861                            &expr.expr, local_vars,
3862                        ))
3863                    } else {
3864                        None
3865                    }
3866                })
3867                .collect();
3868
3869            let format_string = parts_str.join("");
3870            let lit = proc_macro2::Literal::string(&format_string);
3871
3872            quote! { format!(#lit, #(#binding_exprs),*) }
3873        }
3874    }
3875}
3876
3877/// Generate attribute value without `.to_string()` conversion with local variable context
3878fn generate_attribute_value_raw_with_locals(
3879    attr: &AttributeValue,
3880    _model_ident: &syn::Ident,
3881    local_vars: &std::collections::HashSet<String>,
3882) -> TokenStream {
3883    match attr {
3884        AttributeValue::Static(s) => {
3885            let lit = proc_macro2::Literal::string(s);
3886            quote! { #lit }
3887        }
3888        AttributeValue::Binding(expr) => {
3889            super::bindings::generate_bool_expr_with_locals(&expr.expr, local_vars)
3890        }
3891        AttributeValue::Interpolated(parts) => {
3892            let parts_str: Vec<String> = parts
3893                .iter()
3894                .map(|part| match part {
3895                    InterpolatedPart::Literal(s) => s.clone(),
3896                    InterpolatedPart::Binding(_) => "{}".to_string(),
3897                })
3898                .collect();
3899            let binding_exprs: Vec<TokenStream> = parts
3900                .iter()
3901                .filter_map(|part| {
3902                    if let InterpolatedPart::Binding(expr) = part {
3903                        Some(super::bindings::generate_expr_with_locals(
3904                            &expr.expr, local_vars,
3905                        ))
3906                    } else {
3907                        None
3908                    }
3909                })
3910                .collect();
3911
3912            let format_string = parts_str.join("");
3913            let lit = proc_macro2::Literal::string(&format_string);
3914
3915            quote! { format!(#lit, #(#binding_exprs),*) }
3916        }
3917    }
3918}
3919
3920fn generate_canvas_shapes(
3921    nodes: &[crate::WidgetNode],
3922    model_ident: &syn::Ident,
3923) -> Result<Vec<TokenStream>, super::CodegenError> {
3924    let mut shape_exprs = Vec::new();
3925    for node in nodes {
3926        match node.kind {
3927            WidgetKind::CanvasRect => shape_exprs.push(generate_rect_shape(node, model_ident)?),
3928            WidgetKind::CanvasCircle => shape_exprs.push(generate_circle_shape(node, model_ident)?),
3929            WidgetKind::CanvasLine => shape_exprs.push(generate_line_shape(node, model_ident)?),
3930            WidgetKind::CanvasText => shape_exprs.push(generate_text_shape(node, model_ident)?),
3931            WidgetKind::CanvasGroup => shape_exprs.push(generate_group_shape(node, model_ident)?),
3932            _ => {}
3933        }
3934    }
3935    Ok(shape_exprs)
3936}
3937
3938fn generate_rect_shape(
3939    node: &crate::WidgetNode,
3940    model_ident: &syn::Ident,
3941) -> Result<TokenStream, super::CodegenError> {
3942    let x = generate_f32_attr(node, "x", 0.0, model_ident);
3943    let y = generate_f32_attr(node, "y", 0.0, model_ident);
3944    let width = generate_f32_attr(node, "width", 0.0, model_ident);
3945    let height = generate_f32_attr(node, "height", 0.0, model_ident);
3946    let fill = generate_color_option_attr(node, "fill", model_ident);
3947    let stroke = generate_color_option_attr(node, "stroke", model_ident);
3948    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
3949    let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
3950
3951    Ok(quote! {
3952        dampen_iced::canvas::CanvasShape::Rect(dampen_iced::canvas::RectShape {
3953            x: #x,
3954            y: #y,
3955            width: #width,
3956            height: #height,
3957            fill: #fill,
3958            stroke: #stroke,
3959            stroke_width: #stroke_width,
3960            radius: #radius,
3961        })
3962    })
3963}
3964
3965fn generate_circle_shape(
3966    node: &crate::WidgetNode,
3967    model_ident: &syn::Ident,
3968) -> Result<TokenStream, super::CodegenError> {
3969    let cx = generate_f32_attr(node, "cx", 0.0, model_ident);
3970    let cy = generate_f32_attr(node, "cy", 0.0, model_ident);
3971    let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
3972    let fill = generate_color_option_attr(node, "fill", model_ident);
3973    let stroke = generate_color_option_attr(node, "stroke", model_ident);
3974    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
3975
3976    Ok(quote! {
3977        dampen_iced::canvas::CanvasShape::Circle(dampen_iced::canvas::CircleShape {
3978            cx: #cx,
3979            cy: #cy,
3980            radius: #radius,
3981            fill: #fill,
3982            stroke: #stroke,
3983            stroke_width: #stroke_width,
3984        })
3985    })
3986}
3987
3988fn generate_line_shape(
3989    node: &crate::WidgetNode,
3990    model_ident: &syn::Ident,
3991) -> Result<TokenStream, super::CodegenError> {
3992    let x1 = generate_f32_attr(node, "x1", 0.0, model_ident);
3993    let y1 = generate_f32_attr(node, "y1", 0.0, model_ident);
3994    let x2 = generate_f32_attr(node, "x2", 0.0, model_ident);
3995    let y2 = generate_f32_attr(node, "y2", 0.0, model_ident);
3996    let stroke = generate_color_option_attr(node, "stroke", model_ident);
3997    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
3998
3999    Ok(quote! {
4000        dampen_iced::canvas::CanvasShape::Line(dampen_iced::canvas::LineShape {
4001            x1: #x1,
4002            y1: #y1,
4003            x2: #x2,
4004            y2: #y2,
4005            stroke: #stroke,
4006            stroke_width: #stroke_width,
4007        })
4008    })
4009}
4010
4011fn generate_text_shape(
4012    node: &crate::WidgetNode,
4013    model_ident: &syn::Ident,
4014) -> Result<TokenStream, super::CodegenError> {
4015    let x = generate_f32_attr(node, "x", 0.0, model_ident);
4016    let y = generate_f32_attr(node, "y", 0.0, model_ident);
4017    let content = generate_attribute_value(
4018        node.attributes
4019            .get("content")
4020            .unwrap_or(&AttributeValue::Static(String::new())),
4021        model_ident,
4022    );
4023    let size = generate_f32_attr(node, "size", 16.0, model_ident);
4024    let color = generate_color_option_attr(node, "color", model_ident);
4025
4026    Ok(quote! {
4027        dampen_iced::canvas::CanvasShape::Text(dampen_iced::canvas::TextShape {
4028            x: #x,
4029            y: #y,
4030            content: #content,
4031            size: #size,
4032            color: #color,
4033        })
4034    })
4035}
4036
4037fn generate_group_shape(
4038    node: &crate::WidgetNode,
4039    model_ident: &syn::Ident,
4040) -> Result<TokenStream, super::CodegenError> {
4041    let children = generate_canvas_shapes(&node.children, model_ident)?;
4042    let transform = generate_transform_attr(node, model_ident);
4043
4044    Ok(quote! {
4045        dampen_iced::canvas::CanvasShape::Group(dampen_iced::canvas::GroupShape {
4046            transform: #transform,
4047            children: vec![#(#children),*],
4048        })
4049    })
4050}
4051
4052fn generate_f32_attr(
4053    node: &crate::WidgetNode,
4054    name: &str,
4055    default: f32,
4056    _model_ident: &syn::Ident,
4057) -> TokenStream {
4058    if let Some(attr) = node.attributes.get(name) {
4059        match attr {
4060            AttributeValue::Static(s) => {
4061                let val = s.parse::<f32>().unwrap_or(default);
4062                quote! { #val }
4063            }
4064            AttributeValue::Binding(expr) => {
4065                let tokens = super::bindings::generate_bool_expr(&expr.expr);
4066                quote! { (#tokens) as f32 }
4067            }
4068            AttributeValue::Interpolated(_) => quote! { #default },
4069        }
4070    } else {
4071        quote! { #default }
4072    }
4073}
4074
4075fn generate_color_option_attr(
4076    node: &crate::WidgetNode,
4077    name: &str,
4078    _model_ident: &syn::Ident,
4079) -> TokenStream {
4080    if let Some(attr) = node.attributes.get(name) {
4081        match attr {
4082            AttributeValue::Static(s) => {
4083                if let Ok(c) = crate::parser::style_parser::parse_color_attr(s) {
4084                    let r = c.r;
4085                    let g = c.g;
4086                    let b = c.b;
4087                    let a = c.a;
4088                    quote! { Some(iced::Color::from_rgba(#r, #g, #b, #a)) }
4089                } else {
4090                    quote! { None }
4091                }
4092            }
4093            AttributeValue::Binding(expr) => {
4094                let tokens = generate_expr(&expr.expr);
4095                quote! {
4096                    dampen_iced::convert::parse_color_maybe(&(#tokens).to_string())
4097                        .map(|c| iced::Color::from_rgba(c.r, c.g, c.b, c.a))
4098                }
4099            }
4100            _ => quote! { None },
4101        }
4102    } else {
4103        quote! { None }
4104    }
4105}
4106
4107fn generate_transform_attr(node: &crate::WidgetNode, _model_ident: &syn::Ident) -> TokenStream {
4108    if let Some(AttributeValue::Static(s)) = node.attributes.get("transform") {
4109        let s = s.trim();
4110        if let Some(inner) = s
4111            .strip_prefix("translate(")
4112            .and_then(|s| s.strip_suffix(")"))
4113        {
4114            let parts: Vec<f32> = inner
4115                .split(',')
4116                .filter_map(|p| p.trim().parse().ok())
4117                .collect();
4118            if parts.len() == 2 {
4119                let x = parts[0];
4120                let y = parts[1];
4121                return quote! { Some(dampen_iced::canvas::Transform::Translate(#x, #y)) };
4122            }
4123        }
4124        if let Some(inner) = s.strip_prefix("rotate(").and_then(|s| s.strip_suffix(")"))
4125            && let Ok(angle) = inner.trim().parse::<f32>()
4126        {
4127            return quote! { Some(dampen_iced::canvas::Transform::Rotate(#angle)) };
4128        }
4129        if let Some(inner) = s.strip_prefix("scale(").and_then(|s| s.strip_suffix(")")) {
4130            let parts: Vec<f32> = inner
4131                .split(',')
4132                .filter_map(|p| p.trim().parse().ok())
4133                .collect();
4134            if parts.len() == 1 {
4135                let s = parts[0];
4136                return quote! { Some(dampen_iced::canvas::Transform::Scale(#s)) };
4137            } else if parts.len() == 2 {
4138                let x = parts[0];
4139                let y = parts[1];
4140                return quote! { Some(dampen_iced::canvas::Transform::ScaleXY(#x, #y)) };
4141            }
4142        }
4143        if let Some(inner) = s.strip_prefix("matrix(").and_then(|s| s.strip_suffix(")")) {
4144            let parts: Vec<f32> = inner
4145                .split(',')
4146                .filter_map(|p| p.trim().parse().ok())
4147                .collect();
4148            if parts.len() == 6 {
4149                return quote! { Some(dampen_iced::canvas::Transform::Matrix([#(#parts),*])) };
4150            }
4151        }
4152        quote! { None }
4153    } else {
4154        quote! { None }
4155    }
4156}
4157
4158fn generate_canvas_handlers(
4159    node: &crate::WidgetNode,
4160    _model_ident: &syn::Ident,
4161    message_ident: &syn::Ident,
4162) -> Result<Option<TokenStream>, super::CodegenError> {
4163    let on_click = node
4164        .events
4165        .iter()
4166        .find(|e| e.event == crate::EventKind::CanvasClick);
4167    let on_drag = node
4168        .events
4169        .iter()
4170        .find(|e| e.event == crate::EventKind::CanvasDrag);
4171    let on_move = node
4172        .events
4173        .iter()
4174        .find(|e| e.event == crate::EventKind::CanvasMove);
4175    let on_release = node
4176        .events
4177        .iter()
4178        .find(|e| e.event == crate::EventKind::CanvasRelease);
4179
4180    if on_click.is_none() && on_drag.is_none() && on_move.is_none() && on_release.is_none() {
4181        return Ok(None);
4182    }
4183
4184    let mut match_arms = Vec::new();
4185
4186    if let Some(e) = on_click {
4187        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4188        let name = &e.handler;
4189        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4190    }
4191    if let Some(e) = on_drag {
4192        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4193        let name = &e.handler;
4194        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4195    }
4196    if let Some(e) = on_move {
4197        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4198        let name = &e.handler;
4199        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4200    }
4201    if let Some(e) = on_release {
4202        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4203        let name = &e.handler;
4204        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4205    }
4206
4207    let click_name = on_click.map(|e| e.handler.as_str()).unwrap_or("");
4208    let drag_name = on_drag.map(|e| e.handler.as_str()).unwrap_or("");
4209    let move_name = on_move.map(|e| e.handler.as_str()).unwrap_or("");
4210    let release_name = on_release.map(|e| e.handler.as_str()).unwrap_or("");
4211
4212    Ok(Some(quote! {
4213        dampen_iced::canvas::CanvasEventHandlers {
4214            handler_names: dampen_iced::canvas::CanvasHandlerNames {
4215                on_click: if #click_name != "" { Some(#click_name.to_string()) } else { None },
4216                on_drag: if #drag_name != "" { Some(#drag_name.to_string()) } else { None },
4217                on_move: if #move_name != "" { Some(#move_name.to_string()) } else { None },
4218                on_release: if #release_name != "" { Some(#release_name.to_string()) } else { None },
4219            },
4220            msg_factory: |name, event| {
4221                 match name {
4222                     #(#match_arms,)*
4223                     _ => panic!("Unknown canvas handler: {}", name),
4224                 }
4225            }
4226        }
4227    }))
4228}
4229
4230/// Generate Menu widget (MenuBar)
4231fn generate_menu(
4232    node: &crate::WidgetNode,
4233    model_ident: &syn::Ident,
4234    message_ident: &syn::Ident,
4235    style_classes: &HashMap<String, StyleClass>,
4236) -> Result<TokenStream, super::CodegenError> {
4237    let items = generate_menu_items(&node.children, model_ident, message_ident, style_classes)?;
4238    // TODO: Handle layout attributes via container wrapper if needed
4239    Ok(quote! {
4240        iced_aw::menu::MenuBar::new(#items).into()
4241    })
4242}
4243
4244/// Generate items for Menu/MenuBar
4245fn generate_menu_items(
4246    children: &[crate::WidgetNode],
4247    model_ident: &syn::Ident,
4248    message_ident: &syn::Ident,
4249    style_classes: &HashMap<String, StyleClass>,
4250) -> Result<TokenStream, super::CodegenError> {
4251    let mut item_exprs = Vec::new();
4252
4253    for child in children {
4254        match child.kind {
4255            WidgetKind::MenuItem => {
4256                item_exprs.push(generate_menu_item_struct(
4257                    child,
4258                    model_ident,
4259                    message_ident,
4260                    style_classes,
4261                )?);
4262            }
4263            WidgetKind::MenuSeparator => {
4264                item_exprs.push(generate_menu_separator_struct(child)?);
4265            }
4266            _ => {}
4267        }
4268    }
4269
4270    Ok(quote! {
4271        vec![#(#item_exprs),*]
4272    })
4273}
4274
4275/// Generate Item struct for MenuItem
4276fn generate_menu_item_struct(
4277    node: &crate::WidgetNode,
4278    model_ident: &syn::Ident,
4279    message_ident: &syn::Ident,
4280    style_classes: &HashMap<String, StyleClass>,
4281) -> Result<TokenStream, super::CodegenError> {
4282    let label_attr = node.attributes.get("label").ok_or_else(|| {
4283        super::CodegenError::InvalidWidget("MenuItem requires label attribute".to_string())
4284    })?;
4285
4286    let label_expr = generate_attribute_value(label_attr, model_ident);
4287
4288    // Content is a button
4289    let mut btn = quote! {
4290        iced::widget::button(iced::widget::text(#label_expr))
4291            .width(iced::Length::Shrink) // Use Shrink to avoid layout collapse in MenuBar
4292            .style(iced::widget::button::text)
4293    };
4294
4295    if let Some(event) = node
4296        .events
4297        .iter()
4298        .find(|e| e.event == crate::EventKind::Click)
4299    {
4300        let variant_name = to_upper_camel_case(&event.handler);
4301        let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4302
4303        let msg = if let Some(param) = &event.param {
4304            let param_expr = crate::codegen::bindings::generate_expr(&param.expr);
4305            quote! { #message_ident::#variant_ident(#param_expr) }
4306        } else {
4307            quote! { #message_ident::#variant_ident }
4308        };
4309
4310        btn = quote! { #btn.on_press(#msg) };
4311    }
4312
4313    let content = quote! { #btn };
4314
4315    // Check for submenu
4316    if let Some(submenu) = node.children.iter().find(|c| c.kind == WidgetKind::Menu) {
4317        let items =
4318            generate_menu_items(&submenu.children, model_ident, message_ident, style_classes)?;
4319        Ok(quote! {
4320            iced_aw::menu::Item::with_menu(#content, iced_aw::menu::Menu::new(#items))
4321        })
4322    } else {
4323        Ok(quote! {
4324            iced_aw::menu::Item::new(#content)
4325        })
4326    }
4327}
4328
4329fn generate_menu_separator_struct(
4330    _node: &crate::WidgetNode,
4331) -> Result<TokenStream, super::CodegenError> {
4332    Ok(quote! {
4333        iced_aw::menu::Item::new(iced::widget::rule::horizontal(1))
4334    })
4335}
4336
4337fn generate_context_menu(
4338    node: &crate::WidgetNode,
4339    model_ident: &syn::Ident,
4340    message_ident: &syn::Ident,
4341    style_classes: &HashMap<String, StyleClass>,
4342    local_vars: &std::collections::HashSet<String>,
4343) -> Result<TokenStream, super::CodegenError> {
4344    let underlay = node
4345        .children
4346        .first()
4347        .ok_or(super::CodegenError::InvalidWidget(
4348            "ContextMenu requires underlay".into(),
4349        ))?;
4350    let underlay_expr = generate_widget_with_locals(
4351        underlay,
4352        model_ident,
4353        message_ident,
4354        style_classes,
4355        local_vars,
4356    )?;
4357
4358    let menu_node = node
4359        .children
4360        .get(1)
4361        .ok_or(super::CodegenError::InvalidWidget(
4362            "ContextMenu requires menu".into(),
4363        ))?;
4364
4365    if menu_node.kind != WidgetKind::Menu {
4366        return Err(super::CodegenError::InvalidWidget(
4367            "Second child of ContextMenu must be <menu>".into(),
4368        ));
4369    }
4370
4371    // Generate menu content (column of buttons)
4372    let mut buttons = Vec::new();
4373    for child in &menu_node.children {
4374        match child.kind {
4375            WidgetKind::MenuItem => {
4376                let label =
4377                    child
4378                        .attributes
4379                        .get("label")
4380                        .ok_or(super::CodegenError::InvalidWidget(
4381                            "MenuItem requires label".into(),
4382                        ))?;
4383                let label_expr =
4384                    generate_attribute_value_with_locals(label, model_ident, local_vars);
4385
4386                let mut btn = quote! {
4387                    iced::widget::button(iced::widget::text(#label_expr))
4388                        .width(iced::Length::Fill)
4389                        .style(iced::widget::button::text)
4390                };
4391
4392                if let Some(event) = child
4393                    .events
4394                    .iter()
4395                    .find(|e| e.event == crate::EventKind::Click)
4396                {
4397                    let variant_name = to_upper_camel_case(&event.handler);
4398                    let variant_ident =
4399                        syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4400
4401                    let msg = if let Some(param) = &event.param {
4402                        let param_expr = crate::codegen::bindings::generate_expr(&param.expr);
4403                        quote! { #message_ident::#variant_ident(#param_expr) }
4404                    } else {
4405                        quote! { #message_ident::#variant_ident }
4406                    };
4407                    btn = quote! { #btn.on_press(#msg) };
4408                }
4409
4410                buttons.push(quote! { #btn.into() });
4411            }
4412            WidgetKind::MenuSeparator => {
4413                buttons.push(quote! { iced::widget::rule::horizontal(1).into() });
4414            }
4415            _ => {}
4416        }
4417    }
4418
4419    let overlay_content = quote! {
4420        iced::widget::container(
4421            iced::widget::column(vec![#(#buttons),*])
4422                .spacing(2)
4423        )
4424        .padding(5)
4425        .style(iced::widget::container::bordered_box)
4426        .into()
4427    };
4428
4429    Ok(quote! {
4430        iced_aw::ContextMenu::new(
4431            #underlay_expr,
4432            move || #overlay_content
4433        )
4434        .into()
4435    })
4436}
4437
4438fn generate_data_table(
4439    node: &crate::WidgetNode,
4440    model_ident: &syn::Ident,
4441    message_ident: &syn::Ident,
4442    style_classes: &HashMap<String, StyleClass>,
4443) -> Result<TokenStream, super::CodegenError> {
4444    let data_attr = node.attributes.get("data").ok_or_else(|| {
4445        super::CodegenError::InvalidWidget("data_table requires data attribute".to_string())
4446    })?;
4447    let data_expr = generate_attribute_value_raw(data_attr, model_ident);
4448
4449    let mut column_exprs = Vec::new();
4450    for child in &node.children {
4451        if child.kind == WidgetKind::DataColumn {
4452            let header_attr = child.attributes.get("header").ok_or_else(|| {
4453                super::CodegenError::InvalidWidget(
4454                    "data_column requires header attribute".to_string(),
4455                )
4456            })?;
4457            let header_expr = generate_attribute_value(header_attr, model_ident);
4458            let header = quote! { iced::widget::text(#header_expr) };
4459
4460            let field = child.attributes.get("field");
4461
4462            let view_closure = if let Some(AttributeValue::Static(field_name)) = field {
4463                let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
4464                // Assuming item is the model type or struct
4465                quote! {
4466                    |item| iced::widget::text(item.#field_ident.to_string()).into()
4467                }
4468            } else {
4469                // Template handling
4470                // Find template content
4471                let template_content = if let Some(tmpl) = child
4472                    .children
4473                    .iter()
4474                    .find(|c| matches!(c.kind, WidgetKind::Custom(ref s) if s == "template"))
4475                {
4476                    &tmpl.children
4477                } else {
4478                    &child.children
4479                };
4480
4481                if let Some(root) = template_content.first() {
4482                    let mut locals = std::collections::HashSet::new();
4483                    locals.insert("index".to_string());
4484                    locals.insert("item".to_string());
4485
4486                    let widget_expr = generate_widget_with_locals(
4487                        root,
4488                        model_ident,
4489                        message_ident,
4490                        style_classes,
4491                        &locals,
4492                    )?;
4493
4494                    quote! {
4495                        |(index, item)| {
4496                            let _ = index; // Suppress unused warning
4497                            #widget_expr.into()
4498                        }
4499                    }
4500                } else {
4501                    quote! { |(_index, _item)| iced::widget::text("").into() }
4502                }
4503            };
4504
4505            let mut col = quote! {
4506                iced::widget::table::column(#header, #view_closure)
4507            };
4508
4509            if let Some(width) = child.attributes.get("width") {
4510                let width_expr = match width {
4511                    AttributeValue::Static(s) => generate_length_expr(s),
4512                    _ => quote! { iced::Length::Fill },
4513                };
4514                col = quote! { #col.width(#width_expr) };
4515            }
4516
4517            column_exprs.push(col);
4518        }
4519    }
4520
4521    let table = quote! {
4522        iced::widget::table::Table::new(vec![#(#column_exprs),*], #data_expr)
4523    };
4524
4525    // Handle on_row_click
4526    // TODO: Re-enable when Table API for row clicks is identified
4527    /*
4528    if let Some(event) = node.events.iter().find(|e| e.event == crate::ir::EventKind::RowClick) {
4529        let variant_name = to_upper_camel_case(&event.handler);
4530        let handler_ident = format_ident!("{}", variant_name);
4531
4532        // Generate parameter expression
4533        let param_expr = if let Some(ref binding) = event.param {
4534            let mut locals = std::collections::HashSet::new();
4535            locals.insert("index".to_string());
4536            locals.insert("item".to_string());
4537
4538            let expr = crate::codegen::bindings::generate_expr_with_locals(
4539                &binding.expr,
4540                &locals,
4541            );
4542            quote! { (#expr) }
4543        } else {
4544            quote! {}
4545        };
4546
4547        // We assume data_expr evaluates to something indexable (Vec, slice)
4548        // We define `item` as reference to element at index
4549        table = quote! {
4550            #table.on_row_click(move |index: usize| {
4551                let item = &(#data_expr)[index];
4552                #message_ident::#handler_ident #param_expr
4553            })
4554        };
4555    }
4556    */
4557
4558    // Apply layout
4559    Ok(maybe_wrap_in_container(table, node))
4560}
4561
4562/// Generate TreeView widget code
4563fn generate_tree_view(
4564    node: &crate::WidgetNode,
4565    model_ident: &syn::Ident,
4566    message_ident: &syn::Ident,
4567    style_classes: &HashMap<String, StyleClass>,
4568    local_vars: &std::collections::HashSet<String>,
4569) -> Result<TokenStream, super::CodegenError> {
4570    // Get tree configuration from attributes
4571    let indent_size = node
4572        .attributes
4573        .get("indent_size")
4574        .and_then(|attr| match attr {
4575            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4576            _ => None,
4577        })
4578        .unwrap_or(20.0);
4579
4580    let node_height = node
4581        .attributes
4582        .get("node_height")
4583        .and_then(|attr| match attr {
4584            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4585            _ => None,
4586        })
4587        .unwrap_or(30.0);
4588
4589    let _icon_size = node
4590        .attributes
4591        .get("icon_size")
4592        .and_then(|attr| match attr {
4593            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4594            _ => None,
4595        })
4596        .unwrap_or(16.0);
4597
4598    let expand_icon = node
4599        .attributes
4600        .get("expand_icon")
4601        .and_then(|attr| match attr {
4602            AttributeValue::Static(s) => Some(s.clone()),
4603            _ => None,
4604        })
4605        .unwrap_or_else(|| "â–¶".to_string());
4606
4607    let collapse_icon = node
4608        .attributes
4609        .get("collapse_icon")
4610        .and_then(|attr| match attr {
4611            AttributeValue::Static(s) => Some(s.clone()),
4612            _ => None,
4613        })
4614        .unwrap_or_else(|| "â–¼".to_string());
4615
4616    // Check if we have a nodes binding (dynamic tree) or inline children (static tree)
4617    let has_nodes_binding = node.attributes.contains_key("nodes");
4618
4619    if has_nodes_binding {
4620        // Dynamic tree from binding - generate code that builds tree at runtime
4621        let nodes_binding = node.attributes.get("nodes").ok_or_else(|| {
4622            super::CodegenError::InvalidWidget("nodes attribute is required".into())
4623        })?;
4624        let nodes_expr = generate_attribute_value_raw(nodes_binding, model_ident);
4625
4626        // Get expanded IDs binding
4627        let expanded_binding = node.attributes.get("expanded");
4628        let expanded_expr =
4629            expanded_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4630
4631        // Get selected ID binding
4632        let selected_binding = node.attributes.get("selected");
4633        let selected_expr =
4634            selected_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4635
4636        // Generate the tree view using a recursive helper function
4637        let tree_view = quote! {
4638            {
4639                let tree_nodes = #nodes_expr;
4640                let expanded_ids: std::collections::HashSet<String> = #expanded_expr
4641                    .map(|v: Vec<String>| v.into_iter().collect())
4642                    .unwrap_or_default();
4643                let selected_id: Option<String> = #selected_expr;
4644
4645                // Build tree recursively
4646                fn build_tree_nodes(
4647                    nodes: &[TreeNode],
4648                    expanded_ids: &std::collections::HashSet<String>,
4649                    selected_id: &Option<String>,
4650                    depth: usize,
4651                ) -> Vec<iced::Element<'static, #message_ident>> {
4652                    let mut elements = Vec::new();
4653                    for node in nodes {
4654                        let is_expanded = expanded_ids.contains(&node.id);
4655                        let is_selected = selected_id.as_ref() == Some(&node.id);
4656                        let has_children = !node.children.is_empty();
4657
4658                        // Build node row
4659                        let indent = (depth as f32) * #indent_size;
4660                        let node_element = build_tree_node_row(
4661                            node,
4662                            is_expanded,
4663                            is_selected,
4664                            has_children,
4665                            indent,
4666                            #node_height,
4667                            #expand_icon,
4668                            #collapse_icon,
4669                        );
4670                        elements.push(node_element);
4671
4672                        // Add children if expanded
4673                        if is_expanded && has_children {
4674                            let child_elements = build_tree_nodes(
4675                                &node.children,
4676                                expanded_ids,
4677                                selected_id,
4678                                depth + 1,
4679                            );
4680                            elements.extend(child_elements);
4681                        }
4682                    }
4683                    elements
4684                }
4685
4686                iced::widget::column(build_tree_nodes(&tree_nodes, &expanded_ids, &selected_id, 0))
4687                    .spacing(2)
4688                    .into()
4689            }
4690        };
4691
4692        Ok(tree_view)
4693    } else {
4694        // Static tree from inline XML children
4695        let tree_elements: Vec<TokenStream> = node
4696            .children
4697            .iter()
4698            .filter(|c| c.kind == WidgetKind::TreeNode)
4699            .map(|child| {
4700                generate_tree_node(
4701                    child,
4702                    model_ident,
4703                    message_ident,
4704                    style_classes,
4705                    local_vars,
4706                    indent_size,
4707                    node_height,
4708                    &expand_icon,
4709                    &collapse_icon,
4710                    0,
4711                    node,
4712                )
4713            })
4714            .collect::<Result<_, _>>()?;
4715
4716        Ok(quote! {
4717            iced::widget::column(vec![#(#tree_elements),*])
4718                .spacing(2)
4719                .into()
4720        })
4721    }
4722}
4723
4724/// Generate a single tree node element (recursive for children)
4725#[allow(clippy::too_many_arguments)]
4726fn generate_tree_node(
4727    node: &crate::WidgetNode,
4728    _model_ident: &syn::Ident,
4729    message_ident: &syn::Ident,
4730    _style_classes: &HashMap<String, StyleClass>,
4731    _local_vars: &std::collections::HashSet<String>,
4732    indent_size: f32,
4733    node_height: f32,
4734    expand_icon: &str,
4735    collapse_icon: &str,
4736    depth: usize,
4737    parent_node: &crate::WidgetNode,
4738) -> Result<TokenStream, super::CodegenError> {
4739    // T068, T069: Prevent infinite recursion during code generation
4740    if depth > 50 {
4741        return Ok(quote! {
4742            iced::widget::text("... max depth reached").size(12).into()
4743        });
4744    }
4745
4746    let id = node.id.clone().unwrap_or_else(|| "unknown".to_string());
4747
4748    let label = node
4749        .attributes
4750        .get("label")
4751        .and_then(|attr| match attr {
4752            AttributeValue::Static(s) => Some(s.clone()),
4753            _ => None,
4754        })
4755        .unwrap_or_else(|| id.clone());
4756
4757    let icon = node.attributes.get("icon").and_then(|attr| match attr {
4758        AttributeValue::Static(s) => Some(s.clone()),
4759        _ => None,
4760    });
4761
4762    let expanded = node.attributes.get("expanded").and_then(|attr| match attr {
4763        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4764        _ => None,
4765    });
4766
4767    let selected = node.attributes.get("selected").and_then(|attr| match attr {
4768        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4769        _ => None,
4770    });
4771
4772    let _disabled = node.attributes.get("disabled").and_then(|attr| match attr {
4773        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4774        _ => None,
4775    });
4776
4777    let has_children = !node.children.is_empty();
4778    let is_expanded = expanded.unwrap_or(false);
4779    let is_selected = selected.unwrap_or(false);
4780
4781    let indent = (depth as f32) * indent_size;
4782
4783    // Build label text with optional icon
4784    let label_text = if let Some(ref icon_str) = icon {
4785        format!("{} {}", icon_str, label)
4786    } else {
4787        label
4788    };
4789
4790    // Generate expand/collapse button or spacer
4791    let toggle_button = if has_children {
4792        let icon = if is_expanded {
4793            collapse_icon
4794        } else {
4795            expand_icon
4796        };
4797
4798        // Check for on_toggle event handler
4799        if let Some(event) = parent_node
4800            .events
4801            .iter()
4802            .find(|e| matches!(e.event, crate::ir::node::EventKind::Toggle))
4803        {
4804            let variant_name = to_upper_camel_case(&event.handler);
4805            let handler_ident = format_ident!("{}", variant_name);
4806
4807            quote! {
4808                iced::widget::button(iced::widget::text(#icon).size(14))
4809                    .on_press(#message_ident::#handler_ident)
4810                    .width(iced::Length::Fixed(20.0))
4811                    .height(iced::Length::Fixed(#node_height))
4812            }
4813        } else {
4814            quote! {
4815                iced::widget::text(#icon).size(14)
4816            }
4817        }
4818    } else {
4819        quote! {
4820            iced::widget::container(iced::widget::text(""))
4821                .width(iced::Length::Fixed(20.0))
4822        }
4823    };
4824
4825    // Generate label element with selection handling
4826    let label_element = if let Some(event) = parent_node
4827        .events
4828        .iter()
4829        .find(|e| matches!(e.event, crate::ir::node::EventKind::Select))
4830    {
4831        let variant_name = to_upper_camel_case(&event.handler);
4832        let handler_ident = format_ident!("{}", variant_name);
4833
4834        quote! {
4835            iced::widget::button(iced::widget::text(#label_text).size(14))
4836                .on_press(#message_ident::#handler_ident)
4837                .style(|_theme: &iced::Theme, _status: iced::widget::button::Status| {
4838                    if #is_selected {
4839                        iced::widget::button::Style {
4840                            background: Some(iced::Background::Color(
4841                                iced::Color::from_rgb(0.0, 0.48, 0.8),
4842                            )),
4843                            text_color: iced::Color::WHITE,
4844                            ..Default::default()
4845                        }
4846                    } else {
4847                        iced::widget::button::Style::default()
4848                    }
4849                })
4850        }
4851    } else {
4852        quote! {
4853            iced::widget::text(#label_text).size(14)
4854        }
4855    };
4856
4857    // Build node row
4858    let node_row = quote! {
4859        iced::widget::row(vec![#toggle_button.into(), #label_element.into()])
4860            .spacing(4)
4861            .padding(iced::Padding::from([0.0, 0.0, 0.0, #indent]))
4862    };
4863
4864    // If expanded and has children, render them recursively
4865    if is_expanded && has_children {
4866        let child_elements: Vec<TokenStream> = node
4867            .children
4868            .iter()
4869            .filter(|c| c.kind == WidgetKind::TreeNode)
4870            .map(|child| {
4871                generate_tree_node(
4872                    child,
4873                    _model_ident,
4874                    message_ident,
4875                    _style_classes,
4876                    _local_vars,
4877                    indent_size,
4878                    node_height,
4879                    expand_icon,
4880                    collapse_icon,
4881                    depth + 1,
4882                    parent_node,
4883                )
4884            })
4885            .collect::<Result<_, _>>()?;
4886
4887        Ok(quote! {
4888            iced::widget::column(vec![
4889                #node_row.into(),
4890                iced::widget::column(vec![#(#child_elements),*])
4891                    .spacing(2)
4892                    .into(),
4893            ])
4894            .spacing(2)
4895        })
4896    } else {
4897        Ok(node_row)
4898    }
4899}
4900
4901#[cfg(test)]
4902mod tests {
4903    use super::*;
4904    use crate::parse;
4905
4906    #[test]
4907    fn test_view_generation() {
4908        let xml = r#"<column><text value="Hello" /></column>"#;
4909        let doc = parse(xml).unwrap();
4910
4911        let result = generate_view(&doc, "Model", "Message").unwrap();
4912        let code = result.to_string();
4913
4914        assert!(code.contains("text"));
4915        assert!(code.contains("column"));
4916    }
4917
4918    #[test]
4919    fn test_view_generation_with_binding() {
4920        let xml = r#"<column><text value="{name}" /></column>"#;
4921        let doc = parse(xml).unwrap();
4922
4923        let result = generate_view(&doc, "Model", "Message").unwrap();
4924        let code = result.to_string();
4925
4926        assert!(code.contains("name"));
4927        assert!(code.contains("to_string"));
4928    }
4929
4930    #[test]
4931    fn test_button_with_handler() {
4932        let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
4933        let doc = parse(xml).unwrap();
4934
4935        let result = generate_view(&doc, "Model", "Message").unwrap();
4936        let code = result.to_string();
4937
4938        assert!(code.contains("button"));
4939        assert!(code.contains("HandleClick"));
4940    }
4941
4942    #[test]
4943    fn test_container_with_children() {
4944        let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
4945        let doc = parse(xml).unwrap();
4946
4947        let result = generate_view(&doc, "Model", "Message").unwrap();
4948        let code = result.to_string();
4949
4950        assert!(code.contains("column"));
4951        assert!(code.contains("spacing"));
4952    }
4953
4954    #[test]
4955    fn test_button_with_inline_style() {
4956        use crate::ir::node::WidgetNode;
4957        use crate::ir::style::{Background, Color, StyleProperties};
4958        use std::collections::HashMap;
4959
4960        // Manually construct a button with inline style
4961        let button_node = WidgetNode {
4962            kind: WidgetKind::Button,
4963            id: None,
4964            attributes: {
4965                let mut attrs = HashMap::new();
4966                attrs.insert(
4967                    "label".to_string(),
4968                    AttributeValue::Static("Test".to_string()),
4969                );
4970                attrs
4971            },
4972            events: vec![],
4973            children: vec![],
4974            span: Default::default(),
4975            style: Some(StyleProperties {
4976                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
4977                color: Some(Color::from_rgb8(255, 255, 255)),
4978                border: None,
4979                shadow: None,
4980                opacity: None,
4981                transform: None,
4982            }),
4983            layout: None,
4984            theme_ref: None,
4985            classes: vec![],
4986            breakpoint_attributes: HashMap::new(),
4987            inline_state_variants: HashMap::new(),
4988        };
4989
4990        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
4991        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
4992        let style_classes = HashMap::new();
4993
4994        let result =
4995            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
4996        let code = result.to_string();
4997
4998        // Should contain style closure (note: quote! adds spaces)
4999        assert!(code.contains("style"));
5000        assert!(code.contains("button :: Status"));
5001        assert!(code.contains("button :: Style"));
5002        assert!(code.contains("background"));
5003        assert!(code.contains("text_color"));
5004    }
5005
5006    #[test]
5007    fn test_button_with_css_class() {
5008        use crate::ir::node::WidgetNode;
5009        use crate::ir::theme::StyleClass;
5010        use std::collections::HashMap;
5011
5012        // Manually construct a button with CSS class
5013        let button_node = WidgetNode {
5014            kind: WidgetKind::Button,
5015            id: None,
5016            attributes: {
5017                let mut attrs = HashMap::new();
5018                attrs.insert(
5019                    "label".to_string(),
5020                    AttributeValue::Static("Test".to_string()),
5021                );
5022                attrs
5023            },
5024            events: vec![],
5025            children: vec![],
5026            span: Default::default(),
5027            style: None,
5028            layout: None,
5029            theme_ref: None,
5030            classes: vec!["primary-button".to_string()],
5031            breakpoint_attributes: HashMap::new(),
5032            inline_state_variants: HashMap::new(),
5033        };
5034
5035        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5036        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5037        let style_classes: HashMap<String, StyleClass> = HashMap::new();
5038
5039        let result =
5040            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
5041        let code = result.to_string();
5042
5043        // Should call style function (note: quote! adds spaces)
5044        assert!(code.contains("style"));
5045        assert!(code.contains("style_primary_button"));
5046    }
5047
5048    #[test]
5049    fn test_container_with_inline_style() {
5050        use crate::ir::node::WidgetNode;
5051        use crate::ir::style::{
5052            Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
5053        };
5054        use crate::ir::theme::StyleClass;
5055        use std::collections::HashMap;
5056
5057        let container_node = WidgetNode {
5058            kind: WidgetKind::Container,
5059            id: None,
5060            attributes: HashMap::new(),
5061            events: vec![],
5062            children: vec![],
5063            span: Default::default(),
5064            style: Some(StyleProperties {
5065                background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
5066                color: None,
5067                border: Some(Border {
5068                    width: 2.0,
5069                    color: Color::from_rgb8(200, 200, 200),
5070                    radius: BorderRadius {
5071                        top_left: 8.0,
5072                        top_right: 8.0,
5073                        bottom_right: 8.0,
5074                        bottom_left: 8.0,
5075                    },
5076                    style: BorderStyle::Solid,
5077                }),
5078                shadow: None,
5079                opacity: None,
5080                transform: None,
5081            }),
5082            layout: None,
5083            theme_ref: None,
5084            classes: vec![],
5085            breakpoint_attributes: HashMap::new(),
5086            inline_state_variants: HashMap::new(),
5087        };
5088
5089        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5090        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5091        let style_classes: HashMap<String, StyleClass> = HashMap::new();
5092
5093        let result = generate_container(
5094            &container_node,
5095            "container",
5096            &model_ident,
5097            &message_ident,
5098            &style_classes,
5099        )
5100        .unwrap();
5101        let code = result.to_string();
5102
5103        // Should contain style closure (note: quote! adds spaces)
5104        assert!(code.contains("style"));
5105        assert!(code.contains("container :: Style"));
5106        assert!(code.contains("background"));
5107        assert!(code.contains("border"));
5108    }
5109}
5110
5111/// Generate TabBar widget code with content
5112fn generate_tab_bar_with_locals(
5113    node: &crate::WidgetNode,
5114    model_ident: &syn::Ident,
5115    message_ident: &syn::Ident,
5116    style_classes: &HashMap<String, StyleClass>,
5117    local_vars: &std::collections::HashSet<String>,
5118) -> Result<TokenStream, super::CodegenError> {
5119    use proc_macro2::Span;
5120    use quote::quote;
5121
5122    // Get selected index attribute
5123    let selected_attr = node.attributes.get("selected").ok_or_else(|| {
5124        super::CodegenError::InvalidWidget("TabBar requires 'selected' attribute".to_string())
5125    })?;
5126
5127    // Generate selected index expression
5128    let selected_expr = match selected_attr {
5129        AttributeValue::Static(s) => {
5130            let idx: usize = s.parse().map_err(|_| {
5131                super::CodegenError::InvalidWidget(format!("Invalid selected index: {}", s))
5132            })?;
5133            quote! { #idx }
5134        }
5135        AttributeValue::Binding(binding) => {
5136            // Generate binding expression - generate_expr returns a TokenStream that produces a String
5137            let binding_expr = generate_expr(&binding.expr);
5138            quote! { (#binding_expr).parse::<usize>().unwrap_or(0) }
5139        }
5140        _ => quote! { 0usize },
5141    };
5142
5143    // Find on_select event handler
5144    let on_select_handler = node
5145        .events
5146        .iter()
5147        .find(|e| matches!(e.event, crate::ir::EventKind::Select))
5148        .map(|e| syn::Ident::new(&e.handler, Span::call_site()));
5149
5150    // Generate tab labels and content
5151    let _tab_count = node.children.len();
5152    let tab_labels: Vec<_> = node
5153        .children
5154        .iter()
5155        .enumerate()
5156        .map(|(idx, child)| {
5157            let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5158
5159            // Get label from tab
5160            let label_expr = if let Some(label_attr) = child.attributes.get("label") {
5161                match label_attr {
5162                    AttributeValue::Static(s) => Some(quote! { #s.to_string() }),
5163                    _ => None,
5164                }
5165            } else {
5166                None
5167            };
5168
5169            // Get icon from tab
5170            let icon_expr = if let Some(icon_attr) = child.attributes.get("icon") {
5171                match icon_attr {
5172                    AttributeValue::Static(s) => {
5173                        let icon_char = resolve_icon_for_codegen(s);
5174                        Some(quote! { #icon_char })
5175                    }
5176                    _ => None,
5177                }
5178            } else {
5179                None
5180            };
5181
5182            // Build TabLabel expression based on what we have
5183            let tab_label_expr = match (icon_expr, label_expr) {
5184                (Some(icon), Some(label)) => {
5185                    quote! { iced_aw::tab_bar::TabLabel::IconText(#icon, #label) }
5186                }
5187                (Some(icon), None) => {
5188                    quote! { iced_aw::tab_bar::TabLabel::Icon(#icon) }
5189                }
5190                (None, Some(label)) => {
5191                    quote! { iced_aw::tab_bar::TabLabel::Text(#label) }
5192                }
5193                (None, None) => {
5194                    quote! { iced_aw::tab_bar::TabLabel::Text("Tab".to_string()) }
5195                }
5196            };
5197
5198            quote! {
5199                tab_bar = tab_bar.push(#idx_lit, #tab_label_expr);
5200            }
5201        })
5202        .collect();
5203
5204    // Generate content for each tab
5205    let tab_content_arms: Vec<_> = node
5206        .children
5207        .iter()
5208        .enumerate()
5209        .map(|(idx, child)| {
5210            let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5211
5212            // Generate content for this tab's children
5213            let content_widgets: Vec<_> = child
5214                .children
5215                .iter()
5216                .map(|child_node| {
5217                    generate_widget_with_locals(
5218                        child_node,
5219                        model_ident,
5220                        message_ident,
5221                        style_classes,
5222                        local_vars,
5223                    )
5224                })
5225                .collect::<Result<Vec<_>, _>>()?;
5226
5227            Ok::<_, super::CodegenError>(quote! {
5228                #idx_lit => iced::widget::column(vec![#(#content_widgets),*]).into()
5229            })
5230        })
5231        .collect::<Result<Vec<_>, super::CodegenError>>()?;
5232
5233    // Generate on_select callback if handler exists
5234    let on_select_expr = if let Some(handler) = on_select_handler {
5235        quote! {
5236            .on_select(|idx| #message_ident::#handler(idx))
5237        }
5238    } else {
5239        quote! {}
5240    };
5241
5242    // Generate icon_size if specified
5243    let icon_size_expr = if let Some(icon_size_attr) = node.attributes.get("icon_size") {
5244        match icon_size_attr {
5245            AttributeValue::Static(s) => {
5246                if let Ok(icon_size) = s.parse::<f32>() {
5247                    Some(quote! { .icon_size(#icon_size) })
5248                } else {
5249                    None
5250                }
5251            }
5252            _ => None,
5253        }
5254    } else {
5255        None
5256    };
5257
5258    // Generate text_size if specified
5259    let text_size_expr = if let Some(text_size_attr) = node.attributes.get("text_size") {
5260        match text_size_attr {
5261            AttributeValue::Static(s) => {
5262                if let Ok(text_size) = s.parse::<f32>() {
5263                    Some(quote! { .text_size(#text_size) })
5264                } else {
5265                    None
5266                }
5267            }
5268            _ => None,
5269        }
5270    } else {
5271        None
5272    };
5273
5274    // Build the complete TabBar widget with content
5275    let tab_bar_widget = quote! {
5276        {
5277            let mut tab_bar = iced_aw::TabBar::new(#selected_expr)
5278                #on_select_expr
5279                #icon_size_expr
5280                #text_size_expr;
5281
5282            #(#tab_labels)*
5283
5284            tab_bar
5285        }
5286    };
5287
5288    // Build content element using match on selected index
5289    let content_element = if tab_content_arms.is_empty() {
5290        quote! { iced::widget::column(vec![]).into() }
5291    } else {
5292        quote! {
5293            match #selected_expr {
5294                #(#tab_content_arms,)*
5295                _ => iced::widget::column(vec![]).into(),
5296            }
5297        }
5298    };
5299
5300    // Combine TabBar and content in a column
5301    let result = quote! {
5302        iced::widget::column![
5303            #tab_bar_widget,
5304            #content_element
5305        ]
5306    };
5307
5308    Ok(result)
5309}
5310
5311/// Resolve icon name to Unicode character for codegen
5312fn resolve_icon_for_codegen(name: &str) -> char {
5313    match name {
5314        "home" => '\u{F015}',
5315        "settings" => '\u{F013}',
5316        "user" => '\u{F007}',
5317        "search" => '\u{F002}',
5318        "add" => '\u{F067}',
5319        "delete" => '\u{F1F8}',
5320        "edit" => '\u{F044}',
5321        "save" => '\u{F0C7}',
5322        "close" => '\u{F00D}',
5323        "back" => '\u{F060}',
5324        "forward" => '\u{F061}',
5325        _ => '\u{F111}', // Circle as fallback
5326    }
5327}