1use 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
11pub 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
27fn 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
66fn 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
82fn 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(¶m.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
120fn 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
166fn 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
183fn generate_space(_node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
185 Ok(quote! {
186 iced::widget::Space::default()
187 })
188}
189
190fn 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
211fn 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
254fn 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
297fn 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
350fn 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
401fn 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
430fn 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
477fn 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
520fn 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
563fn 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
601fn 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
639fn 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
660fn 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
717fn 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
751fn 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
809fn 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
848fn 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
867fn 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}