dampen_core/codegen/
view.rs

1//! View function generation
2//!
3//! This module generates static Rust code for widget trees with inlined bindings.
4
5use crate::DampenDocument;
6use crate::codegen::bindings::generate_expr;
7use crate::ir::layout::{LayoutConstraints, Length as LayoutLength};
8use crate::ir::node::{AttributeValue, InterpolatedPart, WidgetKind};
9use crate::ir::style::{
10    Background, Border, BorderRadius, Color, Gradient, Shadow, StyleProperties,
11};
12use crate::ir::theme::StyleClass;
13use proc_macro2::TokenStream;
14use quote::{format_ident, quote};
15use std::collections::HashMap;
16
17/// Generate the view function body from a Dampen document
18pub fn generate_view(
19    document: &DampenDocument,
20    _model_name: &str,
21    message_name: &str,
22) -> Result<TokenStream, super::CodegenError> {
23    let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
24    let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
25
26    let root_widget = generate_widget(
27        &document.root,
28        &model_ident,
29        &message_ident,
30        &document.style_classes,
31    )?;
32
33    Ok(quote! {
34        #root_widget
35    })
36}
37
38/// Get merged layout constraints from node.layout and style classes
39fn get_merged_layout<'a>(
40    node: &'a crate::WidgetNode,
41    style_classes: &'a HashMap<String, StyleClass>,
42) -> Option<MergedLayout<'a>> {
43    // Priority: node.layout > style_class.layout
44    let node_layout = node.layout.as_ref();
45    let class_layout = node
46        .classes
47        .first()
48        .and_then(|class_name| style_classes.get(class_name))
49        .and_then(|class| class.layout.as_ref());
50
51    if node_layout.is_some() || class_layout.is_some() {
52        Some(MergedLayout {
53            node_layout,
54            class_layout,
55        })
56    } else {
57        None
58    }
59}
60
61/// Helper struct to hold merged layout info from node and style class
62struct MergedLayout<'a> {
63    node_layout: Option<&'a LayoutConstraints>,
64    class_layout: Option<&'a LayoutConstraints>,
65}
66
67impl<'a> MergedLayout<'a> {
68    fn padding(&self) -> Option<f32> {
69        self.node_layout
70            .and_then(|l| l.padding.as_ref())
71            .map(|p| p.top)
72            .or_else(|| {
73                self.class_layout
74                    .and_then(|l| l.padding.as_ref())
75                    .map(|p| p.top)
76            })
77    }
78
79    fn spacing(&self) -> Option<f32> {
80        self.node_layout
81            .and_then(|l| l.spacing)
82            .or_else(|| self.class_layout.and_then(|l| l.spacing))
83    }
84
85    fn width(&self) -> Option<&'a LayoutLength> {
86        self.node_layout
87            .and_then(|l| l.width.as_ref())
88            .or_else(|| self.class_layout.and_then(|l| l.width.as_ref()))
89    }
90
91    fn height(&self) -> Option<&'a LayoutLength> {
92        self.node_layout
93            .and_then(|l| l.height.as_ref())
94            .or_else(|| self.class_layout.and_then(|l| l.height.as_ref()))
95    }
96}
97
98/// Generate code for a widget node
99fn generate_widget(
100    node: &crate::WidgetNode,
101    model_ident: &syn::Ident,
102    message_ident: &syn::Ident,
103    style_classes: &HashMap<String, StyleClass>,
104) -> Result<TokenStream, super::CodegenError> {
105    match node.kind {
106        WidgetKind::Text => generate_text(node, model_ident, style_classes),
107        WidgetKind::Button => generate_button(node, model_ident, message_ident, style_classes),
108        WidgetKind::Column => {
109            generate_container(node, "column", model_ident, message_ident, style_classes)
110        }
111        WidgetKind::Row => {
112            generate_container(node, "row", model_ident, message_ident, style_classes)
113        }
114        WidgetKind::Container => {
115            generate_container(node, "container", model_ident, message_ident, style_classes)
116        }
117        WidgetKind::Scrollable => generate_container(
118            node,
119            "scrollable",
120            model_ident,
121            message_ident,
122            style_classes,
123        ),
124        WidgetKind::Stack => generate_stack(node, model_ident, message_ident, style_classes),
125        WidgetKind::Space => generate_space(node),
126        WidgetKind::Rule => generate_rule(node),
127        WidgetKind::Checkbox => generate_checkbox(node, model_ident, message_ident, style_classes),
128        WidgetKind::Toggler => generate_toggler(node, model_ident, message_ident, style_classes),
129        WidgetKind::Slider => generate_slider(node, model_ident, message_ident, style_classes),
130        WidgetKind::Radio => generate_radio(node, model_ident, message_ident, style_classes),
131        WidgetKind::ProgressBar => generate_progress_bar(node, model_ident, style_classes),
132        WidgetKind::TextInput => {
133            generate_text_input(node, model_ident, message_ident, style_classes)
134        }
135        WidgetKind::Image => generate_image(node),
136        WidgetKind::Svg => generate_svg(node),
137        WidgetKind::PickList => generate_pick_list(node, model_ident, message_ident, style_classes),
138        WidgetKind::ComboBox => generate_combo_box(node, model_ident, message_ident, style_classes),
139        WidgetKind::Tooltip => generate_tooltip(node, model_ident, message_ident, style_classes),
140        WidgetKind::Grid => generate_grid(node, model_ident, message_ident, style_classes),
141        WidgetKind::Canvas => generate_canvas(node, model_ident, message_ident, style_classes),
142        WidgetKind::Float => generate_float(node, model_ident, message_ident, style_classes),
143        WidgetKind::For => generate_for(node, model_ident, message_ident, style_classes),
144        WidgetKind::Custom(ref name) => {
145            generate_custom_widget(node, name, model_ident, message_ident, style_classes)
146        }
147    }
148}
149
150// ============================================================================
151// Style Application Functions
152// ============================================================================
153
154/// Apply inline styles or CSS classes to a widget
155///
156/// Priority order:
157/// 1. Inline styles (node.style) - highest priority
158/// 2. CSS classes (node.classes) - medium priority
159/// 3. Default Iced styles - lowest priority (fallback)
160fn apply_widget_style(
161    widget: TokenStream,
162    node: &crate::WidgetNode,
163    widget_type: &str,
164    style_classes: &HashMap<String, StyleClass>,
165) -> Result<TokenStream, super::CodegenError> {
166    // Check if widget has any styling
167    let has_inline_style = node.style.is_some();
168    let has_classes = !node.classes.is_empty();
169
170    if !has_inline_style && !has_classes {
171        // No styling needed, return widget as-is
172        return Ok(widget);
173    }
174
175    // Get style class if widget has classes
176    let style_class = if let Some(class_name) = node.classes.first() {
177        style_classes.get(class_name)
178    } else {
179        None
180    };
181
182    // Generate style closure based on priority
183    if let Some(ref style_props) = node.style {
184        // Priority 1: Inline styles
185        let style_closure =
186            generate_inline_style_closure(style_props, widget_type, &node.kind, style_class)?;
187        Ok(quote! {
188            #widget.style(#style_closure)
189        })
190    } else if let Some(class_name) = node.classes.first() {
191        // Priority 2: CSS class (use first class for now)
192        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
193        Ok(quote! {
194            #widget.style(#style_fn_ident)
195        })
196    } else {
197        Ok(widget)
198    }
199}
200
201/// Generate state-specific style application code
202///
203/// Creates a match expression that applies different styles based on widget state.
204/// Merges base style with state-specific overrides.
205///
206/// # Arguments
207/// * `base_style` - The base style struct (used when state is None)
208/// * `style_class` - The style class containing state variants
209/// * `widget_state_ident` - Identifier for the widget_state variable
210/// * `style_struct_fn` - Function to generate style struct from StyleProperties
211fn generate_state_style_match(
212    base_style: TokenStream,
213    style_class: &StyleClass,
214    widget_state_ident: &syn::Ident,
215    style_struct_fn: fn(&StyleProperties) -> Result<TokenStream, super::CodegenError>,
216) -> Result<TokenStream, super::CodegenError> {
217    use crate::ir::theme::WidgetState;
218
219    // Collect all state variants
220    let mut state_arms = Vec::new();
221
222    for (state, state_props) in &style_class.state_variants {
223        let state_variant = match state {
224            WidgetState::Hover => quote! { dampen_core::ir::WidgetState::Hover },
225            WidgetState::Focus => quote! { dampen_core::ir::WidgetState::Focus },
226            WidgetState::Active => quote! { dampen_core::ir::WidgetState::Active },
227            WidgetState::Disabled => quote! { dampen_core::ir::WidgetState::Disabled },
228        };
229
230        // Generate style struct for this state
231        let state_style = style_struct_fn(state_props)?;
232
233        state_arms.push(quote! {
234            Some(#state_variant) => #state_style
235        });
236    }
237
238    // Generate match expression
239    Ok(quote! {
240        match #widget_state_ident {
241            #(#state_arms,)*
242            None => #base_style
243        }
244    })
245}
246
247/// Generate inline style closure for a widget
248///
249/// Creates a closure like: |_theme: &iced::Theme, status| { ... }
250///
251/// # State-Aware Styling
252///
253/// When a widget has state variants (hover, focus, etc.), this generates
254/// code that maps the status parameter to WidgetState and applies the
255/// appropriate style.
256///
257/// # Arguments
258/// * `style_props` - Base style properties for the widget
259/// * `widget_type` - Type of widget ("button", "text_input", etc.)
260/// * `widget_kind` - The WidgetKind enum (needed for status mapping)
261/// * `style_class` - Optional style class with state variants
262fn generate_inline_style_closure(
263    style_props: &StyleProperties,
264    widget_type: &str,
265    widget_kind: &WidgetKind,
266    style_class: Option<&StyleClass>,
267) -> Result<TokenStream, super::CodegenError> {
268    // Check if we have state-specific styling
269    let has_state_variants = style_class
270        .map(|sc| !sc.state_variants.is_empty())
271        .unwrap_or(false);
272
273    match widget_type {
274        "button" => {
275            let base_style = generate_button_style_struct(style_props)?;
276
277            if has_state_variants {
278                // Generate state-aware closure
279                let status_ident = format_ident!("status");
280                if let Some(status_mapping) =
281                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
282                {
283                    let widget_state_ident = format_ident!("widget_state");
284                    // Safe: has_state_variants guarantees style_class.is_some()
285                    let class = style_class.ok_or_else(|| {
286                        super::CodegenError::InvalidWidget(
287                            "Expected style class with state variants".to_string(),
288                        )
289                    })?;
290                    let style_match = generate_state_style_match(
291                        base_style,
292                        class,
293                        &widget_state_ident,
294                        generate_button_style_struct,
295                    )?;
296
297                    Ok(quote! {
298                        |_theme: &iced::Theme, #status_ident: iced::widget::button::Status| {
299                            // Map Iced status to WidgetState
300                            let #widget_state_ident = #status_mapping;
301
302                            // Apply state-specific styling
303                            #style_match
304                        }
305                    })
306                } else {
307                    // Widget kind doesn't support status mapping, fall back to simple closure
308                    Ok(quote! {
309                        |_theme: &iced::Theme, _status: iced::widget::button::Status| {
310                            #base_style
311                        }
312                    })
313                }
314            } else {
315                // No state variants, use simple closure
316                Ok(quote! {
317                    |_theme: &iced::Theme, _status: iced::widget::button::Status| {
318                        #base_style
319                    }
320                })
321            }
322        }
323        "container" => {
324            let style_struct = generate_container_style_struct(style_props)?;
325            Ok(quote! {
326                |_theme: &iced::Theme| {
327                    #style_struct
328                }
329            })
330        }
331        "text_input" => {
332            let base_style = generate_text_input_style_struct(style_props)?;
333
334            if has_state_variants {
335                // Generate state-aware closure
336                let status_ident = format_ident!("status");
337                if let Some(status_mapping) =
338                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
339                {
340                    let widget_state_ident = format_ident!("widget_state");
341                    let class = style_class.ok_or_else(|| {
342                        super::CodegenError::InvalidWidget(
343                            "Expected style class with state variants".to_string(),
344                        )
345                    })?;
346                    let style_match = generate_state_style_match(
347                        base_style,
348                        class,
349                        &widget_state_ident,
350                        generate_text_input_style_struct,
351                    )?;
352
353                    Ok(quote! {
354                        |_theme: &iced::Theme, #status_ident: iced::widget::text_input::Status| {
355                            // Map Iced status to WidgetState
356                            let #widget_state_ident = #status_mapping;
357
358                            // Apply state-specific styling
359                            #style_match
360                        }
361                    })
362                } else {
363                    // Widget kind doesn't support status mapping, fall back to simple closure
364                    Ok(quote! {
365                        |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
366                            #base_style
367                        }
368                    })
369                }
370            } else {
371                // No state variants, use simple closure
372                Ok(quote! {
373                    |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
374                        #base_style
375                    }
376                })
377            }
378        }
379        "checkbox" => {
380            let base_style = generate_checkbox_style_struct(style_props)?;
381
382            if has_state_variants {
383                let status_ident = format_ident!("status");
384                if let Some(status_mapping) =
385                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
386                {
387                    let widget_state_ident = format_ident!("widget_state");
388                    let class = style_class.ok_or_else(|| {
389                        super::CodegenError::InvalidWidget(
390                            "Expected style class with state variants".to_string(),
391                        )
392                    })?;
393                    let style_match = generate_state_style_match(
394                        base_style,
395                        class,
396                        &widget_state_ident,
397                        generate_checkbox_style_struct,
398                    )?;
399
400                    Ok(quote! {
401                        |_theme: &iced::Theme, #status_ident: iced::widget::checkbox::Status| {
402                            let #widget_state_ident = #status_mapping;
403                            #style_match
404                        }
405                    })
406                } else {
407                    Ok(quote! {
408                        |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
409                            #base_style
410                        }
411                    })
412                }
413            } else {
414                Ok(quote! {
415                    |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
416                        #base_style
417                    }
418                })
419            }
420        }
421        "toggler" => {
422            let base_style = generate_toggler_style_struct(style_props)?;
423
424            if has_state_variants {
425                let status_ident = format_ident!("status");
426                if let Some(status_mapping) =
427                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
428                {
429                    let widget_state_ident = format_ident!("widget_state");
430                    let class = style_class.ok_or_else(|| {
431                        super::CodegenError::InvalidWidget(
432                            "Expected style class with state variants".to_string(),
433                        )
434                    })?;
435                    let style_match = generate_state_style_match(
436                        base_style,
437                        class,
438                        &widget_state_ident,
439                        generate_toggler_style_struct,
440                    )?;
441
442                    Ok(quote! {
443                        |_theme: &iced::Theme, #status_ident: iced::widget::toggler::Status| {
444                            let #widget_state_ident = #status_mapping;
445                            #style_match
446                        }
447                    })
448                } else {
449                    Ok(quote! {
450                        |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
451                            #base_style
452                        }
453                    })
454                }
455            } else {
456                Ok(quote! {
457                    |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
458                        #base_style
459                    }
460                })
461            }
462        }
463        "slider" => {
464            let base_style = generate_slider_style_struct(style_props)?;
465
466            if has_state_variants {
467                let status_ident = format_ident!("status");
468                if let Some(status_mapping) =
469                    super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
470                {
471                    let widget_state_ident = format_ident!("widget_state");
472                    let class = style_class.ok_or_else(|| {
473                        super::CodegenError::InvalidWidget(
474                            "Expected style class with state variants".to_string(),
475                        )
476                    })?;
477                    let style_match = generate_state_style_match(
478                        base_style,
479                        class,
480                        &widget_state_ident,
481                        generate_slider_style_struct,
482                    )?;
483
484                    Ok(quote! {
485                        |_theme: &iced::Theme, #status_ident: iced::widget::slider::Status| {
486                            let #widget_state_ident = #status_mapping;
487                            #style_match
488                        }
489                    })
490                } else {
491                    Ok(quote! {
492                        |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
493                            #base_style
494                        }
495                    })
496                }
497            } else {
498                Ok(quote! {
499                    |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
500                        #base_style
501                    }
502                })
503            }
504        }
505        _ => {
506            // For unsupported widgets, return a no-op closure
507            Ok(quote! {
508                |_theme: &iced::Theme| iced::widget::container::Style::default()
509            })
510        }
511    }
512}
513
514// ============================================================================
515// Helper Functions (copied from theme.rs for reuse)
516// ============================================================================
517
518/// Generate iced::Color from Color IR
519fn generate_color_expr(color: &Color) -> TokenStream {
520    let r = color.r;
521    let g = color.g;
522    let b = color.b;
523    let a = color.a;
524    quote! {
525        iced::Color::from_rgba(#r, #g, #b, #a)
526    }
527}
528
529/// Generate iced::Background from Background IR
530fn generate_background_expr(bg: &Background) -> TokenStream {
531    match bg {
532        Background::Color(color) => {
533            let color_expr = generate_color_expr(color);
534            quote! { iced::Background::Color(#color_expr) }
535        }
536        Background::Gradient(gradient) => generate_gradient_expr(gradient),
537        Background::Image { .. } => {
538            quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
539        }
540    }
541}
542
543/// Generate iced::Gradient from Gradient IR
544fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
545    match gradient {
546        Gradient::Linear { angle, stops } => {
547            let radians = angle * (std::f32::consts::PI / 180.0);
548            let color_exprs: Vec<_> = stops
549                .iter()
550                .map(|s| generate_color_expr(&s.color))
551                .collect();
552            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
553
554            quote! {
555                iced::Background::Gradient(iced::Gradient::Linear(
556                    iced::gradient::Linear::new(#radians)
557                        #(.add_stop(#offsets, #color_exprs))*
558                ))
559            }
560        }
561        Gradient::Radial { stops, .. } => {
562            // Fallback to linear for radial (Iced limitation)
563            let color_exprs: Vec<_> = stops
564                .iter()
565                .map(|s| generate_color_expr(&s.color))
566                .collect();
567            let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
568
569            quote! {
570                iced::Background::Gradient(iced::Gradient::Linear(
571                    iced::gradient::Linear::new(0.0)
572                        #(.add_stop(#offsets, #color_exprs))*
573                ))
574            }
575        }
576    }
577}
578
579/// Generate iced::Border from Border IR
580fn generate_border_expr(border: &Border) -> TokenStream {
581    let width = border.width;
582    let color_expr = generate_color_expr(&border.color);
583    let radius_expr = generate_border_radius_expr(&border.radius);
584
585    quote! {
586        iced::Border {
587            width: #width,
588            color: #color_expr,
589            radius: #radius_expr,
590        }
591    }
592}
593
594/// Generate iced::border::Radius from BorderRadius IR
595fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
596    let tl = radius.top_left;
597    let tr = radius.top_right;
598    let br = radius.bottom_right;
599    let bl = radius.bottom_left;
600
601    quote! {
602        iced::border::Radius::from(#tl).top_right(#tr).bottom_right(#br).bottom_left(#bl)
603    }
604}
605
606/// Generate iced::Shadow from Shadow IR
607fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
608    let offset_x = shadow.offset_x;
609    let offset_y = shadow.offset_y;
610    let blur = shadow.blur_radius;
611    let color_expr = generate_color_expr(&shadow.color);
612
613    quote! {
614        iced::Shadow {
615            offset: iced::Vector::new(#offset_x, #offset_y),
616            blur_radius: #blur,
617            color: #color_expr,
618        }
619    }
620}
621
622/// Generate iced::widget::button::Style struct from StyleProperties
623fn generate_button_style_struct(
624    props: &StyleProperties,
625) -> Result<TokenStream, super::CodegenError> {
626    let background_expr = props
627        .background
628        .as_ref()
629        .map(|bg| {
630            let expr = generate_background_expr(bg);
631            quote! { Some(#expr) }
632        })
633        .unwrap_or_else(|| quote! { None });
634
635    let text_color_expr = props
636        .color
637        .as_ref()
638        .map(generate_color_expr)
639        .unwrap_or_else(|| quote! { iced::Color::BLACK });
640
641    let border_expr = props
642        .border
643        .as_ref()
644        .map(generate_border_expr)
645        .unwrap_or_else(|| quote! { iced::Border::default() });
646
647    let shadow_expr = props
648        .shadow
649        .as_ref()
650        .map(generate_shadow_expr)
651        .unwrap_or_else(|| quote! { iced::Shadow::default() });
652
653    Ok(quote! {
654        iced::widget::button::Style {
655            background: #background_expr,
656            text_color: #text_color_expr,
657            border: #border_expr,
658            shadow: #shadow_expr,
659            snap: false,
660        }
661    })
662}
663
664/// Generate iced::widget::container::Style struct from StyleProperties
665fn generate_container_style_struct(
666    props: &StyleProperties,
667) -> Result<TokenStream, super::CodegenError> {
668    let background_expr = props
669        .background
670        .as_ref()
671        .map(|bg| {
672            let expr = generate_background_expr(bg);
673            quote! { Some(#expr) }
674        })
675        .unwrap_or_else(|| quote! { None });
676
677    let text_color_expr = props
678        .color
679        .as_ref()
680        .map(|color| {
681            let color_expr = generate_color_expr(color);
682            quote! { Some(#color_expr) }
683        })
684        .unwrap_or_else(|| quote! { None });
685
686    let border_expr = props
687        .border
688        .as_ref()
689        .map(generate_border_expr)
690        .unwrap_or_else(|| quote! { iced::Border::default() });
691
692    let shadow_expr = props
693        .shadow
694        .as_ref()
695        .map(generate_shadow_expr)
696        .unwrap_or_else(|| quote! { iced::Shadow::default() });
697
698    Ok(quote! {
699        iced::widget::container::Style {
700            background: #background_expr,
701            text_color: #text_color_expr,
702            border: #border_expr,
703            shadow: #shadow_expr,
704            snap: false,
705        }
706    })
707}
708
709/// Generate iced::widget::text_input::Style struct from StyleProperties
710fn generate_text_input_style_struct(
711    props: &StyleProperties,
712) -> Result<TokenStream, super::CodegenError> {
713    let background_expr = props
714        .background
715        .as_ref()
716        .map(|bg| {
717            let expr = generate_background_expr(bg);
718            quote! { #expr }
719        })
720        .unwrap_or_else(|| quote! { iced::Background::Color(iced::Color::WHITE) });
721
722    let border_expr = props
723        .border
724        .as_ref()
725        .map(generate_border_expr)
726        .unwrap_or_else(|| quote! { iced::Border::default() });
727
728    Ok(quote! {
729        iced::widget::text_input::Style {
730            background: #background_expr,
731            border: #border_expr,
732            icon: iced::Color::BLACK,
733            placeholder: iced::Color::from_rgb(0.4, 0.4, 0.4),
734            value: iced::Color::BLACK,
735            selection: iced::Color::from_rgb(0.8, 0.8, 1.0),
736        }
737    })
738}
739
740/// Generate checkbox style struct from StyleProperties
741fn generate_checkbox_style_struct(
742    props: &StyleProperties,
743) -> Result<TokenStream, super::CodegenError> {
744    let background_expr = props
745        .background
746        .as_ref()
747        .map(|bg| {
748            let expr = generate_background_expr(bg);
749            quote! { #expr }
750        })
751        .unwrap_or_else(|| quote! { iced::Background::Color(iced::Color::WHITE) });
752
753    let border_expr = props
754        .border
755        .as_ref()
756        .map(generate_border_expr)
757        .unwrap_or_else(|| quote! { iced::Border::default() });
758
759    let text_color = props
760        .color
761        .as_ref()
762        .map(generate_color_expr)
763        .unwrap_or_else(|| quote! { iced::Color::BLACK });
764
765    Ok(quote! {
766        iced::widget::checkbox::Style {
767            background: #background_expr,
768            icon_color: #text_color,
769            border: #border_expr,
770            text_color: None,
771        }
772    })
773}
774
775/// Generate toggler style struct from StyleProperties
776fn generate_toggler_style_struct(
777    props: &StyleProperties,
778) -> Result<TokenStream, super::CodegenError> {
779    let background_expr = props
780        .background
781        .as_ref()
782        .map(|bg| {
783            let expr = generate_background_expr(bg);
784            quote! { #expr }
785        })
786        .unwrap_or_else(
787            || quote! { iced::Background::Color(iced::Color::from_rgb(0.5, 0.5, 0.5)) },
788        );
789
790    Ok(quote! {
791        iced::widget::toggler::Style {
792            background: #background_expr,
793            background_border_width: 0.0,
794            background_border_color: iced::Color::TRANSPARENT,
795            foreground: iced::Background::Color(iced::Color::WHITE),
796            foreground_border_width: 0.0,
797            foreground_border_color: iced::Color::TRANSPARENT,
798        }
799    })
800}
801
802/// Generate slider style struct from StyleProperties
803fn generate_slider_style_struct(
804    props: &StyleProperties,
805) -> Result<TokenStream, super::CodegenError> {
806    let border_expr = props
807        .border
808        .as_ref()
809        .map(generate_border_expr)
810        .unwrap_or_else(|| quote! { iced::Border::default() });
811
812    Ok(quote! {
813        iced::widget::slider::Style {
814            rail: iced::widget::slider::Rail {
815                colors: (
816                    iced::Color::from_rgb(0.6, 0.6, 0.6),
817                    iced::Color::from_rgb(0.2, 0.6, 1.0),
818                ),
819                width: 4.0,
820                border: #border_expr,
821            },
822            handle: iced::widget::slider::Handle {
823                shape: iced::widget::slider::HandleShape::Circle { radius: 8.0 },
824                color: iced::Color::WHITE,
825                border_width: 1.0,
826                border_color: iced::Color::from_rgb(0.6, 0.6, 0.6),
827            },
828        }
829    })
830}
831
832/// Generate text widget
833fn generate_text(
834    node: &crate::WidgetNode,
835    model_ident: &syn::Ident,
836    _style_classes: &HashMap<String, StyleClass>,
837) -> Result<TokenStream, super::CodegenError> {
838    let value_attr = node.attributes.get("value").ok_or_else(|| {
839        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
840    })?;
841
842    let value_expr = generate_attribute_value(value_attr, model_ident);
843
844    let mut text_widget = quote! {
845        iced::widget::text(#value_expr)
846    };
847
848    // Apply size attribute
849    if let Some(size) = node.attributes.get("size").and_then(|attr| {
850        if let AttributeValue::Static(s) = attr {
851            s.parse::<f32>().ok()
852        } else {
853            None
854        }
855    }) {
856        text_widget = quote! { #text_widget.size(#size) };
857    }
858
859    // Apply weight attribute (bold, normal, etc.)
860    if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
861        if let AttributeValue::Static(s) = attr {
862            Some(s.clone())
863        } else {
864            None
865        }
866    }) {
867        let weight_expr = match weight.to_lowercase().as_str() {
868            "bold" => quote! { iced::font::Weight::Bold },
869            "semibold" => quote! { iced::font::Weight::Semibold },
870            "medium" => quote! { iced::font::Weight::Medium },
871            "light" => quote! { iced::font::Weight::Light },
872            _ => quote! { iced::font::Weight::Normal },
873        };
874        text_widget = quote! {
875            #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
876        };
877    }
878
879    // Apply inline style color if present
880    if let Some(ref style_props) = node.style {
881        if let Some(ref color) = style_props.color {
882            let color_expr = generate_color_expr(color);
883            text_widget = quote! { #text_widget.color(#color_expr) };
884        }
885    }
886
887    // Use helper to wrap in container if layout attributes are present
888    Ok(maybe_wrap_in_container(text_widget, node))
889}
890
891/// Generate Length expression from string
892fn generate_length_expr(s: &str) -> TokenStream {
893    let s = s.trim().to_lowercase();
894    if s == "fill" {
895        quote! { iced::Length::Fill }
896    } else if s == "shrink" {
897        quote! { iced::Length::Shrink }
898    } else if let Some(pct) = s.strip_suffix('%') {
899        if let Ok(p) = pct.parse::<f32>() {
900            // Iced doesn't have a direct percentage, use FillPortion as approximation
901            let portion = ((p / 100.0) * 16.0).round() as u16;
902            let portion = portion.max(1);
903            quote! { iced::Length::FillPortion(#portion) }
904        } else {
905            quote! { iced::Length::Shrink }
906        }
907    } else if let Ok(px) = s.parse::<f32>() {
908        quote! { iced::Length::Fixed(#px) }
909    } else {
910        quote! { iced::Length::Shrink }
911    }
912}
913
914/// Generate Length expression from LayoutLength IR type
915fn generate_layout_length_expr(length: &LayoutLength) -> TokenStream {
916    match length {
917        LayoutLength::Fixed(px) => quote! { iced::Length::Fixed(#px) },
918        LayoutLength::Fill => quote! { iced::Length::Fill },
919        LayoutLength::Shrink => quote! { iced::Length::Shrink },
920        LayoutLength::FillPortion(portion) => {
921            let p = *portion as u16;
922            quote! { iced::Length::FillPortion(#p) }
923        }
924        LayoutLength::Percentage(pct) => {
925            // Iced doesn't have direct percentage, approximate with FillPortion
926            let portion = ((pct / 100.0) * 16.0).round() as u16;
927            let portion = portion.max(1);
928            quote! { iced::Length::FillPortion(#portion) }
929        }
930    }
931}
932
933/// Generate horizontal alignment expression
934fn generate_horizontal_alignment_expr(s: &str) -> TokenStream {
935    match s.trim().to_lowercase().as_str() {
936        "center" => quote! { iced::alignment::Horizontal::Center },
937        "end" | "right" => quote! { iced::alignment::Horizontal::Right },
938        _ => quote! { iced::alignment::Horizontal::Left },
939    }
940}
941
942/// Generate vertical alignment expression
943fn generate_vertical_alignment_expr(s: &str) -> TokenStream {
944    match s.trim().to_lowercase().as_str() {
945        "center" => quote! { iced::alignment::Vertical::Center },
946        "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
947        _ => quote! { iced::alignment::Vertical::Top },
948    }
949}
950
951/// Wraps a widget in a container if layout attributes are present.
952///
953/// This helper provides consistent layout attribute support across all widgets
954/// by wrapping them in a container when needed.
955///
956/// # Arguments
957///
958/// * `widget` - The widget expression to potentially wrap
959/// * `node` - The widget node containing attributes
960///
961/// # Returns
962///
963/// Returns the widget wrapped in a container if layout attributes are present,
964/// otherwise returns the original widget.
965fn maybe_wrap_in_container(widget: TokenStream, node: &crate::WidgetNode) -> TokenStream {
966    // Check if we need to wrap in container for layout/alignment/classes
967    let needs_container = node.layout.is_some()
968        || !node.classes.is_empty()
969        || node.attributes.contains_key("align_x")
970        || node.attributes.contains_key("align_y")
971        || node.attributes.contains_key("width")
972        || node.attributes.contains_key("height")
973        || node.attributes.contains_key("padding");
974
975    if !needs_container {
976        return quote! { #widget.into() };
977    }
978
979    let mut container = quote! {
980        iced::widget::container(#widget)
981    };
982
983    // Apply width
984    if let Some(width) = node.attributes.get("width").and_then(|attr| {
985        if let AttributeValue::Static(s) = attr {
986            Some(s.clone())
987        } else {
988            None
989        }
990    }) {
991        let width_expr = generate_length_expr(&width);
992        container = quote! { #container.width(#width_expr) };
993    }
994
995    // Apply height
996    if let Some(height) = node.attributes.get("height").and_then(|attr| {
997        if let AttributeValue::Static(s) = attr {
998            Some(s.clone())
999        } else {
1000            None
1001        }
1002    }) {
1003        let height_expr = generate_length_expr(&height);
1004        container = quote! { #container.height(#height_expr) };
1005    }
1006
1007    // Apply padding
1008    if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1009        if let AttributeValue::Static(s) = attr {
1010            s.parse::<f32>().ok()
1011        } else {
1012            None
1013        }
1014    }) {
1015        container = quote! { #container.padding(#padding) };
1016    }
1017
1018    // Apply align_x
1019    if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1020        if let AttributeValue::Static(s) = attr {
1021            Some(s.clone())
1022        } else {
1023            None
1024        }
1025    }) {
1026        let align_expr = generate_horizontal_alignment_expr(&align_x);
1027        container = quote! { #container.align_x(#align_expr) };
1028    }
1029
1030    // Apply align_y
1031    if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1032        if let AttributeValue::Static(s) = attr {
1033            Some(s.clone())
1034        } else {
1035            None
1036        }
1037    }) {
1038        let align_expr = generate_vertical_alignment_expr(&align_y);
1039        container = quote! { #container.align_y(#align_expr) };
1040    }
1041
1042    // Apply class style if present
1043    if let Some(class_name) = node.classes.first() {
1044        let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1045        container = quote! { #container.style(#style_fn_ident) };
1046    }
1047
1048    quote! { #container.into() }
1049}
1050
1051/// Generate button widget
1052fn generate_button(
1053    node: &crate::WidgetNode,
1054    model_ident: &syn::Ident,
1055    message_ident: &syn::Ident,
1056    style_classes: &HashMap<String, StyleClass>,
1057) -> Result<TokenStream, super::CodegenError> {
1058    let label_attr = node.attributes.get("label").ok_or_else(|| {
1059        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
1060    })?;
1061
1062    let label_expr = generate_attribute_value(label_attr, model_ident);
1063
1064    let on_click = node
1065        .events
1066        .iter()
1067        .find(|e| e.event == crate::EventKind::Click);
1068
1069    let mut button = quote! {
1070        iced::widget::button(iced::widget::text(#label_expr))
1071    };
1072
1073    // Handle enabled attribute
1074    let enabled_condition = node.attributes.get("enabled").map(|attr| match attr {
1075        AttributeValue::Static(s) => {
1076            // Static enabled values
1077            match s.to_lowercase().as_str() {
1078                "true" | "1" | "yes" | "on" => quote! { true },
1079                "false" | "0" | "no" | "off" => quote! { false },
1080                _ => quote! { true }, // Default to enabled
1081            }
1082        }
1083        AttributeValue::Binding(binding_expr) => {
1084            // Dynamic binding expression - use generate_bool_expr for native boolean
1085            super::bindings::generate_bool_expr(&binding_expr.expr)
1086        }
1087        AttributeValue::Interpolated(_) => {
1088            // Interpolated strings treated as enabled if non-empty
1089            let expr_tokens = generate_attribute_value(attr, model_ident);
1090            quote! { !#expr_tokens.is_empty() && #expr_tokens != "false" && #expr_tokens != "0" }
1091        }
1092    });
1093
1094    if let Some(event) = on_click {
1095        let variant_name = to_upper_camel_case(&event.handler);
1096        let handler_ident = format_ident!("{}", variant_name);
1097
1098        let param_expr = if let Some(ref param) = event.param {
1099            let param_tokens = generate_expr(&param.expr);
1100            quote! { (#param_tokens) }
1101        } else {
1102            quote! {}
1103        };
1104
1105        // Generate on_press call based on enabled condition
1106        button = match enabled_condition {
1107            None => {
1108                // No enabled attribute - always enabled
1109                quote! {
1110                    #button.on_press(#message_ident::#handler_ident #param_expr)
1111                }
1112            }
1113            Some(condition) => {
1114                // Conditional enabled - use on_press_maybe
1115                quote! {
1116                    #button.on_press_maybe(
1117                        if #condition {
1118                            Some(#message_ident::#handler_ident #param_expr)
1119                        } else {
1120                            None
1121                        }
1122                    )
1123                }
1124            }
1125        };
1126    }
1127
1128    // Apply styles (inline or classes)
1129    button = apply_widget_style(button, node, "button", style_classes)?;
1130
1131    Ok(quote! { #button.into() })
1132}
1133
1134/// Helper function to convert snake_case to UpperCamelCase
1135fn to_upper_camel_case(s: &str) -> String {
1136    let mut result = String::new();
1137    let mut capitalize_next = true;
1138    for c in s.chars() {
1139        if c == '_' {
1140            capitalize_next = true;
1141        } else if capitalize_next {
1142            result.push(c.to_ascii_uppercase());
1143            capitalize_next = false;
1144        } else {
1145            result.push(c);
1146        }
1147    }
1148    result
1149}
1150
1151/// Generate container widget (column, row, container, scrollable)
1152fn generate_container(
1153    node: &crate::WidgetNode,
1154    widget_type: &str,
1155    model_ident: &syn::Ident,
1156    message_ident: &syn::Ident,
1157    style_classes: &HashMap<String, StyleClass>,
1158) -> Result<TokenStream, super::CodegenError> {
1159    let children: Vec<TokenStream> = node
1160        .children
1161        .iter()
1162        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1163        .collect::<Result<_, _>>()?;
1164
1165    let widget_ident = format_ident!("{}", widget_type);
1166
1167    // Get merged layout from node.layout and style classes
1168    let merged_layout = get_merged_layout(node, style_classes);
1169
1170    // Get spacing from attributes or merged layout
1171    let spacing = node
1172        .attributes
1173        .get("spacing")
1174        .and_then(|attr| {
1175            if let AttributeValue::Static(s) = attr {
1176                s.parse::<f32>().ok()
1177            } else {
1178                None
1179            }
1180        })
1181        .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
1182
1183    // Get padding from attributes or merged layout
1184    let padding = node
1185        .attributes
1186        .get("padding")
1187        .and_then(|attr| {
1188            if let AttributeValue::Static(s) = attr {
1189                s.parse::<f32>().ok()
1190            } else {
1191                None
1192            }
1193        })
1194        .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
1195
1196    let mut widget = if widget_type == "container" {
1197        // Container in Iced can only have one child
1198        // If multiple children provided, wrap them in a column automatically
1199        // Use let binding with explicit type to help inference when child is a column/row
1200        if children.is_empty() {
1201            quote! {
1202                iced::widget::container(iced::widget::Space::new())
1203            }
1204        } else if children.len() == 1 {
1205            let child = &children[0];
1206            quote! {
1207                {
1208                    let content: iced::Element<'_, _, _> = #child;
1209                    iced::widget::container(content)
1210                }
1211            }
1212        } else {
1213            // Multiple children - wrap in a column
1214            quote! {
1215                {
1216                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1217                    iced::widget::container(content)
1218                }
1219            }
1220        }
1221    } else if widget_type == "scrollable" {
1222        // Scrollable in Iced can only have one child
1223        // If multiple children provided, wrap them in a column automatically
1224        // Use let binding with explicit type to help inference when child is a column/row
1225        if children.is_empty() {
1226            quote! {
1227                iced::widget::scrollable(iced::widget::Space::new())
1228            }
1229        } else if children.len() == 1 {
1230            let child = &children[0];
1231            quote! {
1232                {
1233                    let content: iced::Element<'_, _, _> = #child;
1234                    iced::widget::scrollable(content)
1235                }
1236            }
1237        } else {
1238            // Multiple children - wrap in a column
1239            quote! {
1240                {
1241                    let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1242                    iced::widget::scrollable(content)
1243                }
1244            }
1245        }
1246    } else {
1247        quote! {
1248            iced::widget::#widget_ident(vec![#(#children),*])
1249        }
1250    };
1251
1252    if let Some(s) = spacing {
1253        widget = quote! { #widget.spacing(#s) };
1254    }
1255
1256    if let Some(p) = padding {
1257        widget = quote! { #widget.padding(#p) };
1258    }
1259
1260    // Apply width from attributes or merged layout
1261    let width_from_attr = node.attributes.get("width").and_then(|attr| {
1262        if let AttributeValue::Static(s) = attr {
1263            Some(s.clone())
1264        } else {
1265            None
1266        }
1267    });
1268    let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
1269
1270    if let Some(width) = width_from_attr {
1271        let width_expr = generate_length_expr(&width);
1272        widget = quote! { #widget.width(#width_expr) };
1273    } else if let Some(layout_width) = width_from_layout {
1274        let width_expr = generate_layout_length_expr(layout_width);
1275        widget = quote! { #widget.width(#width_expr) };
1276    }
1277
1278    // Apply height from attributes or merged layout
1279    let height_from_attr = node.attributes.get("height").and_then(|attr| {
1280        if let AttributeValue::Static(s) = attr {
1281            Some(s.clone())
1282        } else {
1283            None
1284        }
1285    });
1286    let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
1287
1288    if let Some(height) = height_from_attr {
1289        let height_expr = generate_length_expr(&height);
1290        widget = quote! { #widget.height(#height_expr) };
1291    } else if let Some(layout_height) = height_from_layout {
1292        let height_expr = generate_layout_length_expr(layout_height);
1293        widget = quote! { #widget.height(#height_expr) };
1294    }
1295
1296    // Apply align_x attribute (for containers)
1297    if widget_type == "container" {
1298        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1299            if let AttributeValue::Static(s) = attr {
1300                Some(s.clone())
1301            } else {
1302                None
1303            }
1304        }) {
1305            let align_expr = generate_horizontal_alignment_expr(&align_x);
1306            widget = quote! { #widget.align_x(#align_expr) };
1307        }
1308
1309        // Apply align_y attribute
1310        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1311            if let AttributeValue::Static(s) = attr {
1312                Some(s.clone())
1313            } else {
1314                None
1315            }
1316        }) {
1317            let align_expr = generate_vertical_alignment_expr(&align_y);
1318            widget = quote! { #widget.align_y(#align_expr) };
1319        }
1320    }
1321
1322    // Apply align_items for column/row (vertical alignment of children)
1323    if widget_type == "column" || widget_type == "row" {
1324        if let Some(align) = node.attributes.get("align_items").and_then(|attr| {
1325            if let AttributeValue::Static(s) = attr {
1326                Some(s.clone())
1327            } else {
1328                None
1329            }
1330        }) {
1331            let align_expr = match align.to_lowercase().as_str() {
1332                "center" => quote! { iced::Alignment::Center },
1333                "end" => quote! { iced::Alignment::End },
1334                _ => quote! { iced::Alignment::Start },
1335            };
1336            widget = quote! { #widget.align_items(#align_expr) };
1337        }
1338    }
1339
1340    // Apply styles (only for container, not column/row/scrollable)
1341    if widget_type == "container" {
1342        widget = apply_widget_style(widget, node, "container", style_classes)?;
1343    }
1344
1345    // Check if Column/Row needs to be wrapped in a container for align_x/align_y
1346    // These attributes position the Column/Row itself within its parent,
1347    // which requires an outer container wrapper
1348    if (widget_type == "column" || widget_type == "row")
1349        && (node.attributes.contains_key("align_x") || node.attributes.contains_key("align_y"))
1350    {
1351        let mut container = quote! { iced::widget::container(#widget) };
1352
1353        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1354            if let AttributeValue::Static(s) = attr {
1355                Some(s.clone())
1356            } else {
1357                None
1358            }
1359        }) {
1360            let align_expr = generate_horizontal_alignment_expr(&align_x);
1361            container = quote! { #container.align_x(#align_expr) };
1362        }
1363
1364        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1365            if let AttributeValue::Static(s) = attr {
1366                Some(s.clone())
1367            } else {
1368                None
1369            }
1370        }) {
1371            let align_expr = generate_vertical_alignment_expr(&align_y);
1372            container = quote! { #container.align_y(#align_expr) };
1373        }
1374
1375        // Container needs explicit width/height to enable alignment
1376        container = quote! { #container.width(iced::Length::Fill).height(iced::Length::Fill) };
1377
1378        return Ok(quote! { #container.into() });
1379    }
1380
1381    Ok(quote! { #widget.into() })
1382}
1383
1384/// Generate stack widget
1385fn generate_stack(
1386    node: &crate::WidgetNode,
1387    model_ident: &syn::Ident,
1388    message_ident: &syn::Ident,
1389    style_classes: &HashMap<String, StyleClass>,
1390) -> Result<TokenStream, super::CodegenError> {
1391    let children: Vec<TokenStream> = node
1392        .children
1393        .iter()
1394        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1395        .collect::<Result<_, _>>()?;
1396
1397    Ok(quote! {
1398        iced::widget::stack(vec![#(#children),*]).into()
1399    })
1400}
1401
1402/// Generate space widget
1403fn generate_space(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1404    // Get width attribute
1405    let width = node.attributes.get("width").and_then(|attr| {
1406        if let AttributeValue::Static(s) = attr {
1407            Some(s.clone())
1408        } else {
1409            None
1410        }
1411    });
1412
1413    // Get height attribute
1414    let height = node.attributes.get("height").and_then(|attr| {
1415        if let AttributeValue::Static(s) = attr {
1416            Some(s.clone())
1417        } else {
1418            None
1419        }
1420    });
1421
1422    let mut space = quote! { iced::widget::Space::new() };
1423
1424    // Apply width
1425    if let Some(w) = width {
1426        let width_expr = generate_length_expr(&w);
1427        space = quote! { #space.width(#width_expr) };
1428    }
1429
1430    // Apply height
1431    if let Some(h) = height {
1432        let height_expr = generate_length_expr(&h);
1433        space = quote! { #space.height(#height_expr) };
1434    }
1435
1436    Ok(quote! { #space.into() })
1437}
1438
1439/// Generate rule (horizontal/vertical line) widget
1440fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1441    // Get direction (default to horizontal)
1442    let direction = node
1443        .attributes
1444        .get("direction")
1445        .and_then(|attr| {
1446            if let AttributeValue::Static(s) = attr {
1447                Some(s.clone())
1448            } else {
1449                None
1450            }
1451        })
1452        .unwrap_or_else(|| "horizontal".to_string());
1453
1454    // Get thickness (default to 1)
1455    let thickness = node
1456        .attributes
1457        .get("thickness")
1458        .and_then(|attr| {
1459            if let AttributeValue::Static(s) = attr {
1460                s.parse::<f32>().ok()
1461            } else {
1462                None
1463            }
1464        })
1465        .unwrap_or(1.0);
1466
1467    let rule = if direction.to_lowercase() == "vertical" {
1468        quote! { iced::widget::rule::vertical(#thickness) }
1469    } else {
1470        quote! { iced::widget::rule::horizontal(#thickness) }
1471    };
1472
1473    Ok(quote! { #rule.into() })
1474}
1475
1476/// Generate checkbox widget
1477fn generate_checkbox(
1478    node: &crate::WidgetNode,
1479    model_ident: &syn::Ident,
1480    message_ident: &syn::Ident,
1481    style_classes: &HashMap<String, StyleClass>,
1482) -> Result<TokenStream, super::CodegenError> {
1483    let label = node
1484        .attributes
1485        .get("label")
1486        .and_then(|attr| {
1487            if let AttributeValue::Static(s) = attr {
1488                Some(s.clone())
1489            } else {
1490                None
1491            }
1492        })
1493        .unwrap_or_default();
1494    let label_lit = proc_macro2::Literal::string(&label);
1495    let label_expr = quote! { #label_lit.to_string() };
1496
1497    let checked_attr = node.attributes.get("checked");
1498    let checked_expr = checked_attr
1499        .map(|attr| generate_attribute_value(attr, model_ident))
1500        .unwrap_or(quote! { false });
1501
1502    let on_toggle = node
1503        .events
1504        .iter()
1505        .find(|e| e.event == crate::EventKind::Toggle);
1506
1507    let checkbox = if let Some(event) = on_toggle {
1508        let variant_name = to_upper_camel_case(&event.handler);
1509        let handler_ident = format_ident!("{}", variant_name);
1510        quote! {
1511            iced::widget::checkbox(#label_expr, #checked_expr)
1512                .on_toggle(#message_ident::#handler_ident)
1513        }
1514    } else {
1515        quote! {
1516            iced::widget::checkbox(#label_expr, #checked_expr)
1517        }
1518    };
1519
1520    // Apply styles
1521    let checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
1522
1523    Ok(quote! { #checkbox.into() })
1524}
1525
1526/// Generate toggler widget
1527fn generate_toggler(
1528    node: &crate::WidgetNode,
1529    model_ident: &syn::Ident,
1530    message_ident: &syn::Ident,
1531    style_classes: &HashMap<String, StyleClass>,
1532) -> Result<TokenStream, super::CodegenError> {
1533    let label = node
1534        .attributes
1535        .get("label")
1536        .and_then(|attr| {
1537            if let AttributeValue::Static(s) = attr {
1538                Some(s.clone())
1539            } else {
1540                None
1541            }
1542        })
1543        .unwrap_or_default();
1544    let label_lit = proc_macro2::Literal::string(&label);
1545    let label_expr = quote! { #label_lit.to_string() };
1546
1547    let is_toggled_attr = node.attributes.get("toggled");
1548    let is_toggled_expr = is_toggled_attr
1549        .map(|attr| generate_attribute_value(attr, model_ident))
1550        .unwrap_or(quote! { false });
1551
1552    let on_toggle = node
1553        .events
1554        .iter()
1555        .find(|e| e.event == crate::EventKind::Toggle);
1556
1557    let toggler = if let Some(event) = on_toggle {
1558        let variant_name = to_upper_camel_case(&event.handler);
1559        let handler_ident = format_ident!("{}", variant_name);
1560        quote! {
1561            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1562                .on_toggle(|_| #message_ident::#handler_ident)
1563        }
1564    } else {
1565        quote! {
1566            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1567        }
1568    };
1569
1570    // Apply styles
1571    let toggler = apply_widget_style(toggler, node, "toggler", style_classes)?;
1572
1573    Ok(quote! { #toggler.into() })
1574}
1575
1576/// Generate slider widget
1577fn generate_slider(
1578    node: &crate::WidgetNode,
1579    model_ident: &syn::Ident,
1580    message_ident: &syn::Ident,
1581    style_classes: &HashMap<String, StyleClass>,
1582) -> Result<TokenStream, super::CodegenError> {
1583    let min = node.attributes.get("min").and_then(|attr| {
1584        if let AttributeValue::Static(s) = attr {
1585            s.parse::<f32>().ok()
1586        } else {
1587            None
1588        }
1589    });
1590
1591    let max = node.attributes.get("max").and_then(|attr| {
1592        if let AttributeValue::Static(s) = attr {
1593            s.parse::<f32>().ok()
1594        } else {
1595            None
1596        }
1597    });
1598
1599    let value_attr = node.attributes.get("value").ok_or_else(|| {
1600        super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
1601    })?;
1602    let value_expr = generate_attribute_value(value_attr, model_ident);
1603
1604    let on_change = node
1605        .events
1606        .iter()
1607        .find(|e| e.event == crate::EventKind::Change);
1608
1609    let mut slider = quote! {
1610        iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
1611    };
1612
1613    if let Some(m) = min {
1614        slider = quote! { #slider.min(#m) };
1615    }
1616    if let Some(m) = max {
1617        slider = quote! { #slider.max(#m) };
1618    }
1619
1620    // Apply step attribute (increment size)
1621    let step = node.attributes.get("step").and_then(|attr| {
1622        if let AttributeValue::Static(s) = attr {
1623            s.parse::<f32>().ok()
1624        } else {
1625            None
1626        }
1627    });
1628
1629    if let Some(s) = step {
1630        slider = quote! { #slider.step(#s) };
1631    }
1632
1633    if let Some(event) = on_change {
1634        let variant_name = to_upper_camel_case(&event.handler);
1635        let handler_ident = format_ident!("{}", variant_name);
1636        slider = quote! {
1637            iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
1638        };
1639    }
1640
1641    // Apply styles
1642    slider = apply_widget_style(slider, node, "slider", style_classes)?;
1643
1644    Ok(quote! { #slider.into() })
1645}
1646
1647/// Generate radio widget
1648fn generate_radio(
1649    node: &crate::WidgetNode,
1650    _model_ident: &syn::Ident,
1651    message_ident: &syn::Ident,
1652    _style_classes: &HashMap<String, StyleClass>,
1653) -> Result<TokenStream, super::CodegenError> {
1654    let label = node
1655        .attributes
1656        .get("label")
1657        .and_then(|attr| {
1658            if let AttributeValue::Static(s) = attr {
1659                Some(s.clone())
1660            } else {
1661                None
1662            }
1663        })
1664        .unwrap_or_default();
1665    let label_lit = proc_macro2::Literal::string(&label);
1666    let label_expr = quote! { #label_lit.to_string() };
1667
1668    let value_attr = node.attributes.get("value").ok_or_else(|| {
1669        super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
1670    })?;
1671    let value_expr = match value_attr {
1672        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
1673        _ => quote! { String::new() },
1674    };
1675
1676    let selected_attr = node.attributes.get("selected");
1677    let selected_expr = match selected_attr {
1678        Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
1679        _ => quote! { None },
1680    };
1681
1682    let on_select = node
1683        .events
1684        .iter()
1685        .find(|e| e.event == crate::EventKind::Select);
1686
1687    if let Some(event) = on_select {
1688        let variant_name = to_upper_camel_case(&event.handler);
1689        let handler_ident = format_ident!("{}", variant_name);
1690        Ok(quote! {
1691            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v)).into()
1692        })
1693    } else {
1694        Ok(quote! {
1695            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ()).into()
1696        })
1697    }
1698}
1699
1700/// Generate progress bar widget
1701fn generate_progress_bar(
1702    node: &crate::WidgetNode,
1703    model_ident: &syn::Ident,
1704    _style_classes: &HashMap<String, StyleClass>,
1705) -> Result<TokenStream, super::CodegenError> {
1706    let value_attr = node.attributes.get("value").ok_or_else(|| {
1707        super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
1708    })?;
1709    let value_expr = generate_attribute_value(value_attr, model_ident);
1710
1711    let max_attr = node.attributes.get("max").and_then(|attr| {
1712        if let AttributeValue::Static(s) = attr {
1713            s.parse::<f32>().ok()
1714        } else {
1715            None
1716        }
1717    });
1718
1719    if let Some(max) = max_attr {
1720        Ok(quote! {
1721            iced::widget::progress_bar(0.0..=#max, #value_expr).into()
1722        })
1723    } else {
1724        Ok(quote! {
1725            iced::widget::progress_bar(0.0..=100.0, #value_expr).into()
1726        })
1727    }
1728}
1729
1730/// Generate text input widget
1731fn generate_text_input(
1732    node: &crate::WidgetNode,
1733    model_ident: &syn::Ident,
1734    message_ident: &syn::Ident,
1735    style_classes: &HashMap<String, StyleClass>,
1736) -> Result<TokenStream, super::CodegenError> {
1737    let value_expr = node
1738        .attributes
1739        .get("value")
1740        .map(|attr| generate_attribute_value(attr, model_ident))
1741        .unwrap_or(quote! { String::new() });
1742
1743    let placeholder = node.attributes.get("placeholder").and_then(|attr| {
1744        if let AttributeValue::Static(s) = attr {
1745            Some(s.clone())
1746        } else {
1747            None
1748        }
1749    });
1750
1751    let on_input = node
1752        .events
1753        .iter()
1754        .find(|e| e.event == crate::EventKind::Input);
1755
1756    let mut text_input = match placeholder {
1757        Some(ph) => {
1758            let ph_lit = proc_macro2::Literal::string(&ph);
1759            quote! {
1760                iced::widget::text_input(#ph_lit, &#value_expr)
1761            }
1762        }
1763        None => quote! {
1764            iced::widget::text_input("", &#value_expr)
1765        },
1766    };
1767
1768    if let Some(event) = on_input {
1769        let variant_name = to_upper_camel_case(&event.handler);
1770        let handler_ident = format_ident!("{}", variant_name);
1771        text_input = quote! {
1772            #text_input.on_input(|v| #message_ident::#handler_ident(v))
1773        };
1774    }
1775
1776    // Apply password/secure attribute (masks input)
1777    let is_password = node
1778        .attributes
1779        .get("password")
1780        .or_else(|| node.attributes.get("secure"))
1781        .and_then(|attr| {
1782            if let AttributeValue::Static(s) = attr {
1783                Some(s.to_lowercase() == "true" || s == "1")
1784            } else {
1785                None
1786            }
1787        })
1788        .unwrap_or(false);
1789
1790    if is_password {
1791        text_input = quote! { #text_input.password() };
1792    }
1793
1794    // Apply styles
1795    text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
1796
1797    Ok(quote! { #text_input.into() })
1798}
1799
1800/// Generate image widget
1801fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1802    let src_attr = node.attributes.get("src").ok_or_else(|| {
1803        super::CodegenError::InvalidWidget("image requires src attribute".to_string())
1804    })?;
1805
1806    let src = match src_attr {
1807        AttributeValue::Static(s) => s.clone(),
1808        _ => String::new(),
1809    };
1810    let src_lit = proc_macro2::Literal::string(&src);
1811
1812    let width = node.attributes.get("width").and_then(|attr| {
1813        if let AttributeValue::Static(s) = attr {
1814            s.parse::<u32>().ok()
1815        } else {
1816            None
1817        }
1818    });
1819
1820    let height = node.attributes.get("height").and_then(|attr| {
1821        if let AttributeValue::Static(s) = attr {
1822            s.parse::<u32>().ok()
1823        } else {
1824            None
1825        }
1826    });
1827
1828    let mut image = quote! {
1829        iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
1830    };
1831
1832    // Apply native width/height if specified with integer values
1833    if let (Some(w), Some(h)) = (width, height) {
1834        image = quote! { #image.width(#w).height(#h) };
1835    } else if let Some(w) = width {
1836        image = quote! { #image.width(#w) };
1837    } else if let Some(h) = height {
1838        image = quote! { #image.height(#h) };
1839    }
1840
1841    // Check if we need container for NON-native layout attributes
1842    // (padding, alignment, classes - NOT width/height since those are native)
1843    // For Image, only wrap if there are alignment/padding/classes
1844    let needs_container = !node.classes.is_empty()
1845        || node.attributes.contains_key("align_x")
1846        || node.attributes.contains_key("align_y")
1847        || node.attributes.contains_key("padding");
1848
1849    if needs_container {
1850        // Wrap with container for layout attributes, but skip width/height (already applied)
1851        let mut container = quote! { iced::widget::container(#image) };
1852
1853        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1854            if let AttributeValue::Static(s) = attr {
1855                s.parse::<f32>().ok()
1856            } else {
1857                None
1858            }
1859        }) {
1860            container = quote! { #container.padding(#padding) };
1861        }
1862
1863        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1864            if let AttributeValue::Static(s) = attr {
1865                Some(s.clone())
1866            } else {
1867                None
1868            }
1869        }) {
1870            let align_expr = generate_horizontal_alignment_expr(&align_x);
1871            container = quote! { #container.align_x(#align_expr) };
1872        }
1873
1874        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1875            if let AttributeValue::Static(s) = attr {
1876                Some(s.clone())
1877            } else {
1878                None
1879            }
1880        }) {
1881            let align_expr = generate_vertical_alignment_expr(&align_y);
1882            container = quote! { #container.align_y(#align_expr) };
1883        }
1884
1885        if let Some(class_name) = node.classes.first() {
1886            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1887            container = quote! { #container.style(#style_fn_ident) };
1888        }
1889
1890        Ok(quote! { #container.into() })
1891    } else {
1892        Ok(quote! { #image.into() })
1893    }
1894}
1895
1896/// Generate SVG widget
1897fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1898    // Support both "src" (standard) and "path" (legacy) for backward compatibility
1899    let path_attr = node
1900        .attributes
1901        .get("src")
1902        .or_else(|| node.attributes.get("path"))
1903        .ok_or_else(|| {
1904            super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
1905        })?;
1906
1907    let path = match path_attr {
1908        AttributeValue::Static(s) => s.clone(),
1909        _ => String::new(),
1910    };
1911    let path_lit = proc_macro2::Literal::string(&path);
1912
1913    let width = node.attributes.get("width").and_then(|attr| {
1914        if let AttributeValue::Static(s) = attr {
1915            s.parse::<u32>().ok()
1916        } else {
1917            None
1918        }
1919    });
1920
1921    let height = node.attributes.get("height").and_then(|attr| {
1922        if let AttributeValue::Static(s) = attr {
1923            s.parse::<u32>().ok()
1924        } else {
1925            None
1926        }
1927    });
1928
1929    let mut svg = quote! {
1930        iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
1931    };
1932
1933    // Apply native width/height if specified with integer values
1934    if let (Some(w), Some(h)) = (width, height) {
1935        svg = quote! { #svg.width(#w).height(#h) };
1936    } else if let Some(w) = width {
1937        svg = quote! { #svg.width(#w) };
1938    } else if let Some(h) = height {
1939        svg = quote! { #svg.height(#h) };
1940    }
1941
1942    // Check if we need container for NON-native layout attributes
1943    // (padding, alignment, classes - NOT width/height since those are native)
1944    // For SVG, only wrap if there are alignment/padding/classes
1945    let needs_container = !node.classes.is_empty()
1946        || node.attributes.contains_key("align_x")
1947        || node.attributes.contains_key("align_y")
1948        || node.attributes.contains_key("padding");
1949
1950    if needs_container {
1951        // Wrap with container for layout attributes, but skip width/height (already applied)
1952        let mut container = quote! { iced::widget::container(#svg) };
1953
1954        if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1955            if let AttributeValue::Static(s) = attr {
1956                s.parse::<f32>().ok()
1957            } else {
1958                None
1959            }
1960        }) {
1961            container = quote! { #container.padding(#padding) };
1962        }
1963
1964        if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1965            if let AttributeValue::Static(s) = attr {
1966                Some(s.clone())
1967            } else {
1968                None
1969            }
1970        }) {
1971            let align_expr = generate_horizontal_alignment_expr(&align_x);
1972            container = quote! { #container.align_x(#align_expr) };
1973        }
1974
1975        if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1976            if let AttributeValue::Static(s) = attr {
1977                Some(s.clone())
1978            } else {
1979                None
1980            }
1981        }) {
1982            let align_expr = generate_vertical_alignment_expr(&align_y);
1983            container = quote! { #container.align_y(#align_expr) };
1984        }
1985
1986        if let Some(class_name) = node.classes.first() {
1987            let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1988            container = quote! { #container.style(#style_fn_ident) };
1989        }
1990
1991        Ok(quote! { #container.into() })
1992    } else {
1993        Ok(quote! { #svg.into() })
1994    }
1995}
1996
1997/// Generate pick list widget
1998fn generate_pick_list(
1999    node: &crate::WidgetNode,
2000    model_ident: &syn::Ident,
2001    message_ident: &syn::Ident,
2002    _style_classes: &HashMap<String, StyleClass>,
2003) -> Result<TokenStream, super::CodegenError> {
2004    let options_attr = node.attributes.get("options").ok_or_else(|| {
2005        super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2006    })?;
2007
2008    let options: Vec<String> = match options_attr {
2009        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2010        _ => Vec::new(),
2011    };
2012    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2013
2014    let selected_attr = node.attributes.get("selected");
2015    let selected_expr = selected_attr
2016        .map(|attr| generate_attribute_value(attr, model_ident))
2017        .unwrap_or(quote! { None });
2018
2019    let on_select = node
2020        .events
2021        .iter()
2022        .find(|e| e.event == crate::EventKind::Select);
2023
2024    if let Some(event) = on_select {
2025        let variant_name = to_upper_camel_case(&event.handler);
2026        let handler_ident = format_ident!("{}", variant_name);
2027        Ok(quote! {
2028            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2029        })
2030    } else {
2031        Ok(quote! {
2032            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2033        })
2034    }
2035}
2036
2037/// Generate combo box widget
2038fn generate_combo_box(
2039    node: &crate::WidgetNode,
2040    model_ident: &syn::Ident,
2041    message_ident: &syn::Ident,
2042    _style_classes: &HashMap<String, StyleClass>,
2043) -> Result<TokenStream, super::CodegenError> {
2044    let options_attr = node.attributes.get("options").ok_or_else(|| {
2045        super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2046    })?;
2047
2048    let options: Vec<String> = match options_attr {
2049        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2050        _ => Vec::new(),
2051    };
2052    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2053
2054    let selected_attr = node.attributes.get("selected");
2055    let selected_expr = selected_attr
2056        .map(|attr| generate_attribute_value(attr, model_ident))
2057        .unwrap_or(quote! { None });
2058
2059    let on_select = node
2060        .events
2061        .iter()
2062        .find(|e| e.event == crate::EventKind::Select);
2063
2064    if let Some(event) = on_select {
2065        let variant_name = to_upper_camel_case(&event.handler);
2066        let handler_ident = format_ident!("{}", variant_name);
2067        Ok(quote! {
2068            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2069        })
2070    } else {
2071        Ok(quote! {
2072            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2073        })
2074    }
2075}
2076
2077/// Generate tooltip widget
2078fn generate_tooltip(
2079    node: &crate::WidgetNode,
2080    model_ident: &syn::Ident,
2081    message_ident: &syn::Ident,
2082    style_classes: &HashMap<String, StyleClass>,
2083) -> Result<TokenStream, super::CodegenError> {
2084    let child = node.children.first().ok_or_else(|| {
2085        super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2086    })?;
2087    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2088
2089    let message_attr = node.attributes.get("message").ok_or_else(|| {
2090        super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2091    })?;
2092    let message_expr = generate_attribute_value(message_attr, model_ident);
2093
2094    Ok(quote! {
2095        iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2096    })
2097}
2098
2099/// Generate grid widget
2100fn generate_grid(
2101    node: &crate::WidgetNode,
2102    model_ident: &syn::Ident,
2103    message_ident: &syn::Ident,
2104    style_classes: &HashMap<String, StyleClass>,
2105) -> Result<TokenStream, super::CodegenError> {
2106    let children: Vec<TokenStream> = node
2107        .children
2108        .iter()
2109        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2110        .collect::<Result<_, _>>()?;
2111
2112    let columns = node
2113        .attributes
2114        .get("columns")
2115        .and_then(|attr| {
2116            if let AttributeValue::Static(s) = attr {
2117                s.parse::<u32>().ok()
2118            } else {
2119                None
2120            }
2121        })
2122        .unwrap_or(1);
2123
2124    let spacing = node.attributes.get("spacing").and_then(|attr| {
2125        if let AttributeValue::Static(s) = attr {
2126            s.parse::<f32>().ok()
2127        } else {
2128            None
2129        }
2130    });
2131
2132    let padding = node.attributes.get("padding").and_then(|attr| {
2133        if let AttributeValue::Static(s) = attr {
2134            s.parse::<f32>().ok()
2135        } else {
2136            None
2137        }
2138    });
2139
2140    let grid = quote! {
2141        iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2142    };
2143
2144    let grid = if let Some(s) = spacing {
2145        quote! { #grid.spacing(#s) }
2146    } else {
2147        grid
2148    };
2149
2150    let grid = if let Some(p) = padding {
2151        quote! { #grid.padding(#p) }
2152    } else {
2153        grid
2154    };
2155
2156    Ok(quote! { #grid.into() })
2157}
2158
2159/// Generate canvas widget
2160fn generate_canvas(
2161    node: &crate::WidgetNode,
2162    _model_ident: &syn::Ident,
2163    _message_ident: &syn::Ident,
2164    _style_classes: &HashMap<String, StyleClass>,
2165) -> Result<TokenStream, super::CodegenError> {
2166    let width = node.attributes.get("width").and_then(|attr| {
2167        if let AttributeValue::Static(s) = attr {
2168            s.parse::<f32>().ok()
2169        } else {
2170            None
2171        }
2172    });
2173
2174    let height = node.attributes.get("height").and_then(|attr| {
2175        if let AttributeValue::Static(s) = attr {
2176            s.parse::<f32>().ok()
2177        } else {
2178            None
2179        }
2180    });
2181
2182    let size = match (width, height) {
2183        (Some(w), Some(h)) => quote! { iced::Size::new(#w, #h) },
2184        (Some(w), None) => quote! { iced::Size::new(#w, 100.0) },
2185        (None, Some(h)) => quote! { iced::Size::new(100.0, #h) },
2186        _ => quote! { iced::Size::new(100.0, 100.0) },
2187    };
2188
2189    Ok(quote! {
2190        iced::widget::canvas(#size).into()
2191    })
2192}
2193
2194/// Generate float widget
2195fn generate_float(
2196    node: &crate::WidgetNode,
2197    model_ident: &syn::Ident,
2198    message_ident: &syn::Ident,
2199    style_classes: &HashMap<String, StyleClass>,
2200) -> Result<TokenStream, super::CodegenError> {
2201    let child = node.children.first().ok_or_else(|| {
2202        super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2203    })?;
2204    let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2205
2206    let position = node
2207        .attributes
2208        .get("position")
2209        .and_then(|attr| {
2210            if let AttributeValue::Static(s) = attr {
2211                Some(s.clone())
2212            } else {
2213                None
2214            }
2215        })
2216        .unwrap_or_else(|| "TopRight".to_string());
2217
2218    let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2219        if let AttributeValue::Static(s) = attr {
2220            s.parse::<f32>().ok()
2221        } else {
2222            None
2223        }
2224    });
2225
2226    let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2227        if let AttributeValue::Static(s) = attr {
2228            s.parse::<f32>().ok()
2229        } else {
2230            None
2231        }
2232    });
2233
2234    let float = match position.as_str() {
2235        "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2236        "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2237        "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2238        "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2239        _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2240    };
2241
2242    let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2243        quote! { #float.offset_x(#ox).offset_y(#oy) }
2244    } else if let Some(ox) = offset_x {
2245        quote! { #float.offset_x(#ox) }
2246    } else if let Some(oy) = offset_y {
2247        quote! { #float.offset_y(#oy) }
2248    } else {
2249        float
2250    };
2251
2252    Ok(quote! { #float.into() })
2253}
2254
2255/// Generate for loop widget (iterates over collection)
2256fn generate_for(
2257    node: &crate::WidgetNode,
2258    model_ident: &syn::Ident,
2259    message_ident: &syn::Ident,
2260    style_classes: &HashMap<String, StyleClass>,
2261) -> Result<TokenStream, super::CodegenError> {
2262    let items_attr = node.attributes.get("items").ok_or_else(|| {
2263        super::CodegenError::InvalidWidget("for requires items attribute".to_string())
2264    })?;
2265
2266    let item_name = node
2267        .attributes
2268        .get("item")
2269        .and_then(|attr| {
2270            if let AttributeValue::Static(s) = attr {
2271                Some(s.clone())
2272            } else {
2273                None
2274            }
2275        })
2276        .unwrap_or_else(|| "item".to_string());
2277
2278    let _children: Vec<TokenStream> = node
2279        .children
2280        .iter()
2281        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2282        .collect::<Result<_, _>>()?;
2283
2284    let _items_expr = generate_attribute_value(items_attr, model_ident);
2285    let _item_ident = format_ident!("{}", item_name);
2286
2287    Ok(quote! {
2288        {
2289            let _items = _items_expr;
2290            iced::widget::column(vec![]).into()
2291        }
2292    })
2293}
2294
2295/// Generate custom widget
2296fn generate_custom_widget(
2297    node: &crate::WidgetNode,
2298    name: &str,
2299    model_ident: &syn::Ident,
2300    message_ident: &syn::Ident,
2301    style_classes: &HashMap<String, StyleClass>,
2302) -> Result<TokenStream, super::CodegenError> {
2303    let widget_ident = format_ident!("{}", name);
2304    let children: Vec<TokenStream> = node
2305        .children
2306        .iter()
2307        .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2308        .collect::<Result<_, _>>()?;
2309
2310    Ok(quote! {
2311        #widget_ident(vec![#(#children),*]).into()
2312    })
2313}
2314
2315/// Generate attribute value expression with inlined bindings
2316fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2317    match attr {
2318        AttributeValue::Static(s) => {
2319            let lit = proc_macro2::Literal::string(s);
2320            quote! { #lit.to_string() }
2321        }
2322        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
2323        AttributeValue::Interpolated(parts) => {
2324            let parts_str: Vec<String> = parts
2325                .iter()
2326                .map(|part| match part {
2327                    InterpolatedPart::Literal(s) => s.clone(),
2328                    InterpolatedPart::Binding(_) => "{}".to_string(),
2329                })
2330                .collect();
2331            let binding_exprs: Vec<TokenStream> = parts
2332                .iter()
2333                .filter_map(|part| {
2334                    if let InterpolatedPart::Binding(expr) = part {
2335                        Some(generate_expr(&expr.expr))
2336                    } else {
2337                        None
2338                    }
2339                })
2340                .collect();
2341
2342            let format_string = parts_str.join("");
2343            let lit = proc_macro2::Literal::string(&format_string);
2344
2345            quote! { format!(#lit, #(#binding_exprs),*) }
2346        }
2347    }
2348}
2349
2350#[cfg(test)]
2351mod tests {
2352    use super::*;
2353    use crate::parse;
2354
2355    #[test]
2356    fn test_view_generation() {
2357        let xml = r#"<column><text value="Hello" /></column>"#;
2358        let doc = parse(xml).unwrap();
2359
2360        let result = generate_view(&doc, "Model", "Message").unwrap();
2361        let code = result.to_string();
2362
2363        assert!(code.contains("text"));
2364        assert!(code.contains("column"));
2365    }
2366
2367    #[test]
2368    fn test_view_generation_with_binding() {
2369        let xml = r#"<column><text value="{name}" /></column>"#;
2370        let doc = parse(xml).unwrap();
2371
2372        let result = generate_view(&doc, "Model", "Message").unwrap();
2373        let code = result.to_string();
2374
2375        assert!(code.contains("name"));
2376        assert!(code.contains("to_string"));
2377    }
2378
2379    #[test]
2380    fn test_button_with_handler() {
2381        let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
2382        let doc = parse(xml).unwrap();
2383
2384        let result = generate_view(&doc, "Model", "Message").unwrap();
2385        let code = result.to_string();
2386
2387        assert!(code.contains("button"));
2388        assert!(code.contains("HandleClick"));
2389    }
2390
2391    #[test]
2392    fn test_container_with_children() {
2393        let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
2394        let doc = parse(xml).unwrap();
2395
2396        let result = generate_view(&doc, "Model", "Message").unwrap();
2397        let code = result.to_string();
2398
2399        assert!(code.contains("column"));
2400        assert!(code.contains("spacing"));
2401    }
2402
2403    #[test]
2404    fn test_button_with_inline_style() {
2405        use crate::ir::node::WidgetNode;
2406        use crate::ir::style::{Background, Color, StyleProperties};
2407        use std::collections::HashMap;
2408
2409        // Manually construct a button with inline style
2410        let button_node = WidgetNode {
2411            kind: WidgetKind::Button,
2412            id: None,
2413            attributes: {
2414                let mut attrs = HashMap::new();
2415                attrs.insert(
2416                    "label".to_string(),
2417                    AttributeValue::Static("Test".to_string()),
2418                );
2419                attrs
2420            },
2421            events: vec![],
2422            children: vec![],
2423            span: Default::default(),
2424            style: Some(StyleProperties {
2425                background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
2426                color: Some(Color::from_rgb8(255, 255, 255)),
2427                border: None,
2428                shadow: None,
2429                opacity: None,
2430                transform: None,
2431            }),
2432            layout: None,
2433            theme_ref: None,
2434            classes: vec![],
2435            breakpoint_attributes: HashMap::new(),
2436            inline_state_variants: HashMap::new(),
2437        };
2438
2439        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2440        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2441        let style_classes = HashMap::new();
2442
2443        let result =
2444            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
2445        let code = result.to_string();
2446
2447        // Should contain style closure (note: quote! adds spaces)
2448        assert!(code.contains("style"));
2449        assert!(code.contains("button :: Status"));
2450        assert!(code.contains("button :: Style"));
2451        assert!(code.contains("background"));
2452        assert!(code.contains("text_color"));
2453    }
2454
2455    #[test]
2456    fn test_button_with_css_class() {
2457        use crate::ir::node::WidgetNode;
2458        use crate::ir::theme::StyleClass;
2459        use std::collections::HashMap;
2460
2461        // Manually construct a button with CSS class
2462        let button_node = WidgetNode {
2463            kind: WidgetKind::Button,
2464            id: None,
2465            attributes: {
2466                let mut attrs = HashMap::new();
2467                attrs.insert(
2468                    "label".to_string(),
2469                    AttributeValue::Static("Test".to_string()),
2470                );
2471                attrs
2472            },
2473            events: vec![],
2474            children: vec![],
2475            span: Default::default(),
2476            style: None,
2477            layout: None,
2478            theme_ref: None,
2479            classes: vec!["primary-button".to_string()],
2480            breakpoint_attributes: HashMap::new(),
2481            inline_state_variants: HashMap::new(),
2482        };
2483
2484        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2485        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2486        let style_classes: HashMap<String, StyleClass> = HashMap::new();
2487
2488        let result =
2489            generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
2490        let code = result.to_string();
2491
2492        // Should call style function (note: quote! adds spaces)
2493        assert!(code.contains("style"));
2494        assert!(code.contains("style_primary_button"));
2495    }
2496
2497    #[test]
2498    fn test_container_with_inline_style() {
2499        use crate::ir::node::WidgetNode;
2500        use crate::ir::style::{
2501            Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
2502        };
2503        use crate::ir::theme::StyleClass;
2504        use std::collections::HashMap;
2505
2506        let container_node = WidgetNode {
2507            kind: WidgetKind::Container,
2508            id: None,
2509            attributes: HashMap::new(),
2510            events: vec![],
2511            children: vec![],
2512            span: Default::default(),
2513            style: Some(StyleProperties {
2514                background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
2515                color: None,
2516                border: Some(Border {
2517                    width: 2.0,
2518                    color: Color::from_rgb8(200, 200, 200),
2519                    radius: BorderRadius {
2520                        top_left: 8.0,
2521                        top_right: 8.0,
2522                        bottom_right: 8.0,
2523                        bottom_left: 8.0,
2524                    },
2525                    style: BorderStyle::Solid,
2526                }),
2527                shadow: None,
2528                opacity: None,
2529                transform: None,
2530            }),
2531            layout: None,
2532            theme_ref: None,
2533            classes: vec![],
2534            breakpoint_attributes: HashMap::new(),
2535            inline_state_variants: HashMap::new(),
2536        };
2537
2538        let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2539        let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2540        let style_classes: HashMap<String, StyleClass> = HashMap::new();
2541
2542        let result = generate_container(
2543            &container_node,
2544            "container",
2545            &model_ident,
2546            &message_ident,
2547            &style_classes,
2548        )
2549        .unwrap();
2550        let code = result.to_string();
2551
2552        // Should contain style closure (note: quote! adds spaces)
2553        assert!(code.contains("style"));
2554        assert!(code.contains("container :: Style"));
2555        assert!(code.contains("background"));
2556        assert!(code.contains("border"));
2557    }
2558}