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::node::{AttributeValue, InterpolatedPart, WidgetKind};
8use proc_macro2::TokenStream;
9use quote::{format_ident, quote};
10
11/// Generate the view function body from a Dampen document
12pub fn generate_view(
13    document: &DampenDocument,
14    _model_name: &str,
15    message_name: &str,
16) -> Result<TokenStream, super::CodegenError> {
17    let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
18    let count_ident = syn::Ident::new("count", proc_macro2::Span::call_site());
19
20    let root_widget = generate_widget(&document.root, &count_ident, &message_ident)?;
21
22    Ok(quote! {
23        #root_widget
24    })
25}
26
27/// Generate code for a widget node
28fn generate_widget(
29    node: &crate::WidgetNode,
30    model_ident: &syn::Ident,
31    message_ident: &syn::Ident,
32) -> Result<TokenStream, super::CodegenError> {
33    match node.kind {
34        WidgetKind::Text => generate_text(node, model_ident),
35        WidgetKind::Button => generate_button(node, model_ident, message_ident),
36        WidgetKind::Column => generate_container(node, "column", model_ident, message_ident),
37        WidgetKind::Row => generate_container(node, "row", model_ident, message_ident),
38        WidgetKind::Container => generate_container(node, "container", model_ident, message_ident),
39        WidgetKind::Scrollable => {
40            generate_container(node, "scrollable", model_ident, message_ident)
41        }
42        WidgetKind::Stack => generate_stack(node, model_ident, message_ident),
43        WidgetKind::Space => generate_space(node),
44        WidgetKind::Rule => generate_rule(node),
45        WidgetKind::Checkbox => generate_checkbox(node, model_ident, message_ident),
46        WidgetKind::Toggler => generate_toggler(node, model_ident, message_ident),
47        WidgetKind::Slider => generate_slider(node, model_ident, message_ident),
48        WidgetKind::Radio => generate_radio(node, model_ident, message_ident),
49        WidgetKind::ProgressBar => generate_progress_bar(node, model_ident),
50        WidgetKind::TextInput => generate_text_input(node, model_ident, message_ident),
51        WidgetKind::Image => generate_image(node),
52        WidgetKind::Svg => generate_svg(node),
53        WidgetKind::PickList => generate_pick_list(node, model_ident, message_ident),
54        WidgetKind::ComboBox => generate_combo_box(node, model_ident, message_ident),
55        WidgetKind::Tooltip => generate_tooltip(node, model_ident, message_ident),
56        WidgetKind::Grid => generate_grid(node, model_ident, message_ident),
57        WidgetKind::Canvas => generate_canvas(node, model_ident, message_ident),
58        WidgetKind::Float => generate_float(node, model_ident, message_ident),
59        WidgetKind::For => generate_for(node, model_ident, message_ident),
60        WidgetKind::Custom(ref name) => {
61            generate_custom_widget(node, name, model_ident, message_ident)
62        }
63    }
64}
65
66/// Generate text widget
67fn generate_text(
68    node: &crate::WidgetNode,
69    model_ident: &syn::Ident,
70) -> Result<TokenStream, super::CodegenError> {
71    let value_attr = node.attributes.get("value").ok_or_else(|| {
72        super::CodegenError::InvalidWidget("text requires value attribute".to_string())
73    })?;
74
75    let value_expr = generate_attribute_value(value_attr, model_ident);
76
77    Ok(quote! {
78        iced::widget::text(#value_expr)
79    })
80}
81
82/// Generate button widget
83fn generate_button(
84    node: &crate::WidgetNode,
85    model_ident: &syn::Ident,
86    message_ident: &syn::Ident,
87) -> Result<TokenStream, super::CodegenError> {
88    let label_attr = node.attributes.get("label").ok_or_else(|| {
89        super::CodegenError::InvalidWidget("button requires label attribute".to_string())
90    })?;
91
92    let label_expr = generate_attribute_value(label_attr, model_ident);
93
94    let on_click = node
95        .events
96        .iter()
97        .find(|e| e.event == crate::EventKind::Click);
98
99    if let Some(event) = on_click {
100        let handler_ident = format_ident!("{}", event.handler);
101
102        let param_expr = if let Some(ref param) = event.param {
103            let param_tokens = generate_expr(&param.expr);
104            quote! { Some(#param_tokens) }
105        } else {
106            quote! { None }
107        };
108
109        Ok(quote! {
110            iced::widget::button(#label_expr)
111                .on_press(#message_ident::#handler_ident(#param_expr))
112        })
113    } else {
114        Ok(quote! {
115            iced::widget::button(#label_expr)
116        })
117    }
118}
119
120/// Generate container widget (column, row, container, scrollable)
121fn generate_container(
122    node: &crate::WidgetNode,
123    widget_type: &str,
124    model_ident: &syn::Ident,
125    message_ident: &syn::Ident,
126) -> Result<TokenStream, super::CodegenError> {
127    let children: Vec<TokenStream> = node
128        .children
129        .iter()
130        .map(|child| generate_widget(child, model_ident, message_ident))
131        .collect::<Result<_, _>>()?;
132
133    let widget_ident = format_ident!("{}", widget_type);
134
135    let spacing = node.attributes.get("spacing").and_then(|attr| {
136        if let AttributeValue::Static(s) = attr {
137            s.parse::<f32>().ok()
138        } else {
139            None
140        }
141    });
142
143    let padding = node.attributes.get("padding").and_then(|attr| {
144        if let AttributeValue::Static(s) = attr {
145            s.parse::<f32>().ok()
146        } else {
147            None
148        }
149    });
150
151    let mut widget = quote! {
152        iced::widget::#widget_ident(vec![#(#children),*])
153    };
154
155    if let Some(s) = spacing {
156        widget = quote! { #widget.spacing(#s) };
157    }
158
159    if let Some(p) = padding {
160        widget = quote! { #widget.padding(#p) };
161    }
162
163    Ok(widget)
164}
165
166/// Generate stack widget
167fn generate_stack(
168    node: &crate::WidgetNode,
169    model_ident: &syn::Ident,
170    message_ident: &syn::Ident,
171) -> Result<TokenStream, super::CodegenError> {
172    let children: Vec<TokenStream> = node
173        .children
174        .iter()
175        .map(|child| generate_widget(child, model_ident, message_ident))
176        .collect::<Result<_, _>>()?;
177
178    Ok(quote! {
179        iced::widget::stack(vec![#(#children),*])
180    })
181}
182
183/// Generate space widget
184fn generate_space(_node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
185    Ok(quote! {
186        iced::widget::Space::default()
187    })
188}
189
190/// Generate rule (horizontal line) widget
191fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
192    let width = node.attributes.get("width").and_then(|attr| {
193        if let AttributeValue::Static(s) = attr {
194            s.parse::<f32>().ok()
195        } else {
196            None
197        }
198    });
199
200    if let Some(w) = width {
201        Ok(quote! {
202            iced::widget::rule::Rule::default().width(#w)
203        })
204    } else {
205        Ok(quote! {
206            iced::widget::Rule::default()
207        })
208    }
209}
210
211/// Generate checkbox widget
212fn generate_checkbox(
213    node: &crate::WidgetNode,
214    _model_ident: &syn::Ident,
215    message_ident: &syn::Ident,
216) -> Result<TokenStream, super::CodegenError> {
217    let label = node
218        .attributes
219        .get("label")
220        .and_then(|attr| {
221            if let AttributeValue::Static(s) = attr {
222                Some(s.clone())
223            } else {
224                None
225            }
226        })
227        .unwrap_or_default();
228    let label_lit = proc_macro2::Literal::string(&label);
229    let label_expr = quote! { #label_lit.to_string() };
230
231    let checked_attr = node.attributes.get("checked");
232    let checked_expr = checked_attr
233        .map(|attr| generate_attribute_value(attr, _model_ident))
234        .unwrap_or(quote! { false });
235
236    let on_toggle = node
237        .events
238        .iter()
239        .find(|e| e.event == crate::EventKind::Toggle);
240
241    if let Some(event) = on_toggle {
242        let handler_ident = format_ident!("{}", event.handler);
243        Ok(quote! {
244            iced::widget::checkbox(#label_expr, #checked_expr)
245                .on_toggle(#message_ident::#handler_ident)
246        })
247    } else {
248        Ok(quote! {
249            iced::widget::checkbox(#label_expr, #checked_expr)
250        })
251    }
252}
253
254/// Generate toggler widget
255fn generate_toggler(
256    node: &crate::WidgetNode,
257    _model_ident: &syn::Ident,
258    message_ident: &syn::Ident,
259) -> Result<TokenStream, super::CodegenError> {
260    let label = node
261        .attributes
262        .get("label")
263        .and_then(|attr| {
264            if let AttributeValue::Static(s) = attr {
265                Some(s.clone())
266            } else {
267                None
268            }
269        })
270        .unwrap_or_default();
271    let label_lit = proc_macro2::Literal::string(&label);
272    let label_expr = quote! { #label_lit.to_string() };
273
274    let is_toggled_attr = node.attributes.get("toggled");
275    let is_toggled_expr = is_toggled_attr
276        .map(|attr| generate_attribute_value(attr, _model_ident))
277        .unwrap_or(quote! { false });
278
279    let on_toggle = node
280        .events
281        .iter()
282        .find(|e| e.event == crate::EventKind::Toggle);
283
284    if let Some(event) = on_toggle {
285        let handler_ident = format_ident!("{}", event.handler);
286        Ok(quote! {
287            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
288                .on_toggle(|_| #message_ident::#handler_ident)
289        })
290    } else {
291        Ok(quote! {
292            iced::widget::toggler(#label_expr, #is_toggled_expr, None)
293        })
294    }
295}
296
297/// Generate slider widget
298fn generate_slider(
299    node: &crate::WidgetNode,
300    model_ident: &syn::Ident,
301    message_ident: &syn::Ident,
302) -> Result<TokenStream, super::CodegenError> {
303    let min = node.attributes.get("min").and_then(|attr| {
304        if let AttributeValue::Static(s) = attr {
305            s.parse::<f32>().ok()
306        } else {
307            None
308        }
309    });
310
311    let max = node.attributes.get("max").and_then(|attr| {
312        if let AttributeValue::Static(s) = attr {
313            s.parse::<f32>().ok()
314        } else {
315            None
316        }
317    });
318
319    let value_attr = node.attributes.get("value").ok_or_else(|| {
320        super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
321    })?;
322    let value_expr = generate_attribute_value(value_attr, model_ident);
323
324    let on_change = node
325        .events
326        .iter()
327        .find(|e| e.event == crate::EventKind::Change);
328
329    let mut slider = quote! {
330        iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
331    };
332
333    if let Some(m) = min {
334        slider = quote! { #slider.min(#m) };
335    }
336    if let Some(m) = max {
337        slider = quote! { #slider.max(#m) };
338    }
339
340    if let Some(event) = on_change {
341        let handler_ident = format_ident!("{}", event.handler);
342        slider = quote! {
343            iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
344        };
345    }
346
347    Ok(slider)
348}
349
350/// Generate radio widget
351fn generate_radio(
352    node: &crate::WidgetNode,
353    _model_ident: &syn::Ident,
354    message_ident: &syn::Ident,
355) -> Result<TokenStream, super::CodegenError> {
356    let label = node
357        .attributes
358        .get("label")
359        .and_then(|attr| {
360            if let AttributeValue::Static(s) = attr {
361                Some(s.clone())
362            } else {
363                None
364            }
365        })
366        .unwrap_or_default();
367    let label_lit = proc_macro2::Literal::string(&label);
368    let label_expr = quote! { #label_lit.to_string() };
369
370    let value_attr = node.attributes.get("value").ok_or_else(|| {
371        super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
372    })?;
373    let value_expr = match value_attr {
374        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
375        _ => quote! { String::new() },
376    };
377
378    let selected_attr = node.attributes.get("selected");
379    let selected_expr = match selected_attr {
380        Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
381        _ => quote! { None },
382    };
383
384    let on_select = node
385        .events
386        .iter()
387        .find(|e| e.event == crate::EventKind::Select);
388
389    if let Some(event) = on_select {
390        let handler_ident = format_ident!("{}", event.handler);
391        Ok(quote! {
392            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v))
393        })
394    } else {
395        Ok(quote! {
396            iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ())
397        })
398    }
399}
400
401/// Generate progress bar widget
402fn generate_progress_bar(
403    node: &crate::WidgetNode,
404    model_ident: &syn::Ident,
405) -> Result<TokenStream, super::CodegenError> {
406    let value_attr = node.attributes.get("value").ok_or_else(|| {
407        super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
408    })?;
409    let value_expr = generate_attribute_value(value_attr, model_ident);
410
411    let max_attr = node.attributes.get("max").and_then(|attr| {
412        if let AttributeValue::Static(s) = attr {
413            s.parse::<f32>().ok()
414        } else {
415            None
416        }
417    });
418
419    if let Some(max) = max_attr {
420        Ok(quote! {
421            iced::widget::progress_bar(0.0..=#max, #value_expr)
422        })
423    } else {
424        Ok(quote! {
425            iced::widget::progress_bar(0.0..=100.0, #value_expr)
426        })
427    }
428}
429
430/// Generate text input widget
431fn generate_text_input(
432    node: &crate::WidgetNode,
433    model_ident: &syn::Ident,
434    message_ident: &syn::Ident,
435) -> Result<TokenStream, super::CodegenError> {
436    let value_expr = node
437        .attributes
438        .get("value")
439        .map(|attr| generate_attribute_value(attr, model_ident))
440        .unwrap_or(quote! { String::new() });
441
442    let placeholder = node.attributes.get("placeholder").and_then(|attr| {
443        if let AttributeValue::Static(s) = attr {
444            Some(s.clone())
445        } else {
446            None
447        }
448    });
449
450    let on_input = node
451        .events
452        .iter()
453        .find(|e| e.event == crate::EventKind::Input);
454
455    let mut text_input = match placeholder {
456        Some(ph) => {
457            let ph_lit = proc_macro2::Literal::string(&ph);
458            quote! {
459                iced::widget::text_input(#ph_lit, &#value_expr)
460            }
461        }
462        None => quote! {
463            iced::widget::text_input("", &#value_expr)
464        },
465    };
466
467    if let Some(event) = on_input {
468        let handler_ident = format_ident!("{}", event.handler);
469        text_input = quote! {
470            #text_input.on_input(|v| #message_ident::#handler_ident(v))
471        };
472    }
473
474    Ok(text_input)
475}
476
477/// Generate image widget
478fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
479    let src_attr = node.attributes.get("src").ok_or_else(|| {
480        super::CodegenError::InvalidWidget("image requires src attribute".to_string())
481    })?;
482
483    let src = match src_attr {
484        AttributeValue::Static(s) => s.clone(),
485        _ => String::new(),
486    };
487    let src_lit = proc_macro2::Literal::string(&src);
488
489    let width = node.attributes.get("width").and_then(|attr| {
490        if let AttributeValue::Static(s) = attr {
491            s.parse::<u32>().ok()
492        } else {
493            None
494        }
495    });
496
497    let height = node.attributes.get("height").and_then(|attr| {
498        if let AttributeValue::Static(s) = attr {
499            s.parse::<u32>().ok()
500        } else {
501            None
502        }
503    });
504
505    let image = quote! {
506        iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
507    };
508
509    if let (Some(w), Some(h)) = (width, height) {
510        Ok(quote! { #image.width(#w).height(#h) })
511    } else if let Some(w) = width {
512        Ok(quote! { #image.width(#w) })
513    } else if let Some(h) = height {
514        Ok(quote! { #image.height(#h) })
515    } else {
516        Ok(image)
517    }
518}
519
520/// Generate SVG widget
521fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
522    let path_attr = node.attributes.get("path").ok_or_else(|| {
523        super::CodegenError::InvalidWidget("svg requires path attribute".to_string())
524    })?;
525
526    let path = match path_attr {
527        AttributeValue::Static(s) => s.clone(),
528        _ => String::new(),
529    };
530    let path_lit = proc_macro2::Literal::string(&path);
531
532    let width = node.attributes.get("width").and_then(|attr| {
533        if let AttributeValue::Static(s) = attr {
534            s.parse::<u32>().ok()
535        } else {
536            None
537        }
538    });
539
540    let height = node.attributes.get("height").and_then(|attr| {
541        if let AttributeValue::Static(s) = attr {
542            s.parse::<u32>().ok()
543        } else {
544            None
545        }
546    });
547
548    let svg = quote! {
549        iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
550    };
551
552    if let (Some(w), Some(h)) = (width, height) {
553        Ok(quote! { #svg.width(#w).height(#h) })
554    } else if let Some(w) = width {
555        Ok(quote! { #svg.width(#w) })
556    } else if let Some(h) = height {
557        Ok(quote! { #svg.height(#h) })
558    } else {
559        Ok(svg)
560    }
561}
562
563/// Generate pick list widget
564fn generate_pick_list(
565    node: &crate::WidgetNode,
566    model_ident: &syn::Ident,
567    message_ident: &syn::Ident,
568) -> Result<TokenStream, super::CodegenError> {
569    let options_attr = node.attributes.get("options").ok_or_else(|| {
570        super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
571    })?;
572
573    let options: Vec<String> = match options_attr {
574        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
575        _ => Vec::new(),
576    };
577    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
578
579    let selected_attr = node.attributes.get("selected");
580    let selected_expr = selected_attr
581        .map(|attr| generate_attribute_value(attr, model_ident))
582        .unwrap_or(quote! { None });
583
584    let on_select = node
585        .events
586        .iter()
587        .find(|e| e.event == crate::EventKind::Select);
588
589    if let Some(event) = on_select {
590        let handler_ident = format_ident!("{}", event.handler);
591        Ok(quote! {
592            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v))
593        })
594    } else {
595        Ok(quote! {
596            iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ())
597        })
598    }
599}
600
601/// Generate combo box widget
602fn generate_combo_box(
603    node: &crate::WidgetNode,
604    model_ident: &syn::Ident,
605    message_ident: &syn::Ident,
606) -> Result<TokenStream, super::CodegenError> {
607    let options_attr = node.attributes.get("options").ok_or_else(|| {
608        super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
609    })?;
610
611    let options: Vec<String> = match options_attr {
612        AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
613        _ => Vec::new(),
614    };
615    let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
616
617    let selected_attr = node.attributes.get("selected");
618    let selected_expr = selected_attr
619        .map(|attr| generate_attribute_value(attr, model_ident))
620        .unwrap_or(quote! { None });
621
622    let on_select = node
623        .events
624        .iter()
625        .find(|e| e.event == crate::EventKind::Select);
626
627    if let Some(event) = on_select {
628        let handler_ident = format_ident!("{}", event.handler);
629        Ok(quote! {
630            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v))
631        })
632    } else {
633        Ok(quote! {
634            iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ())
635        })
636    }
637}
638
639/// Generate tooltip widget
640fn generate_tooltip(
641    node: &crate::WidgetNode,
642    model_ident: &syn::Ident,
643    message_ident: &syn::Ident,
644) -> Result<TokenStream, super::CodegenError> {
645    let child = node.children.first().ok_or_else(|| {
646        super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
647    })?;
648    let child_widget = generate_widget(child, model_ident, message_ident)?;
649
650    let message_attr = node.attributes.get("message").ok_or_else(|| {
651        super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
652    })?;
653    let message_expr = generate_attribute_value(message_attr, model_ident);
654
655    Ok(quote! {
656        iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor)
657    })
658}
659
660/// Generate grid widget
661fn generate_grid(
662    node: &crate::WidgetNode,
663    model_ident: &syn::Ident,
664    message_ident: &syn::Ident,
665) -> Result<TokenStream, super::CodegenError> {
666    let children: Vec<TokenStream> = node
667        .children
668        .iter()
669        .map(|child| generate_widget(child, model_ident, message_ident))
670        .collect::<Result<_, _>>()?;
671
672    let columns = node
673        .attributes
674        .get("columns")
675        .and_then(|attr| {
676            if let AttributeValue::Static(s) = attr {
677                s.parse::<u32>().ok()
678            } else {
679                None
680            }
681        })
682        .unwrap_or(1);
683
684    let spacing = node.attributes.get("spacing").and_then(|attr| {
685        if let AttributeValue::Static(s) = attr {
686            s.parse::<f32>().ok()
687        } else {
688            None
689        }
690    });
691
692    let padding = node.attributes.get("padding").and_then(|attr| {
693        if let AttributeValue::Static(s) = attr {
694            s.parse::<f32>().ok()
695        } else {
696            None
697        }
698    });
699
700    let grid = quote! {
701        iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
702    };
703
704    let grid = if let Some(s) = spacing {
705        quote! { #grid.spacing(#s) }
706    } else {
707        grid
708    };
709
710    Ok(if let Some(p) = padding {
711        quote! { #grid.padding(#p) }
712    } else {
713        grid
714    })
715}
716
717/// Generate canvas widget
718fn generate_canvas(
719    node: &crate::WidgetNode,
720    _model_ident: &syn::Ident,
721    _message_ident: &syn::Ident,
722) -> Result<TokenStream, super::CodegenError> {
723    let width = node.attributes.get("width").and_then(|attr| {
724        if let AttributeValue::Static(s) = attr {
725            s.parse::<f32>().ok()
726        } else {
727            None
728        }
729    });
730
731    let height = node.attributes.get("height").and_then(|attr| {
732        if let AttributeValue::Static(s) = attr {
733            s.parse::<f32>().ok()
734        } else {
735            None
736        }
737    });
738
739    let size = match (width, height) {
740        (Some(w), Some(h)) => quote! { iced::Size::new(#w, #h) },
741        (Some(w), None) => quote! { iced::Size::new(#w, 100.0) },
742        (None, Some(h)) => quote! { iced::Size::new(100.0, #h) },
743        _ => quote! { iced::Size::new(100.0, 100.0) },
744    };
745
746    Ok(quote! {
747        iced::widget::canvas(#size)
748    })
749}
750
751/// Generate float widget
752fn generate_float(
753    node: &crate::WidgetNode,
754    model_ident: &syn::Ident,
755    message_ident: &syn::Ident,
756) -> Result<TokenStream, super::CodegenError> {
757    let child = node.children.first().ok_or_else(|| {
758        super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
759    })?;
760    let child_widget = generate_widget(child, model_ident, message_ident)?;
761
762    let position = node
763        .attributes
764        .get("position")
765        .and_then(|attr| {
766            if let AttributeValue::Static(s) = attr {
767                Some(s.clone())
768            } else {
769                None
770            }
771        })
772        .unwrap_or_else(|| "TopRight".to_string());
773
774    let offset_x = node.attributes.get("offset_x").and_then(|attr| {
775        if let AttributeValue::Static(s) = attr {
776            s.parse::<f32>().ok()
777        } else {
778            None
779        }
780    });
781
782    let offset_y = node.attributes.get("offset_y").and_then(|attr| {
783        if let AttributeValue::Static(s) = attr {
784            s.parse::<f32>().ok()
785        } else {
786            None
787        }
788    });
789
790    let float = match position.as_str() {
791        "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
792        "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
793        "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
794        "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
795        _ => quote! { iced::widget::float::float_top_right(#child_widget) },
796    };
797
798    if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
799        Ok(quote! { #float.offset_x(#ox).offset_y(#oy) })
800    } else if let Some(ox) = offset_x {
801        Ok(quote! { #float.offset_x(#ox) })
802    } else if let Some(oy) = offset_y {
803        Ok(quote! { #float.offset_y(#oy) })
804    } else {
805        Ok(float)
806    }
807}
808
809/// Generate for loop widget (iterates over collection)
810fn generate_for(
811    node: &crate::WidgetNode,
812    model_ident: &syn::Ident,
813    message_ident: &syn::Ident,
814) -> Result<TokenStream, super::CodegenError> {
815    let items_attr = node.attributes.get("items").ok_or_else(|| {
816        super::CodegenError::InvalidWidget("for requires items attribute".to_string())
817    })?;
818
819    let item_name = node
820        .attributes
821        .get("item")
822        .and_then(|attr| {
823            if let AttributeValue::Static(s) = attr {
824                Some(s.clone())
825            } else {
826                None
827            }
828        })
829        .unwrap_or_else(|| "item".to_string());
830
831    let _children: Vec<TokenStream> = node
832        .children
833        .iter()
834        .map(|child| generate_widget(child, model_ident, message_ident))
835        .collect::<Result<_, _>>()?;
836
837    let _items_expr = generate_attribute_value(items_attr, model_ident);
838    let _item_ident = format_ident!("{}", item_name);
839
840    Ok(quote! {
841        {
842            let _items = _items_expr;
843            iced::widget::column(vec![])
844        }
845    })
846}
847
848/// Generate custom widget
849fn generate_custom_widget(
850    node: &crate::WidgetNode,
851    name: &str,
852    model_ident: &syn::Ident,
853    message_ident: &syn::Ident,
854) -> Result<TokenStream, super::CodegenError> {
855    let widget_ident = format_ident!("{}", name);
856    let children: Vec<TokenStream> = node
857        .children
858        .iter()
859        .map(|child| generate_widget(child, model_ident, message_ident))
860        .collect::<Result<_, _>>()?;
861
862    Ok(quote! {
863        #widget_ident(vec![#(#children),*])
864    })
865}
866
867/// Generate attribute value expression with inlined bindings
868fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
869    match attr {
870        AttributeValue::Static(s) => {
871            let lit = proc_macro2::Literal::string(s);
872            quote! { #lit.to_string() }
873        }
874        AttributeValue::Binding(expr) => generate_expr(&expr.expr),
875        AttributeValue::Interpolated(parts) => {
876            let parts_str: Vec<String> = parts
877                .iter()
878                .map(|part| match part {
879                    InterpolatedPart::Literal(s) => s.clone(),
880                    InterpolatedPart::Binding(_) => "{}".to_string(),
881                })
882                .collect();
883            let binding_exprs: Vec<TokenStream> = parts
884                .iter()
885                .filter_map(|part| {
886                    if let InterpolatedPart::Binding(expr) = part {
887                        Some(generate_expr(&expr.expr))
888                    } else {
889                        None
890                    }
891                })
892                .collect();
893
894            let format_string = parts_str.join("");
895            let lit = proc_macro2::Literal::string(&format_string);
896
897            quote! { format!(#lit, #(#binding_exprs),*) }
898        }
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::parse;
906
907    #[test]
908    fn test_view_generation() {
909        let xml = r#"<column><text value="Hello" /></column>"#;
910        let doc = parse(xml).unwrap();
911
912        let result = generate_view(&doc, "Model", "Message").unwrap();
913        let code = result.to_string();
914
915        assert!(code.contains("text"));
916        assert!(code.contains("column"));
917    }
918
919    #[test]
920    fn test_view_generation_with_binding() {
921        let xml = r#"<column><text value="{name}" /></column>"#;
922        let doc = parse(xml).unwrap();
923
924        let result = generate_view(&doc, "Model", "Message").unwrap();
925        let code = result.to_string();
926
927        assert!(code.contains("name"));
928        assert!(code.contains("to_string"));
929    }
930
931    #[test]
932    fn test_button_with_handler() {
933        let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
934        let doc = parse(xml).unwrap();
935
936        let result = generate_view(&doc, "Model", "Message").unwrap();
937        let code = result.to_string();
938
939        assert!(code.contains("button"));
940        assert!(code.contains("handle_click"));
941    }
942
943    #[test]
944    fn test_container_with_children() {
945        let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
946        let doc = parse(xml).unwrap();
947
948        let result = generate_view(&doc, "Model", "Message").unwrap();
949        let code = result.to_string();
950
951        assert!(code.contains("column"));
952        assert!(code.contains("spacing"));
953    }
954}