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_attr = node.attributes.get("bar_color");
2062 let bar_color_static = bar_color_attr.and_then(|attr| {
2063 if let AttributeValue::Static(s) = attr {
2064 parse_color_to_tokens(s)
2065 } else {
2066 None
2067 }
2068 });
2069 let bar_color_binding =
2070 bar_color_attr.filter(|attr| !matches!(attr, AttributeValue::Static(_)));
2071
2072 let background_color = node.attributes.get("background_color").and_then(|attr| {
2073 if let AttributeValue::Static(s) = attr {
2074 parse_color_to_tokens(s)
2075 } else {
2076 None
2077 }
2078 });
2079
2080 let border_radius = node.attributes.get("border_radius").and_then(|attr| {
2082 if let AttributeValue::Static(s) = attr {
2083 s.parse::<f32>().ok()
2084 } else {
2085 None
2086 }
2087 });
2088
2089 let height = node.attributes.get("height").and_then(|attr| {
2091 if let AttributeValue::Static(s) = attr {
2092 s.parse::<f32>().ok()
2093 } else {
2094 None
2095 }
2096 });
2097
2098 let width = node.attributes.get("width").and_then(|attr| {
2100 if let AttributeValue::Static(s) = attr {
2101 Some(generate_length_expr(s))
2102 } else {
2103 None
2104 }
2105 });
2106
2107 let bar_color_expr = if let Some(color_tokens) = bar_color_static {
2109 quote! { #color_tokens }
2110 } else if let Some(attr) = bar_color_binding {
2111 let color_expr = generate_attribute_value(attr, model_ident);
2113 quote! {
2114 {
2115 let color_str = #color_expr;
2116 let parsed_color = (|| {
2118 let s = color_str.trim();
2119 if let Some(hex) = s.strip_prefix('#') {
2121 if hex.len() == 6 {
2122 if let (Ok(r), Ok(g), Ok(b)) = (
2123 u8::from_str_radix(&hex[0..2], 16),
2124 u8::from_str_radix(&hex[2..4], 16),
2125 u8::from_str_radix(&hex[4..6], 16),
2126 ) {
2127 return Some(iced::Color::from_rgb(
2128 r as f32 / 255.0,
2129 g as f32 / 255.0,
2130 b as f32 / 255.0,
2131 ));
2132 }
2133 } else if hex.len() == 8 {
2134 if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2135 u8::from_str_radix(&hex[0..2], 16),
2136 u8::from_str_radix(&hex[2..4], 16),
2137 u8::from_str_radix(&hex[4..6], 16),
2138 u8::from_str_radix(&hex[6..8], 16),
2139 ) {
2140 return Some(iced::Color::from_rgba(
2141 r as f32 / 255.0,
2142 g as f32 / 255.0,
2143 b as f32 / 255.0,
2144 a as f32 / 255.0,
2145 ));
2146 }
2147 }
2148 }
2149 if s.starts_with("rgb(") && s.ends_with(')') {
2151 let inner = &s[4..s.len() - 1];
2152 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2153 if parts.len() == 3 {
2154 if let (Ok(r), Ok(g), Ok(b)) = (
2155 parts[0].parse::<u8>(),
2156 parts[1].parse::<u8>(),
2157 parts[2].parse::<u8>(),
2158 ) {
2159 return Some(iced::Color::from_rgb(
2160 r as f32 / 255.0,
2161 g as f32 / 255.0,
2162 b as f32 / 255.0,
2163 ));
2164 }
2165 }
2166 }
2167 if s.starts_with("rgba(") && s.ends_with(')') {
2169 let inner = &s[5..s.len() - 1];
2170 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2171 if parts.len() == 4 {
2172 if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2173 parts[0].parse::<u8>(),
2174 parts[1].parse::<u8>(),
2175 parts[2].parse::<u8>(),
2176 parts[3].parse::<f32>(),
2177 ) {
2178 return Some(iced::Color::from_rgba(
2179 r as f32 / 255.0,
2180 g as f32 / 255.0,
2181 b as f32 / 255.0,
2182 a,
2183 ));
2184 }
2185 }
2186 }
2187 None
2188 })();
2189 parsed_color.unwrap_or_else(|| palette.primary.base.color)
2190 }
2191 }
2192 } else {
2193 match style_str.as_str() {
2194 "success" => quote! { palette.success.base.color },
2195 "warning" => quote! { palette.warning.base.color },
2196 "danger" => quote! { palette.danger.base.color },
2197 "secondary" => quote! { palette.secondary.base.color },
2198 _ => quote! { palette.primary.base.color }, }
2200 };
2201
2202 let background_color_expr = if let Some(color_tokens) = background_color {
2204 quote! { #color_tokens }
2205 } else {
2206 quote! { palette.background.weak.color }
2207 };
2208
2209 let border_expr = if let Some(radius) = border_radius {
2211 quote! { iced::Border::default().rounded(#radius) }
2212 } else {
2213 quote! { iced::Border::default() }
2214 };
2215
2216 let girth_expr = if let Some(h) = height {
2218 quote! { .girth(#h) }
2219 } else {
2220 quote! {}
2221 };
2222
2223 let width_expr = if let Some(w) = width {
2225 quote! { .length(#w) }
2226 } else {
2227 quote! {}
2228 };
2229
2230 if let Some(max) = max_attr {
2231 Ok(quote! {
2232 iced::widget::progress_bar(0.0..=#max, #value_expr)
2233 #girth_expr
2234 #width_expr
2235 .style(|theme: &iced::Theme| {
2236 let palette = theme.extended_palette();
2237 iced::widget::progress_bar::Style {
2238 background: iced::Background::Color(#background_color_expr),
2239 bar: iced::Background::Color(#bar_color_expr),
2240 border: #border_expr,
2241 }
2242 })
2243 .into()
2244 })
2245 } else {
2246 Ok(quote! {
2247 iced::widget::progress_bar(0.0..=100.0, #value_expr)
2248 #girth_expr
2249 #width_expr
2250 .style(|theme: &iced::Theme| {
2251 let palette = theme.extended_palette();
2252 iced::widget::progress_bar::Style {
2253 background: iced::Background::Color(#background_color_expr),
2254 bar: iced::Background::Color(#bar_color_expr),
2255 border: #border_expr,
2256 }
2257 })
2258 .into()
2259 })
2260 }
2261}
2262
2263fn parse_color_to_tokens(color_str: &str) -> Option<TokenStream> {
2265 if let Some(hex) = color_str.strip_prefix('#') {
2267 if hex.len() == 6 {
2268 if let (Ok(r), Ok(g), Ok(b)) = (
2269 u8::from_str_radix(&hex[0..2], 16),
2270 u8::from_str_radix(&hex[2..4], 16),
2271 u8::from_str_radix(&hex[4..6], 16),
2272 ) {
2273 let rf = r as f32 / 255.0;
2274 let gf = g as f32 / 255.0;
2275 let bf = b as f32 / 255.0;
2276 return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2277 }
2278 } else if hex.len() == 8
2279 && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2280 u8::from_str_radix(&hex[0..2], 16),
2281 u8::from_str_radix(&hex[2..4], 16),
2282 u8::from_str_radix(&hex[4..6], 16),
2283 u8::from_str_radix(&hex[6..8], 16),
2284 )
2285 {
2286 let rf = r as f32 / 255.0;
2287 let gf = g as f32 / 255.0;
2288 let bf = b as f32 / 255.0;
2289 let af = a as f32 / 255.0;
2290 return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #af) });
2291 }
2292 }
2293
2294 if color_str.starts_with("rgb(") && color_str.ends_with(')') {
2296 let inner = &color_str[4..color_str.len() - 1];
2297 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2298 if parts.len() == 3
2299 && let (Ok(r), Ok(g), Ok(b)) = (
2300 parts[0].parse::<u8>(),
2301 parts[1].parse::<u8>(),
2302 parts[2].parse::<u8>(),
2303 )
2304 {
2305 let rf = r as f32 / 255.0;
2306 let gf = g as f32 / 255.0;
2307 let bf = b as f32 / 255.0;
2308 return Some(quote! { iced::Color::from_rgb(#rf, #gf, #bf) });
2309 }
2310 }
2311
2312 if color_str.starts_with("rgba(") && color_str.ends_with(')') {
2314 let inner = &color_str[5..color_str.len() - 1];
2315 let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
2316 if parts.len() == 4
2317 && let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
2318 parts[0].parse::<u8>(),
2319 parts[1].parse::<u8>(),
2320 parts[2].parse::<u8>(),
2321 parts[3].parse::<f32>(),
2322 )
2323 {
2324 let rf = r as f32 / 255.0;
2325 let gf = g as f32 / 255.0;
2326 let bf = b as f32 / 255.0;
2327 return Some(quote! { iced::Color::from_rgba(#rf, #gf, #bf, #a) });
2328 }
2329 }
2330
2331 None
2332}
2333
2334fn generate_text_input(
2336 node: &crate::WidgetNode,
2337 model_ident: &syn::Ident,
2338 message_ident: &syn::Ident,
2339 style_classes: &HashMap<String, StyleClass>,
2340) -> Result<TokenStream, super::CodegenError> {
2341 let value_expr = node
2342 .attributes
2343 .get("value")
2344 .map(|attr| generate_attribute_value(attr, model_ident))
2345 .unwrap_or(quote! { String::new() });
2346
2347 let placeholder = node.attributes.get("placeholder").and_then(|attr| {
2348 if let AttributeValue::Static(s) = attr {
2349 Some(s.clone())
2350 } else {
2351 None
2352 }
2353 });
2354
2355 let on_input = node
2356 .events
2357 .iter()
2358 .find(|e| e.event == crate::EventKind::Input);
2359
2360 let on_submit = node
2361 .events
2362 .iter()
2363 .find(|e| e.event == crate::EventKind::Submit);
2364
2365 let mut text_input = match placeholder {
2366 Some(ph) => {
2367 let ph_lit = proc_macro2::Literal::string(&ph);
2368 quote! {
2369 iced::widget::text_input(#ph_lit, &#value_expr)
2370 }
2371 }
2372 None => quote! {
2373 iced::widget::text_input("", &#value_expr)
2374 },
2375 };
2376
2377 if let Some(event) = on_input {
2378 let variant_name = to_upper_camel_case(&event.handler);
2379 let handler_ident = format_ident!("{}", variant_name);
2380 text_input = quote! {
2381 #text_input.on_input(|v| #message_ident::#handler_ident(v))
2382 };
2383 }
2384
2385 if let Some(event) = on_submit {
2386 let variant_name = to_upper_camel_case(&event.handler);
2387 let handler_ident = format_ident!("{}", variant_name);
2388 text_input = quote! {
2389 #text_input.on_submit(#message_ident::#handler_ident)
2390 };
2391 }
2392
2393 let is_password = node
2395 .attributes
2396 .get("password")
2397 .or_else(|| node.attributes.get("secure"))
2398 .and_then(|attr| {
2399 if let AttributeValue::Static(s) = attr {
2400 Some(s.to_lowercase() == "true" || s == "1")
2401 } else {
2402 None
2403 }
2404 })
2405 .unwrap_or(false);
2406
2407 if is_password {
2408 text_input = quote! { #text_input.password() };
2409 }
2410
2411 text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
2413
2414 Ok(quote! { #text_input.into() })
2415}
2416
2417fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2419 let src_attr = node.attributes.get("src").ok_or_else(|| {
2420 super::CodegenError::InvalidWidget("image requires src attribute".to_string())
2421 })?;
2422
2423 let src = match src_attr {
2424 AttributeValue::Static(s) => s.clone(),
2425 _ => String::new(),
2426 };
2427 let src_lit = proc_macro2::Literal::string(&src);
2428
2429 let width = node.attributes.get("width").and_then(|attr| {
2430 if let AttributeValue::Static(s) = attr {
2431 s.parse::<u32>().ok()
2432 } else {
2433 None
2434 }
2435 });
2436
2437 let height = node.attributes.get("height").and_then(|attr| {
2438 if let AttributeValue::Static(s) = attr {
2439 s.parse::<u32>().ok()
2440 } else {
2441 None
2442 }
2443 });
2444
2445 let mut image = quote! {
2446 iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
2447 };
2448
2449 if let (Some(w), Some(h)) = (width, height) {
2451 image = quote! { #image.width(#w).height(#h) };
2452 } else if let Some(w) = width {
2453 image = quote! { #image.width(#w) };
2454 } else if let Some(h) = height {
2455 image = quote! { #image.height(#h) };
2456 }
2457
2458 let needs_container = !node.classes.is_empty()
2462 || node.attributes.contains_key("align_x")
2463 || node.attributes.contains_key("align_y")
2464 || node.attributes.contains_key("padding");
2465
2466 if needs_container {
2467 let mut container = quote! { iced::widget::container(#image) };
2469
2470 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2471 if let AttributeValue::Static(s) = attr {
2472 s.parse::<f32>().ok()
2473 } else {
2474 None
2475 }
2476 }) {
2477 container = quote! { #container.padding(#padding) };
2478 }
2479
2480 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2481 if let AttributeValue::Static(s) = attr {
2482 Some(s.clone())
2483 } else {
2484 None
2485 }
2486 }) {
2487 let align_expr = generate_horizontal_alignment_expr(&align_x);
2488 container = quote! { #container.align_x(#align_expr) };
2489 }
2490
2491 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2492 if let AttributeValue::Static(s) = attr {
2493 Some(s.clone())
2494 } else {
2495 None
2496 }
2497 }) {
2498 let align_expr = generate_vertical_alignment_expr(&align_y);
2499 container = quote! { #container.align_y(#align_expr) };
2500 }
2501
2502 if let Some(class_name) = node.classes.first() {
2503 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2504 container = quote! { #container.style(#style_fn_ident) };
2505 }
2506
2507 Ok(quote! { #container.into() })
2508 } else {
2509 Ok(quote! { #image.into() })
2510 }
2511}
2512
2513fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2515 let path_attr = node
2517 .attributes
2518 .get("src")
2519 .or_else(|| node.attributes.get("path"))
2520 .ok_or_else(|| {
2521 super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
2522 })?;
2523
2524 let path = match path_attr {
2525 AttributeValue::Static(s) => s.clone(),
2526 _ => String::new(),
2527 };
2528 let path_lit = proc_macro2::Literal::string(&path);
2529
2530 let width = node.attributes.get("width").and_then(|attr| {
2531 if let AttributeValue::Static(s) = attr {
2532 s.parse::<u32>().ok()
2533 } else {
2534 None
2535 }
2536 });
2537
2538 let height = node.attributes.get("height").and_then(|attr| {
2539 if let AttributeValue::Static(s) = attr {
2540 s.parse::<u32>().ok()
2541 } else {
2542 None
2543 }
2544 });
2545
2546 let mut svg = quote! {
2547 iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
2548 };
2549
2550 if let (Some(w), Some(h)) = (width, height) {
2552 svg = quote! { #svg.width(#w).height(#h) };
2553 } else if let Some(w) = width {
2554 svg = quote! { #svg.width(#w) };
2555 } else if let Some(h) = height {
2556 svg = quote! { #svg.height(#h) };
2557 }
2558
2559 let needs_container = !node.classes.is_empty()
2563 || node.attributes.contains_key("align_x")
2564 || node.attributes.contains_key("align_y")
2565 || node.attributes.contains_key("padding");
2566
2567 if needs_container {
2568 let mut container = quote! { iced::widget::container(#svg) };
2570
2571 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2572 if let AttributeValue::Static(s) = attr {
2573 s.parse::<f32>().ok()
2574 } else {
2575 None
2576 }
2577 }) {
2578 container = quote! { #container.padding(#padding) };
2579 }
2580
2581 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2582 if let AttributeValue::Static(s) = attr {
2583 Some(s.clone())
2584 } else {
2585 None
2586 }
2587 }) {
2588 let align_expr = generate_horizontal_alignment_expr(&align_x);
2589 container = quote! { #container.align_x(#align_expr) };
2590 }
2591
2592 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2593 if let AttributeValue::Static(s) = attr {
2594 Some(s.clone())
2595 } else {
2596 None
2597 }
2598 }) {
2599 let align_expr = generate_vertical_alignment_expr(&align_y);
2600 container = quote! { #container.align_y(#align_expr) };
2601 }
2602
2603 if let Some(class_name) = node.classes.first() {
2604 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2605 container = quote! { #container.style(#style_fn_ident) };
2606 }
2607
2608 Ok(quote! { #container.into() })
2609 } else {
2610 Ok(quote! { #svg.into() })
2611 }
2612}
2613
2614fn generate_pick_list(
2616 node: &crate::WidgetNode,
2617 model_ident: &syn::Ident,
2618 message_ident: &syn::Ident,
2619 _style_classes: &HashMap<String, StyleClass>,
2620) -> Result<TokenStream, super::CodegenError> {
2621 let options_attr = node.attributes.get("options").ok_or_else(|| {
2622 super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2623 })?;
2624
2625 let options: Vec<String> = match options_attr {
2626 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2627 _ => Vec::new(),
2628 };
2629 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2630
2631 let selected_attr = node.attributes.get("selected");
2632 let selected_expr = selected_attr
2633 .map(|attr| generate_attribute_value(attr, model_ident))
2634 .unwrap_or(quote! { None });
2635
2636 let on_select = node
2637 .events
2638 .iter()
2639 .find(|e| e.event == crate::EventKind::Select);
2640
2641 if let Some(event) = on_select {
2642 let variant_name = to_upper_camel_case(&event.handler);
2643 let handler_ident = format_ident!("{}", variant_name);
2644 Ok(quote! {
2645 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2646 })
2647 } else {
2648 Ok(quote! {
2649 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2650 })
2651 }
2652}
2653
2654fn generate_combo_box(
2656 node: &crate::WidgetNode,
2657 model_ident: &syn::Ident,
2658 message_ident: &syn::Ident,
2659 _style_classes: &HashMap<String, StyleClass>,
2660) -> Result<TokenStream, super::CodegenError> {
2661 let options_attr = node.attributes.get("options").ok_or_else(|| {
2662 super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2663 })?;
2664
2665 let options: Vec<String> = match options_attr {
2666 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2667 _ => Vec::new(),
2668 };
2669 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2670
2671 let selected_attr = node.attributes.get("selected");
2672 let selected_expr = selected_attr
2673 .map(|attr| generate_attribute_value(attr, model_ident))
2674 .unwrap_or(quote! { None });
2675
2676 let on_select = node
2677 .events
2678 .iter()
2679 .find(|e| e.event == crate::EventKind::Select);
2680
2681 if let Some(event) = on_select {
2682 let variant_name = to_upper_camel_case(&event.handler);
2683 let handler_ident = format_ident!("{}", variant_name);
2684 Ok(quote! {
2685 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2686 })
2687 } else {
2688 Ok(quote! {
2689 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2690 })
2691 }
2692}
2693
2694fn generate_tooltip(
2696 node: &crate::WidgetNode,
2697 model_ident: &syn::Ident,
2698 message_ident: &syn::Ident,
2699 style_classes: &HashMap<String, StyleClass>,
2700) -> Result<TokenStream, super::CodegenError> {
2701 let child = node.children.first().ok_or_else(|| {
2702 super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2703 })?;
2704 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2705
2706 let message_attr = node.attributes.get("message").ok_or_else(|| {
2707 super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2708 })?;
2709 let message_expr = generate_attribute_value(message_attr, model_ident);
2710
2711 Ok(quote! {
2712 iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2713 })
2714}
2715
2716fn generate_grid(
2718 node: &crate::WidgetNode,
2719 model_ident: &syn::Ident,
2720 message_ident: &syn::Ident,
2721 style_classes: &HashMap<String, StyleClass>,
2722) -> Result<TokenStream, super::CodegenError> {
2723 let children: Vec<TokenStream> = node
2724 .children
2725 .iter()
2726 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2727 .collect::<Result<_, _>>()?;
2728
2729 let columns = node
2730 .attributes
2731 .get("columns")
2732 .and_then(|attr| {
2733 if let AttributeValue::Static(s) = attr {
2734 s.parse::<u32>().ok()
2735 } else {
2736 None
2737 }
2738 })
2739 .unwrap_or(1);
2740
2741 let spacing = node.attributes.get("spacing").and_then(|attr| {
2742 if let AttributeValue::Static(s) = attr {
2743 s.parse::<f32>().ok()
2744 } else {
2745 None
2746 }
2747 });
2748
2749 let padding = node.attributes.get("padding").and_then(|attr| {
2750 if let AttributeValue::Static(s) = attr {
2751 s.parse::<f32>().ok()
2752 } else {
2753 None
2754 }
2755 });
2756
2757 let grid = quote! {
2758 iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2759 };
2760
2761 let grid = if let Some(s) = spacing {
2762 quote! { #grid.spacing(#s) }
2763 } else {
2764 grid
2765 };
2766
2767 let grid = if let Some(p) = padding {
2768 quote! { #grid.padding(#p) }
2769 } else {
2770 grid
2771 };
2772
2773 Ok(quote! { #grid.into() })
2774}
2775
2776fn generate_canvas(
2778 node: &crate::WidgetNode,
2779 model_ident: &syn::Ident,
2780 message_ident: &syn::Ident,
2781 _style_classes: &HashMap<String, StyleClass>,
2782) -> Result<TokenStream, super::CodegenError> {
2783 let width = node.attributes.get("width").and_then(|attr| {
2784 if let AttributeValue::Static(s) = attr {
2785 s.parse::<f32>().ok()
2786 } else {
2787 None
2788 }
2789 });
2790
2791 let height = node.attributes.get("height").and_then(|attr| {
2792 if let AttributeValue::Static(s) = attr {
2793 s.parse::<f32>().ok()
2794 } else {
2795 None
2796 }
2797 });
2798
2799 let width_expr = match width {
2800 Some(w) => quote! { iced::Length::Fixed(#w) },
2801 None => quote! { iced::Length::Fixed(400.0) },
2802 };
2803
2804 let height_expr = match height {
2805 Some(h) => quote! { iced::Length::Fixed(#h) },
2806 None => quote! { iced::Length::Fixed(300.0) },
2807 };
2808
2809 let content_expr = if let Some(program_attr) = node.attributes.get("program") {
2811 let program_binding = match program_attr {
2812 AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
2813 _ => quote! { None },
2814 };
2815
2816 let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2818 let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2819 let prog_init = quote! {
2820 dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2821 };
2822 let prog_with_handlers = if let Some(handlers) = handlers_expr {
2823 quote! { #prog_init.with_handlers(#handlers) }
2824 } else {
2825 prog_init
2826 };
2827
2828 quote! {
2829 if let Some(container) = &#program_binding {
2830 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2831 dampen_iced::canvas::CanvasContent::Custom(container.0.clone())
2832 ))
2833 .width(#width_expr)
2834 .height(#height_expr);
2835
2836 iced::Element::from(canvas).map(|()| unreachable!("Custom program action not supported in codegen"))
2837 } else {
2838 let canvas = iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2839 dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2840 ))
2841 .width(#width_expr)
2842 .height(#height_expr);
2843
2844 iced::Element::from(canvas)
2845 }
2846 }
2847 } else {
2848 let shape_exprs = generate_canvas_shapes(&node.children, model_ident)?;
2850
2851 let handlers_expr = generate_canvas_handlers(node, model_ident, message_ident)?;
2853
2854 let prog_init = quote! {
2855 dampen_iced::canvas::DeclarativeProgram::new(vec![#(#shape_exprs),*])
2856 };
2857
2858 let prog_with_handlers = if let Some(handlers) = handlers_expr {
2859 quote! { #prog_init.with_handlers(#handlers) }
2860 } else {
2861 prog_init
2862 };
2863
2864 quote! {
2865 iced::widget::canvas(dampen_iced::canvas::CanvasProgramWrapper::new(
2866 dampen_iced::canvas::CanvasContent::Declarative(#prog_with_handlers)
2867 ))
2868 .width(#width_expr)
2869 .height(#height_expr)
2870 .into()
2871 }
2872 };
2873
2874 Ok(content_expr)
2875}
2876
2877fn generate_float(
2879 node: &crate::WidgetNode,
2880 model_ident: &syn::Ident,
2881 message_ident: &syn::Ident,
2882 style_classes: &HashMap<String, StyleClass>,
2883) -> Result<TokenStream, super::CodegenError> {
2884 let child = node.children.first().ok_or_else(|| {
2885 super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2886 })?;
2887 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2888
2889 let position = node
2890 .attributes
2891 .get("position")
2892 .and_then(|attr| {
2893 if let AttributeValue::Static(s) = attr {
2894 Some(s.clone())
2895 } else {
2896 None
2897 }
2898 })
2899 .unwrap_or_else(|| "TopRight".to_string());
2900
2901 let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2902 if let AttributeValue::Static(s) = attr {
2903 s.parse::<f32>().ok()
2904 } else {
2905 None
2906 }
2907 });
2908
2909 let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2910 if let AttributeValue::Static(s) = attr {
2911 s.parse::<f32>().ok()
2912 } else {
2913 None
2914 }
2915 });
2916
2917 let float = match position.as_str() {
2918 "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2919 "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2920 "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2921 "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2922 _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2923 };
2924
2925 let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2926 quote! { #float.offset_x(#ox).offset_y(#oy) }
2927 } else if let Some(ox) = offset_x {
2928 quote! { #float.offset_x(#ox) }
2929 } else if let Some(oy) = offset_y {
2930 quote! { #float.offset_y(#oy) }
2931 } else {
2932 float
2933 };
2934
2935 Ok(quote! { #float.into() })
2936}
2937
2938fn generate_for(
2944 node: &crate::WidgetNode,
2945 model_ident: &syn::Ident,
2946 message_ident: &syn::Ident,
2947 style_classes: &HashMap<String, StyleClass>,
2948) -> Result<TokenStream, super::CodegenError> {
2949 let in_attr = node.attributes.get("in").ok_or_else(|| {
2951 super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2952 })?;
2953
2954 let var_name = node
2956 .attributes
2957 .get("each")
2958 .and_then(|attr| {
2959 if let AttributeValue::Static(s) = attr {
2960 Some(s.clone())
2961 } else {
2962 None
2963 }
2964 })
2965 .unwrap_or_else(|| "item".to_string());
2966
2967 let var_ident = format_ident!("{}", var_name);
2968
2969 let collection_expr = generate_attribute_value_raw(in_attr, model_ident);
2971
2972 let children: Vec<TokenStream> = node
2974 .children
2975 .iter()
2976 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2977 .collect::<Result<_, _>>()?;
2978
2979 Ok(quote! {
2981 {
2982 let items: Vec<_> = #collection_expr;
2983 let widgets: Vec<iced::Element<'_, #message_ident>> = items
2984 .iter()
2985 .enumerate()
2986 .flat_map(|(index, #var_ident)| {
2987 let _ = index; vec![#(#children),*]
2989 })
2990 .collect();
2991 iced::widget::column(widgets).into()
2992 }
2993 })
2994}
2995
2996fn generate_if(
2998 node: &crate::WidgetNode,
2999 model_ident: &syn::Ident,
3000 message_ident: &syn::Ident,
3001 style_classes: &HashMap<String, StyleClass>,
3002) -> Result<TokenStream, super::CodegenError> {
3003 let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3004 super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3005 })?;
3006
3007 let children: Vec<TokenStream> = node
3008 .children
3009 .iter()
3010 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
3011 .collect::<Result<_, _>>()?;
3012
3013 let condition_expr = generate_attribute_value(condition_attr, model_ident);
3014
3015 Ok(quote! {
3016 if #condition_expr.parse::<bool>().unwrap_or(false) {
3017 iced::widget::column(vec![#(#children),*]).into()
3018 } else {
3019 iced::widget::column(vec![]).into()
3020 }
3021 })
3022}
3023
3024fn generate_date_picker(
3027 node: &crate::WidgetNode,
3028 model_ident: &syn::Ident,
3029 message_ident: &syn::Ident,
3030 style_classes: &HashMap<String, StyleClass>,
3031) -> Result<TokenStream, super::CodegenError> {
3032 let show = node
3033 .attributes
3034 .get("show")
3035 .map(|attr| match attr {
3036 AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3037 AttributeValue::Static(s) => {
3038 let v = s == "true";
3039 quote! { #v }
3040 }
3041 _ => quote! { false },
3042 })
3043 .unwrap_or(quote! { false });
3044
3045 let date = if let Some(attr) = node.attributes.get("value") {
3046 match attr {
3047 AttributeValue::Binding(b) => {
3048 let expr = super::bindings::generate_bool_expr(&b.expr);
3049 quote! { iced_aw::date_picker::Date::from(#expr) }
3050 }
3051 AttributeValue::Static(s) => {
3052 let format = node
3053 .attributes
3054 .get("format")
3055 .map(|f| match f {
3056 AttributeValue::Static(fs) => fs.as_str(),
3057 _ => "%Y-%m-%d",
3058 })
3059 .unwrap_or("%Y-%m-%d");
3060 quote! {
3061 iced_aw::date_picker::Date::from(
3062 chrono::NaiveDate::parse_from_str(#s, #format).unwrap_or_default()
3063 )
3064 }
3065 }
3066 _ => quote! { iced_aw::date_picker::Date::today() },
3067 }
3068 } else {
3069 quote! { iced_aw::date_picker::Date::today() }
3070 };
3071
3072 let on_cancel = if let Some(h) = node
3073 .events
3074 .iter()
3075 .find(|e| e.event == crate::EventKind::Cancel)
3076 {
3077 let msg = format_ident!("{}", h.handler);
3078 quote! { #message_ident::#msg }
3079 } else {
3080 quote! { #message_ident::None }
3081 };
3082
3083 let on_submit = if let Some(h) = node
3084 .events
3085 .iter()
3086 .find(|e| e.event == crate::EventKind::Submit)
3087 {
3088 let msg = format_ident!("{}", h.handler);
3089 quote! {
3090 |date| {
3091 let s = chrono::NaiveDate::from(date).format("%Y-%m-%d").to_string();
3092 #message_ident::#msg(s)
3093 }
3094 }
3095 } else {
3096 quote! { |_| #message_ident::None }
3097 };
3098
3099 let underlay = if let Some(child) = node.children.first() {
3100 generate_widget(child, model_ident, message_ident, style_classes)?
3101 } else {
3102 quote! { iced::widget::text("Missing child") }
3103 };
3104
3105 Ok(quote! {
3106 iced_aw::widgets::date_picker::DatePicker::new(
3107 #show,
3108 #date,
3109 #underlay,
3110 #on_cancel,
3111 #on_submit
3112 )
3113 })
3114}
3115
3116fn generate_color_picker(
3118 node: &crate::WidgetNode,
3119 model_ident: &syn::Ident,
3120 message_ident: &syn::Ident,
3121 style_classes: &HashMap<String, StyleClass>,
3122) -> Result<TokenStream, super::CodegenError> {
3123 let show = node
3124 .attributes
3125 .get("show")
3126 .map(|attr| match attr {
3127 AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3128 AttributeValue::Static(s) => {
3129 let v = s == "true";
3130 quote! { #v }
3131 }
3132 _ => quote! { false },
3133 })
3134 .unwrap_or(quote! { false });
3135
3136 let color = if let Some(attr) = node.attributes.get("value") {
3137 match attr {
3138 AttributeValue::Binding(b) => {
3139 let expr = super::bindings::generate_expr(&b.expr);
3140 quote! { iced::Color::from_hex(&#expr.to_string()).unwrap_or(iced::Color::BLACK) }
3141 }
3142 AttributeValue::Static(s) => {
3143 quote! { iced::Color::from_hex(#s).unwrap_or(iced::Color::BLACK) }
3144 }
3145 _ => quote! { iced::Color::BLACK },
3146 }
3147 } else {
3148 quote! { iced::Color::BLACK }
3149 };
3150
3151 let on_cancel = if let Some(h) = node
3152 .events
3153 .iter()
3154 .find(|e| e.event == crate::EventKind::Cancel)
3155 {
3156 let msg = format_ident!("{}", h.handler);
3157 quote! { #message_ident::#msg }
3158 } else {
3159 quote! { #message_ident::None }
3160 };
3161
3162 let on_submit = if let Some(h) = node
3163 .events
3164 .iter()
3165 .find(|e| e.event == crate::EventKind::Submit)
3166 {
3167 let msg = format_ident!("{}", h.handler);
3168 quote! {
3169 |color| {
3170 let s = iced::color!(color).to_string();
3171 #message_ident::#msg(s)
3172 }
3173 }
3174 } else {
3175 quote! { |_| #message_ident::None }
3176 };
3177
3178 let underlay = if let Some(child) = node.children.first() {
3179 generate_widget(child, model_ident, message_ident, style_classes)?
3180 } else {
3181 quote! { iced::widget::text("Missing child") }
3182 };
3183
3184 Ok(quote! {
3185 iced_aw::widgets::color_picker::ColorPicker::new(
3186 #show,
3187 #color,
3188 #underlay,
3189 #on_cancel,
3190 #on_submit
3191 )
3192 })
3193}
3194
3195fn generate_time_picker(
3197 node: &crate::WidgetNode,
3198 model_ident: &syn::Ident,
3199 message_ident: &syn::Ident,
3200 style_classes: &HashMap<String, StyleClass>,
3201) -> Result<TokenStream, super::CodegenError> {
3202 let show = node
3203 .attributes
3204 .get("show")
3205 .map(|attr| match attr {
3206 AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3207 AttributeValue::Static(s) => {
3208 let v = s == "true";
3209 quote! { #v }
3210 }
3211 _ => quote! { false },
3212 })
3213 .unwrap_or(quote! { false });
3214
3215 let time = if let Some(attr) = node.attributes.get("value") {
3216 match attr {
3217 AttributeValue::Binding(b) => {
3218 let expr = super::bindings::generate_bool_expr(&b.expr);
3219 quote! { iced_aw::time_picker::Time::from(#expr) }
3220 }
3221 AttributeValue::Static(s) => {
3222 let format = node
3223 .attributes
3224 .get("format")
3225 .map(|f| match f {
3226 AttributeValue::Static(fs) => fs.as_str(),
3227 _ => "%H:%M:%S",
3228 })
3229 .unwrap_or("%H:%M:%S");
3230 quote! {
3231 iced_aw::time_picker::Time::from(
3232 chrono::NaiveTime::parse_from_str(#s, #format).unwrap_or_default()
3233 )
3234 }
3235 }
3236 _ => {
3237 quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3238 }
3239 }
3240 } else {
3241 quote! { iced_aw::time_picker::Time::from(chrono::Local::now().naive_local().time()) }
3242 };
3243
3244 let use_24h = node.attributes.get("use_24h").map(|attr| match attr {
3245 AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3246 AttributeValue::Static(s) => {
3247 let v = s == "true";
3248 quote! { #v }
3249 }
3250 _ => quote! { false },
3251 });
3252
3253 let show_seconds = node.attributes.get("show_seconds").map(|attr| match attr {
3254 AttributeValue::Binding(b) => super::bindings::generate_bool_expr(&b.expr),
3255 AttributeValue::Static(s) => {
3256 let v = s == "true";
3257 quote! { #v }
3258 }
3259 _ => quote! { false },
3260 });
3261
3262 let on_cancel = if let Some(h) = node
3263 .events
3264 .iter()
3265 .find(|e| e.event == crate::EventKind::Cancel)
3266 {
3267 let msg = format_ident!("{}", h.handler);
3268 quote! { #message_ident::#msg }
3269 } else {
3270 quote! { #message_ident::None }
3271 };
3272
3273 let on_submit = if let Some(h) = node
3274 .events
3275 .iter()
3276 .find(|e| e.event == crate::EventKind::Submit)
3277 {
3278 let msg = format_ident!("{}", h.handler);
3279 quote! {
3280 |time| {
3281 let s = chrono::NaiveTime::from(time).format("%H:%M:%S").to_string();
3282 #message_ident::#msg(s)
3283 }
3284 }
3285 } else {
3286 quote! { |_| #message_ident::None }
3287 };
3288
3289 let underlay = if let Some(child) = node.children.first() {
3290 generate_widget(child, model_ident, message_ident, style_classes)?
3291 } else {
3292 quote! { iced::widget::text("Missing child") }
3293 };
3294
3295 let mut picker_setup = quote! {
3296 let mut picker = iced_aw::widgets::time_picker::TimePicker::new(
3297 #show,
3298 #time,
3299 #underlay,
3300 #on_cancel,
3301 #on_submit
3302 );
3303 };
3304
3305 if let Some(use_24h_expr) = use_24h {
3306 picker_setup.extend(quote! {
3307 if #use_24h_expr {
3308 picker = picker.use_24h();
3309 }
3310 });
3311 }
3312
3313 if let Some(show_seconds_expr) = show_seconds {
3314 picker_setup.extend(quote! {
3315 if #show_seconds_expr {
3316 picker = picker.show_seconds();
3317 }
3318 });
3319 }
3320
3321 Ok(quote! {
3322 {
3323 #picker_setup
3324 picker
3325 }
3326 })
3327}
3328
3329fn generate_custom_widget(
3330 node: &crate::WidgetNode,
3331 name: &str,
3332 model_ident: &syn::Ident,
3333 message_ident: &syn::Ident,
3334 style_classes: &HashMap<String, StyleClass>,
3335) -> Result<TokenStream, super::CodegenError> {
3336 let widget_ident = format_ident!("{}", name);
3337 let children: Vec<TokenStream> = node
3338 .children
3339 .iter()
3340 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
3341 .collect::<Result<_, _>>()?;
3342
3343 Ok(quote! {
3344 #widget_ident(vec![#(#children),*]).into()
3345 })
3346}
3347
3348fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3350 match attr {
3351 AttributeValue::Static(s) => {
3352 let lit = proc_macro2::Literal::string(s);
3353 quote! { #lit.to_string() }
3354 }
3355 AttributeValue::Binding(expr) => generate_expr(&expr.expr),
3356 AttributeValue::Interpolated(parts) => {
3357 let parts_str: Vec<String> = parts
3358 .iter()
3359 .map(|part| match part {
3360 InterpolatedPart::Literal(s) => s.clone(),
3361 InterpolatedPart::Binding(_) => "{}".to_string(),
3362 })
3363 .collect();
3364 let binding_exprs: Vec<TokenStream> = parts
3365 .iter()
3366 .filter_map(|part| {
3367 if let InterpolatedPart::Binding(expr) = part {
3368 Some(generate_expr(&expr.expr))
3369 } else {
3370 None
3371 }
3372 })
3373 .collect();
3374
3375 let format_string = parts_str.join("");
3376 let lit = proc_macro2::Literal::string(&format_string);
3377
3378 quote! { format!(#lit, #(#binding_exprs),*) }
3379 }
3380 }
3381}
3382
3383fn generate_attribute_value_raw(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
3386 match attr {
3387 AttributeValue::Static(s) => {
3388 let lit = proc_macro2::Literal::string(s);
3389 quote! { #lit }
3390 }
3391 AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
3392 AttributeValue::Interpolated(parts) => {
3393 let parts_str: Vec<String> = parts
3395 .iter()
3396 .map(|part| match part {
3397 InterpolatedPart::Literal(s) => s.clone(),
3398 InterpolatedPart::Binding(_) => "{}".to_string(),
3399 })
3400 .collect();
3401 let binding_exprs: Vec<TokenStream> = parts
3402 .iter()
3403 .filter_map(|part| {
3404 if let InterpolatedPart::Binding(expr) = part {
3405 Some(generate_expr(&expr.expr))
3406 } else {
3407 None
3408 }
3409 })
3410 .collect();
3411
3412 let format_string = parts_str.join("");
3413 let lit = proc_macro2::Literal::string(&format_string);
3414
3415 quote! { format!(#lit, #(#binding_exprs),*) }
3416 }
3417 }
3418}
3419
3420fn generate_text_with_locals(
3426 node: &crate::WidgetNode,
3427 model_ident: &syn::Ident,
3428 _style_classes: &HashMap<String, StyleClass>,
3429 local_vars: &std::collections::HashSet<String>,
3430) -> Result<TokenStream, super::CodegenError> {
3431 let value_attr = node.attributes.get("value").ok_or_else(|| {
3432 super::CodegenError::InvalidWidget("text requires value attribute".to_string())
3433 })?;
3434
3435 let value_expr = generate_attribute_value_with_locals(value_attr, model_ident, local_vars);
3436
3437 let mut text_widget = quote! {
3438 iced::widget::text(#value_expr)
3439 };
3440
3441 if let Some(size) = node.attributes.get("size").and_then(|attr| {
3443 if let AttributeValue::Static(s) = attr {
3444 s.parse::<f32>().ok()
3445 } else {
3446 None
3447 }
3448 }) {
3449 text_widget = quote! { #text_widget.size(#size) };
3450 }
3451
3452 if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
3454 if let AttributeValue::Static(s) = attr {
3455 Some(s.clone())
3456 } else {
3457 None
3458 }
3459 }) {
3460 let weight_expr = match weight.to_lowercase().as_str() {
3461 "bold" => quote! { iced::font::Weight::Bold },
3462 "semibold" => quote! { iced::font::Weight::Semibold },
3463 "medium" => quote! { iced::font::Weight::Medium },
3464 "light" => quote! { iced::font::Weight::Light },
3465 _ => quote! { iced::font::Weight::Normal },
3466 };
3467 text_widget = quote! {
3468 #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
3469 };
3470 }
3471
3472 if let Some(ref style_props) = node.style
3474 && let Some(ref color) = style_props.color
3475 {
3476 let color_expr = generate_color_expr(color);
3477 text_widget = quote! { #text_widget.color(#color_expr) };
3478 }
3479
3480 Ok(maybe_wrap_in_container(text_widget, node))
3481}
3482
3483fn generate_button_with_locals(
3485 node: &crate::WidgetNode,
3486 model_ident: &syn::Ident,
3487 message_ident: &syn::Ident,
3488 style_classes: &HashMap<String, StyleClass>,
3489 local_vars: &std::collections::HashSet<String>,
3490) -> Result<TokenStream, super::CodegenError> {
3491 let label_attr = node.attributes.get("label").ok_or_else(|| {
3492 super::CodegenError::InvalidWidget("button requires label attribute".to_string())
3493 })?;
3494
3495 let label_expr = generate_attribute_value_with_locals(label_attr, model_ident, local_vars);
3496
3497 let on_click = node
3498 .events
3499 .iter()
3500 .find(|e| e.event == crate::EventKind::Click);
3501
3502 let mut button = quote! {
3503 iced::widget::button(iced::widget::text(#label_expr))
3504 };
3505
3506 if let Some(event) = on_click {
3507 let variant_name = to_upper_camel_case(&event.handler);
3508 let handler_ident = format_ident!("{}", variant_name);
3509
3510 let param_expr = if let Some(ref param) = event.param {
3511 let param_tokens = super::bindings::generate_expr_with_locals(¶m.expr, local_vars);
3512 quote! { (#param_tokens) }
3513 } else {
3514 quote! {}
3515 };
3516
3517 button = quote! {
3518 #button.on_press(#message_ident::#handler_ident #param_expr)
3519 };
3520 }
3521
3522 button = apply_widget_style(button, node, "button", style_classes)?;
3524
3525 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#button) })
3526}
3527
3528fn generate_container_with_locals(
3530 node: &crate::WidgetNode,
3531 widget_type: &str,
3532 model_ident: &syn::Ident,
3533 message_ident: &syn::Ident,
3534 style_classes: &HashMap<String, StyleClass>,
3535 local_vars: &std::collections::HashSet<String>,
3536) -> Result<TokenStream, super::CodegenError> {
3537 let children: Vec<TokenStream> = node
3538 .children
3539 .iter()
3540 .map(|child| {
3541 generate_widget_with_locals(
3542 child,
3543 model_ident,
3544 message_ident,
3545 style_classes,
3546 local_vars,
3547 )
3548 })
3549 .collect::<Result<_, _>>()?;
3550
3551 let mut container = match widget_type {
3552 "column" => {
3553 quote! { iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3554 }
3555 "row" => {
3556 quote! { iced::widget::row({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
3557 }
3558 "scrollable" => {
3559 quote! { iced::widget::scrollable(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3560 }
3561 _ => {
3562 if children.len() == 1 {
3564 let child = &children[0];
3565 quote! { iced::widget::container(#child) }
3566 } else {
3567 quote! { iced::widget::container(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
3568 }
3569 }
3570 };
3571
3572 let merged_layout = get_merged_layout(node, style_classes);
3574
3575 let spacing = node
3577 .attributes
3578 .get("spacing")
3579 .and_then(|attr| {
3580 if let AttributeValue::Static(s) = attr {
3581 s.parse::<f32>().ok()
3582 } else {
3583 None
3584 }
3585 })
3586 .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
3587
3588 if let Some(s) = spacing
3590 && (widget_type == "column" || widget_type == "row")
3591 {
3592 container = quote! { #container.spacing(#s) };
3593 }
3594
3595 let padding = node
3597 .attributes
3598 .get("padding")
3599 .and_then(|attr| {
3600 if let AttributeValue::Static(s) = attr {
3601 s.parse::<f32>().ok()
3602 } else {
3603 None
3604 }
3605 })
3606 .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
3607
3608 if let Some(p) = padding {
3610 container = quote! { #container.padding(#p) };
3611 }
3612
3613 let width_from_attr = node.attributes.get("width").and_then(|attr| {
3615 if let AttributeValue::Static(s) = attr {
3616 Some(s.clone())
3617 } else {
3618 None
3619 }
3620 });
3621 let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
3622
3623 if let Some(width) = width_from_attr {
3624 let width_expr = generate_length_expr(&width);
3625 container = quote! { #container.width(#width_expr) };
3626 } else if let Some(layout_width) = width_from_layout {
3627 let width_expr = generate_layout_length_expr(layout_width);
3628 container = quote! { #container.width(#width_expr) };
3629 }
3630
3631 let height_from_attr = node.attributes.get("height").and_then(|attr| {
3633 if let AttributeValue::Static(s) = attr {
3634 Some(s.clone())
3635 } else {
3636 None
3637 }
3638 });
3639 let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
3640
3641 if let Some(height) = height_from_attr {
3642 let height_expr = generate_length_expr(&height);
3643 container = quote! { #container.height(#height_expr) };
3644 } else if let Some(layout_height) = height_from_layout {
3645 let height_expr = generate_layout_length_expr(layout_height);
3646 container = quote! { #container.height(#height_expr) };
3647 }
3648
3649 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
3651 if let AttributeValue::Static(s) = attr {
3652 Some(s.clone())
3653 } else {
3654 None
3655 }
3656 }) && widget_type == "row"
3657 {
3658 let alignment_expr = match align_y.to_lowercase().as_str() {
3659 "top" | "start" => quote! { iced::alignment::Vertical::Top },
3660 "bottom" | "end" => quote! { iced::alignment::Vertical::Bottom },
3661 _ => quote! { iced::alignment::Vertical::Center },
3662 };
3663 container = quote! { #container.align_y(#alignment_expr) };
3664 }
3665
3666 if widget_type == "container" {
3668 container = apply_widget_style(container, node, "container", style_classes)?;
3669 }
3670
3671 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#container) })
3673}
3674
3675fn generate_for_with_locals(
3677 node: &crate::WidgetNode,
3678 model_ident: &syn::Ident,
3679 message_ident: &syn::Ident,
3680 style_classes: &HashMap<String, StyleClass>,
3681 local_vars: &std::collections::HashSet<String>,
3682) -> Result<TokenStream, super::CodegenError> {
3683 let in_attr = node.attributes.get("in").ok_or_else(|| {
3685 super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
3686 })?;
3687
3688 let var_name = node
3690 .attributes
3691 .get("each")
3692 .and_then(|attr| {
3693 if let AttributeValue::Static(s) = attr {
3694 Some(s.clone())
3695 } else {
3696 None
3697 }
3698 })
3699 .unwrap_or_else(|| "item".to_string());
3700
3701 let var_ident = format_ident!("{}", var_name);
3702
3703 let collection_expr =
3705 generate_attribute_value_raw_with_locals(in_attr, model_ident, local_vars);
3706
3707 let mut new_local_vars = local_vars.clone();
3709 new_local_vars.insert(var_name.clone());
3710 new_local_vars.insert("index".to_string());
3711
3712 let children: Vec<TokenStream> = node
3714 .children
3715 .iter()
3716 .map(|child| {
3717 generate_widget_with_locals(
3718 child,
3719 model_ident,
3720 message_ident,
3721 style_classes,
3722 &new_local_vars,
3723 )
3724 })
3725 .collect::<Result<_, _>>()?;
3726
3727 Ok(quote! {
3730 {
3731 let mut widgets: Vec<Element<'_, #message_ident>> = Vec::new();
3732 for (index, #var_ident) in (#collection_expr).iter().enumerate() {
3733 let _ = index;
3734 #(
3735 let child_widget: Element<'_, #message_ident> = #children;
3736 widgets.push(child_widget);
3737 )*
3738 }
3739 Into::<Element<'_, #message_ident>>::into(iced::widget::column(widgets))
3740 }
3741 })
3742}
3743
3744fn generate_if_with_locals(
3746 node: &crate::WidgetNode,
3747 model_ident: &syn::Ident,
3748 message_ident: &syn::Ident,
3749 style_classes: &HashMap<String, StyleClass>,
3750 local_vars: &std::collections::HashSet<String>,
3751) -> Result<TokenStream, super::CodegenError> {
3752 let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3753 super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3754 })?;
3755
3756 let children: Vec<TokenStream> = node
3757 .children
3758 .iter()
3759 .map(|child| {
3760 generate_widget_with_locals(
3761 child,
3762 model_ident,
3763 message_ident,
3764 style_classes,
3765 local_vars,
3766 )
3767 })
3768 .collect::<Result<_, _>>()?;
3769
3770 let condition_expr =
3771 generate_attribute_value_with_locals(condition_attr, model_ident, local_vars);
3772
3773 Ok(quote! {
3774 if #condition_expr.parse::<bool>().unwrap_or(false) {
3775 Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }))
3776 } else {
3777 Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![]; children }))
3778 }
3779 })
3780}
3781
3782fn generate_checkbox_with_locals(
3784 node: &crate::WidgetNode,
3785 model_ident: &syn::Ident,
3786 message_ident: &syn::Ident,
3787 style_classes: &HashMap<String, StyleClass>,
3788 local_vars: &std::collections::HashSet<String>,
3789) -> Result<TokenStream, super::CodegenError> {
3790 let checked_attr = node.attributes.get("checked");
3792 let checked_expr = if let Some(attr) = checked_attr {
3793 generate_attribute_value_raw_with_locals(attr, model_ident, local_vars)
3794 } else {
3795 quote! { false }
3796 };
3797
3798 let on_change = node
3800 .events
3801 .iter()
3802 .find(|e| e.event == crate::EventKind::Change);
3803
3804 let mut checkbox = quote! {
3805 iced::widget::checkbox(#checked_expr)
3806 };
3807
3808 if let Some(event) = on_change {
3809 let variant_name = to_upper_camel_case(&event.handler);
3810 let handler_ident = format_ident!("{}", variant_name);
3811
3812 let param_expr = if let Some(ref param) = event.param {
3813 let param_tokens = super::bindings::generate_expr_with_locals(¶m.expr, local_vars);
3814 quote! { (#param_tokens) }
3815 } else {
3816 quote! {}
3817 };
3818
3819 checkbox = quote! {
3820 #checkbox.on_toggle(move |_| #message_ident::#handler_ident #param_expr)
3821 };
3822 }
3823
3824 if let Some(size) = node.attributes.get("size").and_then(|attr| {
3826 if let AttributeValue::Static(s) = attr {
3827 s.parse::<f32>().ok()
3828 } else {
3829 None
3830 }
3831 }) {
3832 checkbox = quote! { #checkbox.size(#size) };
3833 }
3834
3835 checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
3837
3838 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#checkbox) })
3839}
3840
3841fn generate_text_input_with_locals(
3843 node: &crate::WidgetNode,
3844 model_ident: &syn::Ident,
3845 message_ident: &syn::Ident,
3846 style_classes: &HashMap<String, StyleClass>,
3847 local_vars: &std::collections::HashSet<String>,
3848) -> Result<TokenStream, super::CodegenError> {
3849 let placeholder = node
3851 .attributes
3852 .get("placeholder")
3853 .and_then(|attr| {
3854 if let AttributeValue::Static(s) = attr {
3855 Some(s.clone())
3856 } else {
3857 None
3858 }
3859 })
3860 .unwrap_or_default();
3861 let placeholder_lit = proc_macro2::Literal::string(&placeholder);
3862
3863 let value_attr = node.attributes.get("value");
3865 let value_expr = if let Some(attr) = value_attr {
3866 generate_attribute_value_with_locals(attr, model_ident, local_vars)
3867 } else {
3868 quote! { String::new() }
3869 };
3870
3871 let on_input = node
3872 .events
3873 .iter()
3874 .find(|e| e.event == crate::EventKind::Input);
3875
3876 let on_submit = node
3877 .events
3878 .iter()
3879 .find(|e| e.event == crate::EventKind::Submit);
3880
3881 let mut text_input = quote! {
3882 iced::widget::text_input(#placeholder_lit, &#value_expr)
3883 };
3884
3885 if let Some(event) = on_input {
3887 let variant_name = to_upper_camel_case(&event.handler);
3888 let handler_ident = format_ident!("{}", variant_name);
3889 text_input = quote! { #text_input.on_input(|v| #message_ident::#handler_ident(v)) };
3890 }
3891
3892 if let Some(event) = on_submit {
3894 let variant_name = to_upper_camel_case(&event.handler);
3895 let handler_ident = format_ident!("{}", variant_name);
3896 text_input = quote! { #text_input.on_submit(#message_ident::#handler_ident) };
3897 }
3898
3899 if let Some(size) = node.attributes.get("size").and_then(|attr| {
3901 if let AttributeValue::Static(s) = attr {
3902 s.parse::<f32>().ok()
3903 } else {
3904 None
3905 }
3906 }) {
3907 text_input = quote! { #text_input.size(#size) };
3908 }
3909
3910 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
3912 if let AttributeValue::Static(s) = attr {
3913 s.parse::<f32>().ok()
3914 } else {
3915 None
3916 }
3917 }) {
3918 text_input = quote! { #text_input.padding(#padding) };
3919 }
3920
3921 if let Some(width) = node.attributes.get("width").and_then(|attr| {
3923 if let AttributeValue::Static(s) = attr {
3924 Some(generate_length_expr(s))
3925 } else {
3926 None
3927 }
3928 }) {
3929 text_input = quote! { #text_input.width(#width) };
3930 }
3931
3932 text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
3934
3935 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#text_input) })
3936}
3937
3938fn generate_attribute_value_with_locals(
3940 attr: &AttributeValue,
3941 _model_ident: &syn::Ident,
3942 local_vars: &std::collections::HashSet<String>,
3943) -> TokenStream {
3944 match attr {
3945 AttributeValue::Static(s) => {
3946 let lit = proc_macro2::Literal::string(s);
3947 quote! { #lit.to_string() }
3948 }
3949 AttributeValue::Binding(expr) => {
3950 super::bindings::generate_expr_with_locals(&expr.expr, local_vars)
3951 }
3952 AttributeValue::Interpolated(parts) => {
3953 let parts_str: Vec<String> = parts
3954 .iter()
3955 .map(|part| match part {
3956 InterpolatedPart::Literal(s) => s.clone(),
3957 InterpolatedPart::Binding(_) => "{}".to_string(),
3958 })
3959 .collect();
3960 let binding_exprs: Vec<TokenStream> = parts
3961 .iter()
3962 .filter_map(|part| {
3963 if let InterpolatedPart::Binding(expr) = part {
3964 Some(super::bindings::generate_expr_with_locals(
3965 &expr.expr, local_vars,
3966 ))
3967 } else {
3968 None
3969 }
3970 })
3971 .collect();
3972
3973 let format_string = parts_str.join("");
3974 let lit = proc_macro2::Literal::string(&format_string);
3975
3976 quote! { format!(#lit, #(#binding_exprs),*) }
3977 }
3978 }
3979}
3980
3981fn generate_attribute_value_raw_with_locals(
3983 attr: &AttributeValue,
3984 _model_ident: &syn::Ident,
3985 local_vars: &std::collections::HashSet<String>,
3986) -> TokenStream {
3987 match attr {
3988 AttributeValue::Static(s) => {
3989 let lit = proc_macro2::Literal::string(s);
3990 quote! { #lit }
3991 }
3992 AttributeValue::Binding(expr) => {
3993 super::bindings::generate_bool_expr_with_locals(&expr.expr, local_vars)
3994 }
3995 AttributeValue::Interpolated(parts) => {
3996 let parts_str: Vec<String> = parts
3997 .iter()
3998 .map(|part| match part {
3999 InterpolatedPart::Literal(s) => s.clone(),
4000 InterpolatedPart::Binding(_) => "{}".to_string(),
4001 })
4002 .collect();
4003 let binding_exprs: Vec<TokenStream> = parts
4004 .iter()
4005 .filter_map(|part| {
4006 if let InterpolatedPart::Binding(expr) = part {
4007 Some(super::bindings::generate_expr_with_locals(
4008 &expr.expr, local_vars,
4009 ))
4010 } else {
4011 None
4012 }
4013 })
4014 .collect();
4015
4016 let format_string = parts_str.join("");
4017 let lit = proc_macro2::Literal::string(&format_string);
4018
4019 quote! { format!(#lit, #(#binding_exprs),*) }
4020 }
4021 }
4022}
4023
4024fn generate_canvas_shapes(
4025 nodes: &[crate::WidgetNode],
4026 model_ident: &syn::Ident,
4027) -> Result<Vec<TokenStream>, super::CodegenError> {
4028 let mut shape_exprs = Vec::new();
4029 for node in nodes {
4030 match node.kind {
4031 WidgetKind::CanvasRect => shape_exprs.push(generate_rect_shape(node, model_ident)?),
4032 WidgetKind::CanvasCircle => shape_exprs.push(generate_circle_shape(node, model_ident)?),
4033 WidgetKind::CanvasLine => shape_exprs.push(generate_line_shape(node, model_ident)?),
4034 WidgetKind::CanvasText => shape_exprs.push(generate_text_shape(node, model_ident)?),
4035 WidgetKind::CanvasGroup => shape_exprs.push(generate_group_shape(node, model_ident)?),
4036 _ => {}
4037 }
4038 }
4039 Ok(shape_exprs)
4040}
4041
4042fn generate_rect_shape(
4043 node: &crate::WidgetNode,
4044 model_ident: &syn::Ident,
4045) -> Result<TokenStream, super::CodegenError> {
4046 let x = generate_f32_attr(node, "x", 0.0, model_ident);
4047 let y = generate_f32_attr(node, "y", 0.0, model_ident);
4048 let width = generate_f32_attr(node, "width", 0.0, model_ident);
4049 let height = generate_f32_attr(node, "height", 0.0, model_ident);
4050 let fill = generate_color_option_attr(node, "fill", model_ident);
4051 let stroke = generate_color_option_attr(node, "stroke", model_ident);
4052 let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4053 let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
4054
4055 Ok(quote! {
4056 dampen_iced::canvas::CanvasShape::Rect(dampen_iced::canvas::RectShape {
4057 x: #x,
4058 y: #y,
4059 width: #width,
4060 height: #height,
4061 fill: #fill,
4062 stroke: #stroke,
4063 stroke_width: #stroke_width,
4064 radius: #radius,
4065 })
4066 })
4067}
4068
4069fn generate_circle_shape(
4070 node: &crate::WidgetNode,
4071 model_ident: &syn::Ident,
4072) -> Result<TokenStream, super::CodegenError> {
4073 let cx = generate_f32_attr(node, "cx", 0.0, model_ident);
4074 let cy = generate_f32_attr(node, "cy", 0.0, model_ident);
4075 let radius = generate_f32_attr(node, "radius", 0.0, model_ident);
4076 let fill = generate_color_option_attr(node, "fill", model_ident);
4077 let stroke = generate_color_option_attr(node, "stroke", model_ident);
4078 let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4079
4080 Ok(quote! {
4081 dampen_iced::canvas::CanvasShape::Circle(dampen_iced::canvas::CircleShape {
4082 cx: #cx,
4083 cy: #cy,
4084 radius: #radius,
4085 fill: #fill,
4086 stroke: #stroke,
4087 stroke_width: #stroke_width,
4088 })
4089 })
4090}
4091
4092fn generate_line_shape(
4093 node: &crate::WidgetNode,
4094 model_ident: &syn::Ident,
4095) -> Result<TokenStream, super::CodegenError> {
4096 let x1 = generate_f32_attr(node, "x1", 0.0, model_ident);
4097 let y1 = generate_f32_attr(node, "y1", 0.0, model_ident);
4098 let x2 = generate_f32_attr(node, "x2", 0.0, model_ident);
4099 let y2 = generate_f32_attr(node, "y2", 0.0, model_ident);
4100 let stroke = generate_color_option_attr(node, "stroke", model_ident);
4101 let stroke_width = generate_f32_attr(node, "stroke_width", 1.0, model_ident);
4102
4103 Ok(quote! {
4104 dampen_iced::canvas::CanvasShape::Line(dampen_iced::canvas::LineShape {
4105 x1: #x1,
4106 y1: #y1,
4107 x2: #x2,
4108 y2: #y2,
4109 stroke: #stroke,
4110 stroke_width: #stroke_width,
4111 })
4112 })
4113}
4114
4115fn generate_text_shape(
4116 node: &crate::WidgetNode,
4117 model_ident: &syn::Ident,
4118) -> Result<TokenStream, super::CodegenError> {
4119 let x = generate_f32_attr(node, "x", 0.0, model_ident);
4120 let y = generate_f32_attr(node, "y", 0.0, model_ident);
4121 let content = generate_attribute_value(
4122 node.attributes
4123 .get("content")
4124 .unwrap_or(&AttributeValue::Static(String::new())),
4125 model_ident,
4126 );
4127 let size = generate_f32_attr(node, "size", 16.0, model_ident);
4128 let color = generate_color_option_attr(node, "color", model_ident);
4129
4130 Ok(quote! {
4131 dampen_iced::canvas::CanvasShape::Text(dampen_iced::canvas::TextShape {
4132 x: #x,
4133 y: #y,
4134 content: #content,
4135 size: #size,
4136 color: #color,
4137 })
4138 })
4139}
4140
4141fn generate_group_shape(
4142 node: &crate::WidgetNode,
4143 model_ident: &syn::Ident,
4144) -> Result<TokenStream, super::CodegenError> {
4145 let children = generate_canvas_shapes(&node.children, model_ident)?;
4146 let transform = generate_transform_attr(node, model_ident);
4147
4148 Ok(quote! {
4149 dampen_iced::canvas::CanvasShape::Group(dampen_iced::canvas::GroupShape {
4150 transform: #transform,
4151 children: vec![#(#children),*],
4152 })
4153 })
4154}
4155
4156fn generate_f32_attr(
4157 node: &crate::WidgetNode,
4158 name: &str,
4159 default: f32,
4160 _model_ident: &syn::Ident,
4161) -> TokenStream {
4162 if let Some(attr) = node.attributes.get(name) {
4163 match attr {
4164 AttributeValue::Static(s) => {
4165 let val = s.parse::<f32>().unwrap_or(default);
4166 quote! { #val }
4167 }
4168 AttributeValue::Binding(expr) => {
4169 let tokens = super::bindings::generate_bool_expr(&expr.expr);
4170 quote! { (#tokens) as f32 }
4171 }
4172 AttributeValue::Interpolated(_) => quote! { #default },
4173 }
4174 } else {
4175 quote! { #default }
4176 }
4177}
4178
4179fn generate_color_option_attr(
4180 node: &crate::WidgetNode,
4181 name: &str,
4182 _model_ident: &syn::Ident,
4183) -> TokenStream {
4184 if let Some(attr) = node.attributes.get(name) {
4185 match attr {
4186 AttributeValue::Static(s) => {
4187 if let Ok(c) = crate::parser::style_parser::parse_color_attr(s) {
4188 let r = c.r;
4189 let g = c.g;
4190 let b = c.b;
4191 let a = c.a;
4192 quote! { Some(iced::Color::from_rgba(#r, #g, #b, #a)) }
4193 } else {
4194 quote! { None }
4195 }
4196 }
4197 AttributeValue::Binding(expr) => {
4198 let tokens = generate_expr(&expr.expr);
4199 quote! {
4200 dampen_iced::convert::parse_color_maybe(&(#tokens).to_string())
4201 .map(|c| iced::Color::from_rgba(c.r, c.g, c.b, c.a))
4202 }
4203 }
4204 _ => quote! { None },
4205 }
4206 } else {
4207 quote! { None }
4208 }
4209}
4210
4211fn generate_transform_attr(node: &crate::WidgetNode, _model_ident: &syn::Ident) -> TokenStream {
4212 if let Some(AttributeValue::Static(s)) = node.attributes.get("transform") {
4213 let s = s.trim();
4214 if let Some(inner) = s
4215 .strip_prefix("translate(")
4216 .and_then(|s| s.strip_suffix(")"))
4217 {
4218 let parts: Vec<f32> = inner
4219 .split(',')
4220 .filter_map(|p| p.trim().parse().ok())
4221 .collect();
4222 if parts.len() == 2 {
4223 let x = parts[0];
4224 let y = parts[1];
4225 return quote! { Some(dampen_iced::canvas::Transform::Translate(#x, #y)) };
4226 }
4227 }
4228 if let Some(inner) = s.strip_prefix("rotate(").and_then(|s| s.strip_suffix(")"))
4229 && let Ok(angle) = inner.trim().parse::<f32>()
4230 {
4231 return quote! { Some(dampen_iced::canvas::Transform::Rotate(#angle)) };
4232 }
4233 if let Some(inner) = s.strip_prefix("scale(").and_then(|s| s.strip_suffix(")")) {
4234 let parts: Vec<f32> = inner
4235 .split(',')
4236 .filter_map(|p| p.trim().parse().ok())
4237 .collect();
4238 if parts.len() == 1 {
4239 let s = parts[0];
4240 return quote! { Some(dampen_iced::canvas::Transform::Scale(#s)) };
4241 } else if parts.len() == 2 {
4242 let x = parts[0];
4243 let y = parts[1];
4244 return quote! { Some(dampen_iced::canvas::Transform::ScaleXY(#x, #y)) };
4245 }
4246 }
4247 if let Some(inner) = s.strip_prefix("matrix(").and_then(|s| s.strip_suffix(")")) {
4248 let parts: Vec<f32> = inner
4249 .split(',')
4250 .filter_map(|p| p.trim().parse().ok())
4251 .collect();
4252 if parts.len() == 6 {
4253 return quote! { Some(dampen_iced::canvas::Transform::Matrix([#(#parts),*])) };
4254 }
4255 }
4256 quote! { None }
4257 } else {
4258 quote! { None }
4259 }
4260}
4261
4262fn generate_canvas_handlers(
4263 node: &crate::WidgetNode,
4264 _model_ident: &syn::Ident,
4265 message_ident: &syn::Ident,
4266) -> Result<Option<TokenStream>, super::CodegenError> {
4267 let on_click = node
4268 .events
4269 .iter()
4270 .find(|e| e.event == crate::EventKind::CanvasClick);
4271 let on_drag = node
4272 .events
4273 .iter()
4274 .find(|e| e.event == crate::EventKind::CanvasDrag);
4275 let on_move = node
4276 .events
4277 .iter()
4278 .find(|e| e.event == crate::EventKind::CanvasMove);
4279 let on_release = node
4280 .events
4281 .iter()
4282 .find(|e| e.event == crate::EventKind::CanvasRelease);
4283
4284 if on_click.is_none() && on_drag.is_none() && on_move.is_none() && on_release.is_none() {
4285 return Ok(None);
4286 }
4287
4288 let mut match_arms = Vec::new();
4289
4290 if let Some(e) = on_click {
4291 let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4292 let name = &e.handler;
4293 match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4294 }
4295 if let Some(e) = on_drag {
4296 let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4297 let name = &e.handler;
4298 match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4299 }
4300 if let Some(e) = on_move {
4301 let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4302 let name = &e.handler;
4303 match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4304 }
4305 if let Some(e) = on_release {
4306 let variant = format_ident!("{}", to_upper_camel_case(&e.handler));
4307 let name = &e.handler;
4308 match_arms.push(quote! { #name => #message_ident :: #variant(event) });
4309 }
4310
4311 let click_name = on_click.map(|e| e.handler.as_str()).unwrap_or("");
4312 let drag_name = on_drag.map(|e| e.handler.as_str()).unwrap_or("");
4313 let move_name = on_move.map(|e| e.handler.as_str()).unwrap_or("");
4314 let release_name = on_release.map(|e| e.handler.as_str()).unwrap_or("");
4315
4316 Ok(Some(quote! {
4317 dampen_iced::canvas::CanvasEventHandlers {
4318 handler_names: dampen_iced::canvas::CanvasHandlerNames {
4319 on_click: if #click_name != "" { Some(#click_name.to_string()) } else { None },
4320 on_drag: if #drag_name != "" { Some(#drag_name.to_string()) } else { None },
4321 on_move: if #move_name != "" { Some(#move_name.to_string()) } else { None },
4322 on_release: if #release_name != "" { Some(#release_name.to_string()) } else { None },
4323 },
4324 msg_factory: |name, event| {
4325 match name {
4326 #(#match_arms,)*
4327 _ => panic!("Unknown canvas handler: {}", name),
4328 }
4329 }
4330 }
4331 }))
4332}
4333
4334fn generate_menu(
4336 node: &crate::WidgetNode,
4337 model_ident: &syn::Ident,
4338 message_ident: &syn::Ident,
4339 style_classes: &HashMap<String, StyleClass>,
4340) -> Result<TokenStream, super::CodegenError> {
4341 let items = generate_menu_items(&node.children, model_ident, message_ident, style_classes)?;
4342 Ok(quote! {
4344 iced_aw::menu::MenuBar::new(#items).into()
4345 })
4346}
4347
4348fn generate_menu_items(
4350 children: &[crate::WidgetNode],
4351 model_ident: &syn::Ident,
4352 message_ident: &syn::Ident,
4353 style_classes: &HashMap<String, StyleClass>,
4354) -> Result<TokenStream, super::CodegenError> {
4355 let mut item_exprs = Vec::new();
4356
4357 for child in children {
4358 match child.kind {
4359 WidgetKind::MenuItem => {
4360 item_exprs.push(generate_menu_item_struct(
4361 child,
4362 model_ident,
4363 message_ident,
4364 style_classes,
4365 )?);
4366 }
4367 WidgetKind::MenuSeparator => {
4368 item_exprs.push(generate_menu_separator_struct(child)?);
4369 }
4370 _ => {}
4371 }
4372 }
4373
4374 Ok(quote! {
4375 vec![#(#item_exprs),*]
4376 })
4377}
4378
4379fn generate_menu_item_struct(
4381 node: &crate::WidgetNode,
4382 model_ident: &syn::Ident,
4383 message_ident: &syn::Ident,
4384 style_classes: &HashMap<String, StyleClass>,
4385) -> Result<TokenStream, super::CodegenError> {
4386 let label_attr = node.attributes.get("label").ok_or_else(|| {
4387 super::CodegenError::InvalidWidget("MenuItem requires label attribute".to_string())
4388 })?;
4389
4390 let label_expr = generate_attribute_value(label_attr, model_ident);
4391
4392 let mut btn = quote! {
4394 iced::widget::button(iced::widget::text(#label_expr))
4395 .width(iced::Length::Shrink) .style(iced::widget::button::text)
4397 };
4398
4399 if let Some(event) = node
4400 .events
4401 .iter()
4402 .find(|e| e.event == crate::EventKind::Click)
4403 {
4404 let variant_name = to_upper_camel_case(&event.handler);
4405 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4406
4407 let msg = if let Some(param) = &event.param {
4408 let param_expr = crate::codegen::bindings::generate_expr(¶m.expr);
4409 quote! { #message_ident::#variant_ident(#param_expr) }
4410 } else {
4411 quote! { #message_ident::#variant_ident }
4412 };
4413
4414 btn = quote! { #btn.on_press(#msg) };
4415 }
4416
4417 let content = quote! { #btn };
4418
4419 if let Some(submenu) = node.children.iter().find(|c| c.kind == WidgetKind::Menu) {
4421 let items =
4422 generate_menu_items(&submenu.children, model_ident, message_ident, style_classes)?;
4423 Ok(quote! {
4424 iced_aw::menu::Item::with_menu(#content, iced_aw::menu::Menu::new(#items))
4425 })
4426 } else {
4427 Ok(quote! {
4428 iced_aw::menu::Item::new(#content)
4429 })
4430 }
4431}
4432
4433fn generate_menu_separator_struct(
4434 _node: &crate::WidgetNode,
4435) -> Result<TokenStream, super::CodegenError> {
4436 Ok(quote! {
4437 iced_aw::menu::Item::new(iced::widget::rule::horizontal(1))
4438 })
4439}
4440
4441fn generate_context_menu(
4442 node: &crate::WidgetNode,
4443 model_ident: &syn::Ident,
4444 message_ident: &syn::Ident,
4445 style_classes: &HashMap<String, StyleClass>,
4446 local_vars: &std::collections::HashSet<String>,
4447) -> Result<TokenStream, super::CodegenError> {
4448 let underlay = node
4449 .children
4450 .first()
4451 .ok_or(super::CodegenError::InvalidWidget(
4452 "ContextMenu requires underlay".into(),
4453 ))?;
4454 let underlay_expr = generate_widget_with_locals(
4455 underlay,
4456 model_ident,
4457 message_ident,
4458 style_classes,
4459 local_vars,
4460 )?;
4461
4462 let menu_node = node
4463 .children
4464 .get(1)
4465 .ok_or(super::CodegenError::InvalidWidget(
4466 "ContextMenu requires menu".into(),
4467 ))?;
4468
4469 if menu_node.kind != WidgetKind::Menu {
4470 return Err(super::CodegenError::InvalidWidget(
4471 "Second child of ContextMenu must be <menu>".into(),
4472 ));
4473 }
4474
4475 let mut buttons = Vec::new();
4477 for child in &menu_node.children {
4478 match child.kind {
4479 WidgetKind::MenuItem => {
4480 let label =
4481 child
4482 .attributes
4483 .get("label")
4484 .ok_or(super::CodegenError::InvalidWidget(
4485 "MenuItem requires label".into(),
4486 ))?;
4487 let label_expr =
4488 generate_attribute_value_with_locals(label, model_ident, local_vars);
4489
4490 let mut btn = quote! {
4491 iced::widget::button(iced::widget::text(#label_expr))
4492 .width(iced::Length::Fill)
4493 .style(iced::widget::button::text)
4494 };
4495
4496 if let Some(event) = child
4497 .events
4498 .iter()
4499 .find(|e| e.event == crate::EventKind::Click)
4500 {
4501 let variant_name = to_upper_camel_case(&event.handler);
4502 let variant_ident =
4503 syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
4504
4505 let msg = if let Some(param) = &event.param {
4506 let param_expr = crate::codegen::bindings::generate_expr(¶m.expr);
4507 quote! { #message_ident::#variant_ident(#param_expr) }
4508 } else {
4509 quote! { #message_ident::#variant_ident }
4510 };
4511 btn = quote! { #btn.on_press(#msg) };
4512 }
4513
4514 buttons.push(quote! { #btn.into() });
4515 }
4516 WidgetKind::MenuSeparator => {
4517 buttons.push(quote! { iced::widget::rule::horizontal(1).into() });
4518 }
4519 _ => {}
4520 }
4521 }
4522
4523 let overlay_content = quote! {
4524 iced::widget::container(
4525 iced::widget::column(vec![#(#buttons),*])
4526 .spacing(2)
4527 )
4528 .padding(5)
4529 .style(iced::widget::container::bordered_box)
4530 .into()
4531 };
4532
4533 Ok(quote! {
4534 iced_aw::ContextMenu::new(
4535 #underlay_expr,
4536 move || #overlay_content
4537 )
4538 .into()
4539 })
4540}
4541
4542fn generate_data_table(
4543 node: &crate::WidgetNode,
4544 model_ident: &syn::Ident,
4545 message_ident: &syn::Ident,
4546 style_classes: &HashMap<String, StyleClass>,
4547) -> Result<TokenStream, super::CodegenError> {
4548 let data_attr = node.attributes.get("data").ok_or_else(|| {
4549 super::CodegenError::InvalidWidget("data_table requires data attribute".to_string())
4550 })?;
4551 let data_expr = generate_attribute_value_raw(data_attr, model_ident);
4552
4553 let mut column_exprs = Vec::new();
4554 for child in &node.children {
4555 if child.kind == WidgetKind::DataColumn {
4556 let header_attr = child.attributes.get("header").ok_or_else(|| {
4557 super::CodegenError::InvalidWidget(
4558 "data_column requires header attribute".to_string(),
4559 )
4560 })?;
4561 let header_expr = generate_attribute_value(header_attr, model_ident);
4562 let header = quote! { iced::widget::text(#header_expr) };
4563
4564 let field = child.attributes.get("field");
4565
4566 let view_closure = if let Some(AttributeValue::Static(field_name)) = field {
4567 let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
4568 quote! {
4570 |item| iced::widget::text(item.#field_ident.to_string()).into()
4571 }
4572 } else {
4573 let template_content = if let Some(tmpl) = child
4576 .children
4577 .iter()
4578 .find(|c| matches!(c.kind, WidgetKind::Custom(ref s) if s == "template"))
4579 {
4580 &tmpl.children
4581 } else {
4582 &child.children
4583 };
4584
4585 if let Some(root) = template_content.first() {
4586 let mut locals = std::collections::HashSet::new();
4587 locals.insert("index".to_string());
4588 locals.insert("item".to_string());
4589
4590 let widget_expr = generate_widget_with_locals(
4591 root,
4592 model_ident,
4593 message_ident,
4594 style_classes,
4595 &locals,
4596 )?;
4597
4598 quote! {
4599 |(index, item)| {
4600 let _ = index; #widget_expr.into()
4602 }
4603 }
4604 } else {
4605 quote! { |(_index, _item)| iced::widget::text("").into() }
4606 }
4607 };
4608
4609 let mut col = quote! {
4610 iced::widget::table::column(#header, #view_closure)
4611 };
4612
4613 if let Some(width) = child.attributes.get("width") {
4614 let width_expr = match width {
4615 AttributeValue::Static(s) => generate_length_expr(s),
4616 _ => quote! { iced::Length::Fill },
4617 };
4618 col = quote! { #col.width(#width_expr) };
4619 }
4620
4621 if let Some(align_x) = child.attributes.get("align_x")
4623 && let AttributeValue::Static(s) = align_x
4624 {
4625 let align_expr = match s.to_lowercase().as_str() {
4626 "center" => quote! { iced::alignment::Horizontal::Center },
4627 "end" | "right" => quote! { iced::alignment::Horizontal::Right },
4628 _ => quote! { iced::alignment::Horizontal::Left },
4629 };
4630 col = quote! { #col.align_x(#align_expr) };
4631 }
4632
4633 if let Some(align_y) = child.attributes.get("align_y")
4635 && let AttributeValue::Static(s) = align_y
4636 {
4637 let align_expr = match s.to_lowercase().as_str() {
4638 "center" => quote! { iced::alignment::Vertical::Center },
4639 "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
4640 _ => quote! { iced::alignment::Vertical::Top },
4641 };
4642 col = quote! { #col.align_y(#align_expr) };
4643 }
4644
4645 column_exprs.push(col);
4646 }
4647 }
4648
4649 let table = quote! {
4650 iced::widget::table::Table::new(vec![#(#column_exprs),*], #data_expr)
4651 };
4652
4653 Ok(maybe_wrap_in_container(table, node))
4688}
4689
4690fn generate_tree_view(
4692 node: &crate::WidgetNode,
4693 model_ident: &syn::Ident,
4694 message_ident: &syn::Ident,
4695 style_classes: &HashMap<String, StyleClass>,
4696 local_vars: &std::collections::HashSet<String>,
4697) -> Result<TokenStream, super::CodegenError> {
4698 let indent_size = node
4700 .attributes
4701 .get("indent_size")
4702 .and_then(|attr| match attr {
4703 AttributeValue::Static(s) => s.parse::<f32>().ok(),
4704 _ => None,
4705 })
4706 .unwrap_or(20.0);
4707
4708 let node_height = node
4709 .attributes
4710 .get("node_height")
4711 .and_then(|attr| match attr {
4712 AttributeValue::Static(s) => s.parse::<f32>().ok(),
4713 _ => None,
4714 })
4715 .unwrap_or(30.0);
4716
4717 let _icon_size = node
4718 .attributes
4719 .get("icon_size")
4720 .and_then(|attr| match attr {
4721 AttributeValue::Static(s) => s.parse::<f32>().ok(),
4722 _ => None,
4723 })
4724 .unwrap_or(16.0);
4725
4726 let expand_icon = node
4727 .attributes
4728 .get("expand_icon")
4729 .and_then(|attr| match attr {
4730 AttributeValue::Static(s) => Some(s.clone()),
4731 _ => None,
4732 })
4733 .unwrap_or_else(|| "â–¶".to_string());
4734
4735 let collapse_icon = node
4736 .attributes
4737 .get("collapse_icon")
4738 .and_then(|attr| match attr {
4739 AttributeValue::Static(s) => Some(s.clone()),
4740 _ => None,
4741 })
4742 .unwrap_or_else(|| "â–¼".to_string());
4743
4744 let has_nodes_binding = node.attributes.contains_key("nodes");
4746
4747 if has_nodes_binding {
4748 let nodes_binding = node.attributes.get("nodes").ok_or_else(|| {
4750 super::CodegenError::InvalidWidget("nodes attribute is required".into())
4751 })?;
4752 let nodes_expr = generate_attribute_value_raw(nodes_binding, model_ident);
4753
4754 let expanded_binding = node.attributes.get("expanded");
4756 let expanded_expr =
4757 expanded_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4758
4759 let selected_binding = node.attributes.get("selected");
4761 let selected_expr =
4762 selected_binding.map(|attr| generate_attribute_value_raw(attr, model_ident));
4763
4764 let tree_view = quote! {
4766 {
4767 let tree_nodes = #nodes_expr;
4768 let expanded_ids: std::collections::HashSet<String> = #expanded_expr
4769 .map(|v: Vec<String>| v.into_iter().collect())
4770 .unwrap_or_default();
4771 let selected_id: Option<String> = #selected_expr;
4772
4773 fn build_tree_nodes(
4775 nodes: &[TreeNode],
4776 expanded_ids: &std::collections::HashSet<String>,
4777 selected_id: &Option<String>,
4778 depth: usize,
4779 ) -> Vec<iced::Element<'static, #message_ident>> {
4780 let mut elements = Vec::new();
4781 for node in nodes {
4782 let is_expanded = expanded_ids.contains(&node.id);
4783 let is_selected = selected_id.as_ref() == Some(&node.id);
4784 let has_children = !node.children.is_empty();
4785
4786 let indent = (depth as f32) * #indent_size;
4788 let node_element = build_tree_node_row(
4789 node,
4790 is_expanded,
4791 is_selected,
4792 has_children,
4793 indent,
4794 #node_height,
4795 #expand_icon,
4796 #collapse_icon,
4797 );
4798 elements.push(node_element);
4799
4800 if is_expanded && has_children {
4802 let child_elements = build_tree_nodes(
4803 &node.children,
4804 expanded_ids,
4805 selected_id,
4806 depth + 1,
4807 );
4808 elements.extend(child_elements);
4809 }
4810 }
4811 elements
4812 }
4813
4814 iced::widget::column(build_tree_nodes(&tree_nodes, &expanded_ids, &selected_id, 0))
4815 .spacing(2)
4816 .into()
4817 }
4818 };
4819
4820 Ok(tree_view)
4821 } else {
4822 let tree_elements: Vec<TokenStream> = node
4824 .children
4825 .iter()
4826 .filter(|c| c.kind == WidgetKind::TreeNode)
4827 .map(|child| {
4828 generate_tree_node(
4829 child,
4830 model_ident,
4831 message_ident,
4832 style_classes,
4833 local_vars,
4834 indent_size,
4835 node_height,
4836 &expand_icon,
4837 &collapse_icon,
4838 0,
4839 node,
4840 )
4841 })
4842 .collect::<Result<_, _>>()?;
4843
4844 Ok(quote! {
4845 iced::widget::column(vec![#(#tree_elements),*])
4846 .spacing(2)
4847 .into()
4848 })
4849 }
4850}
4851
4852#[allow(clippy::too_many_arguments)]
4854fn generate_tree_node(
4855 node: &crate::WidgetNode,
4856 _model_ident: &syn::Ident,
4857 message_ident: &syn::Ident,
4858 _style_classes: &HashMap<String, StyleClass>,
4859 _local_vars: &std::collections::HashSet<String>,
4860 indent_size: f32,
4861 node_height: f32,
4862 expand_icon: &str,
4863 collapse_icon: &str,
4864 depth: usize,
4865 parent_node: &crate::WidgetNode,
4866) -> Result<TokenStream, super::CodegenError> {
4867 if depth > 50 {
4869 return Ok(quote! {
4870 iced::widget::text("... max depth reached").size(12).into()
4871 });
4872 }
4873
4874 let id = node.id.clone().unwrap_or_else(|| "unknown".to_string());
4875
4876 let label = node
4877 .attributes
4878 .get("label")
4879 .and_then(|attr| match attr {
4880 AttributeValue::Static(s) => Some(s.clone()),
4881 _ => None,
4882 })
4883 .unwrap_or_else(|| id.clone());
4884
4885 let icon = node.attributes.get("icon").and_then(|attr| match attr {
4886 AttributeValue::Static(s) => Some(s.clone()),
4887 _ => None,
4888 });
4889
4890 let expanded = node.attributes.get("expanded").and_then(|attr| match attr {
4891 AttributeValue::Static(s) => s.parse::<bool>().ok(),
4892 _ => None,
4893 });
4894
4895 let selected = node.attributes.get("selected").and_then(|attr| match attr {
4896 AttributeValue::Static(s) => s.parse::<bool>().ok(),
4897 _ => None,
4898 });
4899
4900 let _disabled = node.attributes.get("disabled").and_then(|attr| match attr {
4901 AttributeValue::Static(s) => s.parse::<bool>().ok(),
4902 _ => None,
4903 });
4904
4905 let has_children = !node.children.is_empty();
4906 let is_expanded = expanded.unwrap_or(false);
4907 let is_selected = selected.unwrap_or(false);
4908
4909 let indent = (depth as f32) * indent_size;
4910
4911 let label_text = if let Some(ref icon_str) = icon {
4913 format!("{} {}", icon_str, label)
4914 } else {
4915 label
4916 };
4917
4918 let toggle_button = if has_children {
4920 let icon = if is_expanded {
4921 collapse_icon
4922 } else {
4923 expand_icon
4924 };
4925
4926 if let Some(event) = parent_node
4928 .events
4929 .iter()
4930 .find(|e| matches!(e.event, crate::ir::node::EventKind::Toggle))
4931 {
4932 let variant_name = to_upper_camel_case(&event.handler);
4933 let handler_ident = format_ident!("{}", variant_name);
4934
4935 quote! {
4936 iced::widget::button(iced::widget::text(#icon).size(14))
4937 .on_press(#message_ident::#handler_ident)
4938 .width(iced::Length::Fixed(20.0))
4939 .height(iced::Length::Fixed(#node_height))
4940 }
4941 } else {
4942 quote! {
4943 iced::widget::text(#icon).size(14)
4944 }
4945 }
4946 } else {
4947 quote! {
4948 iced::widget::container(iced::widget::text(""))
4949 .width(iced::Length::Fixed(20.0))
4950 }
4951 };
4952
4953 let label_element = if let Some(event) = parent_node
4955 .events
4956 .iter()
4957 .find(|e| matches!(e.event, crate::ir::node::EventKind::Select))
4958 {
4959 let variant_name = to_upper_camel_case(&event.handler);
4960 let handler_ident = format_ident!("{}", variant_name);
4961
4962 quote! {
4963 iced::widget::button(iced::widget::text(#label_text).size(14))
4964 .on_press(#message_ident::#handler_ident)
4965 .style(|_theme: &iced::Theme, _status: iced::widget::button::Status| {
4966 if #is_selected {
4967 iced::widget::button::Style {
4968 background: Some(iced::Background::Color(
4969 iced::Color::from_rgb(0.0, 0.48, 0.8),
4970 )),
4971 text_color: iced::Color::WHITE,
4972 ..Default::default()
4973 }
4974 } else {
4975 iced::widget::button::Style::default()
4976 }
4977 })
4978 }
4979 } else {
4980 quote! {
4981 iced::widget::text(#label_text).size(14)
4982 }
4983 };
4984
4985 let node_row = quote! {
4987 iced::widget::row(vec![#toggle_button.into(), #label_element.into()])
4988 .spacing(4)
4989 .padding(iced::Padding::from([0.0, 0.0, 0.0, #indent]))
4990 };
4991
4992 if is_expanded && has_children {
4994 let child_elements: Vec<TokenStream> = node
4995 .children
4996 .iter()
4997 .filter(|c| c.kind == WidgetKind::TreeNode)
4998 .map(|child| {
4999 generate_tree_node(
5000 child,
5001 _model_ident,
5002 message_ident,
5003 _style_classes,
5004 _local_vars,
5005 indent_size,
5006 node_height,
5007 expand_icon,
5008 collapse_icon,
5009 depth + 1,
5010 parent_node,
5011 )
5012 })
5013 .collect::<Result<_, _>>()?;
5014
5015 Ok(quote! {
5016 iced::widget::column(vec![
5017 #node_row.into(),
5018 iced::widget::column(vec![#(#child_elements),*])
5019 .spacing(2)
5020 .into(),
5021 ])
5022 .spacing(2)
5023 })
5024 } else {
5025 Ok(node_row)
5026 }
5027}
5028
5029#[cfg(test)]
5030mod tests {
5031 use super::*;
5032 use crate::parse;
5033
5034 #[test]
5035 fn test_view_generation() {
5036 let xml = r#"<column><text value="Hello" /></column>"#;
5037 let doc = parse(xml).unwrap();
5038
5039 let result = generate_view(&doc, "Model", "Message").unwrap();
5040 let code = result.to_string();
5041
5042 assert!(code.contains("text"));
5043 assert!(code.contains("column"));
5044 }
5045
5046 #[test]
5047 fn test_view_generation_with_binding() {
5048 let xml = r#"<column><text value="{name}" /></column>"#;
5049 let doc = parse(xml).unwrap();
5050
5051 let result = generate_view(&doc, "Model", "Message").unwrap();
5052 let code = result.to_string();
5053
5054 assert!(code.contains("name"));
5055 assert!(code.contains("to_string"));
5056 }
5057
5058 #[test]
5059 fn test_button_with_handler() {
5060 let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
5061 let doc = parse(xml).unwrap();
5062
5063 let result = generate_view(&doc, "Model", "Message").unwrap();
5064 let code = result.to_string();
5065
5066 assert!(code.contains("button"));
5067 assert!(code.contains("HandleClick"));
5068 }
5069
5070 #[test]
5071 fn test_container_with_children() {
5072 let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
5073 let doc = parse(xml).unwrap();
5074
5075 let result = generate_view(&doc, "Model", "Message").unwrap();
5076 let code = result.to_string();
5077
5078 assert!(code.contains("column"));
5079 assert!(code.contains("spacing"));
5080 }
5081
5082 #[test]
5083 fn test_button_with_inline_style() {
5084 use crate::ir::node::WidgetNode;
5085 use crate::ir::style::{Background, Color, StyleProperties};
5086 use std::collections::HashMap;
5087
5088 let button_node = WidgetNode {
5090 kind: WidgetKind::Button,
5091 id: None,
5092 attributes: {
5093 let mut attrs = HashMap::new();
5094 attrs.insert(
5095 "label".to_string(),
5096 AttributeValue::Static("Test".to_string()),
5097 );
5098 attrs
5099 },
5100 events: vec![],
5101 children: vec![],
5102 span: Default::default(),
5103 style: Some(StyleProperties {
5104 background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
5105 color: Some(Color::from_rgb8(255, 255, 255)),
5106 border: None,
5107 shadow: None,
5108 opacity: None,
5109 transform: None,
5110 }),
5111 layout: None,
5112 theme_ref: None,
5113 classes: vec![],
5114 breakpoint_attributes: HashMap::new(),
5115 inline_state_variants: HashMap::new(),
5116 };
5117
5118 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5119 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5120 let style_classes = HashMap::new();
5121
5122 let result =
5123 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
5124 let code = result.to_string();
5125
5126 assert!(code.contains("style"));
5128 assert!(code.contains("button :: Status"));
5129 assert!(code.contains("button :: Style"));
5130 assert!(code.contains("background"));
5131 assert!(code.contains("text_color"));
5132 }
5133
5134 #[test]
5135 fn test_button_with_css_class() {
5136 use crate::ir::node::WidgetNode;
5137 use crate::ir::theme::StyleClass;
5138 use std::collections::HashMap;
5139
5140 let button_node = WidgetNode {
5142 kind: WidgetKind::Button,
5143 id: None,
5144 attributes: {
5145 let mut attrs = HashMap::new();
5146 attrs.insert(
5147 "label".to_string(),
5148 AttributeValue::Static("Test".to_string()),
5149 );
5150 attrs
5151 },
5152 events: vec![],
5153 children: vec![],
5154 span: Default::default(),
5155 style: None,
5156 layout: None,
5157 theme_ref: None,
5158 classes: vec!["primary-button".to_string()],
5159 breakpoint_attributes: HashMap::new(),
5160 inline_state_variants: HashMap::new(),
5161 };
5162
5163 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5164 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5165 let style_classes: HashMap<String, StyleClass> = HashMap::new();
5166
5167 let result =
5168 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
5169 let code = result.to_string();
5170
5171 assert!(code.contains("style"));
5173 assert!(code.contains("style_primary_button"));
5174 }
5175
5176 #[test]
5177 fn test_container_with_inline_style() {
5178 use crate::ir::node::WidgetNode;
5179 use crate::ir::style::{
5180 Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
5181 };
5182 use crate::ir::theme::StyleClass;
5183 use std::collections::HashMap;
5184
5185 let container_node = WidgetNode {
5186 kind: WidgetKind::Container,
5187 id: None,
5188 attributes: HashMap::new(),
5189 events: vec![],
5190 children: vec![],
5191 span: Default::default(),
5192 style: Some(StyleProperties {
5193 background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
5194 color: None,
5195 border: Some(Border {
5196 width: 2.0,
5197 color: Color::from_rgb8(200, 200, 200),
5198 radius: BorderRadius {
5199 top_left: 8.0,
5200 top_right: 8.0,
5201 bottom_right: 8.0,
5202 bottom_left: 8.0,
5203 },
5204 style: BorderStyle::Solid,
5205 }),
5206 shadow: None,
5207 opacity: None,
5208 transform: None,
5209 }),
5210 layout: None,
5211 theme_ref: None,
5212 classes: vec![],
5213 breakpoint_attributes: HashMap::new(),
5214 inline_state_variants: HashMap::new(),
5215 };
5216
5217 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
5218 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
5219 let style_classes: HashMap<String, StyleClass> = HashMap::new();
5220
5221 let result = generate_container(
5222 &container_node,
5223 "container",
5224 &model_ident,
5225 &message_ident,
5226 &style_classes,
5227 )
5228 .unwrap();
5229 let code = result.to_string();
5230
5231 assert!(code.contains("style"));
5233 assert!(code.contains("container :: Style"));
5234 assert!(code.contains("background"));
5235 assert!(code.contains("border"));
5236 }
5237}
5238
5239fn generate_tab_bar_with_locals(
5241 node: &crate::WidgetNode,
5242 model_ident: &syn::Ident,
5243 message_ident: &syn::Ident,
5244 style_classes: &HashMap<String, StyleClass>,
5245 local_vars: &std::collections::HashSet<String>,
5246) -> Result<TokenStream, super::CodegenError> {
5247 use proc_macro2::Span;
5248 use quote::quote;
5249
5250 let selected_attr = node.attributes.get("selected").ok_or_else(|| {
5252 super::CodegenError::InvalidWidget("TabBar requires 'selected' attribute".to_string())
5253 })?;
5254
5255 let selected_expr = match selected_attr {
5257 AttributeValue::Static(s) => {
5258 let idx: usize = s.parse().map_err(|_| {
5259 super::CodegenError::InvalidWidget(format!("Invalid selected index: {}", s))
5260 })?;
5261 quote! { #idx }
5262 }
5263 AttributeValue::Binding(binding) => {
5264 let binding_expr = generate_expr(&binding.expr);
5266 quote! { (#binding_expr).parse::<usize>().unwrap_or(0) }
5267 }
5268 _ => quote! { 0usize },
5269 };
5270
5271 let on_select_handler = node
5273 .events
5274 .iter()
5275 .find(|e| matches!(e.event, crate::ir::EventKind::Select))
5276 .map(|e| syn::Ident::new(&e.handler, Span::call_site()));
5277
5278 let _tab_count = node.children.len();
5280 let tab_labels: Vec<_> = node
5281 .children
5282 .iter()
5283 .enumerate()
5284 .map(|(idx, child)| {
5285 let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5286
5287 let label_expr = if let Some(label_attr) = child.attributes.get("label") {
5289 match label_attr {
5290 AttributeValue::Static(s) => Some(quote! { #s.to_string() }),
5291 _ => None,
5292 }
5293 } else {
5294 None
5295 };
5296
5297 let icon_expr = if let Some(icon_attr) = child.attributes.get("icon") {
5299 match icon_attr {
5300 AttributeValue::Static(s) => {
5301 let icon_char = resolve_icon_for_codegen(s);
5302 Some(quote! { #icon_char })
5303 }
5304 _ => None,
5305 }
5306 } else {
5307 None
5308 };
5309
5310 let tab_label_expr = match (icon_expr, label_expr) {
5312 (Some(icon), Some(label)) => {
5313 quote! { iced_aw::tab_bar::TabLabel::IconText(#icon, #label) }
5314 }
5315 (Some(icon), None) => {
5316 quote! { iced_aw::tab_bar::TabLabel::Icon(#icon) }
5317 }
5318 (None, Some(label)) => {
5319 quote! { iced_aw::tab_bar::TabLabel::Text(#label) }
5320 }
5321 (None, None) => {
5322 quote! { iced_aw::tab_bar::TabLabel::Text("Tab".to_string()) }
5323 }
5324 };
5325
5326 quote! {
5327 tab_bar = tab_bar.push(#idx_lit, #tab_label_expr);
5328 }
5329 })
5330 .collect();
5331
5332 let tab_content_arms: Vec<_> = node
5334 .children
5335 .iter()
5336 .enumerate()
5337 .map(|(idx, child)| {
5338 let idx_lit = proc_macro2::Literal::usize_unsuffixed(idx);
5339
5340 let content_widgets: Vec<_> = child
5342 .children
5343 .iter()
5344 .map(|child_node| {
5345 generate_widget_with_locals(
5346 child_node,
5347 model_ident,
5348 message_ident,
5349 style_classes,
5350 local_vars,
5351 )
5352 })
5353 .collect::<Result<Vec<_>, _>>()?;
5354
5355 Ok::<_, super::CodegenError>(quote! {
5356 #idx_lit => iced::widget::column(vec![#(#content_widgets),*]).into()
5357 })
5358 })
5359 .collect::<Result<Vec<_>, super::CodegenError>>()?;
5360
5361 let on_select_expr = if let Some(handler) = on_select_handler {
5363 quote! {
5364 .on_select(|idx| #message_ident::#handler(idx))
5365 }
5366 } else {
5367 quote! {}
5368 };
5369
5370 let icon_size_expr = if let Some(icon_size_attr) = node.attributes.get("icon_size") {
5372 match icon_size_attr {
5373 AttributeValue::Static(s) => {
5374 if let Ok(icon_size) = s.parse::<f32>() {
5375 Some(quote! { .icon_size(#icon_size) })
5376 } else {
5377 None
5378 }
5379 }
5380 _ => None,
5381 }
5382 } else {
5383 None
5384 };
5385
5386 let text_size_expr = if let Some(text_size_attr) = node.attributes.get("text_size") {
5388 match text_size_attr {
5389 AttributeValue::Static(s) => {
5390 if let Ok(text_size) = s.parse::<f32>() {
5391 Some(quote! { .text_size(#text_size) })
5392 } else {
5393 None
5394 }
5395 }
5396 _ => None,
5397 }
5398 } else {
5399 None
5400 };
5401
5402 let tab_bar_widget = quote! {
5404 {
5405 let mut tab_bar = iced_aw::TabBar::new(#selected_expr)
5406 #on_select_expr
5407 #icon_size_expr
5408 #text_size_expr;
5409
5410 #(#tab_labels)*
5411
5412 tab_bar
5413 }
5414 };
5415
5416 let content_element = if tab_content_arms.is_empty() {
5418 quote! { iced::widget::column(vec![]).into() }
5419 } else {
5420 quote! {
5421 match #selected_expr {
5422 #(#tab_content_arms,)*
5423 _ => iced::widget::column(vec![]).into(),
5424 }
5425 }
5426 };
5427
5428 let result = quote! {
5430 iced::widget::column![
5431 #tab_bar_widget,
5432 #content_element
5433 ]
5434 };
5435
5436 Ok(result)
5437}
5438
5439fn resolve_icon_for_codegen(name: &str) -> char {
5441 match name {
5442 "home" => '\u{F015}',
5443 "settings" => '\u{F013}',
5444 "user" => '\u{F007}',
5445 "search" => '\u{F002}',
5446 "add" => '\u{F067}',
5447 "delete" => '\u{F1F8}',
5448 "edit" => '\u{F044}',
5449 "save" => '\u{F0C7}',
5450 "close" => '\u{F00D}',
5451 "back" => '\u{F060}',
5452 "forward" => '\u{F061}',
5453 _ => '\u{F111}', }
5455}