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