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