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 }
200}
201
202fn apply_widget_style(
213 widget: TokenStream,
214 node: &crate::WidgetNode,
215 widget_type: &str,
216 style_classes: &HashMap<String, StyleClass>,
217) -> Result<TokenStream, super::CodegenError> {
218 let has_inline_style = node.style.is_some();
220 let has_classes = !node.classes.is_empty();
221
222 let class_binding = node.attributes.get("class").and_then(|attr| match attr {
224 AttributeValue::Binding(expr) => Some(expr),
225 _ => None,
226 });
227 let has_class_binding = class_binding.is_some();
228
229 if !has_inline_style && !has_classes && !has_class_binding {
230 return Ok(widget);
232 }
233
234 let style_class = if let Some(class_name) = node.classes.first() {
236 style_classes.get(class_name)
237 } else {
238 None
239 };
240
241 if let Some(ref style_props) = node.style {
243 let style_closure =
245 generate_inline_style_closure(style_props, widget_type, &node.kind, style_class)?;
246 Ok(quote! {
247 #widget.style(#style_closure)
248 })
249 } else if let Some(class_name) = node.classes.first() {
250 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
253
254 match widget_type {
255 "text_input" => {
256 Ok(quote! {
259 #widget.style(|theme: &iced::Theme, _status: iced::widget::text_input::Status| {
260 let container_style = #style_fn_ident(theme);
261 iced::widget::text_input::Style {
262 background: container_style.background.unwrap_or(iced::Background::Color(theme.extended_palette().background.base.color)),
263 border: container_style.border,
264 icon: theme.extended_palette().background.base.text,
265 placeholder: theme.extended_palette().background.weak.text,
266 value: container_style.text_color.unwrap_or(theme.extended_palette().background.base.text),
267 selection: theme.extended_palette().primary.weak.color,
268 }
269 })
270 })
271 }
272 "checkbox" => {
273 let has_state_variants = style_class
276 .map(|sc| !sc.state_variants.is_empty())
277 .unwrap_or(false);
278
279 if has_state_variants {
280 Ok(quote! {
282 #widget.style(|theme: &iced::Theme, status: iced::widget::checkbox::Status| {
283 let button_status = match status {
285 iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
286 iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
287 iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
288 };
289 let button_style = #style_fn_ident(theme, button_status);
290 iced::widget::checkbox::Style {
291 background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
292 icon_color: button_style.text_color,
293 border: button_style.border,
294 text_color: None,
295 }
296 })
297 })
298 } else {
299 Ok(quote! {
301 #widget.style(|theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
302 let container_style = #style_fn_ident(theme);
303 iced::widget::checkbox::Style {
304 background: container_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
305 icon_color: container_style.text_color,
306 border: container_style.border,
307 text_color: None,
308 }
309 })
310 })
311 }
312 }
313 "button" => {
314 Ok(quote! {
317 #widget.style(#style_fn_ident)
318 })
319 }
320 _ => {
321 Ok(quote! {
323 #widget.style(#style_fn_ident)
324 })
325 }
326 }
327 } else if let Some(binding_expr) = class_binding {
328 generate_dynamic_class_style(widget, binding_expr, widget_type, style_classes)
330 } else {
331 Ok(widget)
332 }
333}
334
335fn generate_dynamic_class_style(
340 widget: TokenStream,
341 binding_expr: &crate::expr::BindingExpr,
342 widget_type: &str,
343 style_classes: &HashMap<String, StyleClass>,
344) -> Result<TokenStream, super::CodegenError> {
345 let class_expr = super::bindings::generate_expr(&binding_expr.expr);
347
348 match widget_type {
349 "button" => {
350 let mut match_arms = Vec::new();
353 for (class_name, style_class) in style_classes.iter() {
354 if !style_class.state_variants.is_empty() {
356 let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
357 let class_lit = proc_macro2::Literal::string(class_name);
358 match_arms.push(quote! {
359 #class_lit => #style_fn(_theme, status),
360 });
361 }
362 }
363
364 Ok(quote! {
365 #widget.style({
366 let __class_name = #class_expr;
367 move |_theme: &iced::Theme, status: iced::widget::button::Status| {
368 match __class_name.as_str() {
369 #(#match_arms)*
370 _ => iced::widget::button::Style::default(),
371 }
372 }
373 })
374 })
375 }
376 "checkbox" => {
377 let mut checkbox_match_arms = Vec::new();
380 for (class_name, style_class) in style_classes.iter() {
381 if !style_class.state_variants.is_empty() {
382 let style_fn = format_ident!("style_{}", class_name.replace('-', "_"));
383 let class_lit = proc_macro2::Literal::string(class_name);
384 checkbox_match_arms.push(quote! {
385 #class_lit => {
386 let button_style = #style_fn(_theme, button_status);
387 iced::widget::checkbox::Style {
388 background: button_style.background.unwrap_or(iced::Background::Color(iced::Color::WHITE)),
389 icon_color: button_style.text_color,
390 border: button_style.border,
391 text_color: None,
392 }
393 }
394 });
395 }
396 }
397 Ok(quote! {
398 #widget.style({
399 let __class_name = #class_expr;
400 move |_theme: &iced::Theme, status: iced::widget::checkbox::Status| {
401 let button_status = match status {
402 iced::widget::checkbox::Status::Active { .. } => iced::widget::button::Status::Active,
403 iced::widget::checkbox::Status::Hovered { .. } => iced::widget::button::Status::Hovered,
404 iced::widget::checkbox::Status::Disabled { .. } => iced::widget::button::Status::Disabled,
405 };
406 match __class_name.as_str() {
407 #(#checkbox_match_arms)*
408 _ => iced::widget::checkbox::Style::default(),
409 }
410 }
411 })
412 })
413 }
414 _ => {
415 let mut container_match_arms = Vec::new();
418 for (class_name, style_class) in style_classes.iter() {
419 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 container_match_arms.push(quote! {
423 #class_lit => #style_fn(_theme),
424 });
425 }
426 }
427 Ok(quote! {
428 #widget.style({
429 let __class_name = #class_expr;
430 move |_theme: &iced::Theme| {
431 match __class_name.as_str() {
432 #(#container_match_arms)*
433 _ => iced::widget::container::Style::default(),
434 }
435 }
436 })
437 })
438 }
439 }
440}
441
442fn generate_state_style_match(
453 base_style: TokenStream,
454 style_class: &StyleClass,
455 widget_state_ident: &syn::Ident,
456 style_struct_fn: fn(&StyleProperties) -> Result<TokenStream, super::CodegenError>,
457) -> Result<TokenStream, super::CodegenError> {
458 use crate::ir::theme::WidgetState;
459
460 let mut state_arms = Vec::new();
462
463 for (state, state_props) in &style_class.state_variants {
464 let state_variant = match state {
465 WidgetState::Hover => quote! { dampen_core::ir::WidgetState::Hover },
466 WidgetState::Focus => quote! { dampen_core::ir::WidgetState::Focus },
467 WidgetState::Active => quote! { dampen_core::ir::WidgetState::Active },
468 WidgetState::Disabled => quote! { dampen_core::ir::WidgetState::Disabled },
469 };
470
471 let state_style = style_struct_fn(state_props)?;
473
474 state_arms.push(quote! {
475 Some(#state_variant) => #state_style
476 });
477 }
478
479 Ok(quote! {
481 match #widget_state_ident {
482 #(#state_arms,)*
483 None => #base_style
484 }
485 })
486}
487
488fn generate_inline_style_closure(
504 style_props: &StyleProperties,
505 widget_type: &str,
506 widget_kind: &WidgetKind,
507 style_class: Option<&StyleClass>,
508) -> Result<TokenStream, super::CodegenError> {
509 let has_state_variants = style_class
511 .map(|sc| !sc.state_variants.is_empty())
512 .unwrap_or(false);
513
514 match widget_type {
515 "button" => {
516 let base_style = generate_button_style_struct(style_props)?;
517
518 if has_state_variants {
519 let status_ident = format_ident!("status");
521 if let Some(status_mapping) =
522 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
523 {
524 let widget_state_ident = format_ident!("widget_state");
525 let class = style_class.ok_or_else(|| {
527 super::CodegenError::InvalidWidget(
528 "Expected style class with state variants".to_string(),
529 )
530 })?;
531 let style_match = generate_state_style_match(
532 base_style,
533 class,
534 &widget_state_ident,
535 generate_button_style_struct,
536 )?;
537
538 Ok(quote! {
539 |_theme: &iced::Theme, #status_ident: iced::widget::button::Status| {
540 let #widget_state_ident = #status_mapping;
542
543 #style_match
545 }
546 })
547 } else {
548 Ok(quote! {
550 |_theme: &iced::Theme, _status: iced::widget::button::Status| {
551 #base_style
552 }
553 })
554 }
555 } else {
556 Ok(quote! {
558 |_theme: &iced::Theme, _status: iced::widget::button::Status| {
559 #base_style
560 }
561 })
562 }
563 }
564 "container" => {
565 let style_struct = generate_container_style_struct(style_props)?;
566 Ok(quote! {
567 |_theme: &iced::Theme| {
568 #style_struct
569 }
570 })
571 }
572 "text_input" => {
573 let base_style = generate_text_input_style_struct(style_props)?;
574
575 if has_state_variants {
576 let status_ident = format_ident!("status");
578 if let Some(status_mapping) =
579 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
580 {
581 let widget_state_ident = format_ident!("widget_state");
582 let class = style_class.ok_or_else(|| {
583 super::CodegenError::InvalidWidget(
584 "Expected style class with state variants".to_string(),
585 )
586 })?;
587 let style_match = generate_state_style_match(
588 base_style,
589 class,
590 &widget_state_ident,
591 generate_text_input_style_struct,
592 )?;
593
594 Ok(quote! {
595 |_theme: &iced::Theme, #status_ident: iced::widget::text_input::Status| {
596 let #widget_state_ident = #status_mapping;
598
599 #style_match
601 }
602 })
603 } else {
604 Ok(quote! {
606 |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
607 #base_style
608 }
609 })
610 }
611 } else {
612 Ok(quote! {
614 |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
615 #base_style
616 }
617 })
618 }
619 }
620 "checkbox" => {
621 let base_style = generate_checkbox_style_struct(style_props)?;
622
623 if has_state_variants {
624 let status_ident = format_ident!("status");
625 if let Some(status_mapping) =
626 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
627 {
628 let widget_state_ident = format_ident!("widget_state");
629 let class = style_class.ok_or_else(|| {
630 super::CodegenError::InvalidWidget(
631 "Expected style class with state variants".to_string(),
632 )
633 })?;
634 let style_match = generate_state_style_match(
635 base_style,
636 class,
637 &widget_state_ident,
638 generate_checkbox_style_struct,
639 )?;
640
641 Ok(quote! {
642 |_theme: &iced::Theme, #status_ident: iced::widget::checkbox::Status| {
643 let #widget_state_ident = #status_mapping;
644 #style_match
645 }
646 })
647 } else {
648 Ok(quote! {
649 |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
650 #base_style
651 }
652 })
653 }
654 } else {
655 Ok(quote! {
656 |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
657 #base_style
658 }
659 })
660 }
661 }
662 "toggler" => {
663 let base_style = generate_toggler_style_struct(style_props)?;
664
665 if has_state_variants {
666 let status_ident = format_ident!("status");
667 if let Some(status_mapping) =
668 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
669 {
670 let widget_state_ident = format_ident!("widget_state");
671 let class = style_class.ok_or_else(|| {
672 super::CodegenError::InvalidWidget(
673 "Expected style class with state variants".to_string(),
674 )
675 })?;
676 let style_match = generate_state_style_match(
677 base_style,
678 class,
679 &widget_state_ident,
680 generate_toggler_style_struct,
681 )?;
682
683 Ok(quote! {
684 |_theme: &iced::Theme, #status_ident: iced::widget::toggler::Status| {
685 let #widget_state_ident = #status_mapping;
686 #style_match
687 }
688 })
689 } else {
690 Ok(quote! {
691 |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
692 #base_style
693 }
694 })
695 }
696 } else {
697 Ok(quote! {
698 |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
699 #base_style
700 }
701 })
702 }
703 }
704 "slider" => {
705 let base_style = generate_slider_style_struct(style_props)?;
706
707 if has_state_variants {
708 let status_ident = format_ident!("status");
709 if let Some(status_mapping) =
710 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
711 {
712 let widget_state_ident = format_ident!("widget_state");
713 let class = style_class.ok_or_else(|| {
714 super::CodegenError::InvalidWidget(
715 "Expected style class with state variants".to_string(),
716 )
717 })?;
718 let style_match = generate_state_style_match(
719 base_style,
720 class,
721 &widget_state_ident,
722 generate_slider_style_struct,
723 )?;
724
725 Ok(quote! {
726 |_theme: &iced::Theme, #status_ident: iced::widget::slider::Status| {
727 let #widget_state_ident = #status_mapping;
728 #style_match
729 }
730 })
731 } else {
732 Ok(quote! {
733 |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
734 #base_style
735 }
736 })
737 }
738 } else {
739 Ok(quote! {
740 |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
741 #base_style
742 }
743 })
744 }
745 }
746 _ => {
747 Ok(quote! {
749 |_theme: &iced::Theme| iced::widget::container::Style::default()
750 })
751 }
752 }
753}
754
755fn generate_color_expr(color: &Color) -> TokenStream {
761 let r = color.r;
762 let g = color.g;
763 let b = color.b;
764 let a = color.a;
765 quote! {
766 iced::Color::from_rgba(#r, #g, #b, #a)
767 }
768}
769
770fn generate_background_expr(bg: &Background) -> TokenStream {
772 match bg {
773 Background::Color(color) => {
774 let color_expr = generate_color_expr(color);
775 quote! { iced::Background::Color(#color_expr) }
776 }
777 Background::Gradient(gradient) => generate_gradient_expr(gradient),
778 Background::Image { .. } => {
779 quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
780 }
781 }
782}
783
784fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
786 match gradient {
787 Gradient::Linear { angle, stops } => {
788 let radians = angle * (std::f32::consts::PI / 180.0);
789 let color_exprs: Vec<_> = stops
790 .iter()
791 .map(|s| generate_color_expr(&s.color))
792 .collect();
793 let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
794
795 quote! {
796 iced::Background::Gradient(iced::Gradient::Linear(
797 iced::gradient::Linear::new(#radians)
798 #(.add_stop(#offsets, #color_exprs))*
799 ))
800 }
801 }
802 Gradient::Radial { stops, .. } => {
803 let color_exprs: Vec<_> = stops
805 .iter()
806 .map(|s| generate_color_expr(&s.color))
807 .collect();
808 let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
809
810 quote! {
811 iced::Background::Gradient(iced::Gradient::Linear(
812 iced::gradient::Linear::new(0.0)
813 #(.add_stop(#offsets, #color_exprs))*
814 ))
815 }
816 }
817 }
818}
819
820fn generate_border_expr(border: &Border) -> TokenStream {
822 let width = border.width;
823 let color_expr = generate_color_expr(&border.color);
824 let radius_expr = generate_border_radius_expr(&border.radius);
825
826 quote! {
827 iced::Border {
828 width: #width,
829 color: #color_expr,
830 radius: #radius_expr,
831 }
832 }
833}
834
835fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
837 let tl = radius.top_left;
838 let tr = radius.top_right;
839 let br = radius.bottom_right;
840 let bl = radius.bottom_left;
841
842 quote! {
843 iced::border::Radius::from(#tl).top_right(#tr).bottom_right(#br).bottom_left(#bl)
844 }
845}
846
847fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
849 let offset_x = shadow.offset_x;
850 let offset_y = shadow.offset_y;
851 let blur = shadow.blur_radius;
852 let color_expr = generate_color_expr(&shadow.color);
853
854 quote! {
855 iced::Shadow {
856 offset: iced::Vector::new(#offset_x, #offset_y),
857 blur_radius: #blur,
858 color: #color_expr,
859 }
860 }
861}
862
863fn generate_button_style_struct(
868 props: &StyleProperties,
869) -> Result<TokenStream, super::CodegenError> {
870 let background_expr = props
871 .background
872 .as_ref()
873 .map(|bg| {
874 let expr = generate_background_expr(bg);
875 quote! { Some(#expr) }
876 })
877 .unwrap_or_else(|| quote! { None });
878
879 let text_color_expr = props
881 .color
882 .as_ref()
883 .map(generate_color_expr)
884 .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
885
886 let border_expr = props
887 .border
888 .as_ref()
889 .map(generate_border_expr)
890 .unwrap_or_else(|| quote! { iced::Border::default() });
891
892 let shadow_expr = props
893 .shadow
894 .as_ref()
895 .map(generate_shadow_expr)
896 .unwrap_or_else(|| quote! { iced::Shadow::default() });
897
898 Ok(quote! {
899 iced::widget::button::Style {
900 background: #background_expr,
901 text_color: #text_color_expr,
902 border: #border_expr,
903 shadow: #shadow_expr,
904 snap: false,
905 }
906 })
907}
908
909fn generate_container_style_struct(
911 props: &StyleProperties,
912) -> Result<TokenStream, super::CodegenError> {
913 let background_expr = props
914 .background
915 .as_ref()
916 .map(|bg| {
917 let expr = generate_background_expr(bg);
918 quote! { Some(#expr) }
919 })
920 .unwrap_or_else(|| quote! { None });
921
922 let text_color_expr = props
923 .color
924 .as_ref()
925 .map(|color| {
926 let color_expr = generate_color_expr(color);
927 quote! { Some(#color_expr) }
928 })
929 .unwrap_or_else(|| quote! { None });
930
931 let border_expr = props
932 .border
933 .as_ref()
934 .map(generate_border_expr)
935 .unwrap_or_else(|| quote! { iced::Border::default() });
936
937 let shadow_expr = props
938 .shadow
939 .as_ref()
940 .map(generate_shadow_expr)
941 .unwrap_or_else(|| quote! { iced::Shadow::default() });
942
943 Ok(quote! {
944 iced::widget::container::Style {
945 background: #background_expr,
946 text_color: #text_color_expr,
947 border: #border_expr,
948 shadow: #shadow_expr,
949 snap: false,
950 }
951 })
952}
953
954fn generate_text_input_style_struct(
959 props: &StyleProperties,
960) -> Result<TokenStream, super::CodegenError> {
961 let background_expr = props
962 .background
963 .as_ref()
964 .map(|bg| {
965 let expr = generate_background_expr(bg);
966 quote! { #expr }
967 })
968 .unwrap_or_else(
969 || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
970 );
971
972 let border_expr = props
973 .border
974 .as_ref()
975 .map(generate_border_expr)
976 .unwrap_or_else(|| quote! { iced::Border::default() });
977
978 let value_color = props
980 .color
981 .as_ref()
982 .map(generate_color_expr)
983 .unwrap_or_else(|| quote! { _theme.extended_palette().background.base.text });
984
985 Ok(quote! {
986 iced::widget::text_input::Style {
987 background: #background_expr,
988 border: #border_expr,
989 icon: _theme.extended_palette().background.base.text,
990 placeholder: _theme.extended_palette().background.weak.text,
991 value: #value_color,
992 selection: _theme.extended_palette().primary.weak.color,
993 }
994 })
995}
996
997fn generate_checkbox_style_struct(
1002 props: &StyleProperties,
1003) -> Result<TokenStream, super::CodegenError> {
1004 let background_expr = props
1005 .background
1006 .as_ref()
1007 .map(|bg| {
1008 let expr = generate_background_expr(bg);
1009 quote! { #expr }
1010 })
1011 .unwrap_or_else(
1012 || quote! { iced::Background::Color(_theme.extended_palette().background.base.color) },
1013 );
1014
1015 let border_expr = props
1016 .border
1017 .as_ref()
1018 .map(generate_border_expr)
1019 .unwrap_or_else(|| quote! { iced::Border::default() });
1020
1021 let text_color = props
1023 .color
1024 .as_ref()
1025 .map(generate_color_expr)
1026 .unwrap_or_else(|| quote! { _theme.extended_palette().primary.base.color });
1027
1028 Ok(quote! {
1029 iced::widget::checkbox::Style {
1030 background: #background_expr,
1031 icon_color: #text_color,
1032 border: #border_expr,
1033 text_color: None,
1034 }
1035 })
1036}
1037
1038fn generate_toggler_style_struct(
1040 props: &StyleProperties,
1041) -> Result<TokenStream, super::CodegenError> {
1042 let background_expr = props
1043 .background
1044 .as_ref()
1045 .map(|bg| {
1046 let expr = generate_background_expr(bg);
1047 quote! { #expr }
1048 })
1049 .unwrap_or_else(
1050 || quote! { iced::Background::Color(iced::Color::from_rgb(0.5, 0.5, 0.5)) },
1051 );
1052
1053 Ok(quote! {
1054 iced::widget::toggler::Style {
1055 background: #background_expr,
1056 background_border_width: 0.0,
1057 background_border_color: iced::Color::TRANSPARENT,
1058 foreground: iced::Background::Color(iced::Color::WHITE),
1059 foreground_border_width: 0.0,
1060 foreground_border_color: iced::Color::TRANSPARENT,
1061 }
1062 })
1063}
1064
1065fn generate_slider_style_struct(
1067 props: &StyleProperties,
1068) -> Result<TokenStream, super::CodegenError> {
1069 let border_expr = props
1070 .border
1071 .as_ref()
1072 .map(generate_border_expr)
1073 .unwrap_or_else(|| quote! { iced::Border::default() });
1074
1075 Ok(quote! {
1076 iced::widget::slider::Style {
1077 rail: iced::widget::slider::Rail {
1078 colors: (
1079 iced::Color::from_rgb(0.6, 0.6, 0.6),
1080 iced::Color::from_rgb(0.2, 0.6, 1.0),
1081 ),
1082 width: 4.0,
1083 border: #border_expr,
1084 },
1085 handle: iced::widget::slider::Handle {
1086 shape: iced::widget::slider::HandleShape::Circle { radius: 8.0 },
1087 color: iced::Color::WHITE,
1088 border_width: 1.0,
1089 border_color: iced::Color::from_rgb(0.6, 0.6, 0.6),
1090 },
1091 }
1092 })
1093}
1094
1095fn generate_text(
1097 node: &crate::WidgetNode,
1098 model_ident: &syn::Ident,
1099 _style_classes: &HashMap<String, StyleClass>,
1100) -> Result<TokenStream, super::CodegenError> {
1101 let value_attr = node.attributes.get("value").ok_or_else(|| {
1102 super::CodegenError::InvalidWidget("text requires value attribute".to_string())
1103 })?;
1104
1105 let value_expr = generate_attribute_value(value_attr, model_ident);
1106
1107 let mut text_widget = quote! {
1108 iced::widget::text(#value_expr)
1109 };
1110
1111 if let Some(size) = node.attributes.get("size").and_then(|attr| {
1113 if let AttributeValue::Static(s) = attr {
1114 s.parse::<f32>().ok()
1115 } else {
1116 None
1117 }
1118 }) {
1119 text_widget = quote! { #text_widget.size(#size) };
1120 }
1121
1122 if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
1124 if let AttributeValue::Static(s) = attr {
1125 Some(s.clone())
1126 } else {
1127 None
1128 }
1129 }) {
1130 let weight_expr = match weight.to_lowercase().as_str() {
1131 "bold" => quote! { iced::font::Weight::Bold },
1132 "semibold" => quote! { iced::font::Weight::Semibold },
1133 "medium" => quote! { iced::font::Weight::Medium },
1134 "light" => quote! { iced::font::Weight::Light },
1135 _ => quote! { iced::font::Weight::Normal },
1136 };
1137 text_widget = quote! {
1138 #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
1139 };
1140 }
1141
1142 if let Some(ref style_props) = node.style
1144 && let Some(ref color) = style_props.color
1145 {
1146 let color_expr = generate_color_expr(color);
1147 text_widget = quote! { #text_widget.color(#color_expr) };
1148 }
1149
1150 Ok(maybe_wrap_in_container(text_widget, node))
1152}
1153
1154fn generate_length_expr(s: &str) -> TokenStream {
1156 let s = s.trim().to_lowercase();
1157 if s == "fill" {
1158 quote! { iced::Length::Fill }
1159 } else if s == "shrink" {
1160 quote! { iced::Length::Shrink }
1161 } else if let Some(pct) = s.strip_suffix('%') {
1162 if let Ok(p) = pct.parse::<f32>() {
1163 let portion = ((p / 100.0) * 16.0).round() as u16;
1165 let portion = portion.max(1);
1166 quote! { iced::Length::FillPortion(#portion) }
1167 } else {
1168 quote! { iced::Length::Shrink }
1169 }
1170 } else if let Ok(px) = s.parse::<f32>() {
1171 quote! { iced::Length::Fixed(#px) }
1172 } else {
1173 quote! { iced::Length::Shrink }
1174 }
1175}
1176
1177fn generate_layout_length_expr(length: &LayoutLength) -> TokenStream {
1179 match length {
1180 LayoutLength::Fixed(px) => quote! { iced::Length::Fixed(#px) },
1181 LayoutLength::Fill => quote! { iced::Length::Fill },
1182 LayoutLength::Shrink => quote! { iced::Length::Shrink },
1183 LayoutLength::FillPortion(portion) => {
1184 let p = *portion as u16;
1185 quote! { iced::Length::FillPortion(#p) }
1186 }
1187 LayoutLength::Percentage(pct) => {
1188 let portion = ((pct / 100.0) * 16.0).round() as u16;
1190 let portion = portion.max(1);
1191 quote! { iced::Length::FillPortion(#portion) }
1192 }
1193 }
1194}
1195
1196fn generate_horizontal_alignment_expr(s: &str) -> TokenStream {
1198 match s.trim().to_lowercase().as_str() {
1199 "center" => quote! { iced::alignment::Horizontal::Center },
1200 "end" | "right" => quote! { iced::alignment::Horizontal::Right },
1201 _ => quote! { iced::alignment::Horizontal::Left },
1202 }
1203}
1204
1205fn generate_vertical_alignment_expr(s: &str) -> TokenStream {
1207 match s.trim().to_lowercase().as_str() {
1208 "center" => quote! { iced::alignment::Vertical::Center },
1209 "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
1210 _ => quote! { iced::alignment::Vertical::Top },
1211 }
1212}
1213
1214fn maybe_wrap_in_container(widget: TokenStream, node: &crate::WidgetNode) -> TokenStream {
1229 let needs_container = node.layout.is_some()
1231 || !node.classes.is_empty()
1232 || node.attributes.contains_key("align_x")
1233 || node.attributes.contains_key("align_y")
1234 || node.attributes.contains_key("width")
1235 || node.attributes.contains_key("height")
1236 || node.attributes.contains_key("padding");
1237
1238 if !needs_container {
1239 return quote! { #widget.into() };
1240 }
1241
1242 let mut container = quote! {
1243 iced::widget::container(#widget)
1244 };
1245
1246 if let Some(width) = node.attributes.get("width").and_then(|attr| {
1248 if let AttributeValue::Static(s) = attr {
1249 Some(s.clone())
1250 } else {
1251 None
1252 }
1253 }) {
1254 let width_expr = generate_length_expr(&width);
1255 container = quote! { #container.width(#width_expr) };
1256 }
1257
1258 if let Some(height) = node.attributes.get("height").and_then(|attr| {
1260 if let AttributeValue::Static(s) = attr {
1261 Some(s.clone())
1262 } else {
1263 None
1264 }
1265 }) {
1266 let height_expr = generate_length_expr(&height);
1267 container = quote! { #container.height(#height_expr) };
1268 }
1269
1270 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1272 if let AttributeValue::Static(s) = attr {
1273 s.parse::<f32>().ok()
1274 } else {
1275 None
1276 }
1277 }) {
1278 container = quote! { #container.padding(#padding) };
1279 }
1280
1281 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1283 if let AttributeValue::Static(s) = attr {
1284 Some(s.clone())
1285 } else {
1286 None
1287 }
1288 }) {
1289 let align_expr = generate_horizontal_alignment_expr(&align_x);
1290 container = quote! { #container.align_x(#align_expr) };
1291 }
1292
1293 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1295 if let AttributeValue::Static(s) = attr {
1296 Some(s.clone())
1297 } else {
1298 None
1299 }
1300 }) {
1301 let align_expr = generate_vertical_alignment_expr(&align_y);
1302 container = quote! { #container.align_y(#align_expr) };
1303 }
1304
1305 if let Some(class_name) = node.classes.first() {
1307 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1308 container = quote! { #container.style(#style_fn_ident) };
1309 }
1310
1311 quote! { #container.into() }
1312}
1313
1314fn generate_button(
1316 node: &crate::WidgetNode,
1317 model_ident: &syn::Ident,
1318 message_ident: &syn::Ident,
1319 style_classes: &HashMap<String, StyleClass>,
1320) -> Result<TokenStream, super::CodegenError> {
1321 let label_attr = node.attributes.get("label").ok_or_else(|| {
1322 super::CodegenError::InvalidWidget("button requires label attribute".to_string())
1323 })?;
1324
1325 let label_expr = generate_attribute_value(label_attr, model_ident);
1326
1327 let on_click = node
1328 .events
1329 .iter()
1330 .find(|e| e.event == crate::EventKind::Click);
1331
1332 let mut button = quote! {
1333 iced::widget::button(iced::widget::text(#label_expr))
1334 };
1335
1336 let enabled_condition = node.attributes.get("enabled").map(|attr| match attr {
1338 AttributeValue::Static(s) => {
1339 match s.to_lowercase().as_str() {
1341 "true" | "1" | "yes" | "on" => quote! { true },
1342 "false" | "0" | "no" | "off" => quote! { false },
1343 _ => quote! { true }, }
1345 }
1346 AttributeValue::Binding(binding_expr) => {
1347 super::bindings::generate_bool_expr(&binding_expr.expr)
1349 }
1350 AttributeValue::Interpolated(_) => {
1351 let expr_tokens = generate_attribute_value(attr, model_ident);
1353 quote! { !#expr_tokens.is_empty() && #expr_tokens != "false" && #expr_tokens != "0" }
1354 }
1355 });
1356
1357 if let Some(event) = on_click {
1358 let variant_name = to_upper_camel_case(&event.handler);
1359 let handler_ident = format_ident!("{}", variant_name);
1360
1361 let param_expr = if let Some(ref param) = event.param {
1362 let param_tokens = generate_expr(¶m.expr);
1363 quote! { (#param_tokens) }
1364 } else {
1365 quote! {}
1366 };
1367
1368 button = match enabled_condition {
1370 None => {
1371 quote! {
1373 #button.on_press(#message_ident::#handler_ident #param_expr)
1374 }
1375 }
1376 Some(condition) => {
1377 quote! {
1379 #button.on_press_maybe(
1380 if #condition {
1381 Some(#message_ident::#handler_ident #param_expr)
1382 } else {
1383 None
1384 }
1385 )
1386 }
1387 }
1388 };
1389 }
1390
1391 button = apply_widget_style(button, node, "button", style_classes)?;
1393
1394 Ok(quote! { #button.into() })
1395}
1396
1397fn to_upper_camel_case(s: &str) -> String {
1399 let mut result = String::new();
1400 let mut capitalize_next = true;
1401 for c in s.chars() {
1402 if c == '_' {
1403 capitalize_next = true;
1404 } else if capitalize_next {
1405 result.push(c.to_ascii_uppercase());
1406 capitalize_next = false;
1407 } else {
1408 result.push(c);
1409 }
1410 }
1411 result
1412}
1413
1414fn generate_container(
1416 node: &crate::WidgetNode,
1417 widget_type: &str,
1418 model_ident: &syn::Ident,
1419 message_ident: &syn::Ident,
1420 style_classes: &HashMap<String, StyleClass>,
1421) -> Result<TokenStream, super::CodegenError> {
1422 let children: Vec<TokenStream> = node
1423 .children
1424 .iter()
1425 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1426 .collect::<Result<_, _>>()?;
1427
1428 let widget_ident = format_ident!("{}", widget_type);
1429
1430 let merged_layout = get_merged_layout(node, style_classes);
1432
1433 let spacing = node
1435 .attributes
1436 .get("spacing")
1437 .and_then(|attr| {
1438 if let AttributeValue::Static(s) = attr {
1439 s.parse::<f32>().ok()
1440 } else {
1441 None
1442 }
1443 })
1444 .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
1445
1446 let padding = node
1448 .attributes
1449 .get("padding")
1450 .and_then(|attr| {
1451 if let AttributeValue::Static(s) = attr {
1452 s.parse::<f32>().ok()
1453 } else {
1454 None
1455 }
1456 })
1457 .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
1458
1459 let mut widget = if widget_type == "container" {
1460 if children.is_empty() {
1464 quote! {
1465 iced::widget::container(iced::widget::Space::new())
1466 }
1467 } else if children.len() == 1 {
1468 let child = &children[0];
1469 quote! {
1470 {
1471 let content: iced::Element<'_, _, _> = #child;
1472 iced::widget::container(content)
1473 }
1474 }
1475 } else {
1476 quote! {
1478 {
1479 let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1480 iced::widget::container(content)
1481 }
1482 }
1483 }
1484 } else if widget_type == "scrollable" {
1485 if children.is_empty() {
1489 quote! {
1490 iced::widget::scrollable(iced::widget::Space::new())
1491 }
1492 } else if children.len() == 1 {
1493 let child = &children[0];
1494 quote! {
1495 {
1496 let content: iced::Element<'_, _, _> = #child;
1497 iced::widget::scrollable(content)
1498 }
1499 }
1500 } else {
1501 quote! {
1503 {
1504 let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1505 iced::widget::scrollable(content)
1506 }
1507 }
1508 }
1509 } else {
1510 quote! {
1511 iced::widget::#widget_ident(vec![#(#children),*])
1512 }
1513 };
1514
1515 if let Some(s) = spacing {
1516 widget = quote! { #widget.spacing(#s) };
1517 }
1518
1519 if let Some(p) = padding {
1520 widget = quote! { #widget.padding(#p) };
1521 }
1522
1523 let width_from_attr = node.attributes.get("width").and_then(|attr| {
1525 if let AttributeValue::Static(s) = attr {
1526 Some(s.clone())
1527 } else {
1528 None
1529 }
1530 });
1531 let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
1532
1533 if let Some(width) = width_from_attr {
1534 let width_expr = generate_length_expr(&width);
1535 widget = quote! { #widget.width(#width_expr) };
1536 } else if let Some(layout_width) = width_from_layout {
1537 let width_expr = generate_layout_length_expr(layout_width);
1538 widget = quote! { #widget.width(#width_expr) };
1539 }
1540
1541 let height_from_attr = node.attributes.get("height").and_then(|attr| {
1543 if let AttributeValue::Static(s) = attr {
1544 Some(s.clone())
1545 } else {
1546 None
1547 }
1548 });
1549 let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
1550
1551 if let Some(height) = height_from_attr {
1552 let height_expr = generate_length_expr(&height);
1553 widget = quote! { #widget.height(#height_expr) };
1554 } else if let Some(layout_height) = height_from_layout {
1555 let height_expr = generate_layout_length_expr(layout_height);
1556 widget = quote! { #widget.height(#height_expr) };
1557 }
1558
1559 if widget_type == "container" {
1561 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1562 if let AttributeValue::Static(s) = attr {
1563 Some(s.clone())
1564 } else {
1565 None
1566 }
1567 }) {
1568 let align_expr = generate_horizontal_alignment_expr(&align_x);
1569 widget = quote! { #widget.align_x(#align_expr) };
1570 }
1571
1572 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1574 if let AttributeValue::Static(s) = attr {
1575 Some(s.clone())
1576 } else {
1577 None
1578 }
1579 }) {
1580 let align_expr = generate_vertical_alignment_expr(&align_y);
1581 widget = quote! { #widget.align_y(#align_expr) };
1582 }
1583 }
1584
1585 if (widget_type == "column" || widget_type == "row")
1587 && let Some(align) = node.attributes.get("align_items").and_then(|attr| {
1588 if let AttributeValue::Static(s) = attr {
1589 Some(s.clone())
1590 } else {
1591 None
1592 }
1593 })
1594 {
1595 let align_expr = match align.to_lowercase().as_str() {
1596 "center" => quote! { iced::Alignment::Center },
1597 "end" => quote! { iced::Alignment::End },
1598 _ => quote! { iced::Alignment::Start },
1599 };
1600 widget = quote! { #widget.align_items(#align_expr) };
1601 }
1602
1603 if widget_type == "container" {
1605 widget = apply_widget_style(widget, node, "container", style_classes)?;
1606 }
1607
1608 if (widget_type == "column" || widget_type == "row")
1612 && (node.attributes.contains_key("align_x") || node.attributes.contains_key("align_y"))
1613 {
1614 let mut container = quote! { iced::widget::container(#widget) };
1615
1616 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1617 if let AttributeValue::Static(s) = attr {
1618 Some(s.clone())
1619 } else {
1620 None
1621 }
1622 }) {
1623 let align_expr = generate_horizontal_alignment_expr(&align_x);
1624 container = quote! { #container.align_x(#align_expr) };
1625 }
1626
1627 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1628 if let AttributeValue::Static(s) = attr {
1629 Some(s.clone())
1630 } else {
1631 None
1632 }
1633 }) {
1634 let align_expr = generate_vertical_alignment_expr(&align_y);
1635 container = quote! { #container.align_y(#align_expr) };
1636 }
1637
1638 container = quote! { #container.width(iced::Length::Fill).height(iced::Length::Fill) };
1640
1641 return Ok(quote! { #container.into() });
1642 }
1643
1644 Ok(quote! { #widget.into() })
1645}
1646
1647fn generate_stack(
1649 node: &crate::WidgetNode,
1650 model_ident: &syn::Ident,
1651 message_ident: &syn::Ident,
1652 style_classes: &HashMap<String, StyleClass>,
1653) -> Result<TokenStream, super::CodegenError> {
1654 let children: Vec<TokenStream> = node
1655 .children
1656 .iter()
1657 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1658 .collect::<Result<_, _>>()?;
1659
1660 Ok(quote! {
1661 iced::widget::stack(vec![#(#children),*]).into()
1662 })
1663}
1664
1665fn generate_space(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1667 let width = node.attributes.get("width").and_then(|attr| {
1669 if let AttributeValue::Static(s) = attr {
1670 Some(s.clone())
1671 } else {
1672 None
1673 }
1674 });
1675
1676 let height = node.attributes.get("height").and_then(|attr| {
1678 if let AttributeValue::Static(s) = attr {
1679 Some(s.clone())
1680 } else {
1681 None
1682 }
1683 });
1684
1685 let mut space = quote! { iced::widget::Space::new() };
1686
1687 if let Some(w) = width {
1689 let width_expr = generate_length_expr(&w);
1690 space = quote! { #space.width(#width_expr) };
1691 }
1692
1693 if let Some(h) = height {
1695 let height_expr = generate_length_expr(&h);
1696 space = quote! { #space.height(#height_expr) };
1697 }
1698
1699 Ok(quote! { #space.into() })
1700}
1701
1702fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1704 let direction = node
1706 .attributes
1707 .get("direction")
1708 .and_then(|attr| {
1709 if let AttributeValue::Static(s) = attr {
1710 Some(s.clone())
1711 } else {
1712 None
1713 }
1714 })
1715 .unwrap_or_else(|| "horizontal".to_string());
1716
1717 let thickness = node
1719 .attributes
1720 .get("thickness")
1721 .and_then(|attr| {
1722 if let AttributeValue::Static(s) = attr {
1723 s.parse::<f32>().ok()
1724 } else {
1725 None
1726 }
1727 })
1728 .unwrap_or(1.0);
1729
1730 let rule = if direction.to_lowercase() == "vertical" {
1731 quote! { iced::widget::rule::vertical(#thickness) }
1732 } else {
1733 quote! { iced::widget::rule::horizontal(#thickness) }
1734 };
1735
1736 Ok(quote! { #rule.into() })
1737}
1738
1739fn generate_checkbox(
1741 node: &crate::WidgetNode,
1742 model_ident: &syn::Ident,
1743 message_ident: &syn::Ident,
1744 style_classes: &HashMap<String, StyleClass>,
1745) -> Result<TokenStream, super::CodegenError> {
1746 let label = node
1747 .attributes
1748 .get("label")
1749 .and_then(|attr| {
1750 if let AttributeValue::Static(s) = attr {
1751 Some(s.clone())
1752 } else {
1753 None
1754 }
1755 })
1756 .unwrap_or_default();
1757 let label_lit = proc_macro2::Literal::string(&label);
1758 let label_expr = quote! { #label_lit.to_string() };
1759
1760 let checked_attr = node.attributes.get("checked");
1761 let checked_expr = checked_attr
1762 .map(|attr| generate_attribute_value(attr, model_ident))
1763 .unwrap_or(quote! { false });
1764
1765 let on_toggle = node
1766 .events
1767 .iter()
1768 .find(|e| e.event == crate::EventKind::Toggle);
1769
1770 let checkbox = if let Some(event) = on_toggle {
1771 let variant_name = to_upper_camel_case(&event.handler);
1772 let handler_ident = format_ident!("{}", variant_name);
1773 quote! {
1774 iced::widget::checkbox(#label_expr, #checked_expr)
1775 .on_toggle(#message_ident::#handler_ident)
1776 }
1777 } else {
1778 quote! {
1779 iced::widget::checkbox(#label_expr, #checked_expr)
1780 }
1781 };
1782
1783 let checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
1785
1786 Ok(quote! { #checkbox.into() })
1787}
1788
1789fn generate_toggler(
1791 node: &crate::WidgetNode,
1792 model_ident: &syn::Ident,
1793 message_ident: &syn::Ident,
1794 style_classes: &HashMap<String, StyleClass>,
1795) -> Result<TokenStream, super::CodegenError> {
1796 let label = node
1797 .attributes
1798 .get("label")
1799 .and_then(|attr| {
1800 if let AttributeValue::Static(s) = attr {
1801 Some(s.clone())
1802 } else {
1803 None
1804 }
1805 })
1806 .unwrap_or_default();
1807 let label_lit = proc_macro2::Literal::string(&label);
1808 let label_expr = quote! { #label_lit.to_string() };
1809
1810 let is_toggled_attr = node.attributes.get("toggled");
1811 let is_toggled_expr = is_toggled_attr
1812 .map(|attr| generate_attribute_value(attr, model_ident))
1813 .unwrap_or(quote! { false });
1814
1815 let on_toggle = node
1816 .events
1817 .iter()
1818 .find(|e| e.event == crate::EventKind::Toggle);
1819
1820 let toggler = if let Some(event) = on_toggle {
1821 let variant_name = to_upper_camel_case(&event.handler);
1822 let handler_ident = format_ident!("{}", variant_name);
1823 quote! {
1824 iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1825 .on_toggle(|_| #message_ident::#handler_ident)
1826 }
1827 } else {
1828 quote! {
1829 iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1830 }
1831 };
1832
1833 let toggler = apply_widget_style(toggler, node, "toggler", style_classes)?;
1835
1836 Ok(quote! { #toggler.into() })
1837}
1838
1839fn generate_slider(
1841 node: &crate::WidgetNode,
1842 model_ident: &syn::Ident,
1843 message_ident: &syn::Ident,
1844 style_classes: &HashMap<String, StyleClass>,
1845) -> Result<TokenStream, super::CodegenError> {
1846 let min = node.attributes.get("min").and_then(|attr| {
1847 if let AttributeValue::Static(s) = attr {
1848 s.parse::<f32>().ok()
1849 } else {
1850 None
1851 }
1852 });
1853
1854 let max = node.attributes.get("max").and_then(|attr| {
1855 if let AttributeValue::Static(s) = attr {
1856 s.parse::<f32>().ok()
1857 } else {
1858 None
1859 }
1860 });
1861
1862 let value_attr = node.attributes.get("value").ok_or_else(|| {
1863 super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
1864 })?;
1865 let value_expr = generate_attribute_value(value_attr, model_ident);
1866
1867 let on_change = node
1868 .events
1869 .iter()
1870 .find(|e| e.event == crate::EventKind::Change);
1871
1872 let mut slider = quote! {
1873 iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
1874 };
1875
1876 if let Some(m) = min {
1877 slider = quote! { #slider.min(#m) };
1878 }
1879 if let Some(m) = max {
1880 slider = quote! { #slider.max(#m) };
1881 }
1882
1883 let step = node.attributes.get("step").and_then(|attr| {
1885 if let AttributeValue::Static(s) = attr {
1886 s.parse::<f32>().ok()
1887 } else {
1888 None
1889 }
1890 });
1891
1892 if let Some(s) = step {
1893 slider = quote! { #slider.step(#s) };
1894 }
1895
1896 if let Some(event) = on_change {
1897 let variant_name = to_upper_camel_case(&event.handler);
1898 let handler_ident = format_ident!("{}", variant_name);
1899 slider = quote! {
1900 iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
1901 };
1902 }
1903
1904 slider = apply_widget_style(slider, node, "slider", style_classes)?;
1906
1907 Ok(quote! { #slider.into() })
1908}
1909
1910fn generate_radio(
1912 node: &crate::WidgetNode,
1913 _model_ident: &syn::Ident,
1914 message_ident: &syn::Ident,
1915 _style_classes: &HashMap<String, StyleClass>,
1916) -> Result<TokenStream, super::CodegenError> {
1917 let label = node
1918 .attributes
1919 .get("label")
1920 .and_then(|attr| {
1921 if let AttributeValue::Static(s) = attr {
1922 Some(s.clone())
1923 } else {
1924 None
1925 }
1926 })
1927 .unwrap_or_default();
1928 let label_lit = proc_macro2::Literal::string(&label);
1929 let label_expr = quote! { #label_lit.to_string() };
1930
1931 let value_attr = node.attributes.get("value").ok_or_else(|| {
1932 super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
1933 })?;
1934 let value_expr = match value_attr {
1935 AttributeValue::Binding(expr) => generate_expr(&expr.expr),
1936 _ => quote! { String::new() },
1937 };
1938
1939 let selected_attr = node.attributes.get("selected");
1940 let selected_expr = match selected_attr {
1941 Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
1942 _ => quote! { None },
1943 };
1944
1945 let on_select = node
1946 .events
1947 .iter()
1948 .find(|e| e.event == crate::EventKind::Select);
1949
1950 if let Some(event) = on_select {
1951 let variant_name = to_upper_camel_case(&event.handler);
1952 let handler_ident = format_ident!("{}", variant_name);
1953 Ok(quote! {
1954 iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v)).into()
1955 })
1956 } else {
1957 Ok(quote! {
1958 iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ()).into()
1959 })
1960 }
1961}
1962
1963fn generate_progress_bar(
1965 node: &crate::WidgetNode,
1966 model_ident: &syn::Ident,
1967 _style_classes: &HashMap<String, StyleClass>,
1968) -> Result<TokenStream, super::CodegenError> {
1969 let value_attr = node.attributes.get("value").ok_or_else(|| {
1970 super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
1971 })?;
1972 let value_expr = generate_attribute_value(value_attr, model_ident);
1973
1974 let max_attr = node.attributes.get("max").and_then(|attr| {
1975 if let AttributeValue::Static(s) = attr {
1976 s.parse::<f32>().ok()
1977 } else {
1978 None
1979 }
1980 });
1981
1982 if let Some(max) = max_attr {
1983 Ok(quote! {
1984 iced::widget::progress_bar(0.0..=#max, #value_expr).into()
1985 })
1986 } else {
1987 Ok(quote! {
1988 iced::widget::progress_bar(0.0..=100.0, #value_expr).into()
1989 })
1990 }
1991}
1992
1993fn generate_text_input(
1995 node: &crate::WidgetNode,
1996 model_ident: &syn::Ident,
1997 message_ident: &syn::Ident,
1998 style_classes: &HashMap<String, StyleClass>,
1999) -> Result<TokenStream, super::CodegenError> {
2000 let value_expr = node
2001 .attributes
2002 .get("value")
2003 .map(|attr| generate_attribute_value(attr, model_ident))
2004 .unwrap_or(quote! { String::new() });
2005
2006 let placeholder = node.attributes.get("placeholder").and_then(|attr| {
2007 if let AttributeValue::Static(s) = attr {
2008 Some(s.clone())
2009 } else {
2010 None
2011 }
2012 });
2013
2014 let on_input = node
2015 .events
2016 .iter()
2017 .find(|e| e.event == crate::EventKind::Input);
2018
2019 let on_submit = node
2020 .events
2021 .iter()
2022 .find(|e| e.event == crate::EventKind::Submit);
2023
2024 let mut text_input = match placeholder {
2025 Some(ph) => {
2026 let ph_lit = proc_macro2::Literal::string(&ph);
2027 quote! {
2028 iced::widget::text_input(#ph_lit, &#value_expr)
2029 }
2030 }
2031 None => quote! {
2032 iced::widget::text_input("", &#value_expr)
2033 },
2034 };
2035
2036 if let Some(event) = on_input {
2037 let variant_name = to_upper_camel_case(&event.handler);
2038 let handler_ident = format_ident!("{}", variant_name);
2039 text_input = quote! {
2040 #text_input.on_input(|v| #message_ident::#handler_ident(v))
2041 };
2042 }
2043
2044 if let Some(event) = on_submit {
2045 let variant_name = to_upper_camel_case(&event.handler);
2046 let handler_ident = format_ident!("{}", variant_name);
2047 text_input = quote! {
2048 #text_input.on_submit(#message_ident::#handler_ident)
2049 };
2050 }
2051
2052 let is_password = node
2054 .attributes
2055 .get("password")
2056 .or_else(|| node.attributes.get("secure"))
2057 .and_then(|attr| {
2058 if let AttributeValue::Static(s) = attr {
2059 Some(s.to_lowercase() == "true" || s == "1")
2060 } else {
2061 None
2062 }
2063 })
2064 .unwrap_or(false);
2065
2066 if is_password {
2067 text_input = quote! { #text_input.password() };
2068 }
2069
2070 text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
2072
2073 Ok(quote! { #text_input.into() })
2074}
2075
2076fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2078 let src_attr = node.attributes.get("src").ok_or_else(|| {
2079 super::CodegenError::InvalidWidget("image requires src attribute".to_string())
2080 })?;
2081
2082 let src = match src_attr {
2083 AttributeValue::Static(s) => s.clone(),
2084 _ => String::new(),
2085 };
2086 let src_lit = proc_macro2::Literal::string(&src);
2087
2088 let width = node.attributes.get("width").and_then(|attr| {
2089 if let AttributeValue::Static(s) = attr {
2090 s.parse::<u32>().ok()
2091 } else {
2092 None
2093 }
2094 });
2095
2096 let height = node.attributes.get("height").and_then(|attr| {
2097 if let AttributeValue::Static(s) = attr {
2098 s.parse::<u32>().ok()
2099 } else {
2100 None
2101 }
2102 });
2103
2104 let mut image = quote! {
2105 iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
2106 };
2107
2108 if let (Some(w), Some(h)) = (width, height) {
2110 image = quote! { #image.width(#w).height(#h) };
2111 } else if let Some(w) = width {
2112 image = quote! { #image.width(#w) };
2113 } else if let Some(h) = height {
2114 image = quote! { #image.height(#h) };
2115 }
2116
2117 let needs_container = !node.classes.is_empty()
2121 || node.attributes.contains_key("align_x")
2122 || node.attributes.contains_key("align_y")
2123 || node.attributes.contains_key("padding");
2124
2125 if needs_container {
2126 let mut container = quote! { iced::widget::container(#image) };
2128
2129 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2130 if let AttributeValue::Static(s) = attr {
2131 s.parse::<f32>().ok()
2132 } else {
2133 None
2134 }
2135 }) {
2136 container = quote! { #container.padding(#padding) };
2137 }
2138
2139 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2140 if let AttributeValue::Static(s) = attr {
2141 Some(s.clone())
2142 } else {
2143 None
2144 }
2145 }) {
2146 let align_expr = generate_horizontal_alignment_expr(&align_x);
2147 container = quote! { #container.align_x(#align_expr) };
2148 }
2149
2150 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2151 if let AttributeValue::Static(s) = attr {
2152 Some(s.clone())
2153 } else {
2154 None
2155 }
2156 }) {
2157 let align_expr = generate_vertical_alignment_expr(&align_y);
2158 container = quote! { #container.align_y(#align_expr) };
2159 }
2160
2161 if let Some(class_name) = node.classes.first() {
2162 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2163 container = quote! { #container.style(#style_fn_ident) };
2164 }
2165
2166 Ok(quote! { #container.into() })
2167 } else {
2168 Ok(quote! { #image.into() })
2169 }
2170}
2171
2172fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
2174 let path_attr = node
2176 .attributes
2177 .get("src")
2178 .or_else(|| node.attributes.get("path"))
2179 .ok_or_else(|| {
2180 super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
2181 })?;
2182
2183 let path = match path_attr {
2184 AttributeValue::Static(s) => s.clone(),
2185 _ => String::new(),
2186 };
2187 let path_lit = proc_macro2::Literal::string(&path);
2188
2189 let width = node.attributes.get("width").and_then(|attr| {
2190 if let AttributeValue::Static(s) = attr {
2191 s.parse::<u32>().ok()
2192 } else {
2193 None
2194 }
2195 });
2196
2197 let height = node.attributes.get("height").and_then(|attr| {
2198 if let AttributeValue::Static(s) = attr {
2199 s.parse::<u32>().ok()
2200 } else {
2201 None
2202 }
2203 });
2204
2205 let mut svg = quote! {
2206 iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
2207 };
2208
2209 if let (Some(w), Some(h)) = (width, height) {
2211 svg = quote! { #svg.width(#w).height(#h) };
2212 } else if let Some(w) = width {
2213 svg = quote! { #svg.width(#w) };
2214 } else if let Some(h) = height {
2215 svg = quote! { #svg.height(#h) };
2216 }
2217
2218 let needs_container = !node.classes.is_empty()
2222 || node.attributes.contains_key("align_x")
2223 || node.attributes.contains_key("align_y")
2224 || node.attributes.contains_key("padding");
2225
2226 if needs_container {
2227 let mut container = quote! { iced::widget::container(#svg) };
2229
2230 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
2231 if let AttributeValue::Static(s) = attr {
2232 s.parse::<f32>().ok()
2233 } else {
2234 None
2235 }
2236 }) {
2237 container = quote! { #container.padding(#padding) };
2238 }
2239
2240 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
2241 if let AttributeValue::Static(s) = attr {
2242 Some(s.clone())
2243 } else {
2244 None
2245 }
2246 }) {
2247 let align_expr = generate_horizontal_alignment_expr(&align_x);
2248 container = quote! { #container.align_x(#align_expr) };
2249 }
2250
2251 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2252 if let AttributeValue::Static(s) = attr {
2253 Some(s.clone())
2254 } else {
2255 None
2256 }
2257 }) {
2258 let align_expr = generate_vertical_alignment_expr(&align_y);
2259 container = quote! { #container.align_y(#align_expr) };
2260 }
2261
2262 if let Some(class_name) = node.classes.first() {
2263 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
2264 container = quote! { #container.style(#style_fn_ident) };
2265 }
2266
2267 Ok(quote! { #container.into() })
2268 } else {
2269 Ok(quote! { #svg.into() })
2270 }
2271}
2272
2273fn generate_pick_list(
2275 node: &crate::WidgetNode,
2276 model_ident: &syn::Ident,
2277 message_ident: &syn::Ident,
2278 _style_classes: &HashMap<String, StyleClass>,
2279) -> Result<TokenStream, super::CodegenError> {
2280 let options_attr = node.attributes.get("options").ok_or_else(|| {
2281 super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2282 })?;
2283
2284 let options: Vec<String> = match options_attr {
2285 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2286 _ => Vec::new(),
2287 };
2288 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2289
2290 let selected_attr = node.attributes.get("selected");
2291 let selected_expr = selected_attr
2292 .map(|attr| generate_attribute_value(attr, model_ident))
2293 .unwrap_or(quote! { None });
2294
2295 let on_select = node
2296 .events
2297 .iter()
2298 .find(|e| e.event == crate::EventKind::Select);
2299
2300 if let Some(event) = on_select {
2301 let variant_name = to_upper_camel_case(&event.handler);
2302 let handler_ident = format_ident!("{}", variant_name);
2303 Ok(quote! {
2304 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2305 })
2306 } else {
2307 Ok(quote! {
2308 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2309 })
2310 }
2311}
2312
2313fn generate_combo_box(
2315 node: &crate::WidgetNode,
2316 model_ident: &syn::Ident,
2317 message_ident: &syn::Ident,
2318 _style_classes: &HashMap<String, StyleClass>,
2319) -> Result<TokenStream, super::CodegenError> {
2320 let options_attr = node.attributes.get("options").ok_or_else(|| {
2321 super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2322 })?;
2323
2324 let options: Vec<String> = match options_attr {
2325 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2326 _ => Vec::new(),
2327 };
2328 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2329
2330 let selected_attr = node.attributes.get("selected");
2331 let selected_expr = selected_attr
2332 .map(|attr| generate_attribute_value(attr, model_ident))
2333 .unwrap_or(quote! { None });
2334
2335 let on_select = node
2336 .events
2337 .iter()
2338 .find(|e| e.event == crate::EventKind::Select);
2339
2340 if let Some(event) = on_select {
2341 let variant_name = to_upper_camel_case(&event.handler);
2342 let handler_ident = format_ident!("{}", variant_name);
2343 Ok(quote! {
2344 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2345 })
2346 } else {
2347 Ok(quote! {
2348 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2349 })
2350 }
2351}
2352
2353fn generate_tooltip(
2355 node: &crate::WidgetNode,
2356 model_ident: &syn::Ident,
2357 message_ident: &syn::Ident,
2358 style_classes: &HashMap<String, StyleClass>,
2359) -> Result<TokenStream, super::CodegenError> {
2360 let child = node.children.first().ok_or_else(|| {
2361 super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2362 })?;
2363 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2364
2365 let message_attr = node.attributes.get("message").ok_or_else(|| {
2366 super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2367 })?;
2368 let message_expr = generate_attribute_value(message_attr, model_ident);
2369
2370 Ok(quote! {
2371 iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2372 })
2373}
2374
2375fn generate_grid(
2377 node: &crate::WidgetNode,
2378 model_ident: &syn::Ident,
2379 message_ident: &syn::Ident,
2380 style_classes: &HashMap<String, StyleClass>,
2381) -> Result<TokenStream, super::CodegenError> {
2382 let children: Vec<TokenStream> = node
2383 .children
2384 .iter()
2385 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2386 .collect::<Result<_, _>>()?;
2387
2388 let columns = node
2389 .attributes
2390 .get("columns")
2391 .and_then(|attr| {
2392 if let AttributeValue::Static(s) = attr {
2393 s.parse::<u32>().ok()
2394 } else {
2395 None
2396 }
2397 })
2398 .unwrap_or(1);
2399
2400 let spacing = node.attributes.get("spacing").and_then(|attr| {
2401 if let AttributeValue::Static(s) = attr {
2402 s.parse::<f32>().ok()
2403 } else {
2404 None
2405 }
2406 });
2407
2408 let padding = node.attributes.get("padding").and_then(|attr| {
2409 if let AttributeValue::Static(s) = attr {
2410 s.parse::<f32>().ok()
2411 } else {
2412 None
2413 }
2414 });
2415
2416 let grid = quote! {
2417 iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2418 };
2419
2420 let grid = if let Some(s) = spacing {
2421 quote! { #grid.spacing(#s) }
2422 } else {
2423 grid
2424 };
2425
2426 let grid = if let Some(p) = padding {
2427 quote! { #grid.padding(#p) }
2428 } else {
2429 grid
2430 };
2431
2432 Ok(quote! { #grid.into() })
2433}
2434
2435fn generate_canvas(
2437 node: &crate::WidgetNode,
2438 _model_ident: &syn::Ident,
2439 _message_ident: &syn::Ident,
2440 _style_classes: &HashMap<String, StyleClass>,
2441) -> Result<TokenStream, super::CodegenError> {
2442 let width = node.attributes.get("width").and_then(|attr| {
2443 if let AttributeValue::Static(s) = attr {
2444 s.parse::<f32>().ok()
2445 } else {
2446 None
2447 }
2448 });
2449
2450 let height = node.attributes.get("height").and_then(|attr| {
2451 if let AttributeValue::Static(s) = attr {
2452 s.parse::<f32>().ok()
2453 } else {
2454 None
2455 }
2456 });
2457
2458 let size = match (width, height) {
2459 (Some(w), Some(h)) => quote! { iced::Size::new(#w, #h) },
2460 (Some(w), None) => quote! { iced::Size::new(#w, 100.0) },
2461 (None, Some(h)) => quote! { iced::Size::new(100.0, #h) },
2462 _ => quote! { iced::Size::new(100.0, 100.0) },
2463 };
2464
2465 Ok(quote! {
2466 iced::widget::canvas(#size).into()
2467 })
2468}
2469
2470fn generate_float(
2472 node: &crate::WidgetNode,
2473 model_ident: &syn::Ident,
2474 message_ident: &syn::Ident,
2475 style_classes: &HashMap<String, StyleClass>,
2476) -> Result<TokenStream, super::CodegenError> {
2477 let child = node.children.first().ok_or_else(|| {
2478 super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2479 })?;
2480 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2481
2482 let position = node
2483 .attributes
2484 .get("position")
2485 .and_then(|attr| {
2486 if let AttributeValue::Static(s) = attr {
2487 Some(s.clone())
2488 } else {
2489 None
2490 }
2491 })
2492 .unwrap_or_else(|| "TopRight".to_string());
2493
2494 let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2495 if let AttributeValue::Static(s) = attr {
2496 s.parse::<f32>().ok()
2497 } else {
2498 None
2499 }
2500 });
2501
2502 let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2503 if let AttributeValue::Static(s) = attr {
2504 s.parse::<f32>().ok()
2505 } else {
2506 None
2507 }
2508 });
2509
2510 let float = match position.as_str() {
2511 "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2512 "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2513 "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2514 "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2515 _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2516 };
2517
2518 let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2519 quote! { #float.offset_x(#ox).offset_y(#oy) }
2520 } else if let Some(ox) = offset_x {
2521 quote! { #float.offset_x(#ox) }
2522 } else if let Some(oy) = offset_y {
2523 quote! { #float.offset_y(#oy) }
2524 } else {
2525 float
2526 };
2527
2528 Ok(quote! { #float.into() })
2529}
2530
2531fn generate_for(
2537 node: &crate::WidgetNode,
2538 model_ident: &syn::Ident,
2539 message_ident: &syn::Ident,
2540 style_classes: &HashMap<String, StyleClass>,
2541) -> Result<TokenStream, super::CodegenError> {
2542 let in_attr = node.attributes.get("in").ok_or_else(|| {
2544 super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2545 })?;
2546
2547 let var_name = node
2549 .attributes
2550 .get("each")
2551 .and_then(|attr| {
2552 if let AttributeValue::Static(s) = attr {
2553 Some(s.clone())
2554 } else {
2555 None
2556 }
2557 })
2558 .unwrap_or_else(|| "item".to_string());
2559
2560 let var_ident = format_ident!("{}", var_name);
2561
2562 let collection_expr = generate_attribute_value_raw(in_attr, model_ident);
2564
2565 let children: Vec<TokenStream> = node
2567 .children
2568 .iter()
2569 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2570 .collect::<Result<_, _>>()?;
2571
2572 Ok(quote! {
2574 {
2575 let items: Vec<_> = #collection_expr;
2576 let widgets: Vec<iced::Element<'_, #message_ident>> = items
2577 .iter()
2578 .enumerate()
2579 .flat_map(|(index, #var_ident)| {
2580 let _ = index; vec![#(#children),*]
2582 })
2583 .collect();
2584 iced::widget::column(widgets).into()
2585 }
2586 })
2587}
2588
2589fn generate_if(
2591 node: &crate::WidgetNode,
2592 model_ident: &syn::Ident,
2593 message_ident: &syn::Ident,
2594 style_classes: &HashMap<String, StyleClass>,
2595) -> Result<TokenStream, super::CodegenError> {
2596 let condition_attr = node.attributes.get("condition").ok_or_else(|| {
2597 super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
2598 })?;
2599
2600 let children: Vec<TokenStream> = node
2601 .children
2602 .iter()
2603 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2604 .collect::<Result<_, _>>()?;
2605
2606 let condition_expr = generate_attribute_value(condition_attr, model_ident);
2607
2608 Ok(quote! {
2609 if #condition_expr.parse::<bool>().unwrap_or(false) {
2610 iced::widget::column(vec![#(#children),*]).into()
2611 } else {
2612 iced::widget::column(vec![]).into()
2613 }
2614 })
2615}
2616
2617fn generate_custom_widget(
2619 node: &crate::WidgetNode,
2620 name: &str,
2621 model_ident: &syn::Ident,
2622 message_ident: &syn::Ident,
2623 style_classes: &HashMap<String, StyleClass>,
2624) -> Result<TokenStream, super::CodegenError> {
2625 let widget_ident = format_ident!("{}", name);
2626 let children: Vec<TokenStream> = node
2627 .children
2628 .iter()
2629 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2630 .collect::<Result<_, _>>()?;
2631
2632 Ok(quote! {
2633 #widget_ident(vec![#(#children),*]).into()
2634 })
2635}
2636
2637fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2639 match attr {
2640 AttributeValue::Static(s) => {
2641 let lit = proc_macro2::Literal::string(s);
2642 quote! { #lit.to_string() }
2643 }
2644 AttributeValue::Binding(expr) => generate_expr(&expr.expr),
2645 AttributeValue::Interpolated(parts) => {
2646 let parts_str: Vec<String> = parts
2647 .iter()
2648 .map(|part| match part {
2649 InterpolatedPart::Literal(s) => s.clone(),
2650 InterpolatedPart::Binding(_) => "{}".to_string(),
2651 })
2652 .collect();
2653 let binding_exprs: Vec<TokenStream> = parts
2654 .iter()
2655 .filter_map(|part| {
2656 if let InterpolatedPart::Binding(expr) = part {
2657 Some(generate_expr(&expr.expr))
2658 } else {
2659 None
2660 }
2661 })
2662 .collect();
2663
2664 let format_string = parts_str.join("");
2665 let lit = proc_macro2::Literal::string(&format_string);
2666
2667 quote! { format!(#lit, #(#binding_exprs),*) }
2668 }
2669 }
2670}
2671
2672fn generate_attribute_value_raw(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2675 match attr {
2676 AttributeValue::Static(s) => {
2677 let lit = proc_macro2::Literal::string(s);
2678 quote! { #lit }
2679 }
2680 AttributeValue::Binding(expr) => super::bindings::generate_bool_expr(&expr.expr),
2681 AttributeValue::Interpolated(parts) => {
2682 let parts_str: Vec<String> = parts
2684 .iter()
2685 .map(|part| match part {
2686 InterpolatedPart::Literal(s) => s.clone(),
2687 InterpolatedPart::Binding(_) => "{}".to_string(),
2688 })
2689 .collect();
2690 let binding_exprs: Vec<TokenStream> = parts
2691 .iter()
2692 .filter_map(|part| {
2693 if let InterpolatedPart::Binding(expr) = part {
2694 Some(generate_expr(&expr.expr))
2695 } else {
2696 None
2697 }
2698 })
2699 .collect();
2700
2701 let format_string = parts_str.join("");
2702 let lit = proc_macro2::Literal::string(&format_string);
2703
2704 quote! { format!(#lit, #(#binding_exprs),*) }
2705 }
2706 }
2707}
2708
2709fn generate_text_with_locals(
2715 node: &crate::WidgetNode,
2716 model_ident: &syn::Ident,
2717 _style_classes: &HashMap<String, StyleClass>,
2718 local_vars: &std::collections::HashSet<String>,
2719) -> Result<TokenStream, super::CodegenError> {
2720 let value_attr = node.attributes.get("value").ok_or_else(|| {
2721 super::CodegenError::InvalidWidget("text requires value attribute".to_string())
2722 })?;
2723
2724 let value_expr = generate_attribute_value_with_locals(value_attr, model_ident, local_vars);
2725
2726 let mut text_widget = quote! {
2727 iced::widget::text(#value_expr)
2728 };
2729
2730 if let Some(size) = node.attributes.get("size").and_then(|attr| {
2732 if let AttributeValue::Static(s) = attr {
2733 s.parse::<f32>().ok()
2734 } else {
2735 None
2736 }
2737 }) {
2738 text_widget = quote! { #text_widget.size(#size) };
2739 }
2740
2741 if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
2743 if let AttributeValue::Static(s) = attr {
2744 Some(s.clone())
2745 } else {
2746 None
2747 }
2748 }) {
2749 let weight_expr = match weight.to_lowercase().as_str() {
2750 "bold" => quote! { iced::font::Weight::Bold },
2751 "semibold" => quote! { iced::font::Weight::Semibold },
2752 "medium" => quote! { iced::font::Weight::Medium },
2753 "light" => quote! { iced::font::Weight::Light },
2754 _ => quote! { iced::font::Weight::Normal },
2755 };
2756 text_widget = quote! {
2757 #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
2758 };
2759 }
2760
2761 if let Some(ref style_props) = node.style
2763 && let Some(ref color) = style_props.color
2764 {
2765 let color_expr = generate_color_expr(color);
2766 text_widget = quote! { #text_widget.color(#color_expr) };
2767 }
2768
2769 Ok(maybe_wrap_in_container(text_widget, node))
2770}
2771
2772fn generate_button_with_locals(
2774 node: &crate::WidgetNode,
2775 model_ident: &syn::Ident,
2776 message_ident: &syn::Ident,
2777 style_classes: &HashMap<String, StyleClass>,
2778 local_vars: &std::collections::HashSet<String>,
2779) -> Result<TokenStream, super::CodegenError> {
2780 let label_attr = node.attributes.get("label").ok_or_else(|| {
2781 super::CodegenError::InvalidWidget("button requires label attribute".to_string())
2782 })?;
2783
2784 let label_expr = generate_attribute_value_with_locals(label_attr, model_ident, local_vars);
2785
2786 let on_click = node
2787 .events
2788 .iter()
2789 .find(|e| e.event == crate::EventKind::Click);
2790
2791 let mut button = quote! {
2792 iced::widget::button(iced::widget::text(#label_expr))
2793 };
2794
2795 if let Some(event) = on_click {
2796 let variant_name = to_upper_camel_case(&event.handler);
2797 let handler_ident = format_ident!("{}", variant_name);
2798
2799 let param_expr = if let Some(ref param) = event.param {
2800 let param_tokens = super::bindings::generate_expr_with_locals(¶m.expr, local_vars);
2801 quote! { (#param_tokens) }
2802 } else {
2803 quote! {}
2804 };
2805
2806 button = quote! {
2807 #button.on_press(#message_ident::#handler_ident #param_expr)
2808 };
2809 }
2810
2811 button = apply_widget_style(button, node, "button", style_classes)?;
2813
2814 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#button) })
2815}
2816
2817fn generate_container_with_locals(
2819 node: &crate::WidgetNode,
2820 widget_type: &str,
2821 model_ident: &syn::Ident,
2822 message_ident: &syn::Ident,
2823 style_classes: &HashMap<String, StyleClass>,
2824 local_vars: &std::collections::HashSet<String>,
2825) -> Result<TokenStream, super::CodegenError> {
2826 let children: Vec<TokenStream> = node
2827 .children
2828 .iter()
2829 .map(|child| {
2830 generate_widget_with_locals(
2831 child,
2832 model_ident,
2833 message_ident,
2834 style_classes,
2835 local_vars,
2836 )
2837 })
2838 .collect::<Result<_, _>>()?;
2839
2840 let mut container = match widget_type {
2841 "column" => {
2842 quote! { iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
2843 }
2844 "row" => {
2845 quote! { iced::widget::row({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }) }
2846 }
2847 "scrollable" => {
2848 quote! { iced::widget::scrollable(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
2849 }
2850 _ => {
2851 if children.len() == 1 {
2853 let child = &children[0];
2854 quote! { iced::widget::container(#child) }
2855 } else {
2856 quote! { iced::widget::container(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children })) }
2857 }
2858 }
2859 };
2860
2861 let merged_layout = get_merged_layout(node, style_classes);
2863
2864 let spacing = node
2866 .attributes
2867 .get("spacing")
2868 .and_then(|attr| {
2869 if let AttributeValue::Static(s) = attr {
2870 s.parse::<f32>().ok()
2871 } else {
2872 None
2873 }
2874 })
2875 .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
2876
2877 if let Some(s) = spacing
2879 && (widget_type == "column" || widget_type == "row")
2880 {
2881 container = quote! { #container.spacing(#s) };
2882 }
2883
2884 let padding = node
2886 .attributes
2887 .get("padding")
2888 .and_then(|attr| {
2889 if let AttributeValue::Static(s) = attr {
2890 s.parse::<f32>().ok()
2891 } else {
2892 None
2893 }
2894 })
2895 .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
2896
2897 if let Some(p) = padding {
2899 container = quote! { #container.padding(#p) };
2900 }
2901
2902 let width_from_attr = node.attributes.get("width").and_then(|attr| {
2904 if let AttributeValue::Static(s) = attr {
2905 Some(s.clone())
2906 } else {
2907 None
2908 }
2909 });
2910 let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
2911
2912 if let Some(width) = width_from_attr {
2913 let width_expr = generate_length_expr(&width);
2914 container = quote! { #container.width(#width_expr) };
2915 } else if let Some(layout_width) = width_from_layout {
2916 let width_expr = generate_layout_length_expr(layout_width);
2917 container = quote! { #container.width(#width_expr) };
2918 }
2919
2920 let height_from_attr = node.attributes.get("height").and_then(|attr| {
2922 if let AttributeValue::Static(s) = attr {
2923 Some(s.clone())
2924 } else {
2925 None
2926 }
2927 });
2928 let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
2929
2930 if let Some(height) = height_from_attr {
2931 let height_expr = generate_length_expr(&height);
2932 container = quote! { #container.height(#height_expr) };
2933 } else if let Some(layout_height) = height_from_layout {
2934 let height_expr = generate_layout_length_expr(layout_height);
2935 container = quote! { #container.height(#height_expr) };
2936 }
2937
2938 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
2940 if let AttributeValue::Static(s) = attr {
2941 Some(s.clone())
2942 } else {
2943 None
2944 }
2945 }) && widget_type == "row"
2946 {
2947 let alignment_expr = match align_y.to_lowercase().as_str() {
2948 "top" | "start" => quote! { iced::alignment::Vertical::Top },
2949 "bottom" | "end" => quote! { iced::alignment::Vertical::Bottom },
2950 _ => quote! { iced::alignment::Vertical::Center },
2951 };
2952 container = quote! { #container.align_y(#alignment_expr) };
2953 }
2954
2955 if widget_type == "container" {
2957 container = apply_widget_style(container, node, "container", style_classes)?;
2958 }
2959
2960 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#container) })
2962}
2963
2964fn generate_for_with_locals(
2966 node: &crate::WidgetNode,
2967 model_ident: &syn::Ident,
2968 message_ident: &syn::Ident,
2969 style_classes: &HashMap<String, StyleClass>,
2970 local_vars: &std::collections::HashSet<String>,
2971) -> Result<TokenStream, super::CodegenError> {
2972 let in_attr = node.attributes.get("in").ok_or_else(|| {
2974 super::CodegenError::InvalidWidget("for requires 'in' attribute".to_string())
2975 })?;
2976
2977 let var_name = node
2979 .attributes
2980 .get("each")
2981 .and_then(|attr| {
2982 if let AttributeValue::Static(s) = attr {
2983 Some(s.clone())
2984 } else {
2985 None
2986 }
2987 })
2988 .unwrap_or_else(|| "item".to_string());
2989
2990 let var_ident = format_ident!("{}", var_name);
2991
2992 let collection_expr =
2994 generate_attribute_value_raw_with_locals(in_attr, model_ident, local_vars);
2995
2996 let mut new_local_vars = local_vars.clone();
2998 new_local_vars.insert(var_name.clone());
2999 new_local_vars.insert("index".to_string());
3000
3001 let children: Vec<TokenStream> = node
3003 .children
3004 .iter()
3005 .map(|child| {
3006 generate_widget_with_locals(
3007 child,
3008 model_ident,
3009 message_ident,
3010 style_classes,
3011 &new_local_vars,
3012 )
3013 })
3014 .collect::<Result<_, _>>()?;
3015
3016 Ok(quote! {
3019 {
3020 let mut widgets: Vec<Element<'_, #message_ident>> = Vec::new();
3021 for (index, #var_ident) in (#collection_expr).iter().enumerate() {
3022 let _ = index;
3023 #(
3024 let child_widget: Element<'_, #message_ident> = #children;
3025 widgets.push(child_widget);
3026 )*
3027 }
3028 Into::<Element<'_, #message_ident>>::into(iced::widget::column(widgets))
3029 }
3030 })
3031}
3032
3033fn generate_if_with_locals(
3035 node: &crate::WidgetNode,
3036 model_ident: &syn::Ident,
3037 message_ident: &syn::Ident,
3038 style_classes: &HashMap<String, StyleClass>,
3039 local_vars: &std::collections::HashSet<String>,
3040) -> Result<TokenStream, super::CodegenError> {
3041 let condition_attr = node.attributes.get("condition").ok_or_else(|| {
3042 super::CodegenError::InvalidWidget("if requires condition attribute".to_string())
3043 })?;
3044
3045 let children: Vec<TokenStream> = node
3046 .children
3047 .iter()
3048 .map(|child| {
3049 generate_widget_with_locals(
3050 child,
3051 model_ident,
3052 message_ident,
3053 style_classes,
3054 local_vars,
3055 )
3056 })
3057 .collect::<Result<_, _>>()?;
3058
3059 let condition_expr =
3060 generate_attribute_value_with_locals(condition_attr, model_ident, local_vars);
3061
3062 Ok(quote! {
3063 if #condition_expr.parse::<bool>().unwrap_or(false) {
3064 Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![#(#children),*]; children }))
3065 } else {
3066 Into::<Element<'_, #message_ident>>::into(iced::widget::column({ let children: Vec<Element<'_, #message_ident>> = vec![]; children }))
3067 }
3068 })
3069}
3070
3071fn generate_checkbox_with_locals(
3073 node: &crate::WidgetNode,
3074 model_ident: &syn::Ident,
3075 message_ident: &syn::Ident,
3076 style_classes: &HashMap<String, StyleClass>,
3077 local_vars: &std::collections::HashSet<String>,
3078) -> Result<TokenStream, super::CodegenError> {
3079 let checked_attr = node.attributes.get("checked");
3081 let checked_expr = if let Some(attr) = checked_attr {
3082 generate_attribute_value_raw_with_locals(attr, model_ident, local_vars)
3083 } else {
3084 quote! { false }
3085 };
3086
3087 let on_change = node
3089 .events
3090 .iter()
3091 .find(|e| e.event == crate::EventKind::Change);
3092
3093 let mut checkbox = quote! {
3094 iced::widget::checkbox(#checked_expr)
3095 };
3096
3097 if let Some(event) = on_change {
3098 let variant_name = to_upper_camel_case(&event.handler);
3099 let handler_ident = format_ident!("{}", variant_name);
3100
3101 let param_expr = if let Some(ref param) = event.param {
3102 let param_tokens = super::bindings::generate_expr_with_locals(¶m.expr, local_vars);
3103 quote! { (#param_tokens) }
3104 } else {
3105 quote! {}
3106 };
3107
3108 checkbox = quote! {
3109 #checkbox.on_toggle(move |_| #message_ident::#handler_ident #param_expr)
3110 };
3111 }
3112
3113 if let Some(size) = node.attributes.get("size").and_then(|attr| {
3115 if let AttributeValue::Static(s) = attr {
3116 s.parse::<f32>().ok()
3117 } else {
3118 None
3119 }
3120 }) {
3121 checkbox = quote! { #checkbox.size(#size) };
3122 }
3123
3124 checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
3126
3127 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#checkbox) })
3128}
3129
3130fn generate_text_input_with_locals(
3132 node: &crate::WidgetNode,
3133 model_ident: &syn::Ident,
3134 message_ident: &syn::Ident,
3135 style_classes: &HashMap<String, StyleClass>,
3136 local_vars: &std::collections::HashSet<String>,
3137) -> Result<TokenStream, super::CodegenError> {
3138 let placeholder = node
3140 .attributes
3141 .get("placeholder")
3142 .and_then(|attr| {
3143 if let AttributeValue::Static(s) = attr {
3144 Some(s.clone())
3145 } else {
3146 None
3147 }
3148 })
3149 .unwrap_or_default();
3150 let placeholder_lit = proc_macro2::Literal::string(&placeholder);
3151
3152 let value_attr = node.attributes.get("value");
3154 let value_expr = if let Some(attr) = value_attr {
3155 generate_attribute_value_with_locals(attr, model_ident, local_vars)
3156 } else {
3157 quote! { String::new() }
3158 };
3159
3160 let on_input = node
3161 .events
3162 .iter()
3163 .find(|e| e.event == crate::EventKind::Input);
3164
3165 let on_submit = node
3166 .events
3167 .iter()
3168 .find(|e| e.event == crate::EventKind::Submit);
3169
3170 let mut text_input = quote! {
3171 iced::widget::text_input(#placeholder_lit, &#value_expr)
3172 };
3173
3174 if let Some(event) = on_input {
3176 let variant_name = to_upper_camel_case(&event.handler);
3177 let handler_ident = format_ident!("{}", variant_name);
3178 text_input = quote! { #text_input.on_input(|v| #message_ident::#handler_ident(v)) };
3179 }
3180
3181 if let Some(event) = on_submit {
3183 let variant_name = to_upper_camel_case(&event.handler);
3184 let handler_ident = format_ident!("{}", variant_name);
3185 text_input = quote! { #text_input.on_submit(#message_ident::#handler_ident) };
3186 }
3187
3188 if let Some(size) = node.attributes.get("size").and_then(|attr| {
3190 if let AttributeValue::Static(s) = attr {
3191 s.parse::<f32>().ok()
3192 } else {
3193 None
3194 }
3195 }) {
3196 text_input = quote! { #text_input.size(#size) };
3197 }
3198
3199 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
3201 if let AttributeValue::Static(s) = attr {
3202 s.parse::<f32>().ok()
3203 } else {
3204 None
3205 }
3206 }) {
3207 text_input = quote! { #text_input.padding(#padding) };
3208 }
3209
3210 if let Some(width) = node.attributes.get("width").and_then(|attr| {
3212 if let AttributeValue::Static(s) = attr {
3213 Some(generate_length_expr(s))
3214 } else {
3215 None
3216 }
3217 }) {
3218 text_input = quote! { #text_input.width(#width) };
3219 }
3220
3221 text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
3223
3224 Ok(quote! { Into::<Element<'_, #message_ident>>::into(#text_input) })
3225}
3226
3227fn generate_attribute_value_with_locals(
3229 attr: &AttributeValue,
3230 _model_ident: &syn::Ident,
3231 local_vars: &std::collections::HashSet<String>,
3232) -> TokenStream {
3233 match attr {
3234 AttributeValue::Static(s) => {
3235 let lit = proc_macro2::Literal::string(s);
3236 quote! { #lit.to_string() }
3237 }
3238 AttributeValue::Binding(expr) => {
3239 super::bindings::generate_expr_with_locals(&expr.expr, local_vars)
3240 }
3241 AttributeValue::Interpolated(parts) => {
3242 let parts_str: Vec<String> = parts
3243 .iter()
3244 .map(|part| match part {
3245 InterpolatedPart::Literal(s) => s.clone(),
3246 InterpolatedPart::Binding(_) => "{}".to_string(),
3247 })
3248 .collect();
3249 let binding_exprs: Vec<TokenStream> = parts
3250 .iter()
3251 .filter_map(|part| {
3252 if let InterpolatedPart::Binding(expr) = part {
3253 Some(super::bindings::generate_expr_with_locals(
3254 &expr.expr, local_vars,
3255 ))
3256 } else {
3257 None
3258 }
3259 })
3260 .collect();
3261
3262 let format_string = parts_str.join("");
3263 let lit = proc_macro2::Literal::string(&format_string);
3264
3265 quote! { format!(#lit, #(#binding_exprs),*) }
3266 }
3267 }
3268}
3269
3270fn generate_attribute_value_raw_with_locals(
3272 attr: &AttributeValue,
3273 _model_ident: &syn::Ident,
3274 local_vars: &std::collections::HashSet<String>,
3275) -> TokenStream {
3276 match attr {
3277 AttributeValue::Static(s) => {
3278 let lit = proc_macro2::Literal::string(s);
3279 quote! { #lit }
3280 }
3281 AttributeValue::Binding(expr) => {
3282 super::bindings::generate_bool_expr_with_locals(&expr.expr, local_vars)
3283 }
3284 AttributeValue::Interpolated(parts) => {
3285 let parts_str: Vec<String> = parts
3286 .iter()
3287 .map(|part| match part {
3288 InterpolatedPart::Literal(s) => s.clone(),
3289 InterpolatedPart::Binding(_) => "{}".to_string(),
3290 })
3291 .collect();
3292 let binding_exprs: Vec<TokenStream> = parts
3293 .iter()
3294 .filter_map(|part| {
3295 if let InterpolatedPart::Binding(expr) = part {
3296 Some(super::bindings::generate_expr_with_locals(
3297 &expr.expr, local_vars,
3298 ))
3299 } else {
3300 None
3301 }
3302 })
3303 .collect();
3304
3305 let format_string = parts_str.join("");
3306 let lit = proc_macro2::Literal::string(&format_string);
3307
3308 quote! { format!(#lit, #(#binding_exprs),*) }
3309 }
3310 }
3311}
3312
3313#[cfg(test)]
3314mod tests {
3315 use super::*;
3316 use crate::parse;
3317
3318 #[test]
3319 fn test_view_generation() {
3320 let xml = r#"<column><text value="Hello" /></column>"#;
3321 let doc = parse(xml).unwrap();
3322
3323 let result = generate_view(&doc, "Model", "Message").unwrap();
3324 let code = result.to_string();
3325
3326 assert!(code.contains("text"));
3327 assert!(code.contains("column"));
3328 }
3329
3330 #[test]
3331 fn test_view_generation_with_binding() {
3332 let xml = r#"<column><text value="{name}" /></column>"#;
3333 let doc = parse(xml).unwrap();
3334
3335 let result = generate_view(&doc, "Model", "Message").unwrap();
3336 let code = result.to_string();
3337
3338 assert!(code.contains("name"));
3339 assert!(code.contains("to_string"));
3340 }
3341
3342 #[test]
3343 fn test_button_with_handler() {
3344 let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
3345 let doc = parse(xml).unwrap();
3346
3347 let result = generate_view(&doc, "Model", "Message").unwrap();
3348 let code = result.to_string();
3349
3350 assert!(code.contains("button"));
3351 assert!(code.contains("HandleClick"));
3352 }
3353
3354 #[test]
3355 fn test_container_with_children() {
3356 let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
3357 let doc = parse(xml).unwrap();
3358
3359 let result = generate_view(&doc, "Model", "Message").unwrap();
3360 let code = result.to_string();
3361
3362 assert!(code.contains("column"));
3363 assert!(code.contains("spacing"));
3364 }
3365
3366 #[test]
3367 fn test_button_with_inline_style() {
3368 use crate::ir::node::WidgetNode;
3369 use crate::ir::style::{Background, Color, StyleProperties};
3370 use std::collections::HashMap;
3371
3372 let button_node = WidgetNode {
3374 kind: WidgetKind::Button,
3375 id: None,
3376 attributes: {
3377 let mut attrs = HashMap::new();
3378 attrs.insert(
3379 "label".to_string(),
3380 AttributeValue::Static("Test".to_string()),
3381 );
3382 attrs
3383 },
3384 events: vec![],
3385 children: vec![],
3386 span: Default::default(),
3387 style: Some(StyleProperties {
3388 background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
3389 color: Some(Color::from_rgb8(255, 255, 255)),
3390 border: None,
3391 shadow: None,
3392 opacity: None,
3393 transform: None,
3394 }),
3395 layout: None,
3396 theme_ref: None,
3397 classes: vec![],
3398 breakpoint_attributes: HashMap::new(),
3399 inline_state_variants: HashMap::new(),
3400 };
3401
3402 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3403 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3404 let style_classes = HashMap::new();
3405
3406 let result =
3407 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
3408 let code = result.to_string();
3409
3410 assert!(code.contains("style"));
3412 assert!(code.contains("button :: Status"));
3413 assert!(code.contains("button :: Style"));
3414 assert!(code.contains("background"));
3415 assert!(code.contains("text_color"));
3416 }
3417
3418 #[test]
3419 fn test_button_with_css_class() {
3420 use crate::ir::node::WidgetNode;
3421 use crate::ir::theme::StyleClass;
3422 use std::collections::HashMap;
3423
3424 let button_node = WidgetNode {
3426 kind: WidgetKind::Button,
3427 id: None,
3428 attributes: {
3429 let mut attrs = HashMap::new();
3430 attrs.insert(
3431 "label".to_string(),
3432 AttributeValue::Static("Test".to_string()),
3433 );
3434 attrs
3435 },
3436 events: vec![],
3437 children: vec![],
3438 span: Default::default(),
3439 style: None,
3440 layout: None,
3441 theme_ref: None,
3442 classes: vec!["primary-button".to_string()],
3443 breakpoint_attributes: HashMap::new(),
3444 inline_state_variants: HashMap::new(),
3445 };
3446
3447 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3448 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3449 let style_classes: HashMap<String, StyleClass> = HashMap::new();
3450
3451 let result =
3452 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
3453 let code = result.to_string();
3454
3455 assert!(code.contains("style"));
3457 assert!(code.contains("style_primary_button"));
3458 }
3459
3460 #[test]
3461 fn test_container_with_inline_style() {
3462 use crate::ir::node::WidgetNode;
3463 use crate::ir::style::{
3464 Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
3465 };
3466 use crate::ir::theme::StyleClass;
3467 use std::collections::HashMap;
3468
3469 let container_node = WidgetNode {
3470 kind: WidgetKind::Container,
3471 id: None,
3472 attributes: HashMap::new(),
3473 events: vec![],
3474 children: vec![],
3475 span: Default::default(),
3476 style: Some(StyleProperties {
3477 background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
3478 color: None,
3479 border: Some(Border {
3480 width: 2.0,
3481 color: Color::from_rgb8(200, 200, 200),
3482 radius: BorderRadius {
3483 top_left: 8.0,
3484 top_right: 8.0,
3485 bottom_right: 8.0,
3486 bottom_left: 8.0,
3487 },
3488 style: BorderStyle::Solid,
3489 }),
3490 shadow: None,
3491 opacity: None,
3492 transform: None,
3493 }),
3494 layout: None,
3495 theme_ref: None,
3496 classes: vec![],
3497 breakpoint_attributes: HashMap::new(),
3498 inline_state_variants: HashMap::new(),
3499 };
3500
3501 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
3502 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
3503 let style_classes: HashMap<String, StyleClass> = HashMap::new();
3504
3505 let result = generate_container(
3506 &container_node,
3507 "container",
3508 &model_ident,
3509 &message_ident,
3510 &style_classes,
3511 )
3512 .unwrap();
3513 let code = result.to_string();
3514
3515 assert!(code.contains("style"));
3517 assert!(code.contains("container :: Style"));
3518 assert!(code.contains("background"));
3519 assert!(code.contains("border"));
3520 }
3521}