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    // bar_color is bindable - handle both static and binding cases
2061    let bar_color_attr = node.attributes.get("bar_color");
2062    let bar_color_static = bar_color_attr.and_then(|attr| {
2063        if let AttributeValue::Static(s) = attr {
2064            parse_color_to_tokens(s)
2065        } else {
2066            None
2067        }
2068    });
2069    let bar_color_binding =
2070        bar_color_attr.filter(|attr| !matches!(attr, AttributeValue::Static(_)));
2071
2072    let background_color = node.attributes.get("background_color").and_then(|attr| {
2073        if let AttributeValue::Static(s) = attr {
2074            parse_color_to_tokens(s)
2075        } else {
2076            None
2077        }
2078    });
2079
2080    // Parse border radius
2081    let border_radius = node.attributes.get("border_radius").and_then(|attr| {
2082        if let AttributeValue::Static(s) = attr {
2083            s.parse::<f32>().ok()
2084        } else {
2085            None
2086        }
2087    });
2088
2089    // Parse height (girth)
2090    let height = node.attributes.get("height").and_then(|attr| {
2091        if let AttributeValue::Static(s) = attr {
2092            s.parse::<f32>().ok()
2093        } else {
2094            None
2095        }
2096    });
2097
2098    // Parse width
2099    let width = node.attributes.get("width").and_then(|attr| {
2100        if let AttributeValue::Static(s) = attr {
2101            Some(generate_length_expr(s))
2102        } else {
2103            None
2104        }
2105    });
2106
2107    // Generate style closure based on style attribute
2108    let bar_color_expr = if let Some(color_tokens) = bar_color_static {
2109        quote! { #color_tokens }
2110    } else if let Some(attr) = bar_color_binding {
2111        // bar_color is bindable - generate inline runtime color parsing
2112        let color_expr = generate_attribute_value(attr, model_ident);
2113        quote! {
2114            {
2115                let color_str = #color_expr;
2116                // Inline color parsing for bindable bar_color
2117                let parsed_color = (|| {
2118                    let s = color_str.trim();
2119                    // Try hex color (#RRGGBB or #RRGGBBAA)
2120                    if let Some(hex) = s.strip_prefix('#') {
2121                        if hex.len() == 6 {
2122                            if let (Ok(r), Ok(g), Ok(b)) = (
2123                                u8::from_str_radix(&hex[0..2], 16),
2124                                u8::from_str_radix(&hex[2..4], 16),
2125                                u8::from_str_radix(&hex[4..6], 16),
2126                            ) {
2127                                return Some(iced::Color::from_rgb(
2128                                    r as f32 / 255.0,
2129                                    g as f32 / 255.0,
2130                                    b as f32 / 255.0,
2131                                ));
2132                            }
2133                        } else if hex.len() == 8 {
2134                            if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2135                                u8::from_str_radix(&hex[0..2], 16),
2136                                u8::from_str_radix(&hex[2..4], 16),
2137                                u8::from_str_radix(&hex[4..6], 16),
2138                                u8::from_str_radix(&hex[6..8], 16),
2139                            ) {
2140                                return Some(iced::Color::from_rgba(
2141                                    r as f32 / 255.0,
2142                                    g as f32 / 255.0,
2143                                    b as f32 / 255.0,
2144                                    a as f32 / 255.0,
2145                                ));
2146                            }
2147                        }
2148                    }
2149                    // Try RGB format: rgb(r,g,b)
2150                    if s.starts_with("rgb(") && s.ends_with(')') {
2151                        let inner = &s[4..s.len() - 1];
2152                        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2153                        if parts.len() == 3 {
2154                            if let (Ok(r), Ok(g), Ok(b)) = (
2155                                parts[0].parse::<u8>(),
2156                                parts[1].parse::<u8>(),
2157                                parts[2].parse::<u8>(),
2158                            ) {
2159                                return Some(iced::Color::from_rgb(
2160                                    r as f32 / 255.0,
2161                                    g as f32 / 255.0,
2162                                    b as f32 / 255.0,
2163                                ));
2164                            }
2165                        }
2166                    }
2167                    // Try RGBA format: rgba(r,g,b,a)
2168                    if s.starts_with("rgba(") && s.ends_with(')') {
2169                        let inner = &s[5..s.len() - 1];
2170                        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2171                        if parts.len() == 4 {
2172                            if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2173                                parts[0].parse::<u8>(),
2174                                parts[1].parse::<u8>(),
2175                                parts[2].parse::<u8>(),
2176                                parts[3].parse::<f32>(),
2177                            ) {
2178                                return Some(iced::Color::from_rgba(
2179                                    r as f32 / 255.0,
2180                                    g as f32 / 255.0,
2181                                    b as f32 / 255.0,
2182                                    a,
2183                                ));
2184                            }
2185                        }
2186                    }
2187                    None
2188                })();
2189                parsed_color.unwrap_or_else(|| palette.primary.base.color)
2190            }
2191        }
2192    } else {
2193        match style_str.as_str() {
2194            "success" => quote! { palette.success.base.color },
2195            "warning" => quote! { palette.warning.base.color },
2196            "danger" => quote! { palette.danger.base.color },
2197            "secondary" => quote! { palette.secondary.base.color },
2198            _ => quote! { palette.primary.base.color }, // default to primary
2199        }
2200    };
2201
2202    // Generate background color expression
2203    let background_color_expr = if let Some(color_tokens) = background_color {
2204        quote! { #color_tokens }
2205    } else {
2206        quote! { palette.background.weak.color }
2207    };
2208
2209    // Generate border expression
2210    let border_expr = if let Some(radius) = border_radius {
2211        quote! { iced::Border::default().rounded(#radius) }
2212    } else {
2213        quote! { iced::Border::default() }
2214    };
2215
2216    // Generate height/girth expression
2217    let girth_expr = if let Some(h) = height {
2218        quote! { .girth(#h) }
2219    } else {
2220        quote! {}
2221    };
2222
2223    // Generate width expression (using length() method)
2224    let width_expr = if let Some(w) = width {
2225        quote! { .length(#w) }
2226    } else {
2227        quote! {}
2228    };
2229
2230    if let Some(max) = max_attr {
2231        Ok(quote! {
2232            iced::widget::progress_bar(0.0..=#max, #value_expr)
2233                #girth_expr
2234                #width_expr
2235                .style(|theme: &iced::Theme| {
2236                    let palette = theme.extended_palette();
2237                    iced::widget::progress_bar::Style {
2238                        background: iced::Background::Color(#background_color_expr),
2239                        bar: iced::Background::Color(#bar_color_expr),
2240                        border: #border_expr,
2241                    }
2242                })
2243                .into()
2244        })
2245    } else {
2246        Ok(quote! {
2247            iced::widget::progress_bar(0.0..=100.0, #value_expr)
2248                #girth_expr
2249                #width_expr
2250                .style(|theme: &iced::Theme| {
2251                    let palette = theme.extended_palette();
2252                    iced::widget::progress_bar::Style {
2253                        background: iced::Background::Color(#background_color_expr),
2254                        bar: iced::Background::Color(#bar_color_expr),
2255                        border: #border_expr,
2256                    }
2257                })
2258                .into()
2259        })
2260    }
2261}
2262
2263/// Parse a color string into TokenStream for code generation
2264fn parse_color_to_tokens(color_str: &str) -> Option<TokenStream> {
2265    // Try hex color (#RRGGBB or #RRGGBBAA)
2266    if let Some(hex) = color_str.strip_prefix('#') {
2267        if hex.len() == 6 {
2268            if let (Ok(r), Ok(g), Ok(b)) = (
2269                u8::from_str_radix(&hex[0..2], 16),
2270                u8::from_str_radix(&hex[2..4], 16),
2271                u8::from_str_radix(&hex[4..6], 16),
2272            ) {
2273                let rf = r as f32 / 255.0;
2274                let gf = g as f32 / 255.0;
2275                let bf = b as f32 / 255.0;
2276                return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2277            }
2278        } else if hex.len() == 8
2279            && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2280                u8::from_str_radix(&hex[0..2], 16),
2281                u8::from_str_radix(&hex[2..4], 16),
2282                u8::from_str_radix(&hex[4..6], 16),
2283                u8::from_str_radix(&hex[6..8], 16),
2284            )
2285        {
2286            let rf = r as f32 / 255.0;
2287            let gf = g as f32 / 255.0;
2288            let bf = b as f32 / 255.0;
2289            let af = a as f32 / 255.0;
2290            return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #af) });
2291        }
2292    }
2293
2294    // Try RGB format: rgb(r,g,b)
2295    if color_str.starts_with("rgb(") && color_str.ends_with(')') {
2296        let inner = &color_str[4..color_str.len() - 1];
2297        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2298        if parts.len() == 3
2299            && let (Ok(r), Ok(g), Ok(b)) = (
2300                parts[0].parse::<u8>(),
2301                parts[1].parse::<u8>(),
2302                parts[2].parse::<u8>(),
2303            )
2304        {
2305            let rf = r as f32 / 255.0;
2306            let gf = g as f32 / 255.0;
2307            let bf = b as f32 / 255.0;
2308            return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2309        }
2310    }
2311
2312    // Try RGBA format: rgba(r,g,b,a)
2313    if color_str.starts_with("rgba(") && color_str.ends_with(')') {
2314        let inner = &color_str[5..color_str.len() - 1];
2315        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2316        if parts.len() == 4
2317            && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2318                parts[0].parse::<u8>(),
2319                parts[1].parse::<u8>(),
2320                parts[2].parse::<u8>(),
2321                parts[3].parse::<f32>(),
2322            )
2323        {
2324            let rf = r as f32 / 255.0;
2325            let gf = g as f32 / 255.0;
2326            let bf = b as f32 / 255.0;
2327            return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #a) });
2328        }
2329    }
2330
2331    None
2332}
2333
2334/// Generate text input widget
2335fn generate_text_input(
2336    node: &crate::WidgetNode,
2337    model_ident: &syn::Ident,
2338    message_ident: &syn::Ident,
2339    style_classes: &HashMap<String, StyleClass>,
2340) -> Result<TokenStream, super::CodegenError> {
2341    let value_expr = node
2342        .attributes
2343        .get("value")
2344        .map(|attr| generate_attribute_value(attr, model_ident))
2345        .unwrap_or(quote! { String::new() });
2346
2347    let placeholder = node.attributes.get("placeholder").and_then(|attr| {
2348        if let AttributeValue::Static(s) = attr {
2349            Some(s.clone())
2350        } else {
2351            None
2352        }
2353    });
2354
2355    let on_input = node
2356        .events
2357        .iter()
2358        .find(|e| e.event == crate::EventKind::Input);
2359
2360    let on_submit = node
2361        .events
2362        .iter()
2363        .find(|e| e.event == crate::EventKind::Submit);
2364
2365    let mut text_input = match placeholder {
2366        Some(ph) => {
2367            let ph_lit = proc_macro2::Literal::string(&ph);
2368            quote! {
2369                iced::widget::text_input(#ph_lit, &#value_expr)
2370            }
2371        }
2372        None => quote! {
2373            iced::widget::text_input("", &#value_expr)
2374        },
2375    };
2376
2377    if let Some(event) = on_input {
2378        let variant_name = to_upper_camel_case(&event.handler);
2379        let handler_ident = format_ident!("{}", variant_name);
2380        text_input = quote! {
2381            #text_input.on_input(|v| #message_ident::#handler_ident(v))
2382        };
2383    }
2384
2385    if let Some(event) = on_submit {
2386        let variant_name = to_upper_camel_case(&event.handler);
2387        let handler_ident = format_ident!("{}", variant_name);
2388        text_input = quote! {
2389            #text_input.on_submit(#message_ident::#handler_ident)
2390        };
2391    }
2392
2393    // Apply password/secure attribute (masks input)
2394    let is_password = node
2395        .attributes
2396        .get("password")
2397        .or_else(|| node.attributes.get("secure"))
2398        .and_then(|attr| {
2399            if let AttributeValue::Static(s) = attr {
2400                Some(s.to_lowercase() == "true" || s == "1")
2401            } else {
2402                None
2403            }
2404        })
2405        .unwrap_or(false);
2406
2407    if is_password {
2408        text_input = quote! { #text_input.password() };
2409    }
2410
2411    // Apply styles
2412    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
2413
2414    Ok(quote! { #text_input.into() })
2415}
2416
2417/// Generate image widget
2418fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2419    let src_attr = node.attributes.get("src").ok_or_else(|| {
2420        super::CodegenError::InvalidWidget("image requires src attribute".to_string())
2421    })?;
2422
2423    let src = match src_attr {
2424        AttributeValue::Static(s) => s.clone(),
2425        _ => String::new(),
2426    };
2427    let src_lit = proc_macro2::Literal::string(&src);
2428
2429    let width = node.attributes.get("width").and_then(|attr| {
2430        if let AttributeValue::Static(s) = attr {
2431            s.parse::<u32>().ok()
2432        } else {
2433            None
2434        }
2435    });
2436
2437    let height = node.attributes.get("height").and_then(|attr| {
2438        if let AttributeValue::Static(s) = attr {
2439            s.parse::<u32>().ok()
2440        } else {
2441            None
2442        }
2443    });
2444
2445    let mut image = quote! {
2446        iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
2447    };
2448
2449    // Apply native width/height if specified with integer values
2450    if let (Some(w), Some(h)) = (width, height) {
2451        image = quote! { #image.width(#w).height(#h) };
2452    } else if let Some(w) = width {
2453        image = quote! { #image.width(#w) };
2454    } else if let Some(h) = height {
2455        image = quote! { #image.height(#h) };
2456    }
2457
2458    // Check if we need container for NON-native layout attributes
2459    // (padding, alignment, classes - NOT width/height since those are native)
2460    // For Image, only wrap if there are alignment/padding/classes
2461    let needs_container = !node.classes.is_empty()
2462        || node.attributes.contains_key("align_x")
2463        || node.attributes.contains_key("align_y")
2464        || node.attributes.contains_key("padding");
2465
2466    if needs_container {
2467        // Wrap with container for layout attributes, but skip width/height (already applied)
2468        let mut container = quote! { iced::widget::container(#image) };
2469
2470        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2471            if let AttributeValue::Static(s) = attr {
2472                s.parse::<f32>().ok()
2473            } else {
2474                None
2475            }
2476        }) {
2477            container = quote! { #container.padding(#padding) };
2478        }
2479
2480        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2481            if let AttributeValue::Static(s) = attr {
2482                Some(s.clone())
2483            } else {
2484                None
2485            }
2486        }) {
2487            let align_expr = generate_horizontal_alignment_expr(&align_x);
2488            container = quote! { #container.align_x(#align_expr) };
2489        }
2490
2491        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2492            if let AttributeValue::Static(s) = attr {
2493                Some(s.clone())
2494            } else {
2495                None
2496            }
2497        }) {
2498            let align_expr = generate_vertical_alignment_expr(&align_y);
2499            container = quote! { #container.align_y(#align_expr) };
2500        }
2501
2502        if let Some(class_name) = node.classes.first() {
2503            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2504            container = quote! { #container.style(#style_fn_ident) };
2505        }
2506
2507        Ok(quote! { #container.into() })
2508    } else {
2509        Ok(quote! { #image.into() })
2510    }
2511}
2512
2513/// Generate SVG widget
2514fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2515    // Support both "src" (standard) and "path" (legacy) for backward compatibility
2516    let path_attr = node
2517        .attributes
2518        .get("src")
2519        .or_else(|| node.attributes.get("path"))
2520        .ok_or_else(|| {
2521            super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
2522        })?;
2523
2524    let path = match path_attr {
2525        AttributeValue::Static(s) => s.clone(),
2526        _ => String::new(),
2527    };
2528    let path_lit = proc_macro2::Literal::string(&path);
2529
2530    let width = node.attributes.get("width").and_then(|attr| {
2531        if let AttributeValue::Static(s) = attr {
2532            s.parse::<u32>().ok()
2533        } else {
2534            None
2535        }
2536    });
2537
2538    let height = node.attributes.get("height").and_then(|attr| {
2539        if let AttributeValue::Static(s) = attr {
2540            s.parse::<u32>().ok()
2541        } else {
2542            None
2543        }
2544    });
2545
2546    let mut svg = quote! {
2547        iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
2548    };
2549
2550    // Apply native width/height if specified with integer values
2551    if let (Some(w), Some(h)) = (width, height) {
2552        svg = quote! { #svg.width(#w).height(#h) };
2553    } else if let Some(w) = width {
2554        svg = quote! { #svg.width(#w) };
2555    } else if let Some(h) = height {
2556        svg = quote! { #svg.height(#h) };
2557    }
2558
2559    // Check if we need container for NON-native layout attributes
2560    // (padding, alignment, classes - NOT width/height since those are native)
2561    // For SVG, only wrap if there are alignment/padding/classes
2562    let needs_container = !node.classes.is_empty()
2563        || node.attributes.contains_key("align_x")
2564        || node.attributes.contains_key("align_y")
2565        || node.attributes.contains_key("padding");
2566
2567    if needs_container {
2568        // Wrap with container for layout attributes, but skip width/height (already applied)
2569        let mut container = quote! { iced::widget::container(#svg) };
2570
2571        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2572            if let AttributeValue::Static(s) = attr {
2573                s.parse::<f32>().ok()
2574            } else {
2575                None
2576            }
2577        }) {
2578            container = quote! { #container.padding(#padding) };
2579        }
2580
2581        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2582            if let AttributeValue::Static(s) = attr {
2583                Some(s.clone())
2584            } else {
2585                None
2586            }
2587        }) {
2588            let align_expr = generate_horizontal_alignment_expr(&align_x);
2589            container = quote! { #container.align_x(#align_expr) };
2590        }
2591
2592        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2593            if let AttributeValue::Static(s) = attr {
2594                Some(s.clone())
2595            } else {
2596                None
2597            }
2598        }) {
2599            let align_expr = generate_vertical_alignment_expr(&align_y);
2600            container = quote! { #container.align_y(#align_expr) };
2601        }
2602
2603        if let Some(class_name) = node.classes.first() {
2604            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2605            container = quote! { #container.style(#style_fn_ident) };
2606        }
2607
2608        Ok(quote! { #container.into() })
2609    } else {
2610        Ok(quote! { #svg.into() })
2611    }
2612}
2613
2614/// Generate pick list widget
2615fn generate_pick_list(
2616    node: &crate::WidgetNode,
2617    model_ident: &syn::Ident,
2618    message_ident: &syn::Ident,
2619    _style_classes: &HashMap<String, StyleClass>,
2620) -> Result<TokenStream, super::CodegenError> {
2621    let options_attr = node.attributes.get("options").ok_or_else(|| {
2622        super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2623    })?;
2624
2625    let options: Vec<String> = match options_attr {
2626        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2627        _ => Vec::new(),
2628    };
2629    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2630
2631    let selected_attr = node.attributes.get("selected");
2632    let selected_expr = selected_attr
2633        .map(|attr| generate_attribute_value(attr, model_ident))
2634        .unwrap_or(quote! { None });
2635
2636    let on_select = node
2637        .events
2638        .iter()
2639        .find(|e| e.event == crate::EventKind::Select);
2640
2641    if let Some(event) = on_select {
2642        let variant_name = to_upper_camel_case(&event.handler);
2643        let handler_ident = format_ident!("{}", variant_name);
2644        Ok(quote! {
2645            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2646        })
2647    } else {
2648        Ok(quote! {
2649            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2650        })
2651    }
2652}
2653
2654/// Generate combo box widget
2655fn generate_combo_box(
2656    node: &crate::WidgetNode,
2657    model_ident: &syn::Ident,
2658    message_ident: &syn::Ident,
2659    _style_classes: &HashMap<String, StyleClass>,
2660) -> Result<TokenStream, super::CodegenError> {
2661    let options_attr = node.attributes.get("options").ok_or_else(|| {
2662        super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2663    })?;
2664
2665    let options: Vec<String> = match options_attr {
2666        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2667        _ => Vec::new(),
2668    };
2669    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2670
2671    let selected_attr = node.attributes.get("selected");
2672    let selected_expr = selected_attr
2673        .map(|attr| generate_attribute_value(attr, model_ident))
2674        .unwrap_or(quote! { None });
2675
2676    let on_select = node
2677        .events
2678        .iter()
2679        .find(|e| e.event == crate::EventKind::Select);
2680
2681    if let Some(event) = on_select {
2682        let variant_name = to_upper_camel_case(&event.handler);
2683        let handler_ident = format_ident!("{}", variant_name);
2684        Ok(quote! {
2685            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2686        })
2687    } else {
2688        Ok(quote! {
2689            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2690        })
2691    }
2692}
2693
2694/// Generate tooltip widget
2695fn generate_tooltip(
2696    node: &crate::WidgetNode,
2697    model_ident: &syn::Ident,
2698    message_ident: &syn::Ident,
2699    style_classes: &HashMap<String, StyleClass>,
2700) -> Result<TokenStream, super::CodegenError> {
2701    let child = node.children.first().ok_or_else(|| {
2702        super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2703    })?;
2704    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2705
2706    let message_attr = node.attributes.get("message").ok_or_else(|| {
2707        super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2708    })?;
2709    let message_expr = generate_attribute_value(message_attr, model_ident);
2710
2711    Ok(quote! {
2712        iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2713    })
2714}
2715
2716/// Generate grid widget
2717fn generate_grid(
2718    node: &crate::WidgetNode,
2719    model_ident: &syn::Ident,
2720    message_ident: &syn::Ident,
2721    style_classes: &HashMap<String, StyleClass>,
2722) -> Result<TokenStream, super::CodegenError> {
2723    let children: Vec<TokenStream> = node
2724        .children
2725        .iter()
2726        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2727        .collect::<Result<_, _>>()?;
2728
2729    let columns = node
2730        .attributes
2731        .get("columns")
2732        .and_then(|attr| {
2733            if let AttributeValue::Static(s) = attr {
2734                s.parse::<u32>().ok()
2735            } else {
2736                None
2737            }
2738        })
2739        .unwrap_or(1);
2740
2741    let spacing = node.attributes.get("spacing").and_then(|attr| {
2742        if let AttributeValue::Static(s) = attr {
2743            s.parse::<f32>().ok()
2744        } else {
2745            None
2746        }
2747    });
2748
2749    let padding = node.attributes.get("padding").and_then(|attr| {
2750        if let AttributeValue::Static(s) = attr {
2751            s.parse::<f32>().ok()
2752        } else {
2753            None
2754        }
2755    });
2756
2757    let grid = quote! {
2758        iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2759    };
2760
2761    let grid = if let Some(s) = spacing {
2762        quote! { #grid.spacing(#s) }
2763    } else {
2764        grid
2765    };
2766
2767    let grid = if let Some(p) = padding {
2768        quote! { #grid.padding(#p) }
2769    } else {
2770        grid
2771    };
2772
2773    Ok(quote! { #grid.into() })
2774}
2775
2776/// Generate canvas widget
2777fn generate_canvas(
2778    node: &crate::WidgetNode,
2779    model_ident: &syn::Ident,
2780    message_ident: &syn::Ident,
2781    _style_classes: &HashMap<String, StyleClass>,
2782) -> Result<TokenStream, super::CodegenError> {
2783    let width = node.attributes.get("width").and_then(|attr| {
2784        if let AttributeValue::Static(s) = attr {
2785            s.parse::<f32>().ok()
2786        } else {
2787            None
2788        }
2789    });
2790
2791    let height = node.attributes.get("height").and_then(|attr| {
2792        if let AttributeValue::Static(s) = attr {
2793            s.parse::<f32>().ok()
2794        } else {
2795            None
2796        }
2797    });
2798
2799    let width_expr = match width {
2800        Some(w) => quote! { iced::Length::Fixed(#w) },
2801        None => quote! { iced::Length::Fixed(400.0) },
2802    };
2803
2804    let height_expr = match height {
2805        Some(h) => quote! { iced::Length::Fixed(#h) },
2806        None => quote! { iced::Length::Fixed(300.0) },
2807    };
2808
2809    // Check for custom program binding
2810    let content_expr = if let Some(program_attr) = node.attributes.get("program") {
2811        let program_binding = match program_attr {
2812            AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
2813            _ => quote! { None },
2814        };
2815
2816        // Generate declarative canvas for the 'else' case
2817        let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2818        let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2819        let prog_init = quote! {
2820            dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2821        };
2822        let prog_with_handlers = if let Some(handlers) = handlers_expr {
2823            quote! { #prog_init.with_handlers(#handlers) }
2824        } else {
2825            prog_init
2826        };
2827
2828        quote! {
2829            if let Some(container) = &#program_binding {
2830                 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2831                     dampen_iced::canvas::CanvasContent::Custom(container.0.clone())
2832                 ))
2833                 .width(#width_expr)
2834                 .height(#height_expr);
2835
2836                 iced::Element::from(canvas).map(|()| unreachable!("Custom program action not supported in codegen"))
2837            } else {
2838                 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2839                     dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2840                 ))
2841                 .width(#width_expr)
2842                 .height(#height_expr);
2843
2844                 iced::Element::from(canvas)
2845            }
2846        }
2847    } else {
2848        // Generate declarative canvas
2849        let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2850
2851        // Parse event handlers
2852        let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2853
2854        let prog_init = quote! {
2855            dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2856        };
2857
2858        let prog_with_handlers = if let Some(handlers) = handlers_expr {
2859            quote! { #prog_init.with_handlers(#handlers) }
2860        } else {
2861            prog_init
2862        };
2863
2864        quote! {
2865            iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2866                dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2867            ))
2868            .width(#width_expr)
2869            .height(#height_expr)
2870            .into()
2871        }
2872    };
2873
2874    Ok(content_expr)
2875}
2876
2877/// Generate float widget
2878fn generate_float(
2879    node: &crate::WidgetNode,
2880    model_ident: &syn::Ident,
2881    message_ident: &syn::Ident,
2882    style_classes: &HashMap<String, StyleClass>,
2883) -> Result<TokenStream, super::CodegenError> {
2884    let child = node.children.first().ok_or_else(|| {
2885        super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2886    })?;
2887    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2888
2889    let position = node
2890        .attributes
2891        .get("position")
2892        .and_then(|attr| {
2893            if let AttributeValue::Static(s) = attr {
2894                Some(s.clone())
2895            } else {
2896                None
2897            }
2898        })
2899        .unwrap_or_else(|| "TopRight".to_string());
2900
2901    let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2902        if let AttributeValue::Static(s) = attr {
2903            s.parse::<f32>().ok()
2904        } else {
2905            None
2906        }
2907    });
2908
2909    let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2910        if let AttributeValue::Static(s) = attr {
2911            s.parse::<f32>().ok()
2912        } else {
2913            None
2914        }
2915    });
2916
2917    let float = match position.as_str() {
2918        "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2919        "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2920        "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2921        "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2922        _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2923    };
2924
2925    let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2926        quote! { #float.offset_x(#ox).offset_y(#oy) }
2927    } else if let Some(ox) = offset_x {
2928        quote! { #float.offset_x(#ox) }
2929    } else if let Some(oy) = offset_y {
2930        quote! { #float.offset_y(#oy) }
2931    } else {
2932        float
2933    };
2934
2935    Ok(quote! { #float.into() })
2936}
2937
2938/// Generate for loop widget (iterates over collection)
2939///
2940/// Expects attributes:
2941/// - `each`: variable name for each item (e.g., "task")
2942/// - `in`: binding expression for the collection (e.g., "{filtered_tasks}")
2943fn generate_for(
2944    node: &crate::WidgetNode,
2945    model_ident: &syn::Ident,
2946    message_ident: &syn::Ident,
2947    style_classes: &HashMap<String, StyleClass>,
2948) -> Result<TokenStream, super::CodegenError> {
2949    // Get the 'in' attribute (collection to iterate)
2950    let in_attr = node.attributes.get("in").ok_or_else(|| {
2951        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2952    })?;
2953
2954    // Get the 'each' attribute (loop variable name)
2955    let var_name = node
2956        .attributes
2957        .get("each")
2958        .and_then(|attr| {
2959            if let AttributeValue::Static(s) = attr {
2960                Some(s.clone())
2961            } else {
2962                None
2963            }
2964        })
2965        .unwrap_or_else(|| "item".to_string());
2966
2967    let var_ident = format_ident!("{}", var_name);
2968
2969    // Generate the collection expression (raw, without .to_string())
2970    let collection_expr = generate_attribute_value_raw(in_attr, model_ident);
2971
2972    // Generate children widgets
2973    let children: Vec<TokenStream> = node
2974        .children
2975        .iter()
2976        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2977        .collect::<Result<_, _>>()?;
2978
2979    // Generate the for loop that builds widgets
2980    Ok(quote! {
2981        {
2982            let items: Vec<_> = #collection_expr;
2983            let widgets: Vec<iced::Element<'_, #message_ident>> = items
2984                .iter()
2985                .enumerate()
2986                .flat_map(|(index, #var_ident)| {
2987                    let _ = index; // Suppress unused warning if not used
2988                    vec![#(#children),*]
2989                })
2990                .collect();
2991            iced::widget::column(widgets).into()
2992        }
2993    })
2994}
2995
2996/// Generate if widget
2997fn generate_if(
2998    node: &crate::WidgetNode,
2999    model_ident: &syn::Ident,
3000    message_ident: &syn::Ident,
3001    style_classes: &HashMap<String, StyleClass>,
3002) -> Result<TokenStream, super::CodegenError> {
3003    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3004        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3005    })?;
3006
3007    let children: Vec<TokenStream> = node
3008        .children
3009        .iter()
3010        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
3011        .collect::<Result<_, _>>()?;
3012
3013    let condition_expr = generate_attribute_value(condition_attr, model_ident);
3014
3015    Ok(quote! {
3016        if #condition_expr.parse::<bool>().unwrap_or(false) {
3017            iced::widget::column(vec![#(#children),*]).into()
3018        } else {
3019            iced::widget::column(vec![]).into()
3020        }
3021    })
3022}
3023
3024/// Generate custom widget
3025/// Generate DatePicker widget
3026fn generate_date_picker(
3027    node: &crate::WidgetNode,
3028    model_ident: &syn::Ident,
3029    message_ident: &syn::Ident,
3030    style_classes: &HashMap<String, StyleClass>,
3031) -> Result<TokenStream, super::CodegenError> {
3032    let show = node
3033        .attributes
3034        .get("show")
3035        .map(|attr| match attr {
3036            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3037            AttributeValue::Static(s) => {
3038                let v = s == "true";
3039                quote! { #v }
3040            }
3041            _ => quote! { false },
3042        })
3043        .unwrap_or(quote! { false });
3044
3045    let date = if let Some(attr) = node.attributes.get("value") {
3046        match attr {
3047            AttributeValue::Binding(b) => {
3048                let expr = super::bindings::generate_bool_expr(&b.expr);
3049                quote! { iced_aw::date_picker::Date::from(#expr) }
3050            }
3051            AttributeValue::Static(s) => {
3052                let format = node
3053                    .attributes
3054                    .get("format")
3055                    .map(|f| match f {
3056                        AttributeValue::Static(fs) => fs.as_str(),
3057                        _ => "%Y-%m-%d",
3058                    })
3059                    .unwrap_or("%Y-%m-%d");
3060                quote! {
3061                    iced_aw::date_picker::Date::from(
3062                        chrono::NaiveDate::parse_from_str(#s, #format).unwrap_or_default()
3063                    )
3064                }
3065            }
3066            _ => quote! { iced_aw::date_picker::Date::today() },
3067        }
3068    } else {
3069        quote! { iced_aw::date_picker::Date::today() }
3070    };
3071
3072    let on_cancel = if let Some(h) = node
3073        .events
3074        .iter()
3075        .find(|e| e.event == crate::EventKind::Cancel)
3076    {
3077        let msg = format_ident!("{}", h.handler);
3078        quote! { #message_ident::#msg }
3079    } else {
3080        quote! { #message_ident::None }
3081    };
3082
3083    let on_submit = if let Some(h) = node
3084        .events
3085        .iter()
3086        .find(|e| e.event == crate::EventKind::Submit)
3087    {
3088        let msg = format_ident!("{}", h.handler);
3089        quote! {
3090            |date| {
3091                let s = chrono::NaiveDate::from(date).format("%Y-%m-%d").to_string();
3092                #message_ident::#msg(s)
3093            }
3094        }
3095    } else {
3096        quote! { |_| #message_ident::None }
3097    };
3098
3099    let underlay = if let Some(child) = node.children.first() {
3100        generate_widget(child, model_ident, message_ident, style_classes)?
3101    } else {
3102        quote! { iced::widget::text("Missing child") }
3103    };
3104
3105    Ok(quote! {
3106        iced_aw::widgets::date_picker::DatePicker::new(
3107            #show,
3108            #date,
3109            #underlay,
3110            #on_cancel,
3111            #on_submit
3112        )
3113    })
3114}
3115
3116/// Generate ColorPicker widget
3117fn generate_color_picker(
3118    node: &crate::WidgetNode,
3119    model_ident: &syn::Ident,
3120    message_ident: &syn::Ident,
3121    style_classes: &HashMap<String, StyleClass>,
3122) -> Result<TokenStream, super::CodegenError> {
3123    let show = node
3124        .attributes
3125        .get("show")
3126        .map(|attr| match attr {
3127            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3128            AttributeValue::Static(s) => {
3129                let v = s == "true";
3130                quote! { #v }
3131            }
3132            _ => quote! { false },
3133        })
3134        .unwrap_or(quote! { false });
3135
3136    let color = if let Some(attr) = node.attributes.get("value") {
3137        match attr {
3138            AttributeValue::Binding(b) => {
3139                let expr = super::bindings::generate_expr(&b.expr);
3140                quote! { iced::Color::from_hex(&#expr.to_string()).unwrap_or(iced::Color::BLACK) }
3141            }
3142            AttributeValue::Static(s) => {
3143                quote! { iced::Color::from_hex(#s).unwrap_or(iced::Color::BLACK) }
3144            }
3145            _ => quote! { iced::Color::BLACK },
3146        }
3147    } else {
3148        quote! { iced::Color::BLACK }
3149    };
3150
3151    let on_cancel = if let Some(h) = node
3152        .events
3153        .iter()
3154        .find(|e| e.event == crate::EventKind::Cancel)
3155    {
3156        let msg = format_ident!("{}", h.handler);
3157        quote! { #message_ident::#msg }
3158    } else {
3159        quote! { #message_ident::None }
3160    };
3161
3162    let on_submit = if let Some(h) = node
3163        .events
3164        .iter()
3165        .find(|e| e.event == crate::EventKind::Submit)
3166    {
3167        let msg = format_ident!("{}", h.handler);
3168        quote! {
3169            |color| {
3170                let s = iced::color!(color).to_string();
3171                #message_ident::#msg(s)
3172            }
3173        }
3174    } else {
3175        quote! { |_| #message_ident::None }
3176    };
3177
3178    let underlay = if let Some(child) = node.children.first() {
3179        generate_widget(child, model_ident, message_ident, style_classes)?
3180    } else {
3181        quote! { iced::widget::text("Missing child") }
3182    };
3183
3184    Ok(quote! {
3185        iced_aw::widgets::color_picker::ColorPicker::new(
3186            #show,
3187            #color,
3188            #underlay,
3189            #on_cancel,
3190            #on_submit
3191        )
3192    })
3193}
3194
3195/// Generate TimePicker widget
3196fn generate_time_picker(
3197    node: &crate::WidgetNode,
3198    model_ident: &syn::Ident,
3199    message_ident: &syn::Ident,
3200    style_classes: &HashMap<String, StyleClass>,
3201) -> Result<TokenStream, super::CodegenError> {
3202    let show = node
3203        .attributes
3204        .get("show")
3205        .map(|attr| match attr {
3206            AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3207            AttributeValue::Static(s) => {
3208                let v = s == "true";
3209                quote! { #v }
3210            }
3211            _ => quote! { false },
3212        })
3213        .unwrap_or(quote! { false });
3214
3215    let time = if let Some(attr) = node.attributes.get("value") {
3216        match attr {
3217            AttributeValue::Binding(b) => {
3218                let expr = super::bindings::generate_bool_expr(&b.expr);
3219                quote! { iced_aw::time_picker::Time::from(#expr) }
3220            }
3221            AttributeValue::Static(s) => {
3222                let format = node
3223                    .attributes
3224                    .get("format")
3225                    .map(|f| match f {
3226                        AttributeValue::Static(fs) => fs.as_str(),
3227                        _ => "%H:%M:%S",
3228                    })
3229                    .unwrap_or("%H:%M:%S");
3230                quote! {
3231                    iced_aw::time_picker::Time::from(
3232                        chrono::NaiveTime::parse_from_str(#s, #format).unwrap_or_default()
3233                    )
3234                }
3235            }
3236            _ => {
3237                quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3238            }
3239        }
3240    } else {
3241        quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3242    };
3243
3244    let use_24h = node.attributes.get("use_24h").map(|attr| match attr {
3245        AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3246        AttributeValue::Static(s) => {
3247            let v = s == "true";
3248            quote! { #v }
3249        }
3250        _ => quote! { false },
3251    });
3252
3253    let show_seconds = node.attributes.get("show_seconds").map(|attr| match attr {
3254        AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3255        AttributeValue::Static(s) => {
3256            let v = s == "true";
3257            quote! { #v }
3258        }
3259        _ => quote! { false },
3260    });
3261
3262    let on_cancel = if let Some(h) = node
3263        .events
3264        .iter()
3265        .find(|e| e.event == crate::EventKind::Cancel)
3266    {
3267        let msg = format_ident!("{}", h.handler);
3268        quote! { #message_ident::#msg }
3269    } else {
3270        quote! { #message_ident::None }
3271    };
3272
3273    let on_submit = if let Some(h) = node
3274        .events
3275        .iter()
3276        .find(|e| e.event == crate::EventKind::Submit)
3277    {
3278        let msg = format_ident!("{}", h.handler);
3279        quote! {
3280            |time| {
3281                let s = chrono::NaiveTime::from(time).format("%H:%M:%S").to_string();
3282                #message_ident::#msg(s)
3283            }
3284        }
3285    } else {
3286        quote! { |_| #message_ident::None }
3287    };
3288
3289    let underlay = if let Some(child) = node.children.first() {
3290        generate_widget(child, model_ident, message_ident, style_classes)?
3291    } else {
3292        quote! { iced::widget::text("Missing child") }
3293    };
3294
3295    let mut picker_setup = quote! {
3296        let mut picker = iced_aw::widgets::time_picker::TimePicker::new(
3297            #show,
3298            #time,
3299            #underlay,
3300            #on_cancel,
3301            #on_submit
3302        );
3303    };
3304
3305    if let Some(use_24h_expr) = use_24h {
3306        picker_setup.extend(quote! {
3307            if #use_24h_expr {
3308                picker = picker.use_24h();
3309            }
3310        });
3311    }
3312
3313    if let Some(show_seconds_expr) = show_seconds {
3314        picker_setup.extend(quote! {
3315            if #show_seconds_expr {
3316                picker = picker.show_seconds();
3317            }
3318        });
3319    }
3320
3321    Ok(quote! {
3322        {
3323            #picker_setup
3324            picker
3325        }
3326    })
3327}
3328
3329fn generate_custom_widget(
3330    node: &crate::WidgetNode,
3331    name: &str,
3332    model_ident: &syn::Ident,
3333    message_ident: &syn::Ident,
3334    style_classes: &HashMap<String, StyleClass>,
3335) -> Result<TokenStream, super::CodegenError> {
3336    let widget_ident = format_ident!("{}", name);
3337    let children: Vec<TokenStream> = node
3338        .children
3339        .iter()
3340        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
3341        .collect::<Result<_, _>>()?;
3342
3343    Ok(quote! {
3344        #widget_ident(vec![#(#children),*]).into()
3345    })
3346}
3347
3348/// Generate attribute value expression with inlined bindings
3349fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3350    match attr {
3351        AttributeValue::Static(s) => {
3352            let lit = proc_macro2::Literal::string(s);
3353            quote! { #lit.to_string() }
3354        }
3355        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
3356        AttributeValue::Interpolated(parts) => {
3357            let parts_str: Vec<String> = parts
3358                .iter()
3359                .map(|part| match part {
3360                    InterpolatedPart::Literal(s) => s.clone(),
3361                    InterpolatedPart::Binding(_) => "{}".to_string(),
3362                })
3363                .collect();
3364            let binding_exprs: Vec<TokenStream> = parts
3365                .iter()
3366                .filter_map(|part| {
3367                    if let InterpolatedPart::Binding(expr) = part {
3368                        Some(generate_expr(&expr.expr))
3369                    } else {
3370                        None
3371                    }
3372                })
3373                .collect();
3374
3375            let format_string = parts_str.join("");
3376            let lit = proc_macro2::Literal::string(&format_string);
3377
3378            quote! { format!(#lit, #(#binding_exprs),*) }
3379        }
3380    }
3381}
3382
3383/// Generate attribute value without `.to_string()` conversion
3384/// Used for collections in for loops where we need the raw value
3385fn generate_attribute_value_raw(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3386    match attr {
3387        AttributeValue::Static(s) => {
3388            let lit = proc_macro2::Literal::string(s);
3389            quote! { #lit }
3390        }
3391        AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
3392        AttributeValue::Interpolated(parts) => {
3393            // For interpolated, we still need to generate a string
3394            let parts_str: Vec<String> = parts
3395                .iter()
3396                .map(|part| match part {
3397                    InterpolatedPart::Literal(s) => s.clone(),
3398                    InterpolatedPart::Binding(_) => "{}".to_string(),
3399                })
3400                .collect();
3401            let binding_exprs: Vec<TokenStream> = parts
3402                .iter()
3403                .filter_map(|part| {
3404                    if let InterpolatedPart::Binding(expr) = part {
3405                        Some(generate_expr(&expr.expr))
3406                    } else {
3407                        None
3408                    }
3409                })
3410                .collect();
3411
3412            let format_string = parts_str.join("");
3413            let lit = proc_macro2::Literal::string(&format_string);
3414
3415            quote! { format!(#lit, #(#binding_exprs),*) }
3416        }
3417    }
3418}
3419
3420// ============================================================================
3421// Functions with local variable context support (for loops)
3422// ============================================================================
3423
3424/// Generate text widget with local variable context
3425fn generate_text_with_locals(
3426    node: &crate::WidgetNode,
3427    model_ident: &syn::Ident,
3428    _style_classes: &HashMap<String, StyleClass>,
3429    local_vars: &std::collections::HashSet<String>,
3430) -> Result<TokenStream, super::CodegenError> {
3431    let value_attr = node.attributes.get("value").ok_or_else(|| {
3432        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
3433    })?;
3434
3435    let value_expr = generate_attribute_value_with_locals(value_attr, model_ident, local_vars);
3436
3437    let mut text_widget = quote! {
3438        iced::widget::text(#value_expr)
3439    };
3440
3441    // Apply size attribute
3442    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3443        if let AttributeValue::Static(s) = attr {
3444            s.parse::<f32>().ok()
3445        } else {
3446            None
3447        }
3448    }) {
3449        text_widget = quote! { #text_widget.size(#size) };
3450    }
3451
3452    // Apply weight attribute
3453    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
3454        if let AttributeValue::Static(s) = attr {
3455            Some(s.clone())
3456        } else {
3457            None
3458        }
3459    }) {
3460        let weight_expr = match weight.to_lowercase().as_str() {
3461            "bold" => quote! { iced::font::Weight::Bold },
3462            "semibold" => quote! { iced::font::Weight::Semibold },
3463            "medium" => quote! { iced::font::Weight::Medium },
3464            "light" => quote! { iced::font::Weight::Light },
3465            _ => quote! { iced::font::Weight::Normal },
3466        };
3467        text_widget = quote! {
3468            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
3469        };
3470    }
3471
3472    // Apply inline style color if present
3473    if let Some(ref style_props) = node.style
3474        && let Some(ref color) = style_props.color
3475    {
3476        let color_expr = generate_color_expr(color);
3477        text_widget = quote! { #text_widget.color(#color_expr) };
3478    }
3479
3480    Ok(maybe_wrap_in_container(text_widget, node))
3481}
3482
3483/// Generate button widget with local variable context
3484fn generate_button_with_locals(
3485    node: &crate::WidgetNode,
3486    model_ident: &syn::Ident,
3487    message_ident: &syn::Ident,
3488    style_classes: &HashMap<String, StyleClass>,
3489    local_vars: &std::collections::HashSet<String>,
3490) -> Result<TokenStream, super::CodegenError> {
3491    let label_attr = node.attributes.get("label").ok_or_else(|| {
3492        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
3493    })?;
3494
3495    let label_expr = generate_attribute_value_with_locals(label_attr, model_ident, local_vars);
3496
3497    let on_click = node
3498        .events
3499        .iter()
3500        .find(|e| e.event == crate::EventKind::Click);
3501
3502    let mut button = quote! {
3503        iced::widget::button(iced::widget::text(#label_expr))
3504    };
3505
3506    if let Some(event) = on_click {
3507        let variant_name = to_upper_camel_case(&event.handler);
3508        let handler_ident = format_ident!("{}", variant_name);
3509
3510        let param_expr = if let Some(ref param) = event.param {
3511            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
3512            quote! { (#param_tokens) }
3513        } else {
3514            quote! {}
3515        };
3516
3517        button = quote! {
3518            #button.on_press(#message_ident::#handler_ident #param_expr)
3519        };
3520    }
3521
3522    // Apply styles
3523    button = apply_widget_style(button, node, "button", style_classes)?;
3524
3525    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#button) })
3526}
3527
3528/// Generate container widget with local variable context
3529fn generate_container_with_locals(
3530    node: &crate::WidgetNode,
3531    widget_type: &str,
3532    model_ident: &syn::Ident,
3533    message_ident: &syn::Ident,
3534    style_classes: &HashMap<String, StyleClass>,
3535    local_vars: &std::collections::HashSet<String>,
3536) -> Result<TokenStream, super::CodegenError> {
3537    let children: Vec<TokenStream> = node
3538        .children
3539        .iter()
3540        .map(|child| {
3541            generate_widget_with_locals(
3542                child,
3543                model_ident,
3544                message_ident,
3545                style_classes,
3546                local_vars,
3547            )
3548        })
3549        .collect::<Result<_, _>>()?;
3550
3551    let mut container = match widget_type {
3552        "column" => {
3553            quote! { iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3554        }
3555        "row" => {
3556            quote! { iced::widget::row({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3557        }
3558        "scrollable" => {
3559            quote! { iced::widget::scrollable(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3560        }
3561        _ => {
3562            // container wraps a single child
3563            if children.len() == 1 {
3564                let child = &children[0];
3565                quote! { iced::widget::container(#child) }
3566            } else {
3567                quote! { iced::widget::container(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3568            }
3569        }
3570    };
3571
3572    // Get merged layout from node.layout and style classes
3573    let merged_layout = get_merged_layout(node, style_classes);
3574
3575    // Get spacing from attributes or merged layout
3576    let spacing = node
3577        .attributes
3578        .get("spacing")
3579        .and_then(|attr| {
3580            if let AttributeValue::Static(s) = attr {
3581                s.parse::<f32>().ok()
3582            } else {
3583                None
3584            }
3585        })
3586        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
3587
3588    // Apply spacing for column/row
3589    if let Some(s) = spacing
3590        && (widget_type == "column" || widget_type == "row")
3591    {
3592        container = quote! { #container.spacing(#s) };
3593    }
3594
3595    // Get padding from attributes or merged layout
3596    let padding = node
3597        .attributes
3598        .get("padding")
3599        .and_then(|attr| {
3600            if let AttributeValue::Static(s) = attr {
3601                s.parse::<f32>().ok()
3602            } else {
3603                None
3604            }
3605        })
3606        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
3607
3608    // Apply padding
3609    if let Some(p) = padding {
3610        container = quote! { #container.padding(#p) };
3611    }
3612
3613    // Apply width from attributes or merged layout
3614    let width_from_attr = node.attributes.get("width").and_then(|attr| {
3615        if let AttributeValue::Static(s) = attr {
3616            Some(s.clone())
3617        } else {
3618            None
3619        }
3620    });
3621    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
3622
3623    if let Some(width) = width_from_attr {
3624        let width_expr = generate_length_expr(&width);
3625        container = quote! { #container.width(#width_expr) };
3626    } else if let Some(layout_width) = width_from_layout {
3627        let width_expr = generate_layout_length_expr(layout_width);
3628        container = quote! { #container.width(#width_expr) };
3629    }
3630
3631    // Apply height from attributes or merged layout
3632    let height_from_attr = node.attributes.get("height").and_then(|attr| {
3633        if let AttributeValue::Static(s) = attr {
3634            Some(s.clone())
3635        } else {
3636            None
3637        }
3638    });
3639    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
3640
3641    if let Some(height) = height_from_attr {
3642        let height_expr = generate_length_expr(&height);
3643        container = quote! { #container.height(#height_expr) };
3644    } else if let Some(layout_height) = height_from_layout {
3645        let height_expr = generate_layout_length_expr(layout_height);
3646        container = quote! { #container.height(#height_expr) };
3647    }
3648
3649    // Apply alignment for row/column
3650    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
3651        if let AttributeValue::Static(s) = attr {
3652            Some(s.clone())
3653        } else {
3654            None
3655        }
3656    }) && widget_type == "row"
3657    {
3658        let alignment_expr = match align_y.to_lowercase().as_str() {
3659            "top" | "start" => quote! { iced::alignment::Vertical::Top },
3660            "bottom" | "end" => quote! { iced::alignment::Vertical::Bottom },
3661            _ => quote! { iced::alignment::Vertical::Center },
3662        };
3663        container = quote! { #container.align_y(#alignment_expr) };
3664    }
3665
3666    // Apply styles
3667    if widget_type == "container" {
3668        container = apply_widget_style(container, node, "container", style_classes)?;
3669    }
3670
3671    // Use explicit into() conversion to help type inference with nested containers
3672    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#container) })
3673}
3674
3675/// Generate for loop widget with local variable context
3676fn generate_for_with_locals(
3677    node: &crate::WidgetNode,
3678    model_ident: &syn::Ident,
3679    message_ident: &syn::Ident,
3680    style_classes: &HashMap<String, StyleClass>,
3681    local_vars: &std::collections::HashSet<String>,
3682) -> Result<TokenStream, super::CodegenError> {
3683    // Get the 'in' attribute (collection to iterate)
3684    let in_attr = node.attributes.get("in").ok_or_else(|| {
3685        super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
3686    })?;
3687
3688    // Get the 'each' attribute (loop variable name)
3689    let var_name = node
3690        .attributes
3691        .get("each")
3692        .and_then(|attr| {
3693            if let AttributeValue::Static(s) = attr {
3694                Some(s.clone())
3695            } else {
3696                None
3697            }
3698        })
3699        .unwrap_or_else(|| "item".to_string());
3700
3701    let var_ident = format_ident!("{}", var_name);
3702
3703    // Generate the collection expression (raw, without .to_string())
3704    let collection_expr =
3705        generate_attribute_value_raw_with_locals(in_attr, model_ident, local_vars);
3706
3707    // Create new local vars set including the loop variable
3708    let mut new_local_vars = local_vars.clone();
3709    new_local_vars.insert(var_name.clone());
3710    new_local_vars.insert("index".to_string());
3711
3712    // Generate children widgets with the new local context
3713    let children: Vec<TokenStream> = node
3714        .children
3715        .iter()
3716        .map(|child| {
3717            generate_widget_with_locals(
3718                child,
3719                model_ident,
3720                message_ident,
3721                style_classes,
3722                &new_local_vars,
3723            )
3724        })
3725        .collect::<Result<_, _>>()?;
3726
3727    // Generate the for loop that builds widgets
3728    // Use explicit type annotations to help Rust's type inference
3729    Ok(quote! {
3730        {
3731            let mut widgets: Vec<Element<'_, #message_ident>> = Vec::new();
3732            for (index, #var_ident) in (#collection_expr).iter().enumerate() {
3733                let _ = index;
3734                #(
3735                    let child_widget: Element<'_, #message_ident> = #children;
3736                    widgets.push(child_widget);
3737                )*
3738            }
3739            Into::<Element<'_, #message_ident>>::into(iced::widget::column(widgets))
3740        }
3741    })
3742}
3743
3744/// Generate if widget with local variable context
3745fn generate_if_with_locals(
3746    node: &crate::WidgetNode,
3747    model_ident: &syn::Ident,
3748    message_ident: &syn::Ident,
3749    style_classes: &HashMap<String, StyleClass>,
3750    local_vars: &std::collections::HashSet<String>,
3751) -> Result<TokenStream, super::CodegenError> {
3752    let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3753        super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3754    })?;
3755
3756    let children: Vec<TokenStream> = node
3757        .children
3758        .iter()
3759        .map(|child| {
3760            generate_widget_with_locals(
3761                child,
3762                model_ident,
3763                message_ident,
3764                style_classes,
3765                local_vars,
3766            )
3767        })
3768        .collect::<Result<_, _>>()?;
3769
3770    let condition_expr =
3771        generate_attribute_value_with_locals(condition_attr, model_ident, local_vars);
3772
3773    Ok(quote! {
3774        if #condition_expr.parse::<bool>().unwrap_or(false) {
3775            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }))
3776        } else {
3777            Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![]; children }))
3778        }
3779    })
3780}
3781
3782/// Generate checkbox widget with local variable context
3783fn generate_checkbox_with_locals(
3784    node: &crate::WidgetNode,
3785    model_ident: &syn::Ident,
3786    message_ident: &syn::Ident,
3787    style_classes: &HashMap<String, StyleClass>,
3788    local_vars: &std::collections::HashSet<String>,
3789) -> Result<TokenStream, super::CodegenError> {
3790    // Get checked attribute
3791    let checked_attr = node.attributes.get("checked");
3792    let checked_expr = if let Some(attr) = checked_attr {
3793        generate_attribute_value_raw_with_locals(attr, model_ident, local_vars)
3794    } else {
3795        quote! { false }
3796    };
3797
3798    // Get on_change event
3799    let on_change = node
3800        .events
3801        .iter()
3802        .find(|e| e.event == crate::EventKind::Change);
3803
3804    let mut checkbox = quote! {
3805        iced::widget::checkbox(#checked_expr)
3806    };
3807
3808    if let Some(event) = on_change {
3809        let variant_name = to_upper_camel_case(&event.handler);
3810        let handler_ident = format_ident!("{}", variant_name);
3811
3812        let param_expr = if let Some(ref param) = event.param {
3813            let param_tokens = super::bindings::generate_expr_with_locals(&param.expr, local_vars);
3814            quote! { (#param_tokens) }
3815        } else {
3816            quote! {}
3817        };
3818
3819        checkbox = quote! {
3820            #checkbox.on_toggle(move |_| #message_ident::#handler_ident #param_expr)
3821        };
3822    }
3823
3824    // Apply size
3825    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3826        if let AttributeValue::Static(s) = attr {
3827            s.parse::<f32>().ok()
3828        } else {
3829            None
3830        }
3831    }) {
3832        checkbox = quote! { #checkbox.size(#size) };
3833    }
3834
3835    // Apply styles
3836    checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
3837
3838    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#checkbox) })
3839}
3840
3841/// Generate text_input widget with local variable context
3842fn generate_text_input_with_locals(
3843    node: &crate::WidgetNode,
3844    model_ident: &syn::Ident,
3845    message_ident: &syn::Ident,
3846    style_classes: &HashMap<String, StyleClass>,
3847    local_vars: &std::collections::HashSet<String>,
3848) -> Result<TokenStream, super::CodegenError> {
3849    // Get placeholder
3850    let placeholder = node
3851        .attributes
3852        .get("placeholder")
3853        .and_then(|attr| {
3854            if let AttributeValue::Static(s) = attr {
3855                Some(s.clone())
3856            } else {
3857                None
3858            }
3859        })
3860        .unwrap_or_default();
3861    let placeholder_lit = proc_macro2::Literal::string(&placeholder);
3862
3863    // Get value attribute
3864    let value_attr = node.attributes.get("value");
3865    let value_expr = if let Some(attr) = value_attr {
3866        generate_attribute_value_with_locals(attr, model_ident, local_vars)
3867    } else {
3868        quote! { String::new() }
3869    };
3870
3871    let on_input = node
3872        .events
3873        .iter()
3874        .find(|e| e.event == crate::EventKind::Input);
3875
3876    let on_submit = node
3877        .events
3878        .iter()
3879        .find(|e| e.event == crate::EventKind::Submit);
3880
3881    let mut text_input = quote! {
3882        iced::widget::text_input(#placeholder_lit, &#value_expr)
3883    };
3884
3885    // Apply on_input
3886    if let Some(event) = on_input {
3887        let variant_name = to_upper_camel_case(&event.handler);
3888        let handler_ident = format_ident!("{}", variant_name);
3889        text_input = quote! { #text_input.on_input(|v| #message_ident::#handler_ident(v)) };
3890    }
3891
3892    // Apply on_submit
3893    if let Some(event) = on_submit {
3894        let variant_name = to_upper_camel_case(&event.handler);
3895        let handler_ident = format_ident!("{}", variant_name);
3896        text_input = quote! { #text_input.on_submit(#message_ident::#handler_ident) };
3897    }
3898
3899    // Apply size
3900    if let Some(size) = node.attributes.get("size").and_then(|attr| {
3901        if let AttributeValue::Static(s) = attr {
3902            s.parse::<f32>().ok()
3903        } else {
3904            None
3905        }
3906    }) {
3907        text_input = quote! { #text_input.size(#size) };
3908    }
3909
3910    // Apply padding
3911    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
3912        if let AttributeValue::Static(s) = attr {
3913            s.parse::<f32>().ok()
3914        } else {
3915            None
3916        }
3917    }) {
3918        text_input = quote! { #text_input.padding(#padding) };
3919    }
3920
3921    // Apply width
3922    if let Some(width) = node.attributes.get("width").and_then(|attr| {
3923        if let AttributeValue::Static(s) = attr {
3924            Some(generate_length_expr(s))
3925        } else {
3926            None
3927        }
3928    }) {
3929        text_input = quote! { #text_input.width(#width) };
3930    }
3931
3932    // Apply styles
3933    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
3934
3935    Ok(quote! { Into::<Element<'_, #message_ident>>::into(#text_input) })
3936}
3937
3938/// Generate attribute value expression with local variable context
3939fn generate_attribute_value_with_locals(
3940    attr: &AttributeValue,
3941    _model_ident: &syn::Ident,
3942    local_vars: &std::collections::HashSet<String>,
3943) -> TokenStream {
3944    match attr {
3945        AttributeValue::Static(s) => {
3946            let lit = proc_macro2::Literal::string(s);
3947            quote! { #lit.to_string() }
3948        }
3949        AttributeValue::Binding(expr) => {
3950            super::bindings::generate_expr_with_locals(&expr.expr, local_vars)
3951        }
3952        AttributeValue::Interpolated(parts) => {
3953            let parts_str: Vec<String> = parts
3954                .iter()
3955                .map(|part| match part {
3956                    InterpolatedPart::Literal(s) => s.clone(),
3957                    InterpolatedPart::Binding(_) => "{}".to_string(),
3958                })
3959                .collect();
3960            let binding_exprs: Vec<TokenStream> = parts
3961                .iter()
3962                .filter_map(|part| {
3963                    if let InterpolatedPart::Binding(expr) = part {
3964                        Some(super::bindings::generate_expr_with_locals(
3965                            &expr.expr, local_vars,
3966                        ))
3967                    } else {
3968                        None
3969                    }
3970                })
3971                .collect();
3972
3973            let format_string = parts_str.join("");
3974            let lit = proc_macro2::Literal::string(&format_string);
3975
3976            quote! { format!(#lit, #(#binding_exprs),*) }
3977        }
3978    }
3979}
3980
3981/// Generate attribute value without `.to_string()` conversion with local variable context
3982fn generate_attribute_value_raw_with_locals(
3983    attr: &AttributeValue,
3984    _model_ident: &syn::Ident,
3985    local_vars: &std::collections::HashSet<String>,
3986) -> TokenStream {
3987    match attr {
3988        AttributeValue::Static(s) => {
3989            let lit = proc_macro2::Literal::string(s);
3990            quote! { #lit }
3991        }
3992        AttributeValue::Binding(expr) => {
3993            super::bindings::generate_bool_expr_with_locals(&expr.expr, local_vars)
3994        }
3995        AttributeValue::Interpolated(parts) => {
3996            let parts_str: Vec<String> = parts
3997                .iter()
3998                .map(|part| match part {
3999                    InterpolatedPart::Literal(s) => s.clone(),
4000                    InterpolatedPart::Binding(_) => "{}".to_string(),
4001                })
4002                .collect();
4003            let binding_exprs: Vec<TokenStream> = parts
4004                .iter()
4005                .filter_map(|part| {
4006                    if let InterpolatedPart::Binding(expr) = part {
4007                        Some(super::bindings::generate_expr_with_locals(
4008                            &expr.expr, local_vars,
4009                        ))
4010                    } else {
4011                        None
4012                    }
4013                })
4014                .collect();
4015
4016            let format_string = parts_str.join("");
4017            let lit = proc_macro2::Literal::string(&format_string);
4018
4019            quote! { format!(#lit, #(#binding_exprs),*) }
4020        }
4021    }
4022}
4023
4024fn generate_canvas_shapes(
4025    nodes: &[crate::WidgetNode],
4026    model_ident: &syn::Ident,
4027) -> Result<Vec<TokenStream>, super::CodegenError> {
4028    let mut shape_exprs = Vec::new();
4029    for node in nodes {
4030        match node.kind {
4031            WidgetKind::CanvasRect => shape_exprs.push(generate_rect_shape(node, model_ident)?),
4032            WidgetKind::CanvasCircle => shape_exprs.push(generate_circle_shape(node, model_ident)?),
4033            WidgetKind::CanvasLine => shape_exprs.push(generate_line_shape(node, model_ident)?),
4034            WidgetKind::CanvasText => shape_exprs.push(generate_text_shape(node, model_ident)?),
4035            WidgetKind::CanvasGroup => shape_exprs.push(generate_group_shape(node, model_ident)?),
4036            _ => {}
4037        }
4038    }
4039    Ok(shape_exprs)
4040}
4041
4042fn generate_rect_shape(
4043    node: &crate::WidgetNode,
4044    model_ident: &syn::Ident,
4045) -> Result<TokenStream, super::CodegenError> {
4046    let x = generate_f32_attr(node, "x", 0.0, model_ident);
4047    let y = generate_f32_attr(node, "y", 0.0, model_ident);
4048    let width = generate_f32_attr(node, "width", 0.0, model_ident);
4049    let height = generate_f32_attr(node, "height", 0.0, model_ident);
4050    let fill = generate_color_option_attr(node, "fill", model_ident);
4051    let stroke = generate_color_option_attr(node, "stroke", model_ident);
4052    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4053    let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
4054
4055    Ok(quote! {
4056        dampen_iced::canvas::CanvasShape::Rect(dampen_iced::canvas::RectShape {
4057            x: #x,
4058            y: #y,
4059            width: #width,
4060            height: #height,
4061            fill: #fill,
4062            stroke: #stroke,
4063            stroke_width: #stroke_width,
4064            radius: #radius,
4065        })
4066    })
4067}
4068
4069fn generate_circle_shape(
4070    node: &crate::WidgetNode,
4071    model_ident: &syn::Ident,
4072) -> Result<TokenStream, super::CodegenError> {
4073    let cx = generate_f32_attr(node, "cx", 0.0, model_ident);
4074    let cy = generate_f32_attr(node, "cy", 0.0, model_ident);
4075    let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
4076    let fill = generate_color_option_attr(node, "fill", model_ident);
4077    let stroke = generate_color_option_attr(node, "stroke", model_ident);
4078    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4079
4080    Ok(quote! {
4081        dampen_iced::canvas::CanvasShape::Circle(dampen_iced::canvas::CircleShape {
4082            cx: #cx,
4083            cy: #cy,
4084            radius: #radius,
4085            fill: #fill,
4086            stroke: #stroke,
4087            stroke_width: #stroke_width,
4088        })
4089    })
4090}
4091
4092fn generate_line_shape(
4093    node: &crate::WidgetNode,
4094    model_ident: &syn::Ident,
4095) -> Result<TokenStream, super::CodegenError> {
4096    let x1 = generate_f32_attr(node, "x1", 0.0, model_ident);
4097    let y1 = generate_f32_attr(node, "y1", 0.0, model_ident);
4098    let x2 = generate_f32_attr(node, "x2", 0.0, model_ident);
4099    let y2 = generate_f32_attr(node, "y2", 0.0, model_ident);
4100    let stroke = generate_color_option_attr(node, "stroke", model_ident);
4101    let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4102
4103    Ok(quote! {
4104        dampen_iced::canvas::CanvasShape::Line(dampen_iced::canvas::LineShape {
4105            x1: #x1,
4106            y1: #y1,
4107            x2: #x2,
4108            y2: #y2,
4109            stroke: #stroke,
4110            stroke_width: #stroke_width,
4111        })
4112    })
4113}
4114
4115fn generate_text_shape(
4116    node: &crate::WidgetNode,
4117    model_ident: &syn::Ident,
4118) -> Result<TokenStream, super::CodegenError> {
4119    let x = generate_f32_attr(node, "x", 0.0, model_ident);
4120    let y = generate_f32_attr(node, "y", 0.0, model_ident);
4121    let content = generate_attribute_value(
4122        node.attributes
4123            .get("content")
4124            .unwrap_or(&AttributeValue::Static(String::new())),
4125        model_ident,
4126    );
4127    let size = generate_f32_attr(node, "size", 16.0, model_ident);
4128    let color = generate_color_option_attr(node, "color", model_ident);
4129
4130    Ok(quote! {
4131        dampen_iced::canvas::CanvasShape::Text(dampen_iced::canvas::TextShape {
4132            x: #x,
4133            y: #y,
4134            content: #content,
4135            size: #size,
4136            color: #color,
4137        })
4138    })
4139}
4140
4141fn generate_group_shape(
4142    node: &crate::WidgetNode,
4143    model_ident: &syn::Ident,
4144) -> Result<TokenStream, super::CodegenError> {
4145    let children = generate_canvas_shapes(&node.children, model_ident)?;
4146    let transform = generate_transform_attr(node, model_ident);
4147
4148    Ok(quote! {
4149        dampen_iced::canvas::CanvasShape::Group(dampen_iced::canvas::GroupShape {
4150            transform: #transform,
4151            children: vec![#(#children),*],
4152        })
4153    })
4154}
4155
4156fn generate_f32_attr(
4157    node: &crate::WidgetNode,
4158    name: &str,
4159    default: f32,
4160    _model_ident: &syn::Ident,
4161) -> TokenStream {
4162    if let Some(attr) = node.attributes.get(name) {
4163        match attr {
4164            AttributeValue::Static(s) => {
4165                let val = s.parse::<f32>().unwrap_or(default);
4166                quote! { #val }
4167            }
4168            AttributeValue::Binding(expr) => {
4169                let tokens = super::bindings::generate_bool_expr(&expr.expr);
4170                quote! { (#tokens) as f32 }
4171            }
4172            AttributeValue::Interpolated(_) => quote! { #default },
4173        }
4174    } else {
4175        quote! { #default }
4176    }
4177}
4178
4179fn generate_color_option_attr(
4180    node: &crate::WidgetNode,
4181    name: &str,
4182    _model_ident: &syn::Ident,
4183) -> TokenStream {
4184    if let Some(attr) = node.attributes.get(name) {
4185        match attr {
4186            AttributeValue::Static(s) => {
4187                if let Ok(c) = crate::parser::style_parser::parse_color_attr(s) {
4188                    let r = c.r;
4189                    let g = c.g;
4190                    let b = c.b;
4191                    let a = c.a;
4192                    quote! { Some(iced::Color::from_rgba(#r, #g, #b, #a)) }
4193                } else {
4194                    quote! { None }
4195                }
4196            }
4197            AttributeValue::Binding(expr) => {
4198                let tokens = generate_expr(&expr.expr);
4199                quote! {
4200                    dampen_iced::convert::parse_color_maybe(&(#tokens).to_string())
4201                        .map(|c| iced::Color::from_rgba(c.r, c.g, c.b, c.a))
4202                }
4203            }
4204            _ => quote! { None },
4205        }
4206    } else {
4207        quote! { None }
4208    }
4209}
4210
4211fn generate_transform_attr(node: &crate::WidgetNode, _model_ident: &syn::Ident) -> TokenStream {
4212    if let Some(AttributeValue::Static(s)) = node.attributes.get("transform") {
4213        let s = s.trim();
4214        if let Some(inner) = s
4215            .strip_prefix("translate(")
4216            .and_then(|s| s.strip_suffix(")"))
4217        {
4218            let parts: Vec<f32> = inner
4219                .split(',')
4220                .filter_map(|p| p.trim().parse().ok())
4221                .collect();
4222            if parts.len() == 2 {
4223                let x = parts[0];
4224                let y = parts[1];
4225                return quote! { Some(dampen_iced::canvas::Transform::Translate(#x, #y)) };
4226            }
4227        }
4228        if let Some(inner) = s.strip_prefix("rotate(").and_then(|s| s.strip_suffix(")"))
4229            && let Ok(angle) = inner.trim().parse::<f32>()
4230        {
4231            return quote! { Some(dampen_iced::canvas::Transform::Rotate(#angle)) };
4232        }
4233        if let Some(inner) = s.strip_prefix("scale(").and_then(|s| s.strip_suffix(")")) {
4234            let parts: Vec<f32> = inner
4235                .split(',')
4236                .filter_map(|p| p.trim().parse().ok())
4237                .collect();
4238            if parts.len() == 1 {
4239                let s = parts[0];
4240                return quote! { Some(dampen_iced::canvas::Transform::Scale(#s)) };
4241            } else if parts.len() == 2 {
4242                let x = parts[0];
4243                let y = parts[1];
4244                return quote! { Some(dampen_iced::canvas::Transform::ScaleXY(#x, #y)) };
4245            }
4246        }
4247        if let Some(inner) = s.strip_prefix("matrix(").and_then(|s| s.strip_suffix(")")) {
4248            let parts: Vec<f32> = inner
4249                .split(',')
4250                .filter_map(|p| p.trim().parse().ok())
4251                .collect();
4252            if parts.len() == 6 {
4253                return quote! { Some(dampen_iced::canvas::Transform::Matrix([#(#parts),*])) };
4254            }
4255        }
4256        quote! { None }
4257    } else {
4258        quote! { None }
4259    }
4260}
4261
4262fn generate_canvas_handlers(
4263    node: &crate::WidgetNode,
4264    _model_ident: &syn::Ident,
4265    message_ident: &syn::Ident,
4266) -> Result<Option<TokenStream>, super::CodegenError> {
4267    let on_click = node
4268        .events
4269        .iter()
4270        .find(|e| e.event == crate::EventKind::CanvasClick);
4271    let on_drag = node
4272        .events
4273        .iter()
4274        .find(|e| e.event == crate::EventKind::CanvasDrag);
4275    let on_move = node
4276        .events
4277        .iter()
4278        .find(|e| e.event == crate::EventKind::CanvasMove);
4279    let on_release = node
4280        .events
4281        .iter()
4282        .find(|e| e.event == crate::EventKind::CanvasRelease);
4283
4284    if on_click.is_none() && on_drag.is_none() && on_move.is_none() && on_release.is_none() {
4285        return Ok(None);
4286    }
4287
4288    let mut match_arms = Vec::new();
4289
4290    if let Some(e) = on_click {
4291        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4292        let name = &e.handler;
4293        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4294    }
4295    if let Some(e) = on_drag {
4296        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4297        let name = &e.handler;
4298        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4299    }
4300    if let Some(e) = on_move {
4301        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4302        let name = &e.handler;
4303        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4304    }
4305    if let Some(e) = on_release {
4306        let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4307        let name = &e.handler;
4308        match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4309    }
4310
4311    let click_name = on_click.map(|e| e.handler.as_str()).unwrap_or("");
4312    let drag_name = on_drag.map(|e| e.handler.as_str()).unwrap_or("");
4313    let move_name = on_move.map(|e| e.handler.as_str()).unwrap_or("");
4314    let release_name = on_release.map(|e| e.handler.as_str()).unwrap_or("");
4315
4316    Ok(Some(quote! {
4317        dampen_iced::canvas::CanvasEventHandlers {
4318            handler_names: dampen_iced::canvas::CanvasHandlerNames {
4319                on_click: if #click_name != "" { Some(#click_name.to_string()) } else { None },
4320                on_drag: if #drag_name != "" { Some(#drag_name.to_string()) } else { None },
4321                on_move: if #move_name != "" { Some(#move_name.to_string()) } else { None },
4322                on_release: if #release_name != "" { Some(#release_name.to_string()) } else { None },
4323            },
4324            msg_factory: |name, event| {
4325                 match name {
4326                     #(#match_arms,)*
4327                     _ => panic!("Unknown canvas handler: {}", name),
4328                 }
4329            }
4330        }
4331    }))
4332}
4333
4334/// Generate Menu widget (MenuBar)
4335fn generate_menu(
4336    node: &crate::WidgetNode,
4337    model_ident: &syn::Ident,
4338    message_ident: &syn::Ident,
4339    style_classes: &HashMap<String, StyleClass>,
4340) -> Result<TokenStream, super::CodegenError> {
4341    let items = generate_menu_items(&node.children, model_ident, message_ident, style_classes)?;
4342    // TODO: Handle layout attributes via container wrapper if needed
4343    Ok(quote! {
4344        iced_aw::menu::MenuBar::new(#items).into()
4345    })
4346}
4347
4348/// Generate items for Menu/MenuBar
4349fn generate_menu_items(
4350    children: &[crate::WidgetNode],
4351    model_ident: &syn::Ident,
4352    message_ident: &syn::Ident,
4353    style_classes: &HashMap<String, StyleClass>,
4354) -> Result<TokenStream, super::CodegenError> {
4355    let mut item_exprs = Vec::new();
4356
4357    for child in children {
4358        match child.kind {
4359            WidgetKind::MenuItem => {
4360                item_exprs.push(generate_menu_item_struct(
4361                    child,
4362                    model_ident,
4363                    message_ident,
4364                    style_classes,
4365                )?);
4366            }
4367            WidgetKind::MenuSeparator => {
4368                item_exprs.push(generate_menu_separator_struct(child)?);
4369            }
4370            _ => {}
4371        }
4372    }
4373
4374    Ok(quote! {
4375        vec![#(#item_exprs),*]
4376    })
4377}
4378
4379/// Generate Item struct for MenuItem
4380fn generate_menu_item_struct(
4381    node: &crate::WidgetNode,
4382    model_ident: &syn::Ident,
4383    message_ident: &syn::Ident,
4384    style_classes: &HashMap<String, StyleClass>,
4385) -> Result<TokenStream, super::CodegenError> {
4386    let label_attr = node.attributes.get("label").ok_or_else(|| {
4387        super::CodegenError::InvalidWidget("MenuItem requires label attribute".to_string())
4388    })?;
4389
4390    let label_expr = generate_attribute_value(label_attr, model_ident);
4391
4392    // Content is a button
4393    let mut btn = quote! {
4394        iced::widget::button(iced::widget::text(#label_expr))
4395            .width(iced::Length::Shrink) // Use Shrink to avoid layout collapse in MenuBar
4396            .style(iced::widget::button::text)
4397    };
4398
4399    if let Some(event) = node
4400        .events
4401        .iter()
4402        .find(|e| e.event == crate::EventKind::Click)
4403    {
4404        let variant_name = to_upper_camel_case(&event.handler);
4405        let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4406
4407        let msg = if let Some(param) = &event.param {
4408            let param_expr = crate::codegen::bindings::generate_expr(&param.expr);
4409            quote! { #message_ident::#variant_ident(#param_expr) }
4410        } else {
4411            quote! { #message_ident::#variant_ident }
4412        };
4413
4414        btn = quote! { #btn.on_press(#msg) };
4415    }
4416
4417    let content = quote! { #btn };
4418
4419    // Check for submenu
4420    if let Some(submenu) = node.children.iter().find(|c| c.kind == WidgetKind::Menu) {
4421        let items =
4422            generate_menu_items(&submenu.children, model_ident, message_ident, style_classes)?;
4423        Ok(quote! {
4424            iced_aw::menu::Item::with_menu(#content, iced_aw::menu::Menu::new(#items))
4425        })
4426    } else {
4427        Ok(quote! {
4428            iced_aw::menu::Item::new(#content)
4429        })
4430    }
4431}
4432
4433fn generate_menu_separator_struct(
4434    _node: &crate::WidgetNode,
4435) -> Result<TokenStream, super::CodegenError> {
4436    Ok(quote! {
4437        iced_aw::menu::Item::new(iced::widget::rule::horizontal(1))
4438    })
4439}
4440
4441fn generate_context_menu(
4442    node: &crate::WidgetNode,
4443    model_ident: &syn::Ident,
4444    message_ident: &syn::Ident,
4445    style_classes: &HashMap<String, StyleClass>,
4446    local_vars: &std::collections::HashSet<String>,
4447) -> Result<TokenStream, super::CodegenError> {
4448    let underlay = node
4449        .children
4450        .first()
4451        .ok_or(super::CodegenError::InvalidWidget(
4452            "ContextMenu requires underlay".into(),
4453        ))?;
4454    let underlay_expr = generate_widget_with_locals(
4455        underlay,
4456        model_ident,
4457        message_ident,
4458        style_classes,
4459        local_vars,
4460    )?;
4461
4462    let menu_node = node
4463        .children
4464        .get(1)
4465        .ok_or(super::CodegenError::InvalidWidget(
4466            "ContextMenu requires menu".into(),
4467        ))?;
4468
4469    if menu_node.kind != WidgetKind::Menu {
4470        return Err(super::CodegenError::InvalidWidget(
4471            "Second child of ContextMenu must be <menu>".into(),
4472        ));
4473    }
4474
4475    // Generate menu content (column of buttons)
4476    let mut buttons = Vec::new();
4477    for child in &menu_node.children {
4478        match child.kind {
4479            WidgetKind::MenuItem => {
4480                let label =
4481                    child
4482                        .attributes
4483                        .get("label")
4484                        .ok_or(super::CodegenError::InvalidWidget(
4485                            "MenuItem requires label".into(),
4486                        ))?;
4487                let label_expr =
4488                    generate_attribute_value_with_locals(label, model_ident, local_vars);
4489
4490                let mut btn = quote! {
4491                    iced::widget::button(iced::widget::text(#label_expr))
4492                        .width(iced::Length::Fill)
4493                        .style(iced::widget::button::text)
4494                };
4495
4496                if let Some(event) = child
4497                    .events
4498                    .iter()
4499                    .find(|e| e.event == crate::EventKind::Click)
4500                {
4501                    let variant_name = to_upper_camel_case(&event.handler);
4502                    let variant_ident =
4503                        syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4504
4505                    let msg = if let Some(param) = &event.param {
4506                        let param_expr = crate::codegen::bindings::generate_expr(&param.expr);
4507                        quote! { #message_ident::#variant_ident(#param_expr) }
4508                    } else {
4509                        quote! { #message_ident::#variant_ident }
4510                    };
4511                    btn = quote! { #btn.on_press(#msg) };
4512                }
4513
4514                buttons.push(quote! { #btn.into() });
4515            }
4516            WidgetKind::MenuSeparator => {
4517                buttons.push(quote! { iced::widget::rule::horizontal(1).into() });
4518            }
4519            _ => {}
4520        }
4521    }
4522
4523    let overlay_content = quote! {
4524        iced::widget::container(
4525            iced::widget::column(vec![#(#buttons),*])
4526                .spacing(2)
4527        )
4528        .padding(5)
4529        .style(iced::widget::container::bordered_box)
4530        .into()
4531    };
4532
4533    Ok(quote! {
4534        iced_aw::ContextMenu::new(
4535            #underlay_expr,
4536            move || #overlay_content
4537        )
4538        .into()
4539    })
4540}
4541
4542fn generate_data_table(
4543    node: &crate::WidgetNode,
4544    model_ident: &syn::Ident,
4545    message_ident: &syn::Ident,
4546    style_classes: &HashMap<String, StyleClass>,
4547) -> Result<TokenStream, super::CodegenError> {
4548    let data_attr = node.attributes.get("data").ok_or_else(|| {
4549        super::CodegenError::InvalidWidget("data_table requires data attribute".to_string())
4550    })?;
4551    let data_expr = generate_attribute_value_raw(data_attr, model_ident);
4552
4553    let mut column_exprs = Vec::new();
4554    for child in &node.children {
4555        if child.kind == WidgetKind::DataColumn {
4556            let header_attr = child.attributes.get("header").ok_or_else(|| {
4557                super::CodegenError::InvalidWidget(
4558                    "data_column requires header attribute".to_string(),
4559                )
4560            })?;
4561            let header_expr = generate_attribute_value(header_attr, model_ident);
4562            let header = quote! { iced::widget::text(#header_expr) };
4563
4564            let field = child.attributes.get("field");
4565
4566            let view_closure = if let Some(AttributeValue::Static(field_name)) = field {
4567                let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
4568                // Assuming item is the model type or struct
4569                quote! {
4570                    |item| iced::widget::text(item.#field_ident.to_string()).into()
4571                }
4572            } else {
4573                // Template handling
4574                // Find template content
4575                let template_content = if let Some(tmpl) = child
4576                    .children
4577                    .iter()
4578                    .find(|c| matches!(c.kind, WidgetKind::Custom(ref s) if s == "template"))
4579                {
4580                    &tmpl.children
4581                } else {
4582                    &child.children
4583                };
4584
4585                if let Some(root) = template_content.first() {
4586                    let mut locals = std::collections::HashSet::new();
4587                    locals.insert("index".to_string());
4588                    locals.insert("item".to_string());
4589
4590                    let widget_expr = generate_widget_with_locals(
4591                        root,
4592                        model_ident,
4593                        message_ident,
4594                        style_classes,
4595                        &locals,
4596                    )?;
4597
4598                    quote! {
4599                        |(index, item)| {
4600                            let _ = index; // Suppress unused warning
4601                            #widget_expr.into()
4602                        }
4603                    }
4604                } else {
4605                    quote! { |(_index, _item)| iced::widget::text("").into() }
4606                }
4607            };
4608
4609            let mut col = quote! {
4610                iced::widget::table::column(#header, #view_closure)
4611            };
4612
4613            if let Some(width) = child.attributes.get("width") {
4614                let width_expr = match width {
4615                    AttributeValue::Static(s) => generate_length_expr(s),
4616                    _ => quote! { iced::Length::Fill },
4617                };
4618                col = quote! { #col.width(#width_expr) };
4619            }
4620
4621            // Apply align_x if specified
4622            if let Some(align_x) = child.attributes.get("align_x")
4623                && let AttributeValue::Static(s) = align_x
4624            {
4625                let align_expr = match s.to_lowercase().as_str() {
4626                    "center" => quote! { iced::alignment::Horizontal::Center },
4627                    "end" | "right" => quote! { iced::alignment::Horizontal::Right },
4628                    _ => quote! { iced::alignment::Horizontal::Left },
4629                };
4630                col = quote! { #col.align_x(#align_expr) };
4631            }
4632
4633            // Apply align_y if specified
4634            if let Some(align_y) = child.attributes.get("align_y")
4635                && let AttributeValue::Static(s) = align_y
4636            {
4637                let align_expr = match s.to_lowercase().as_str() {
4638                    "center" => quote! { iced::alignment::Vertical::Center },
4639                    "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
4640                    _ => quote! { iced::alignment::Vertical::Top },
4641                };
4642                col = quote! { #col.align_y(#align_expr) };
4643            }
4644
4645            column_exprs.push(col);
4646        }
4647    }
4648
4649    let table = quote! {
4650        iced::widget::table::Table::new(vec![#(#column_exprs),*], #data_expr)
4651    };
4652
4653    // Handle on_row_click
4654    // TODO: Re-enable when Table API for row clicks is identified
4655    /*
4656    if let Some(event) = node.events.iter().find(|e| e.event == crate::ir::EventKind::RowClick) {
4657        let variant_name = to_upper_camel_case(&event.handler);
4658        let handler_ident = format_ident!("{}", variant_name);
4659
4660        // Generate parameter expression
4661        let param_expr = if let Some(ref binding) = event.param {
4662            let mut locals = std::collections::HashSet::new();
4663            locals.insert("index".to_string());
4664            locals.insert("item".to_string());
4665
4666            let expr = crate::codegen::bindings::generate_expr_with_locals(
4667                &binding.expr,
4668                &locals,
4669            );
4670            quote! { (#expr) }
4671        } else {
4672            quote! {}
4673        };
4674
4675        // We assume data_expr evaluates to something indexable (Vec, slice)
4676        // We define `item` as reference to element at index
4677        table = quote! {
4678            #table.on_row_click(move |index: usize| {
4679                let item = &(#data_expr)[index];
4680                #message_ident::#handler_ident #param_expr
4681            })
4682        };
4683    }
4684    */
4685
4686    // Apply layout
4687    Ok(maybe_wrap_in_container(table, node))
4688}
4689
4690/// Generate TreeView widget code
4691fn generate_tree_view(
4692    node: &crate::WidgetNode,
4693    model_ident: &syn::Ident,
4694    message_ident: &syn::Ident,
4695    style_classes: &HashMap<String, StyleClass>,
4696    local_vars: &std::collections::HashSet<String>,
4697) -> Result<TokenStream, super::CodegenError> {
4698    // Get tree configuration from attributes
4699    let indent_size = node
4700        .attributes
4701        .get("indent_size")
4702        .and_then(|attr| match attr {
4703            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4704            _ => None,
4705        })
4706        .unwrap_or(20.0);
4707
4708    let node_height = node
4709        .attributes
4710        .get("node_height")
4711        .and_then(|attr| match attr {
4712            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4713            _ => None,
4714        })
4715        .unwrap_or(30.0);
4716
4717    let _icon_size = node
4718        .attributes
4719        .get("icon_size")
4720        .and_then(|attr| match attr {
4721            AttributeValue::Static(s) => s.parse::<f32>().ok(),
4722            _ => None,
4723        })
4724        .unwrap_or(16.0);
4725
4726    let expand_icon = node
4727        .attributes
4728        .get("expand_icon")
4729        .and_then(|attr| match attr {
4730            AttributeValue::Static(s) => Some(s.clone()),
4731            _ => None,
4732        })
4733        .unwrap_or_else(|| "â–¶".to_string());
4734
4735    let collapse_icon = node
4736        .attributes
4737        .get("collapse_icon")
4738        .and_then(|attr| match attr {
4739            AttributeValue::Static(s) => Some(s.clone()),
4740            _ => None,
4741        })
4742        .unwrap_or_else(|| "â–¼".to_string());
4743
4744    // Check if we have a nodes binding (dynamic tree) or inline children (static tree)
4745    let has_nodes_binding = node.attributes.contains_key("nodes");
4746
4747    if has_nodes_binding {
4748        // Dynamic tree from binding - generate code that builds tree at runtime
4749        let nodes_binding = node.attributes.get("nodes").ok_or_else(|| {
4750            super::CodegenError::InvalidWidget("nodes attribute is required".into())
4751        })?;
4752        let nodes_expr = generate_attribute_value_raw(nodes_binding, model_ident);
4753
4754        // Get expanded IDs binding
4755        let expanded_binding = node.attributes.get("expanded");
4756        let expanded_expr =
4757            expanded_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4758
4759        // Get selected ID binding
4760        let selected_binding = node.attributes.get("selected");
4761        let selected_expr =
4762            selected_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4763
4764        // Generate the tree view using a recursive helper function
4765        let tree_view = quote! {
4766            {
4767                let tree_nodes = #nodes_expr;
4768                let expanded_ids: std::collections::HashSet<String> = #expanded_expr
4769                    .map(|v: Vec<String>| v.into_iter().collect())
4770                    .unwrap_or_default();
4771                let selected_id: Option<String> = #selected_expr;
4772
4773                // Build tree recursively
4774                fn build_tree_nodes(
4775                    nodes: &[TreeNode],
4776                    expanded_ids: &std::collections::HashSet<String>,
4777                    selected_id: &Option<String>,
4778                    depth: usize,
4779                ) -> Vec<iced::Element<'static, #message_ident>> {
4780                    let mut elements = Vec::new();
4781                    for node in nodes {
4782                        let is_expanded = expanded_ids.contains(&node.id);
4783                        let is_selected = selected_id.as_ref() == Some(&node.id);
4784                        let has_children = !node.children.is_empty();
4785
4786                        // Build node row
4787                        let indent = (depth as f32) * #indent_size;
4788                        let node_element = build_tree_node_row(
4789                            node,
4790                            is_expanded,
4791                            is_selected,
4792                            has_children,
4793                            indent,
4794                            #node_height,
4795                            #expand_icon,
4796                            #collapse_icon,
4797                        );
4798                        elements.push(node_element);
4799
4800                        // Add children if expanded
4801                        if is_expanded && has_children {
4802                            let child_elements = build_tree_nodes(
4803                                &node.children,
4804                                expanded_ids,
4805                                selected_id,
4806                                depth + 1,
4807                            );
4808                            elements.extend(child_elements);
4809                        }
4810                    }
4811                    elements
4812                }
4813
4814                iced::widget::column(build_tree_nodes(&tree_nodes, &expanded_ids, &selected_id, 0))
4815                    .spacing(2)
4816                    .into()
4817            }
4818        };
4819
4820        Ok(tree_view)
4821    } else {
4822        // Static tree from inline XML children
4823        let tree_elements: Vec<TokenStream> = node
4824            .children
4825            .iter()
4826            .filter(|c| c.kind == WidgetKind::TreeNode)
4827            .map(|child| {
4828                generate_tree_node(
4829                    child,
4830                    model_ident,
4831                    message_ident,
4832                    style_classes,
4833                    local_vars,
4834                    indent_size,
4835                    node_height,
4836                    &expand_icon,
4837                    &collapse_icon,
4838                    0,
4839                    node,
4840                )
4841            })
4842            .collect::<Result<_, _>>()?;
4843
4844        Ok(quote! {
4845            iced::widget::column(vec![#(#tree_elements),*])
4846                .spacing(2)
4847                .into()
4848        })
4849    }
4850}
4851
4852/// Generate a single tree node element (recursive for children)
4853#[allow(clippy::too_many_arguments)]
4854fn generate_tree_node(
4855    node: &crate::WidgetNode,
4856    _model_ident: &syn::Ident,
4857    message_ident: &syn::Ident,
4858    _style_classes: &HashMap<String, StyleClass>,
4859    _local_vars: &std::collections::HashSet<String>,
4860    indent_size: f32,
4861    node_height: f32,
4862    expand_icon: &str,
4863    collapse_icon: &str,
4864    depth: usize,
4865    parent_node: &crate::WidgetNode,
4866) -> Result<TokenStream, super::CodegenError> {
4867    // T068, T069: Prevent infinite recursion during code generation
4868    if depth > 50 {
4869        return Ok(quote! {
4870            iced::widget::text("... max depth reached").size(12).into()
4871        });
4872    }
4873
4874    let id = node.id.clone().unwrap_or_else(|| "unknown".to_string());
4875
4876    let label = node
4877        .attributes
4878        .get("label")
4879        .and_then(|attr| match attr {
4880            AttributeValue::Static(s) => Some(s.clone()),
4881            _ => None,
4882        })
4883        .unwrap_or_else(|| id.clone());
4884
4885    let icon = node.attributes.get("icon").and_then(|attr| match attr {
4886        AttributeValue::Static(s) => Some(s.clone()),
4887        _ => None,
4888    });
4889
4890    let expanded = node.attributes.get("expanded").and_then(|attr| match attr {
4891        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4892        _ => None,
4893    });
4894
4895    let selected = node.attributes.get("selected").and_then(|attr| match attr {
4896        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4897        _ => None,
4898    });
4899
4900    let _disabled = node.attributes.get("disabled").and_then(|attr| match attr {
4901        AttributeValue::Static(s) => s.parse::<bool>().ok(),
4902        _ => None,
4903    });
4904
4905    let has_children = !node.children.is_empty();
4906    let is_expanded = expanded.unwrap_or(false);
4907    let is_selected = selected.unwrap_or(false);
4908
4909    let indent = (depth as f32) * indent_size;
4910
4911    // Build label text with optional icon
4912    let label_text = if let Some(ref icon_str) = icon {
4913        format!("{} {}", icon_str, label)
4914    } else {
4915        label
4916    };
4917
4918    // Generate expand/collapse button or spacer
4919    let toggle_button = if has_children {
4920        let icon = if is_expanded {
4921            collapse_icon
4922        } else {
4923            expand_icon
4924        };
4925
4926        // Check for on_toggle event handler
4927        if let Some(event) = parent_node
4928            .events
4929            .iter()
4930            .find(|e| matches!(e.event, crate::ir::node::EventKind::Toggle))
4931        {
4932            let variant_name = to_upper_camel_case(&event.handler);
4933            let handler_ident = format_ident!("{}", variant_name);
4934
4935            quote! {
4936                iced::widget::button(iced::widget::text(#icon).size(14))
4937                    .on_press(#message_ident::#handler_ident)
4938                    .width(iced::Length::Fixed(20.0))
4939                    .height(iced::Length::Fixed(#node_height))
4940            }
4941        } else {
4942            quote! {
4943                iced::widget::text(#icon).size(14)
4944            }
4945        }
4946    } else {
4947        quote! {
4948            iced::widget::container(iced::widget::text(""))
4949                .width(iced::Length::Fixed(20.0))
4950        }
4951    };
4952
4953    // Generate label element with selection handling
4954    let label_element = if let Some(event) = parent_node
4955        .events
4956        .iter()
4957        .find(|e| matches!(e.event, crate::ir::node::EventKind::Select))
4958    {
4959        let variant_name = to_upper_camel_case(&event.handler);
4960        let handler_ident = format_ident!("{}", variant_name);
4961
4962        quote! {
4963            iced::widget::button(iced::widget::text(#label_text).size(14))
4964                .on_press(#message_ident::#handler_ident)
4965                .style(|_theme: &iced::Theme, _status: iced::widget::button::Status| {
4966                    if #is_selected {
4967                        iced::widget::button::Style {
4968                            background: Some(iced::Background::Color(
4969                                iced::Color::from_rgb(0.0, 0.48, 0.8),
4970                            )),
4971                            text_color: iced::Color::WHITE,
4972                            ..Default::default()
4973                        }
4974                    } else {
4975                        iced::widget::button::Style::default()
4976                    }
4977                })
4978        }
4979    } else {
4980        quote! {
4981            iced::widget::text(#label_text).size(14)
4982        }
4983    };
4984
4985    // Build node row
4986    let node_row = quote! {
4987        iced::widget::row(vec![#toggle_button.into(), #label_element.into()])
4988            .spacing(4)
4989            .padding(iced::Padding::from([0.0, 0.0, 0.0, #indent]))
4990    };
4991
4992    // If expanded and has children, render them recursively
4993    if is_expanded && has_children {
4994        let child_elements: Vec<TokenStream> = node
4995            .children
4996            .iter()
4997            .filter(|c| c.kind == WidgetKind::TreeNode)
4998            .map(|child| {
4999                generate_tree_node(
5000                    child,
5001                    _model_ident,
5002                    message_ident,
5003                    _style_classes,
5004                    _local_vars,
5005                    indent_size,
5006                    node_height,
5007                    expand_icon,
5008                    collapse_icon,
5009                    depth + 1,
5010                    parent_node,
5011                )
5012            })
5013            .collect::<Result<_, _>>()?;
5014
5015        Ok(quote! {
5016            iced::widget::column(vec![
5017                #node_row.into(),
5018                iced::widget::column(vec![#(#child_elements),*])
5019                    .spacing(2)
5020                    .into(),
5021            ])
5022            .spacing(2)
5023        })
5024    } else {
5025        Ok(node_row)
5026    }
5027}
5028
5029#[cfg(test)]
5030mod tests {
5031    use super::*;
5032    use crate::parse;
5033
5034    #[test]
5035    fn test_view_generation() {
5036        let xml = r#"<column><text value="Hello" /></column>"#;
5037        let doc = parse(xml).unwrap();
5038
5039        let result = generate_view(&doc, "Model", "Message").unwrap();
5040        let code = result.to_string();
5041
5042        assert!(code.contains("text"));
5043        assert!(code.contains("column"));
5044    }
5045
5046    #[test]
5047    fn test_view_generation_with_binding() {
5048        let xml = r#"<column><text value="{name}" /></column>"#;
5049        let doc = parse(xml).unwrap();
5050
5051        let result = generate_view(&doc, "Model", "Message").unwrap();
5052        let code = result.to_string();
5053
5054        assert!(code.contains("name"));
5055        assert!(code.contains("to_string"));
5056    }
5057
5058    #[test]
5059    fn test_button_with_handler() {
5060        let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
5061        let doc = parse(xml).unwrap();
5062
5063        let result = generate_view(&doc, "Model", "Message").unwrap();
5064        let code = result.to_string();
5065
5066        assert!(code.contains("button"));
5067        assert!(code.contains("HandleClick"));
5068    }
5069
5070    #[test]
5071    fn test_container_with_children() {
5072        let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
5073        let doc = parse(xml).unwrap();
5074
5075        let result = generate_view(&doc, "Model", "Message").unwrap();
5076        let code = result.to_string();
5077
5078        assert!(code.contains("column"));
5079        assert!(code.contains("spacing"));
5080    }
5081
5082    #[test]
5083    fn test_button_with_inline_style() {
5084        use crate::ir::node::WidgetNode;
5085        use crate::ir::style::{Background, Color, StyleProperties};
5086        use std::collections::HashMap;
5087
5088        // Manually construct a button with inline style
5089        let button_node = WidgetNode {
5090            kind: WidgetKind::Button,
5091            id: None,
5092            attributes: {
5093                let mut attrs = HashMap::new();
5094                attrs.insert(
5095                    "label".to_string(),
5096                    AttributeValue::Static("Test".to_string()),
5097                );
5098                attrs
5099            },
5100            events: vec![],
5101            children: vec![],
5102            span: Default::default(),
5103            style: Some(StyleProperties {
5104                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
5105                color: Some(Color::from_rgb8(255, 255, 255)),
5106                border: None,
5107                shadow: None,
5108                opacity: None,
5109                transform: None,
5110            }),
5111            layout: None,
5112            theme_ref: None,
5113            classes: vec![],
5114            breakpoint_attributes: HashMap::new(),
5115            inline_state_variants: HashMap::new(),
5116        };
5117
5118        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5119        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5120        let style_classes = HashMap::new();
5121
5122        let result =
5123            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
5124        let code = result.to_string();
5125
5126        // Should contain style closure (note: quote! adds spaces)
5127        assert!(code.contains("style"));
5128        assert!(code.contains("button :: Status"));
5129        assert!(code.contains("button :: Style"));
5130        assert!(code.contains("background"));
5131        assert!(code.contains("text_color"));
5132    }
5133
5134    #[test]
5135    fn test_button_with_css_class() {
5136        use crate::ir::node::WidgetNode;
5137        use crate::ir::theme::StyleClass;
5138        use std::collections::HashMap;
5139
5140        // Manually construct a button with CSS class
5141        let button_node = WidgetNode {
5142            kind: WidgetKind::Button,
5143            id: None,
5144            attributes: {
5145                let mut attrs = HashMap::new();
5146                attrs.insert(
5147                    "label".to_string(),
5148                    AttributeValue::Static("Test".to_string()),
5149                );
5150                attrs
5151            },
5152            events: vec![],
5153            children: vec![],
5154            span: Default::default(),
5155            style: None,
5156            layout: None,
5157            theme_ref: None,
5158            classes: vec!["primary-button".to_string()],
5159            breakpoint_attributes: HashMap::new(),
5160            inline_state_variants: HashMap::new(),
5161        };
5162
5163        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5164        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5165        let style_classes: HashMap<String, StyleClass> = HashMap::new();
5166
5167        let result =
5168            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
5169        let code = result.to_string();
5170
5171        // Should call style function (note: quote! adds spaces)
5172        assert!(code.contains("style"));
5173        assert!(code.contains("style_primary_button"));
5174    }
5175
5176    #[test]
5177    fn test_container_with_inline_style() {
5178        use crate::ir::node::WidgetNode;
5179        use crate::ir::style::{
5180            Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
5181        };
5182        use crate::ir::theme::StyleClass;
5183        use std::collections::HashMap;
5184
5185        let container_node = WidgetNode {
5186            kind: WidgetKind::Container,
5187            id: None,
5188            attributes: HashMap::new(),
5189            events: vec![],
5190            children: vec![],
5191            span: Default::default(),
5192            style: Some(StyleProperties {
5193                background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
5194                color: None,
5195                border: Some(Border {
5196                    width: 2.0,
5197                    color: Color::from_rgb8(200, 200, 200),
5198                    radius: BorderRadius {
5199                        top_left: 8.0,
5200                        top_right: 8.0,
5201                        bottom_right: 8.0,
5202                        bottom_left: 8.0,
5203                    },
5204                    style: BorderStyle::Solid,
5205                }),
5206                shadow: None,
5207                opacity: None,
5208                transform: None,
5209            }),
5210            layout: None,
5211            theme_ref: None,
5212            classes: vec![],
5213            breakpoint_attributes: HashMap::new(),
5214            inline_state_variants: HashMap::new(),
5215        };
5216
5217        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5218        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5219        let style_classes: HashMap<String, StyleClass> = HashMap::new();
5220
5221        let result = generate_container(
5222            &container_node,
5223            "container",
5224            &model_ident,
5225            &message_ident,
5226            &style_classes,
5227        )
5228        .unwrap();
5229        let code = result.to_string();
5230
5231        // Should contain style closure (note: quote! adds spaces)
5232        assert!(code.contains("style"));
5233        assert!(code.contains("container :: Style"));
5234        assert!(code.contains("background"));
5235        assert!(code.contains("border"));
5236    }
5237}
5238
5239/// Generate TabBar widget code with content
5240fn generate_tab_bar_with_locals(
5241    node: &crate::WidgetNode,
5242    model_ident: &syn::Ident,
5243    message_ident: &syn::Ident,
5244    style_classes: &HashMap<String, StyleClass>,
5245    local_vars: &std::collections::HashSet<String>,
5246) -> Result<TokenStream, super::CodegenError> {
5247    use proc_macro2::Span;
5248    use quote::quote;
5249
5250    // Get selected index attribute
5251    let selected_attr = node.attributes.get("selected").ok_or_else(|| {
5252        super::CodegenError::InvalidWidget("TabBar requires 'selected' attribute".to_string())
5253    })?;
5254
5255    // Generate selected index expression
5256    let selected_expr = match selected_attr {
5257        AttributeValue::Static(s) => {
5258            let idx: usize = s.parse().map_err(|_| {
5259                super::CodegenError::InvalidWidget(format!("Invalid selected index: {}", s))
5260            })?;
5261            quote! { #idx }
5262        }
5263        AttributeValue::Binding(binding) => {
5264            // Generate binding expression - generate_expr returns a TokenStream that produces a String
5265            let binding_expr = generate_expr(&binding.expr);
5266            quote! { (#binding_expr).parse::<usize>().unwrap_or(0) }
5267        }
5268        _ => quote! { 0usize },
5269    };
5270
5271    // Find on_select event handler
5272    let on_select_handler = node
5273        .events
5274        .iter()
5275        .find(|e| matches!(e.event, crate::ir::EventKind::Select))
5276        .map(|e| syn::Ident::new(&e.handler, Span::call_site()));
5277
5278    // Generate tab labels and content
5279    let _tab_count = node.children.len();
5280    let tab_labels: Vec<_> = node
5281        .children
5282        .iter()
5283        .enumerate()
5284        .map(|(idx, child)| {
5285            let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5286
5287            // Get label from tab
5288            let label_expr = if let Some(label_attr) = child.attributes.get("label") {
5289                match label_attr {
5290                    AttributeValue::Static(s) => Some(quote! { #s.to_string() }),
5291                    _ => None,
5292                }
5293            } else {
5294                None
5295            };
5296
5297            // Get icon from tab
5298            let icon_expr = if let Some(icon_attr) = child.attributes.get("icon") {
5299                match icon_attr {
5300                    AttributeValue::Static(s) => {
5301                        let icon_char = resolve_icon_for_codegen(s);
5302                        Some(quote! { #icon_char })
5303                    }
5304                    _ => None,
5305                }
5306            } else {
5307                None
5308            };
5309
5310            // Build TabLabel expression based on what we have
5311            let tab_label_expr = match (icon_expr, label_expr) {
5312                (Some(icon), Some(label)) => {
5313                    quote! { iced_aw::tab_bar::TabLabel::IconText(#icon, #label) }
5314                }
5315                (Some(icon), None) => {
5316                    quote! { iced_aw::tab_bar::TabLabel::Icon(#icon) }
5317                }
5318                (None, Some(label)) => {
5319                    quote! { iced_aw::tab_bar::TabLabel::Text(#label) }
5320                }
5321                (None, None) => {
5322                    quote! { iced_aw::tab_bar::TabLabel::Text("Tab".to_string()) }
5323                }
5324            };
5325
5326            quote! {
5327                tab_bar = tab_bar.push(#idx_lit, #tab_label_expr);
5328            }
5329        })
5330        .collect();
5331
5332    // Generate content for each tab
5333    let tab_content_arms: Vec<_> = node
5334        .children
5335        .iter()
5336        .enumerate()
5337        .map(|(idx, child)| {
5338            let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5339
5340            // Generate content for this tab's children
5341            let content_widgets: Vec<_> = child
5342                .children
5343                .iter()
5344                .map(|child_node| {
5345                    generate_widget_with_locals(
5346                        child_node,
5347                        model_ident,
5348                        message_ident,
5349                        style_classes,
5350                        local_vars,
5351                    )
5352                })
5353                .collect::<Result<Vec<_>, _>>()?;
5354
5355            Ok::<_, super::CodegenError>(quote! {
5356                #idx_lit => iced::widget::column(vec![#(#content_widgets),*]).into()
5357            })
5358        })
5359        .collect::<Result<Vec<_>, super::CodegenError>>()?;
5360
5361    // Generate on_select callback if handler exists
5362    let on_select_expr = if let Some(handler) = on_select_handler {
5363        quote! {
5364            .on_select(|idx| #message_ident::#handler(idx))
5365        }
5366    } else {
5367        quote! {}
5368    };
5369
5370    // Generate icon_size if specified
5371    let icon_size_expr = if let Some(icon_size_attr) = node.attributes.get("icon_size") {
5372        match icon_size_attr {
5373            AttributeValue::Static(s) => {
5374                if let Ok(icon_size) = s.parse::<f32>() {
5375                    Some(quote! { .icon_size(#icon_size) })
5376                } else {
5377                    None
5378                }
5379            }
5380            _ => None,
5381        }
5382    } else {
5383        None
5384    };
5385
5386    // Generate text_size if specified
5387    let text_size_expr = if let Some(text_size_attr) = node.attributes.get("text_size") {
5388        match text_size_attr {
5389            AttributeValue::Static(s) => {
5390                if let Ok(text_size) = s.parse::<f32>() {
5391                    Some(quote! { .text_size(#text_size) })
5392                } else {
5393                    None
5394                }
5395            }
5396            _ => None,
5397        }
5398    } else {
5399        None
5400    };
5401
5402    // Build the complete TabBar widget with content
5403    let tab_bar_widget = quote! {
5404        {
5405            let mut tab_bar = iced_aw::TabBar::new(#selected_expr)
5406                #on_select_expr
5407                #icon_size_expr
5408                #text_size_expr;
5409
5410            #(#tab_labels)*
5411
5412            tab_bar
5413        }
5414    };
5415
5416    // Build content element using match on selected index
5417    let content_element = if tab_content_arms.is_empty() {
5418        quote! { iced::widget::column(vec![]).into() }
5419    } else {
5420        quote! {
5421            match #selected_expr {
5422                #(#tab_content_arms,)*
5423                _ => iced::widget::column(vec![]).into(),
5424            }
5425        }
5426    };
5427
5428    // Combine TabBar and content in a column
5429    let result = quote! {
5430        iced::widget::column![
5431            #tab_bar_widget,
5432            #content_element
5433        ]
5434    };
5435
5436    Ok(result)
5437}
5438
5439/// Resolve icon name to Unicode character for codegen
5440fn resolve_icon_for_codegen(name: &str) -> char {
5441    match name {
5442        "home" => '\u{F015}',
5443        "settings" => '\u{F013}',
5444        "user" => '\u{F007}',
5445        "search" => '\u{F002}',
5446        "add" => '\u{F067}',
5447        "delete" => '\u{F1F8}',
5448        "edit" => '\u{F044}',
5449        "save" => '\u{F0C7}',
5450        "close" => '\u{F00D}',
5451        "back" => '\u{F060}',
5452        "forward" => '\u{F061}',
5453        _ => '\u{F111}', // Circle as fallback
5454    }
5455}