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