1use crate::DampenDocument;
6use crate::codegen::bindings::generate_expr;
7use crate::ir::layout::{LayoutConstraints, Length as LayoutLength};
8use crate::ir::node::{AttributeValue, InterpolatedPart, WidgetKind};
9use crate::ir::style::{
10 Background, Border, BorderRadius, Color, Gradient, Shadow, StyleProperties,
11};
12use crate::ir::theme::StyleClass;
13use proc_macro2::TokenStream;
14use quote::{format_ident, quote};
15use std::collections::HashMap;
16
17pub fn generate_view(
19 document: &DampenDocument,
20 _model_name: &str,
21 message_name: &str,
22) -> Result<TokenStream, super::CodegenError> {
23 let message_ident = syn::Ident::new(message_name, proc_macro2::Span::call_site());
24 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
25
26 let root_widget = generate_widget(
27 &document.root,
28 &model_ident,
29 &message_ident,
30 &document.style_classes,
31 )?;
32
33 Ok(quote! {
34 #root_widget
35 })
36}
37
38fn get_merged_layout<'a>(
40 node: &'a crate::WidgetNode,
41 style_classes: &'a HashMap<String, StyleClass>,
42) -> Option<MergedLayout<'a>> {
43 let node_layout = node.layout.as_ref();
45 let class_layout = node
46 .classes
47 .first()
48 .and_then(|class_name| style_classes.get(class_name))
49 .and_then(|class| class.layout.as_ref());
50
51 if node_layout.is_some() || class_layout.is_some() {
52 Some(MergedLayout {
53 node_layout,
54 class_layout,
55 })
56 } else {
57 None
58 }
59}
60
61struct MergedLayout<'a> {
63 node_layout: Option<&'a LayoutConstraints>,
64 class_layout: Option<&'a LayoutConstraints>,
65}
66
67impl<'a> MergedLayout<'a> {
68 fn padding(&self) -> Option<f32> {
69 self.node_layout
70 .and_then(|l| l.padding.as_ref())
71 .map(|p| p.top)
72 .or_else(|| {
73 self.class_layout
74 .and_then(|l| l.padding.as_ref())
75 .map(|p| p.top)
76 })
77 }
78
79 fn spacing(&self) -> Option<f32> {
80 self.node_layout
81 .and_then(|l| l.spacing)
82 .or_else(|| self.class_layout.and_then(|l| l.spacing))
83 }
84
85 fn width(&self) -> Option<&'a LayoutLength> {
86 self.node_layout
87 .and_then(|l| l.width.as_ref())
88 .or_else(|| self.class_layout.and_then(|l| l.width.as_ref()))
89 }
90
91 fn height(&self) -> Option<&'a LayoutLength> {
92 self.node_layout
93 .and_then(|l| l.height.as_ref())
94 .or_else(|| self.class_layout.and_then(|l| l.height.as_ref()))
95 }
96}
97
98fn generate_widget(
100 node: &crate::WidgetNode,
101 model_ident: &syn::Ident,
102 message_ident: &syn::Ident,
103 style_classes: &HashMap<String, StyleClass>,
104) -> Result<TokenStream, super::CodegenError> {
105 match node.kind {
106 WidgetKind::Text => generate_text(node, model_ident, style_classes),
107 WidgetKind::Button => generate_button(node, model_ident, message_ident, style_classes),
108 WidgetKind::Column => {
109 generate_container(node, "column", model_ident, message_ident, style_classes)
110 }
111 WidgetKind::Row => {
112 generate_container(node, "row", model_ident, message_ident, style_classes)
113 }
114 WidgetKind::Container => {
115 generate_container(node, "container", model_ident, message_ident, style_classes)
116 }
117 WidgetKind::Scrollable => generate_container(
118 node,
119 "scrollable",
120 model_ident,
121 message_ident,
122 style_classes,
123 ),
124 WidgetKind::Stack => generate_stack(node, model_ident, message_ident, style_classes),
125 WidgetKind::Space => generate_space(node),
126 WidgetKind::Rule => generate_rule(node),
127 WidgetKind::Checkbox => generate_checkbox(node, model_ident, message_ident, style_classes),
128 WidgetKind::Toggler => generate_toggler(node, model_ident, message_ident, style_classes),
129 WidgetKind::Slider => generate_slider(node, model_ident, message_ident, style_classes),
130 WidgetKind::Radio => generate_radio(node, model_ident, message_ident, style_classes),
131 WidgetKind::ProgressBar => generate_progress_bar(node, model_ident, style_classes),
132 WidgetKind::TextInput => {
133 generate_text_input(node, model_ident, message_ident, style_classes)
134 }
135 WidgetKind::Image => generate_image(node),
136 WidgetKind::Svg => generate_svg(node),
137 WidgetKind::PickList => generate_pick_list(node, model_ident, message_ident, style_classes),
138 WidgetKind::ComboBox => generate_combo_box(node, model_ident, message_ident, style_classes),
139 WidgetKind::Tooltip => generate_tooltip(node, model_ident, message_ident, style_classes),
140 WidgetKind::Grid => generate_grid(node, model_ident, message_ident, style_classes),
141 WidgetKind::Canvas => generate_canvas(node, model_ident, message_ident, style_classes),
142 WidgetKind::Float => generate_float(node, model_ident, message_ident, style_classes),
143 WidgetKind::For => generate_for(node, model_ident, message_ident, style_classes),
144 WidgetKind::Custom(ref name) => {
145 generate_custom_widget(node, name, model_ident, message_ident, style_classes)
146 }
147 }
148}
149
150fn apply_widget_style(
161 widget: TokenStream,
162 node: &crate::WidgetNode,
163 widget_type: &str,
164 style_classes: &HashMap<String, StyleClass>,
165) -> Result<TokenStream, super::CodegenError> {
166 let has_inline_style = node.style.is_some();
168 let has_classes = !node.classes.is_empty();
169
170 if !has_inline_style && !has_classes {
171 return Ok(widget);
173 }
174
175 let style_class = if let Some(class_name) = node.classes.first() {
177 style_classes.get(class_name)
178 } else {
179 None
180 };
181
182 if let Some(ref style_props) = node.style {
184 let style_closure =
186 generate_inline_style_closure(style_props, widget_type, &node.kind, style_class)?;
187 Ok(quote! {
188 #widget.style(#style_closure)
189 })
190 } else if let Some(class_name) = node.classes.first() {
191 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
193 Ok(quote! {
194 #widget.style(#style_fn_ident)
195 })
196 } else {
197 Ok(widget)
198 }
199}
200
201fn generate_state_style_match(
212 base_style: TokenStream,
213 style_class: &StyleClass,
214 widget_state_ident: &syn::Ident,
215 style_struct_fn: fn(&StyleProperties) -> Result<TokenStream, super::CodegenError>,
216) -> Result<TokenStream, super::CodegenError> {
217 use crate::ir::theme::WidgetState;
218
219 let mut state_arms = Vec::new();
221
222 for (state, state_props) in &style_class.state_variants {
223 let state_variant = match state {
224 WidgetState::Hover => quote! { dampen_core::ir::WidgetState::Hover },
225 WidgetState::Focus => quote! { dampen_core::ir::WidgetState::Focus },
226 WidgetState::Active => quote! { dampen_core::ir::WidgetState::Active },
227 WidgetState::Disabled => quote! { dampen_core::ir::WidgetState::Disabled },
228 };
229
230 let state_style = style_struct_fn(state_props)?;
232
233 state_arms.push(quote! {
234 Some(#state_variant) => #state_style
235 });
236 }
237
238 Ok(quote! {
240 match #widget_state_ident {
241 #(#state_arms,)*
242 None => #base_style
243 }
244 })
245}
246
247fn generate_inline_style_closure(
263 style_props: &StyleProperties,
264 widget_type: &str,
265 widget_kind: &WidgetKind,
266 style_class: Option<&StyleClass>,
267) -> Result<TokenStream, super::CodegenError> {
268 let has_state_variants = style_class
270 .map(|sc| !sc.state_variants.is_empty())
271 .unwrap_or(false);
272
273 match widget_type {
274 "button" => {
275 let base_style = generate_button_style_struct(style_props)?;
276
277 if has_state_variants {
278 let status_ident = format_ident!("status");
280 if let Some(status_mapping) =
281 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
282 {
283 let widget_state_ident = format_ident!("widget_state");
284 let class = style_class.ok_or_else(|| {
286 super::CodegenError::InvalidWidget(
287 "Expected style class with state variants".to_string(),
288 )
289 })?;
290 let style_match = generate_state_style_match(
291 base_style,
292 class,
293 &widget_state_ident,
294 generate_button_style_struct,
295 )?;
296
297 Ok(quote! {
298 |_theme: &iced::Theme, #status_ident: iced::widget::button::Status| {
299 let #widget_state_ident = #status_mapping;
301
302 #style_match
304 }
305 })
306 } else {
307 Ok(quote! {
309 |_theme: &iced::Theme, _status: iced::widget::button::Status| {
310 #base_style
311 }
312 })
313 }
314 } else {
315 Ok(quote! {
317 |_theme: &iced::Theme, _status: iced::widget::button::Status| {
318 #base_style
319 }
320 })
321 }
322 }
323 "container" => {
324 let style_struct = generate_container_style_struct(style_props)?;
325 Ok(quote! {
326 |_theme: &iced::Theme| {
327 #style_struct
328 }
329 })
330 }
331 "text_input" => {
332 let base_style = generate_text_input_style_struct(style_props)?;
333
334 if has_state_variants {
335 let status_ident = format_ident!("status");
337 if let Some(status_mapping) =
338 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
339 {
340 let widget_state_ident = format_ident!("widget_state");
341 let class = style_class.ok_or_else(|| {
342 super::CodegenError::InvalidWidget(
343 "Expected style class with state variants".to_string(),
344 )
345 })?;
346 let style_match = generate_state_style_match(
347 base_style,
348 class,
349 &widget_state_ident,
350 generate_text_input_style_struct,
351 )?;
352
353 Ok(quote! {
354 |_theme: &iced::Theme, #status_ident: iced::widget::text_input::Status| {
355 let #widget_state_ident = #status_mapping;
357
358 #style_match
360 }
361 })
362 } else {
363 Ok(quote! {
365 |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
366 #base_style
367 }
368 })
369 }
370 } else {
371 Ok(quote! {
373 |_theme: &iced::Theme, _status: iced::widget::text_input::Status| {
374 #base_style
375 }
376 })
377 }
378 }
379 "checkbox" => {
380 let base_style = generate_checkbox_style_struct(style_props)?;
381
382 if has_state_variants {
383 let status_ident = format_ident!("status");
384 if let Some(status_mapping) =
385 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
386 {
387 let widget_state_ident = format_ident!("widget_state");
388 let class = style_class.ok_or_else(|| {
389 super::CodegenError::InvalidWidget(
390 "Expected style class with state variants".to_string(),
391 )
392 })?;
393 let style_match = generate_state_style_match(
394 base_style,
395 class,
396 &widget_state_ident,
397 generate_checkbox_style_struct,
398 )?;
399
400 Ok(quote! {
401 |_theme: &iced::Theme, #status_ident: iced::widget::checkbox::Status| {
402 let #widget_state_ident = #status_mapping;
403 #style_match
404 }
405 })
406 } else {
407 Ok(quote! {
408 |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
409 #base_style
410 }
411 })
412 }
413 } else {
414 Ok(quote! {
415 |_theme: &iced::Theme, _status: iced::widget::checkbox::Status| {
416 #base_style
417 }
418 })
419 }
420 }
421 "toggler" => {
422 let base_style = generate_toggler_style_struct(style_props)?;
423
424 if has_state_variants {
425 let status_ident = format_ident!("status");
426 if let Some(status_mapping) =
427 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
428 {
429 let widget_state_ident = format_ident!("widget_state");
430 let class = style_class.ok_or_else(|| {
431 super::CodegenError::InvalidWidget(
432 "Expected style class with state variants".to_string(),
433 )
434 })?;
435 let style_match = generate_state_style_match(
436 base_style,
437 class,
438 &widget_state_ident,
439 generate_toggler_style_struct,
440 )?;
441
442 Ok(quote! {
443 |_theme: &iced::Theme, #status_ident: iced::widget::toggler::Status| {
444 let #widget_state_ident = #status_mapping;
445 #style_match
446 }
447 })
448 } else {
449 Ok(quote! {
450 |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
451 #base_style
452 }
453 })
454 }
455 } else {
456 Ok(quote! {
457 |_theme: &iced::Theme, _status: iced::widget::toggler::Status| {
458 #base_style
459 }
460 })
461 }
462 }
463 "slider" => {
464 let base_style = generate_slider_style_struct(style_props)?;
465
466 if has_state_variants {
467 let status_ident = format_ident!("status");
468 if let Some(status_mapping) =
469 super::status_mapping::generate_status_mapping(widget_kind, &status_ident)
470 {
471 let widget_state_ident = format_ident!("widget_state");
472 let class = style_class.ok_or_else(|| {
473 super::CodegenError::InvalidWidget(
474 "Expected style class with state variants".to_string(),
475 )
476 })?;
477 let style_match = generate_state_style_match(
478 base_style,
479 class,
480 &widget_state_ident,
481 generate_slider_style_struct,
482 )?;
483
484 Ok(quote! {
485 |_theme: &iced::Theme, #status_ident: iced::widget::slider::Status| {
486 let #widget_state_ident = #status_mapping;
487 #style_match
488 }
489 })
490 } else {
491 Ok(quote! {
492 |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
493 #base_style
494 }
495 })
496 }
497 } else {
498 Ok(quote! {
499 |_theme: &iced::Theme, _status: iced::widget::slider::Status| {
500 #base_style
501 }
502 })
503 }
504 }
505 _ => {
506 Ok(quote! {
508 |_theme: &iced::Theme| iced::widget::container::Style::default()
509 })
510 }
511 }
512}
513
514fn generate_color_expr(color: &Color) -> TokenStream {
520 let r = color.r;
521 let g = color.g;
522 let b = color.b;
523 let a = color.a;
524 quote! {
525 iced::Color::from_rgba(#r, #g, #b, #a)
526 }
527}
528
529fn generate_background_expr(bg: &Background) -> TokenStream {
531 match bg {
532 Background::Color(color) => {
533 let color_expr = generate_color_expr(color);
534 quote! { iced::Background::Color(#color_expr) }
535 }
536 Background::Gradient(gradient) => generate_gradient_expr(gradient),
537 Background::Image { .. } => {
538 quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
539 }
540 }
541}
542
543fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
545 match gradient {
546 Gradient::Linear { angle, stops } => {
547 let radians = angle * (std::f32::consts::PI / 180.0);
548 let color_exprs: Vec<_> = stops
549 .iter()
550 .map(|s| generate_color_expr(&s.color))
551 .collect();
552 let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
553
554 quote! {
555 iced::Background::Gradient(iced::Gradient::Linear(
556 iced::gradient::Linear::new(#radians)
557 #(.add_stop(#offsets, #color_exprs))*
558 ))
559 }
560 }
561 Gradient::Radial { stops, .. } => {
562 let color_exprs: Vec<_> = stops
564 .iter()
565 .map(|s| generate_color_expr(&s.color))
566 .collect();
567 let offsets: Vec<_> = stops.iter().map(|s| s.offset).collect();
568
569 quote! {
570 iced::Background::Gradient(iced::Gradient::Linear(
571 iced::gradient::Linear::new(0.0)
572 #(.add_stop(#offsets, #color_exprs))*
573 ))
574 }
575 }
576 }
577}
578
579fn generate_border_expr(border: &Border) -> TokenStream {
581 let width = border.width;
582 let color_expr = generate_color_expr(&border.color);
583 let radius_expr = generate_border_radius_expr(&border.radius);
584
585 quote! {
586 iced::Border {
587 width: #width,
588 color: #color_expr,
589 radius: #radius_expr,
590 }
591 }
592}
593
594fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
596 let tl = radius.top_left;
597 let tr = radius.top_right;
598 let br = radius.bottom_right;
599 let bl = radius.bottom_left;
600
601 quote! {
602 iced::border::Radius::from(#tl).top_right(#tr).bottom_right(#br).bottom_left(#bl)
603 }
604}
605
606fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
608 let offset_x = shadow.offset_x;
609 let offset_y = shadow.offset_y;
610 let blur = shadow.blur_radius;
611 let color_expr = generate_color_expr(&shadow.color);
612
613 quote! {
614 iced::Shadow {
615 offset: iced::Vector::new(#offset_x, #offset_y),
616 blur_radius: #blur,
617 color: #color_expr,
618 }
619 }
620}
621
622fn generate_button_style_struct(
624 props: &StyleProperties,
625) -> Result<TokenStream, super::CodegenError> {
626 let background_expr = props
627 .background
628 .as_ref()
629 .map(|bg| {
630 let expr = generate_background_expr(bg);
631 quote! { Some(#expr) }
632 })
633 .unwrap_or_else(|| quote! { None });
634
635 let text_color_expr = props
636 .color
637 .as_ref()
638 .map(generate_color_expr)
639 .unwrap_or_else(|| quote! { iced::Color::BLACK });
640
641 let border_expr = props
642 .border
643 .as_ref()
644 .map(generate_border_expr)
645 .unwrap_or_else(|| quote! { iced::Border::default() });
646
647 let shadow_expr = props
648 .shadow
649 .as_ref()
650 .map(generate_shadow_expr)
651 .unwrap_or_else(|| quote! { iced::Shadow::default() });
652
653 Ok(quote! {
654 iced::widget::button::Style {
655 background: #background_expr,
656 text_color: #text_color_expr,
657 border: #border_expr,
658 shadow: #shadow_expr,
659 snap: false,
660 }
661 })
662}
663
664fn generate_container_style_struct(
666 props: &StyleProperties,
667) -> Result<TokenStream, super::CodegenError> {
668 let background_expr = props
669 .background
670 .as_ref()
671 .map(|bg| {
672 let expr = generate_background_expr(bg);
673 quote! { Some(#expr) }
674 })
675 .unwrap_or_else(|| quote! { None });
676
677 let text_color_expr = props
678 .color
679 .as_ref()
680 .map(|color| {
681 let color_expr = generate_color_expr(color);
682 quote! { Some(#color_expr) }
683 })
684 .unwrap_or_else(|| quote! { None });
685
686 let border_expr = props
687 .border
688 .as_ref()
689 .map(generate_border_expr)
690 .unwrap_or_else(|| quote! { iced::Border::default() });
691
692 let shadow_expr = props
693 .shadow
694 .as_ref()
695 .map(generate_shadow_expr)
696 .unwrap_or_else(|| quote! { iced::Shadow::default() });
697
698 Ok(quote! {
699 iced::widget::container::Style {
700 background: #background_expr,
701 text_color: #text_color_expr,
702 border: #border_expr,
703 shadow: #shadow_expr,
704 snap: false,
705 }
706 })
707}
708
709fn generate_text_input_style_struct(
711 props: &StyleProperties,
712) -> Result<TokenStream, super::CodegenError> {
713 let background_expr = props
714 .background
715 .as_ref()
716 .map(|bg| {
717 let expr = generate_background_expr(bg);
718 quote! { #expr }
719 })
720 .unwrap_or_else(|| quote! { iced::Background::Color(iced::Color::WHITE) });
721
722 let border_expr = props
723 .border
724 .as_ref()
725 .map(generate_border_expr)
726 .unwrap_or_else(|| quote! { iced::Border::default() });
727
728 Ok(quote! {
729 iced::widget::text_input::Style {
730 background: #background_expr,
731 border: #border_expr,
732 icon: iced::Color::BLACK,
733 placeholder: iced::Color::from_rgb(0.4, 0.4, 0.4),
734 value: iced::Color::BLACK,
735 selection: iced::Color::from_rgb(0.8, 0.8, 1.0),
736 }
737 })
738}
739
740fn generate_checkbox_style_struct(
742 props: &StyleProperties,
743) -> Result<TokenStream, super::CodegenError> {
744 let background_expr = props
745 .background
746 .as_ref()
747 .map(|bg| {
748 let expr = generate_background_expr(bg);
749 quote! { #expr }
750 })
751 .unwrap_or_else(|| quote! { iced::Background::Color(iced::Color::WHITE) });
752
753 let border_expr = props
754 .border
755 .as_ref()
756 .map(generate_border_expr)
757 .unwrap_or_else(|| quote! { iced::Border::default() });
758
759 let text_color = props
760 .color
761 .as_ref()
762 .map(generate_color_expr)
763 .unwrap_or_else(|| quote! { iced::Color::BLACK });
764
765 Ok(quote! {
766 iced::widget::checkbox::Style {
767 background: #background_expr,
768 icon_color: #text_color,
769 border: #border_expr,
770 text_color: None,
771 }
772 })
773}
774
775fn generate_toggler_style_struct(
777 props: &StyleProperties,
778) -> Result<TokenStream, super::CodegenError> {
779 let background_expr = props
780 .background
781 .as_ref()
782 .map(|bg| {
783 let expr = generate_background_expr(bg);
784 quote! { #expr }
785 })
786 .unwrap_or_else(
787 || quote! { iced::Background::Color(iced::Color::from_rgb(0.5, 0.5, 0.5)) },
788 );
789
790 Ok(quote! {
791 iced::widget::toggler::Style {
792 background: #background_expr,
793 background_border_width: 0.0,
794 background_border_color: iced::Color::TRANSPARENT,
795 foreground: iced::Background::Color(iced::Color::WHITE),
796 foreground_border_width: 0.0,
797 foreground_border_color: iced::Color::TRANSPARENT,
798 }
799 })
800}
801
802fn generate_slider_style_struct(
804 props: &StyleProperties,
805) -> Result<TokenStream, super::CodegenError> {
806 let border_expr = props
807 .border
808 .as_ref()
809 .map(generate_border_expr)
810 .unwrap_or_else(|| quote! { iced::Border::default() });
811
812 Ok(quote! {
813 iced::widget::slider::Style {
814 rail: iced::widget::slider::Rail {
815 colors: (
816 iced::Color::from_rgb(0.6, 0.6, 0.6),
817 iced::Color::from_rgb(0.2, 0.6, 1.0),
818 ),
819 width: 4.0,
820 border: #border_expr,
821 },
822 handle: iced::widget::slider::Handle {
823 shape: iced::widget::slider::HandleShape::Circle { radius: 8.0 },
824 color: iced::Color::WHITE,
825 border_width: 1.0,
826 border_color: iced::Color::from_rgb(0.6, 0.6, 0.6),
827 },
828 }
829 })
830}
831
832fn generate_text(
834 node: &crate::WidgetNode,
835 model_ident: &syn::Ident,
836 _style_classes: &HashMap<String, StyleClass>,
837) -> Result<TokenStream, super::CodegenError> {
838 let value_attr = node.attributes.get("value").ok_or_else(|| {
839 super::CodegenError::InvalidWidget("text requires value attribute".to_string())
840 })?;
841
842 let value_expr = generate_attribute_value(value_attr, model_ident);
843
844 let mut text_widget = quote! {
845 iced::widget::text(#value_expr)
846 };
847
848 if let Some(size) = node.attributes.get("size").and_then(|attr| {
850 if let AttributeValue::Static(s) = attr {
851 s.parse::<f32>().ok()
852 } else {
853 None
854 }
855 }) {
856 text_widget = quote! { #text_widget.size(#size) };
857 }
858
859 if let Some(weight) = node.attributes.get("weight").and_then(|attr| {
861 if let AttributeValue::Static(s) = attr {
862 Some(s.clone())
863 } else {
864 None
865 }
866 }) {
867 let weight_expr = match weight.to_lowercase().as_str() {
868 "bold" => quote! { iced::font::Weight::Bold },
869 "semibold" => quote! { iced::font::Weight::Semibold },
870 "medium" => quote! { iced::font::Weight::Medium },
871 "light" => quote! { iced::font::Weight::Light },
872 _ => quote! { iced::font::Weight::Normal },
873 };
874 text_widget = quote! {
875 #text_widget.font(iced::Font { weight: #weight_expr, ..Default::default() })
876 };
877 }
878
879 if let Some(ref style_props) = node.style {
881 if let Some(ref color) = style_props.color {
882 let color_expr = generate_color_expr(color);
883 text_widget = quote! { #text_widget.color(#color_expr) };
884 }
885 }
886
887 Ok(maybe_wrap_in_container(text_widget, node))
889}
890
891fn generate_length_expr(s: &str) -> TokenStream {
893 let s = s.trim().to_lowercase();
894 if s == "fill" {
895 quote! { iced::Length::Fill }
896 } else if s == "shrink" {
897 quote! { iced::Length::Shrink }
898 } else if let Some(pct) = s.strip_suffix('%') {
899 if let Ok(p) = pct.parse::<f32>() {
900 let portion = ((p / 100.0) * 16.0).round() as u16;
902 let portion = portion.max(1);
903 quote! { iced::Length::FillPortion(#portion) }
904 } else {
905 quote! { iced::Length::Shrink }
906 }
907 } else if let Ok(px) = s.parse::<f32>() {
908 quote! { iced::Length::Fixed(#px) }
909 } else {
910 quote! { iced::Length::Shrink }
911 }
912}
913
914fn generate_layout_length_expr(length: &LayoutLength) -> TokenStream {
916 match length {
917 LayoutLength::Fixed(px) => quote! { iced::Length::Fixed(#px) },
918 LayoutLength::Fill => quote! { iced::Length::Fill },
919 LayoutLength::Shrink => quote! { iced::Length::Shrink },
920 LayoutLength::FillPortion(portion) => {
921 let p = *portion as u16;
922 quote! { iced::Length::FillPortion(#p) }
923 }
924 LayoutLength::Percentage(pct) => {
925 let portion = ((pct / 100.0) * 16.0).round() as u16;
927 let portion = portion.max(1);
928 quote! { iced::Length::FillPortion(#portion) }
929 }
930 }
931}
932
933fn generate_horizontal_alignment_expr(s: &str) -> TokenStream {
935 match s.trim().to_lowercase().as_str() {
936 "center" => quote! { iced::alignment::Horizontal::Center },
937 "end" | "right" => quote! { iced::alignment::Horizontal::Right },
938 _ => quote! { iced::alignment::Horizontal::Left },
939 }
940}
941
942fn generate_vertical_alignment_expr(s: &str) -> TokenStream {
944 match s.trim().to_lowercase().as_str() {
945 "center" => quote! { iced::alignment::Vertical::Center },
946 "end" | "bottom" => quote! { iced::alignment::Vertical::Bottom },
947 _ => quote! { iced::alignment::Vertical::Top },
948 }
949}
950
951fn maybe_wrap_in_container(widget: TokenStream, node: &crate::WidgetNode) -> TokenStream {
966 let needs_container = node.layout.is_some()
968 || !node.classes.is_empty()
969 || node.attributes.contains_key("align_x")
970 || node.attributes.contains_key("align_y")
971 || node.attributes.contains_key("width")
972 || node.attributes.contains_key("height")
973 || node.attributes.contains_key("padding");
974
975 if !needs_container {
976 return quote! { #widget.into() };
977 }
978
979 let mut container = quote! {
980 iced::widget::container(#widget)
981 };
982
983 if let Some(width) = node.attributes.get("width").and_then(|attr| {
985 if let AttributeValue::Static(s) = attr {
986 Some(s.clone())
987 } else {
988 None
989 }
990 }) {
991 let width_expr = generate_length_expr(&width);
992 container = quote! { #container.width(#width_expr) };
993 }
994
995 if let Some(height) = node.attributes.get("height").and_then(|attr| {
997 if let AttributeValue::Static(s) = attr {
998 Some(s.clone())
999 } else {
1000 None
1001 }
1002 }) {
1003 let height_expr = generate_length_expr(&height);
1004 container = quote! { #container.height(#height_expr) };
1005 }
1006
1007 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1009 if let AttributeValue::Static(s) = attr {
1010 s.parse::<f32>().ok()
1011 } else {
1012 None
1013 }
1014 }) {
1015 container = quote! { #container.padding(#padding) };
1016 }
1017
1018 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1020 if let AttributeValue::Static(s) = attr {
1021 Some(s.clone())
1022 } else {
1023 None
1024 }
1025 }) {
1026 let align_expr = generate_horizontal_alignment_expr(&align_x);
1027 container = quote! { #container.align_x(#align_expr) };
1028 }
1029
1030 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1032 if let AttributeValue::Static(s) = attr {
1033 Some(s.clone())
1034 } else {
1035 None
1036 }
1037 }) {
1038 let align_expr = generate_vertical_alignment_expr(&align_y);
1039 container = quote! { #container.align_y(#align_expr) };
1040 }
1041
1042 if let Some(class_name) = node.classes.first() {
1044 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1045 container = quote! { #container.style(#style_fn_ident) };
1046 }
1047
1048 quote! { #container.into() }
1049}
1050
1051fn generate_button(
1053 node: &crate::WidgetNode,
1054 model_ident: &syn::Ident,
1055 message_ident: &syn::Ident,
1056 style_classes: &HashMap<String, StyleClass>,
1057) -> Result<TokenStream, super::CodegenError> {
1058 let label_attr = node.attributes.get("label").ok_or_else(|| {
1059 super::CodegenError::InvalidWidget("button requires label attribute".to_string())
1060 })?;
1061
1062 let label_expr = generate_attribute_value(label_attr, model_ident);
1063
1064 let on_click = node
1065 .events
1066 .iter()
1067 .find(|e| e.event == crate::EventKind::Click);
1068
1069 let mut button = quote! {
1070 iced::widget::button(iced::widget::text(#label_expr))
1071 };
1072
1073 let enabled_condition = node.attributes.get("enabled").map(|attr| match attr {
1075 AttributeValue::Static(s) => {
1076 match s.to_lowercase().as_str() {
1078 "true" | "1" | "yes" | "on" => quote! { true },
1079 "false" | "0" | "no" | "off" => quote! { false },
1080 _ => quote! { true }, }
1082 }
1083 AttributeValue::Binding(binding_expr) => {
1084 super::bindings::generate_bool_expr(&binding_expr.expr)
1086 }
1087 AttributeValue::Interpolated(_) => {
1088 let expr_tokens = generate_attribute_value(attr, model_ident);
1090 quote! { !#expr_tokens.is_empty() && #expr_tokens != "false" && #expr_tokens != "0" }
1091 }
1092 });
1093
1094 if let Some(event) = on_click {
1095 let variant_name = to_upper_camel_case(&event.handler);
1096 let handler_ident = format_ident!("{}", variant_name);
1097
1098 let param_expr = if let Some(ref param) = event.param {
1099 let param_tokens = generate_expr(¶m.expr);
1100 quote! { (#param_tokens) }
1101 } else {
1102 quote! {}
1103 };
1104
1105 button = match enabled_condition {
1107 None => {
1108 quote! {
1110 #button.on_press(#message_ident::#handler_ident #param_expr)
1111 }
1112 }
1113 Some(condition) => {
1114 quote! {
1116 #button.on_press_maybe(
1117 if #condition {
1118 Some(#message_ident::#handler_ident #param_expr)
1119 } else {
1120 None
1121 }
1122 )
1123 }
1124 }
1125 };
1126 }
1127
1128 button = apply_widget_style(button, node, "button", style_classes)?;
1130
1131 Ok(quote! { #button.into() })
1132}
1133
1134fn to_upper_camel_case(s: &str) -> String {
1136 let mut result = String::new();
1137 let mut capitalize_next = true;
1138 for c in s.chars() {
1139 if c == '_' {
1140 capitalize_next = true;
1141 } else if capitalize_next {
1142 result.push(c.to_ascii_uppercase());
1143 capitalize_next = false;
1144 } else {
1145 result.push(c);
1146 }
1147 }
1148 result
1149}
1150
1151fn generate_container(
1153 node: &crate::WidgetNode,
1154 widget_type: &str,
1155 model_ident: &syn::Ident,
1156 message_ident: &syn::Ident,
1157 style_classes: &HashMap<String, StyleClass>,
1158) -> Result<TokenStream, super::CodegenError> {
1159 let children: Vec<TokenStream> = node
1160 .children
1161 .iter()
1162 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1163 .collect::<Result<_, _>>()?;
1164
1165 let widget_ident = format_ident!("{}", widget_type);
1166
1167 let merged_layout = get_merged_layout(node, style_classes);
1169
1170 let spacing = node
1172 .attributes
1173 .get("spacing")
1174 .and_then(|attr| {
1175 if let AttributeValue::Static(s) = attr {
1176 s.parse::<f32>().ok()
1177 } else {
1178 None
1179 }
1180 })
1181 .or_else(|| merged_layout.as_ref().and_then(|l| l.spacing()));
1182
1183 let padding = node
1185 .attributes
1186 .get("padding")
1187 .and_then(|attr| {
1188 if let AttributeValue::Static(s) = attr {
1189 s.parse::<f32>().ok()
1190 } else {
1191 None
1192 }
1193 })
1194 .or_else(|| merged_layout.as_ref().and_then(|l| l.padding()));
1195
1196 let mut widget = if widget_type == "container" {
1197 if children.is_empty() {
1201 quote! {
1202 iced::widget::container(iced::widget::Space::new())
1203 }
1204 } else if children.len() == 1 {
1205 let child = &children[0];
1206 quote! {
1207 {
1208 let content: iced::Element<'_, _, _> = #child;
1209 iced::widget::container(content)
1210 }
1211 }
1212 } else {
1213 quote! {
1215 {
1216 let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1217 iced::widget::container(content)
1218 }
1219 }
1220 }
1221 } else if widget_type == "scrollable" {
1222 if children.is_empty() {
1226 quote! {
1227 iced::widget::scrollable(iced::widget::Space::new())
1228 }
1229 } else if children.len() == 1 {
1230 let child = &children[0];
1231 quote! {
1232 {
1233 let content: iced::Element<'_, _, _> = #child;
1234 iced::widget::scrollable(content)
1235 }
1236 }
1237 } else {
1238 quote! {
1240 {
1241 let content: iced::Element<'_, _, _> = iced::widget::column(vec![#(#children),*]).into();
1242 iced::widget::scrollable(content)
1243 }
1244 }
1245 }
1246 } else {
1247 quote! {
1248 iced::widget::#widget_ident(vec![#(#children),*])
1249 }
1250 };
1251
1252 if let Some(s) = spacing {
1253 widget = quote! { #widget.spacing(#s) };
1254 }
1255
1256 if let Some(p) = padding {
1257 widget = quote! { #widget.padding(#p) };
1258 }
1259
1260 let width_from_attr = node.attributes.get("width").and_then(|attr| {
1262 if let AttributeValue::Static(s) = attr {
1263 Some(s.clone())
1264 } else {
1265 None
1266 }
1267 });
1268 let width_from_layout = merged_layout.as_ref().and_then(|l| l.width());
1269
1270 if let Some(width) = width_from_attr {
1271 let width_expr = generate_length_expr(&width);
1272 widget = quote! { #widget.width(#width_expr) };
1273 } else if let Some(layout_width) = width_from_layout {
1274 let width_expr = generate_layout_length_expr(layout_width);
1275 widget = quote! { #widget.width(#width_expr) };
1276 }
1277
1278 let height_from_attr = node.attributes.get("height").and_then(|attr| {
1280 if let AttributeValue::Static(s) = attr {
1281 Some(s.clone())
1282 } else {
1283 None
1284 }
1285 });
1286 let height_from_layout = merged_layout.as_ref().and_then(|l| l.height());
1287
1288 if let Some(height) = height_from_attr {
1289 let height_expr = generate_length_expr(&height);
1290 widget = quote! { #widget.height(#height_expr) };
1291 } else if let Some(layout_height) = height_from_layout {
1292 let height_expr = generate_layout_length_expr(layout_height);
1293 widget = quote! { #widget.height(#height_expr) };
1294 }
1295
1296 if widget_type == "container" {
1298 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1299 if let AttributeValue::Static(s) = attr {
1300 Some(s.clone())
1301 } else {
1302 None
1303 }
1304 }) {
1305 let align_expr = generate_horizontal_alignment_expr(&align_x);
1306 widget = quote! { #widget.align_x(#align_expr) };
1307 }
1308
1309 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1311 if let AttributeValue::Static(s) = attr {
1312 Some(s.clone())
1313 } else {
1314 None
1315 }
1316 }) {
1317 let align_expr = generate_vertical_alignment_expr(&align_y);
1318 widget = quote! { #widget.align_y(#align_expr) };
1319 }
1320 }
1321
1322 if widget_type == "column" || widget_type == "row" {
1324 if let Some(align) = node.attributes.get("align_items").and_then(|attr| {
1325 if let AttributeValue::Static(s) = attr {
1326 Some(s.clone())
1327 } else {
1328 None
1329 }
1330 }) {
1331 let align_expr = match align.to_lowercase().as_str() {
1332 "center" => quote! { iced::Alignment::Center },
1333 "end" => quote! { iced::Alignment::End },
1334 _ => quote! { iced::Alignment::Start },
1335 };
1336 widget = quote! { #widget.align_items(#align_expr) };
1337 }
1338 }
1339
1340 if widget_type == "container" {
1342 widget = apply_widget_style(widget, node, "container", style_classes)?;
1343 }
1344
1345 if (widget_type == "column" || widget_type == "row")
1349 && (node.attributes.contains_key("align_x") || node.attributes.contains_key("align_y"))
1350 {
1351 let mut container = quote! { iced::widget::container(#widget) };
1352
1353 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1354 if let AttributeValue::Static(s) = attr {
1355 Some(s.clone())
1356 } else {
1357 None
1358 }
1359 }) {
1360 let align_expr = generate_horizontal_alignment_expr(&align_x);
1361 container = quote! { #container.align_x(#align_expr) };
1362 }
1363
1364 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1365 if let AttributeValue::Static(s) = attr {
1366 Some(s.clone())
1367 } else {
1368 None
1369 }
1370 }) {
1371 let align_expr = generate_vertical_alignment_expr(&align_y);
1372 container = quote! { #container.align_y(#align_expr) };
1373 }
1374
1375 container = quote! { #container.width(iced::Length::Fill).height(iced::Length::Fill) };
1377
1378 return Ok(quote! { #container.into() });
1379 }
1380
1381 Ok(quote! { #widget.into() })
1382}
1383
1384fn generate_stack(
1386 node: &crate::WidgetNode,
1387 model_ident: &syn::Ident,
1388 message_ident: &syn::Ident,
1389 style_classes: &HashMap<String, StyleClass>,
1390) -> Result<TokenStream, super::CodegenError> {
1391 let children: Vec<TokenStream> = node
1392 .children
1393 .iter()
1394 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
1395 .collect::<Result<_, _>>()?;
1396
1397 Ok(quote! {
1398 iced::widget::stack(vec![#(#children),*]).into()
1399 })
1400}
1401
1402fn generate_space(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1404 let width = node.attributes.get("width").and_then(|attr| {
1406 if let AttributeValue::Static(s) = attr {
1407 Some(s.clone())
1408 } else {
1409 None
1410 }
1411 });
1412
1413 let height = node.attributes.get("height").and_then(|attr| {
1415 if let AttributeValue::Static(s) = attr {
1416 Some(s.clone())
1417 } else {
1418 None
1419 }
1420 });
1421
1422 let mut space = quote! { iced::widget::Space::new() };
1423
1424 if let Some(w) = width {
1426 let width_expr = generate_length_expr(&w);
1427 space = quote! { #space.width(#width_expr) };
1428 }
1429
1430 if let Some(h) = height {
1432 let height_expr = generate_length_expr(&h);
1433 space = quote! { #space.height(#height_expr) };
1434 }
1435
1436 Ok(quote! { #space.into() })
1437}
1438
1439fn generate_rule(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1441 let direction = node
1443 .attributes
1444 .get("direction")
1445 .and_then(|attr| {
1446 if let AttributeValue::Static(s) = attr {
1447 Some(s.clone())
1448 } else {
1449 None
1450 }
1451 })
1452 .unwrap_or_else(|| "horizontal".to_string());
1453
1454 let thickness = node
1456 .attributes
1457 .get("thickness")
1458 .and_then(|attr| {
1459 if let AttributeValue::Static(s) = attr {
1460 s.parse::<f32>().ok()
1461 } else {
1462 None
1463 }
1464 })
1465 .unwrap_or(1.0);
1466
1467 let rule = if direction.to_lowercase() == "vertical" {
1468 quote! { iced::widget::rule::vertical(#thickness) }
1469 } else {
1470 quote! { iced::widget::rule::horizontal(#thickness) }
1471 };
1472
1473 Ok(quote! { #rule.into() })
1474}
1475
1476fn generate_checkbox(
1478 node: &crate::WidgetNode,
1479 model_ident: &syn::Ident,
1480 message_ident: &syn::Ident,
1481 style_classes: &HashMap<String, StyleClass>,
1482) -> Result<TokenStream, super::CodegenError> {
1483 let label = node
1484 .attributes
1485 .get("label")
1486 .and_then(|attr| {
1487 if let AttributeValue::Static(s) = attr {
1488 Some(s.clone())
1489 } else {
1490 None
1491 }
1492 })
1493 .unwrap_or_default();
1494 let label_lit = proc_macro2::Literal::string(&label);
1495 let label_expr = quote! { #label_lit.to_string() };
1496
1497 let checked_attr = node.attributes.get("checked");
1498 let checked_expr = checked_attr
1499 .map(|attr| generate_attribute_value(attr, model_ident))
1500 .unwrap_or(quote! { false });
1501
1502 let on_toggle = node
1503 .events
1504 .iter()
1505 .find(|e| e.event == crate::EventKind::Toggle);
1506
1507 let checkbox = if let Some(event) = on_toggle {
1508 let variant_name = to_upper_camel_case(&event.handler);
1509 let handler_ident = format_ident!("{}", variant_name);
1510 quote! {
1511 iced::widget::checkbox(#label_expr, #checked_expr)
1512 .on_toggle(#message_ident::#handler_ident)
1513 }
1514 } else {
1515 quote! {
1516 iced::widget::checkbox(#label_expr, #checked_expr)
1517 }
1518 };
1519
1520 let checkbox = apply_widget_style(checkbox, node, "checkbox", style_classes)?;
1522
1523 Ok(quote! { #checkbox.into() })
1524}
1525
1526fn generate_toggler(
1528 node: &crate::WidgetNode,
1529 model_ident: &syn::Ident,
1530 message_ident: &syn::Ident,
1531 style_classes: &HashMap<String, StyleClass>,
1532) -> Result<TokenStream, super::CodegenError> {
1533 let label = node
1534 .attributes
1535 .get("label")
1536 .and_then(|attr| {
1537 if let AttributeValue::Static(s) = attr {
1538 Some(s.clone())
1539 } else {
1540 None
1541 }
1542 })
1543 .unwrap_or_default();
1544 let label_lit = proc_macro2::Literal::string(&label);
1545 let label_expr = quote! { #label_lit.to_string() };
1546
1547 let is_toggled_attr = node.attributes.get("toggled");
1548 let is_toggled_expr = is_toggled_attr
1549 .map(|attr| generate_attribute_value(attr, model_ident))
1550 .unwrap_or(quote! { false });
1551
1552 let on_toggle = node
1553 .events
1554 .iter()
1555 .find(|e| e.event == crate::EventKind::Toggle);
1556
1557 let toggler = if let Some(event) = on_toggle {
1558 let variant_name = to_upper_camel_case(&event.handler);
1559 let handler_ident = format_ident!("{}", variant_name);
1560 quote! {
1561 iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1562 .on_toggle(|_| #message_ident::#handler_ident)
1563 }
1564 } else {
1565 quote! {
1566 iced::widget::toggler(#label_expr, #is_toggled_expr, None)
1567 }
1568 };
1569
1570 let toggler = apply_widget_style(toggler, node, "toggler", style_classes)?;
1572
1573 Ok(quote! { #toggler.into() })
1574}
1575
1576fn generate_slider(
1578 node: &crate::WidgetNode,
1579 model_ident: &syn::Ident,
1580 message_ident: &syn::Ident,
1581 style_classes: &HashMap<String, StyleClass>,
1582) -> Result<TokenStream, super::CodegenError> {
1583 let min = node.attributes.get("min").and_then(|attr| {
1584 if let AttributeValue::Static(s) = attr {
1585 s.parse::<f32>().ok()
1586 } else {
1587 None
1588 }
1589 });
1590
1591 let max = node.attributes.get("max").and_then(|attr| {
1592 if let AttributeValue::Static(s) = attr {
1593 s.parse::<f32>().ok()
1594 } else {
1595 None
1596 }
1597 });
1598
1599 let value_attr = node.attributes.get("value").ok_or_else(|| {
1600 super::CodegenError::InvalidWidget("slider requires value attribute".to_string())
1601 })?;
1602 let value_expr = generate_attribute_value(value_attr, model_ident);
1603
1604 let on_change = node
1605 .events
1606 .iter()
1607 .find(|e| e.event == crate::EventKind::Change);
1608
1609 let mut slider = quote! {
1610 iced::widget::slider(0.0..=100.0, #value_expr, |v| {})
1611 };
1612
1613 if let Some(m) = min {
1614 slider = quote! { #slider.min(#m) };
1615 }
1616 if let Some(m) = max {
1617 slider = quote! { #slider.max(#m) };
1618 }
1619
1620 let step = node.attributes.get("step").and_then(|attr| {
1622 if let AttributeValue::Static(s) = attr {
1623 s.parse::<f32>().ok()
1624 } else {
1625 None
1626 }
1627 });
1628
1629 if let Some(s) = step {
1630 slider = quote! { #slider.step(#s) };
1631 }
1632
1633 if let Some(event) = on_change {
1634 let variant_name = to_upper_camel_case(&event.handler);
1635 let handler_ident = format_ident!("{}", variant_name);
1636 slider = quote! {
1637 iced::widget::slider(0.0..=100.0, #value_expr, |v| #message_ident::#handler_ident(v))
1638 };
1639 }
1640
1641 slider = apply_widget_style(slider, node, "slider", style_classes)?;
1643
1644 Ok(quote! { #slider.into() })
1645}
1646
1647fn generate_radio(
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 label = node
1655 .attributes
1656 .get("label")
1657 .and_then(|attr| {
1658 if let AttributeValue::Static(s) = attr {
1659 Some(s.clone())
1660 } else {
1661 None
1662 }
1663 })
1664 .unwrap_or_default();
1665 let label_lit = proc_macro2::Literal::string(&label);
1666 let label_expr = quote! { #label_lit.to_string() };
1667
1668 let value_attr = node.attributes.get("value").ok_or_else(|| {
1669 super::CodegenError::InvalidWidget("radio requires value attribute".to_string())
1670 })?;
1671 let value_expr = match value_attr {
1672 AttributeValue::Binding(expr) => generate_expr(&expr.expr),
1673 _ => quote! { String::new() },
1674 };
1675
1676 let selected_attr = node.attributes.get("selected");
1677 let selected_expr = match selected_attr {
1678 Some(AttributeValue::Binding(expr)) => generate_expr(&expr.expr),
1679 _ => quote! { None },
1680 };
1681
1682 let on_select = node
1683 .events
1684 .iter()
1685 .find(|e| e.event == crate::EventKind::Select);
1686
1687 if let Some(event) = on_select {
1688 let variant_name = to_upper_camel_case(&event.handler);
1689 let handler_ident = format_ident!("{}", variant_name);
1690 Ok(quote! {
1691 iced::widget::radio(#label_expr, #value_expr, #selected_expr, |v| #message_ident::#handler_ident(v)).into()
1692 })
1693 } else {
1694 Ok(quote! {
1695 iced::widget::radio(#label_expr, #value_expr, #selected_expr, |_| ()).into()
1696 })
1697 }
1698}
1699
1700fn generate_progress_bar(
1702 node: &crate::WidgetNode,
1703 model_ident: &syn::Ident,
1704 _style_classes: &HashMap<String, StyleClass>,
1705) -> Result<TokenStream, super::CodegenError> {
1706 let value_attr = node.attributes.get("value").ok_or_else(|| {
1707 super::CodegenError::InvalidWidget("progress_bar requires value attribute".to_string())
1708 })?;
1709 let value_expr = generate_attribute_value(value_attr, model_ident);
1710
1711 let max_attr = node.attributes.get("max").and_then(|attr| {
1712 if let AttributeValue::Static(s) = attr {
1713 s.parse::<f32>().ok()
1714 } else {
1715 None
1716 }
1717 });
1718
1719 if let Some(max) = max_attr {
1720 Ok(quote! {
1721 iced::widget::progress_bar(0.0..=#max, #value_expr).into()
1722 })
1723 } else {
1724 Ok(quote! {
1725 iced::widget::progress_bar(0.0..=100.0, #value_expr).into()
1726 })
1727 }
1728}
1729
1730fn generate_text_input(
1732 node: &crate::WidgetNode,
1733 model_ident: &syn::Ident,
1734 message_ident: &syn::Ident,
1735 style_classes: &HashMap<String, StyleClass>,
1736) -> Result<TokenStream, super::CodegenError> {
1737 let value_expr = node
1738 .attributes
1739 .get("value")
1740 .map(|attr| generate_attribute_value(attr, model_ident))
1741 .unwrap_or(quote! { String::new() });
1742
1743 let placeholder = node.attributes.get("placeholder").and_then(|attr| {
1744 if let AttributeValue::Static(s) = attr {
1745 Some(s.clone())
1746 } else {
1747 None
1748 }
1749 });
1750
1751 let on_input = node
1752 .events
1753 .iter()
1754 .find(|e| e.event == crate::EventKind::Input);
1755
1756 let mut text_input = match placeholder {
1757 Some(ph) => {
1758 let ph_lit = proc_macro2::Literal::string(&ph);
1759 quote! {
1760 iced::widget::text_input(#ph_lit, &#value_expr)
1761 }
1762 }
1763 None => quote! {
1764 iced::widget::text_input("", &#value_expr)
1765 },
1766 };
1767
1768 if let Some(event) = on_input {
1769 let variant_name = to_upper_camel_case(&event.handler);
1770 let handler_ident = format_ident!("{}", variant_name);
1771 text_input = quote! {
1772 #text_input.on_input(|v| #message_ident::#handler_ident(v))
1773 };
1774 }
1775
1776 let is_password = node
1778 .attributes
1779 .get("password")
1780 .or_else(|| node.attributes.get("secure"))
1781 .and_then(|attr| {
1782 if let AttributeValue::Static(s) = attr {
1783 Some(s.to_lowercase() == "true" || s == "1")
1784 } else {
1785 None
1786 }
1787 })
1788 .unwrap_or(false);
1789
1790 if is_password {
1791 text_input = quote! { #text_input.password() };
1792 }
1793
1794 text_input = apply_widget_style(text_input, node, "text_input", style_classes)?;
1796
1797 Ok(quote! { #text_input.into() })
1798}
1799
1800fn generate_image(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1802 let src_attr = node.attributes.get("src").ok_or_else(|| {
1803 super::CodegenError::InvalidWidget("image requires src attribute".to_string())
1804 })?;
1805
1806 let src = match src_attr {
1807 AttributeValue::Static(s) => s.clone(),
1808 _ => String::new(),
1809 };
1810 let src_lit = proc_macro2::Literal::string(&src);
1811
1812 let width = node.attributes.get("width").and_then(|attr| {
1813 if let AttributeValue::Static(s) = attr {
1814 s.parse::<u32>().ok()
1815 } else {
1816 None
1817 }
1818 });
1819
1820 let height = node.attributes.get("height").and_then(|attr| {
1821 if let AttributeValue::Static(s) = attr {
1822 s.parse::<u32>().ok()
1823 } else {
1824 None
1825 }
1826 });
1827
1828 let mut image = quote! {
1829 iced::widget::image::Image::new(iced::widget::image::Handle::from_memory(std::fs::read(#src_lit).unwrap_or_default()))
1830 };
1831
1832 if let (Some(w), Some(h)) = (width, height) {
1834 image = quote! { #image.width(#w).height(#h) };
1835 } else if let Some(w) = width {
1836 image = quote! { #image.width(#w) };
1837 } else if let Some(h) = height {
1838 image = quote! { #image.height(#h) };
1839 }
1840
1841 let needs_container = !node.classes.is_empty()
1845 || node.attributes.contains_key("align_x")
1846 || node.attributes.contains_key("align_y")
1847 || node.attributes.contains_key("padding");
1848
1849 if needs_container {
1850 let mut container = quote! { iced::widget::container(#image) };
1852
1853 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1854 if let AttributeValue::Static(s) = attr {
1855 s.parse::<f32>().ok()
1856 } else {
1857 None
1858 }
1859 }) {
1860 container = quote! { #container.padding(#padding) };
1861 }
1862
1863 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1864 if let AttributeValue::Static(s) = attr {
1865 Some(s.clone())
1866 } else {
1867 None
1868 }
1869 }) {
1870 let align_expr = generate_horizontal_alignment_expr(&align_x);
1871 container = quote! { #container.align_x(#align_expr) };
1872 }
1873
1874 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1875 if let AttributeValue::Static(s) = attr {
1876 Some(s.clone())
1877 } else {
1878 None
1879 }
1880 }) {
1881 let align_expr = generate_vertical_alignment_expr(&align_y);
1882 container = quote! { #container.align_y(#align_expr) };
1883 }
1884
1885 if let Some(class_name) = node.classes.first() {
1886 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1887 container = quote! { #container.style(#style_fn_ident) };
1888 }
1889
1890 Ok(quote! { #container.into() })
1891 } else {
1892 Ok(quote! { #image.into() })
1893 }
1894}
1895
1896fn generate_svg(node: &crate::WidgetNode) -> Result<TokenStream, super::CodegenError> {
1898 let path_attr = node
1900 .attributes
1901 .get("src")
1902 .or_else(|| node.attributes.get("path"))
1903 .ok_or_else(|| {
1904 super::CodegenError::InvalidWidget("svg requires src attribute".to_string())
1905 })?;
1906
1907 let path = match path_attr {
1908 AttributeValue::Static(s) => s.clone(),
1909 _ => String::new(),
1910 };
1911 let path_lit = proc_macro2::Literal::string(&path);
1912
1913 let width = node.attributes.get("width").and_then(|attr| {
1914 if let AttributeValue::Static(s) = attr {
1915 s.parse::<u32>().ok()
1916 } else {
1917 None
1918 }
1919 });
1920
1921 let height = node.attributes.get("height").and_then(|attr| {
1922 if let AttributeValue::Static(s) = attr {
1923 s.parse::<u32>().ok()
1924 } else {
1925 None
1926 }
1927 });
1928
1929 let mut svg = quote! {
1930 iced::widget::svg::Svg::new(iced::widget::svg::Handle::from_path(#path_lit))
1931 };
1932
1933 if let (Some(w), Some(h)) = (width, height) {
1935 svg = quote! { #svg.width(#w).height(#h) };
1936 } else if let Some(w) = width {
1937 svg = quote! { #svg.width(#w) };
1938 } else if let Some(h) = height {
1939 svg = quote! { #svg.height(#h) };
1940 }
1941
1942 let needs_container = !node.classes.is_empty()
1946 || node.attributes.contains_key("align_x")
1947 || node.attributes.contains_key("align_y")
1948 || node.attributes.contains_key("padding");
1949
1950 if needs_container {
1951 let mut container = quote! { iced::widget::container(#svg) };
1953
1954 if let Some(padding) = node.attributes.get("padding").and_then(|attr| {
1955 if let AttributeValue::Static(s) = attr {
1956 s.parse::<f32>().ok()
1957 } else {
1958 None
1959 }
1960 }) {
1961 container = quote! { #container.padding(#padding) };
1962 }
1963
1964 if let Some(align_x) = node.attributes.get("align_x").and_then(|attr| {
1965 if let AttributeValue::Static(s) = attr {
1966 Some(s.clone())
1967 } else {
1968 None
1969 }
1970 }) {
1971 let align_expr = generate_horizontal_alignment_expr(&align_x);
1972 container = quote! { #container.align_x(#align_expr) };
1973 }
1974
1975 if let Some(align_y) = node.attributes.get("align_y").and_then(|attr| {
1976 if let AttributeValue::Static(s) = attr {
1977 Some(s.clone())
1978 } else {
1979 None
1980 }
1981 }) {
1982 let align_expr = generate_vertical_alignment_expr(&align_y);
1983 container = quote! { #container.align_y(#align_expr) };
1984 }
1985
1986 if let Some(class_name) = node.classes.first() {
1987 let style_fn_ident = format_ident!("style_{}", class_name.replace('-', "_"));
1988 container = quote! { #container.style(#style_fn_ident) };
1989 }
1990
1991 Ok(quote! { #container.into() })
1992 } else {
1993 Ok(quote! { #svg.into() })
1994 }
1995}
1996
1997fn generate_pick_list(
1999 node: &crate::WidgetNode,
2000 model_ident: &syn::Ident,
2001 message_ident: &syn::Ident,
2002 _style_classes: &HashMap<String, StyleClass>,
2003) -> Result<TokenStream, super::CodegenError> {
2004 let options_attr = node.attributes.get("options").ok_or_else(|| {
2005 super::CodegenError::InvalidWidget("pick_list requires options attribute".to_string())
2006 })?;
2007
2008 let options: Vec<String> = match options_attr {
2009 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2010 _ => Vec::new(),
2011 };
2012 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2013
2014 let selected_attr = node.attributes.get("selected");
2015 let selected_expr = selected_attr
2016 .map(|attr| generate_attribute_value(attr, model_ident))
2017 .unwrap_or(quote! { None });
2018
2019 let on_select = node
2020 .events
2021 .iter()
2022 .find(|e| e.event == crate::EventKind::Select);
2023
2024 if let Some(event) = on_select {
2025 let variant_name = to_upper_camel_case(&event.handler);
2026 let handler_ident = format_ident!("{}", variant_name);
2027 Ok(quote! {
2028 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |v| #message_ident::#handler_ident(v)).into()
2029 })
2030 } else {
2031 Ok(quote! {
2032 iced::widget::pick_list(&[#(#options_ref),*], #selected_expr, |_| ()).into()
2033 })
2034 }
2035}
2036
2037fn generate_combo_box(
2039 node: &crate::WidgetNode,
2040 model_ident: &syn::Ident,
2041 message_ident: &syn::Ident,
2042 _style_classes: &HashMap<String, StyleClass>,
2043) -> Result<TokenStream, super::CodegenError> {
2044 let options_attr = node.attributes.get("options").ok_or_else(|| {
2045 super::CodegenError::InvalidWidget("combobox requires options attribute".to_string())
2046 })?;
2047
2048 let options: Vec<String> = match options_attr {
2049 AttributeValue::Static(s) => s.split(',').map(|s| s.trim().to_string()).collect(),
2050 _ => Vec::new(),
2051 };
2052 let options_ref: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
2053
2054 let selected_attr = node.attributes.get("selected");
2055 let selected_expr = selected_attr
2056 .map(|attr| generate_attribute_value(attr, model_ident))
2057 .unwrap_or(quote! { None });
2058
2059 let on_select = node
2060 .events
2061 .iter()
2062 .find(|e| e.event == crate::EventKind::Select);
2063
2064 if let Some(event) = on_select {
2065 let variant_name = to_upper_camel_case(&event.handler);
2066 let handler_ident = format_ident!("{}", variant_name);
2067 Ok(quote! {
2068 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |v, _| #message_ident::#handler_ident(v)).into()
2069 })
2070 } else {
2071 Ok(quote! {
2072 iced::widget::combo_box(&[#(#options_ref),*], "", #selected_expr, |_, _| ()).into()
2073 })
2074 }
2075}
2076
2077fn generate_tooltip(
2079 node: &crate::WidgetNode,
2080 model_ident: &syn::Ident,
2081 message_ident: &syn::Ident,
2082 style_classes: &HashMap<String, StyleClass>,
2083) -> Result<TokenStream, super::CodegenError> {
2084 let child = node.children.first().ok_or_else(|| {
2085 super::CodegenError::InvalidWidget("tooltip must have exactly one child".to_string())
2086 })?;
2087 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2088
2089 let message_attr = node.attributes.get("message").ok_or_else(|| {
2090 super::CodegenError::InvalidWidget("tooltip requires message attribute".to_string())
2091 })?;
2092 let message_expr = generate_attribute_value(message_attr, model_ident);
2093
2094 Ok(quote! {
2095 iced::widget::tooltip(#child_widget, #message_expr, iced::widget::tooltip::Position::FollowCursor).into()
2096 })
2097}
2098
2099fn generate_grid(
2101 node: &crate::WidgetNode,
2102 model_ident: &syn::Ident,
2103 message_ident: &syn::Ident,
2104 style_classes: &HashMap<String, StyleClass>,
2105) -> Result<TokenStream, super::CodegenError> {
2106 let children: Vec<TokenStream> = node
2107 .children
2108 .iter()
2109 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2110 .collect::<Result<_, _>>()?;
2111
2112 let columns = node
2113 .attributes
2114 .get("columns")
2115 .and_then(|attr| {
2116 if let AttributeValue::Static(s) = attr {
2117 s.parse::<u32>().ok()
2118 } else {
2119 None
2120 }
2121 })
2122 .unwrap_or(1);
2123
2124 let spacing = node.attributes.get("spacing").and_then(|attr| {
2125 if let AttributeValue::Static(s) = attr {
2126 s.parse::<f32>().ok()
2127 } else {
2128 None
2129 }
2130 });
2131
2132 let padding = node.attributes.get("padding").and_then(|attr| {
2133 if let AttributeValue::Static(s) = attr {
2134 s.parse::<f32>().ok()
2135 } else {
2136 None
2137 }
2138 });
2139
2140 let grid = quote! {
2141 iced::widget::grid::Grid::new_with_children(vec![#(#children),*], #columns)
2142 };
2143
2144 let grid = if let Some(s) = spacing {
2145 quote! { #grid.spacing(#s) }
2146 } else {
2147 grid
2148 };
2149
2150 let grid = if let Some(p) = padding {
2151 quote! { #grid.padding(#p) }
2152 } else {
2153 grid
2154 };
2155
2156 Ok(quote! { #grid.into() })
2157}
2158
2159fn generate_canvas(
2161 node: &crate::WidgetNode,
2162 _model_ident: &syn::Ident,
2163 _message_ident: &syn::Ident,
2164 _style_classes: &HashMap<String, StyleClass>,
2165) -> Result<TokenStream, super::CodegenError> {
2166 let width = node.attributes.get("width").and_then(|attr| {
2167 if let AttributeValue::Static(s) = attr {
2168 s.parse::<f32>().ok()
2169 } else {
2170 None
2171 }
2172 });
2173
2174 let height = node.attributes.get("height").and_then(|attr| {
2175 if let AttributeValue::Static(s) = attr {
2176 s.parse::<f32>().ok()
2177 } else {
2178 None
2179 }
2180 });
2181
2182 let size = match (width, height) {
2183 (Some(w), Some(h)) => quote! { iced::Size::new(#w, #h) },
2184 (Some(w), None) => quote! { iced::Size::new(#w, 100.0) },
2185 (None, Some(h)) => quote! { iced::Size::new(100.0, #h) },
2186 _ => quote! { iced::Size::new(100.0, 100.0) },
2187 };
2188
2189 Ok(quote! {
2190 iced::widget::canvas(#size).into()
2191 })
2192}
2193
2194fn generate_float(
2196 node: &crate::WidgetNode,
2197 model_ident: &syn::Ident,
2198 message_ident: &syn::Ident,
2199 style_classes: &HashMap<String, StyleClass>,
2200) -> Result<TokenStream, super::CodegenError> {
2201 let child = node.children.first().ok_or_else(|| {
2202 super::CodegenError::InvalidWidget("float must have exactly one child".to_string())
2203 })?;
2204 let child_widget = generate_widget(child, model_ident, message_ident, style_classes)?;
2205
2206 let position = node
2207 .attributes
2208 .get("position")
2209 .and_then(|attr| {
2210 if let AttributeValue::Static(s) = attr {
2211 Some(s.clone())
2212 } else {
2213 None
2214 }
2215 })
2216 .unwrap_or_else(|| "TopRight".to_string());
2217
2218 let offset_x = node.attributes.get("offset_x").and_then(|attr| {
2219 if let AttributeValue::Static(s) = attr {
2220 s.parse::<f32>().ok()
2221 } else {
2222 None
2223 }
2224 });
2225
2226 let offset_y = node.attributes.get("offset_y").and_then(|attr| {
2227 if let AttributeValue::Static(s) = attr {
2228 s.parse::<f32>().ok()
2229 } else {
2230 None
2231 }
2232 });
2233
2234 let float = match position.as_str() {
2235 "TopLeft" => quote! { iced::widget::float::float_top_left(#child_widget) },
2236 "TopRight" => quote! { iced::widget::float::float_top_right(#child_widget) },
2237 "BottomLeft" => quote! { iced::widget::float::float_bottom_left(#child_widget) },
2238 "BottomRight" => quote! { iced::widget::float::float_bottom_right(#child_widget) },
2239 _ => quote! { iced::widget::float::float_top_right(#child_widget) },
2240 };
2241
2242 let float = if let (Some(ox), Some(oy)) = (offset_x, offset_y) {
2243 quote! { #float.offset_x(#ox).offset_y(#oy) }
2244 } else if let Some(ox) = offset_x {
2245 quote! { #float.offset_x(#ox) }
2246 } else if let Some(oy) = offset_y {
2247 quote! { #float.offset_y(#oy) }
2248 } else {
2249 float
2250 };
2251
2252 Ok(quote! { #float.into() })
2253}
2254
2255fn generate_for(
2257 node: &crate::WidgetNode,
2258 model_ident: &syn::Ident,
2259 message_ident: &syn::Ident,
2260 style_classes: &HashMap<String, StyleClass>,
2261) -> Result<TokenStream, super::CodegenError> {
2262 let items_attr = node.attributes.get("items").ok_or_else(|| {
2263 super::CodegenError::InvalidWidget("for requires items attribute".to_string())
2264 })?;
2265
2266 let item_name = node
2267 .attributes
2268 .get("item")
2269 .and_then(|attr| {
2270 if let AttributeValue::Static(s) = attr {
2271 Some(s.clone())
2272 } else {
2273 None
2274 }
2275 })
2276 .unwrap_or_else(|| "item".to_string());
2277
2278 let _children: Vec<TokenStream> = node
2279 .children
2280 .iter()
2281 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2282 .collect::<Result<_, _>>()?;
2283
2284 let _items_expr = generate_attribute_value(items_attr, model_ident);
2285 let _item_ident = format_ident!("{}", item_name);
2286
2287 Ok(quote! {
2288 {
2289 let _items = _items_expr;
2290 iced::widget::column(vec![]).into()
2291 }
2292 })
2293}
2294
2295fn generate_custom_widget(
2297 node: &crate::WidgetNode,
2298 name: &str,
2299 model_ident: &syn::Ident,
2300 message_ident: &syn::Ident,
2301 style_classes: &HashMap<String, StyleClass>,
2302) -> Result<TokenStream, super::CodegenError> {
2303 let widget_ident = format_ident!("{}", name);
2304 let children: Vec<TokenStream> = node
2305 .children
2306 .iter()
2307 .map(|child| generate_widget(child, model_ident, message_ident, style_classes))
2308 .collect::<Result<_, _>>()?;
2309
2310 Ok(quote! {
2311 #widget_ident(vec![#(#children),*]).into()
2312 })
2313}
2314
2315fn generate_attribute_value(attr: &AttributeValue, _model_ident: &syn::Ident) -> TokenStream {
2317 match attr {
2318 AttributeValue::Static(s) => {
2319 let lit = proc_macro2::Literal::string(s);
2320 quote! { #lit.to_string() }
2321 }
2322 AttributeValue::Binding(expr) => generate_expr(&expr.expr),
2323 AttributeValue::Interpolated(parts) => {
2324 let parts_str: Vec<String> = parts
2325 .iter()
2326 .map(|part| match part {
2327 InterpolatedPart::Literal(s) => s.clone(),
2328 InterpolatedPart::Binding(_) => "{}".to_string(),
2329 })
2330 .collect();
2331 let binding_exprs: Vec<TokenStream> = parts
2332 .iter()
2333 .filter_map(|part| {
2334 if let InterpolatedPart::Binding(expr) = part {
2335 Some(generate_expr(&expr.expr))
2336 } else {
2337 None
2338 }
2339 })
2340 .collect();
2341
2342 let format_string = parts_str.join("");
2343 let lit = proc_macro2::Literal::string(&format_string);
2344
2345 quote! { format!(#lit, #(#binding_exprs),*) }
2346 }
2347 }
2348}
2349
2350#[cfg(test)]
2351mod tests {
2352 use super::*;
2353 use crate::parse;
2354
2355 #[test]
2356 fn test_view_generation() {
2357 let xml = r#"<column><text value="Hello" /></column>"#;
2358 let doc = parse(xml).unwrap();
2359
2360 let result = generate_view(&doc, "Model", "Message").unwrap();
2361 let code = result.to_string();
2362
2363 assert!(code.contains("text"));
2364 assert!(code.contains("column"));
2365 }
2366
2367 #[test]
2368 fn test_view_generation_with_binding() {
2369 let xml = r#"<column><text value="{name}" /></column>"#;
2370 let doc = parse(xml).unwrap();
2371
2372 let result = generate_view(&doc, "Model", "Message").unwrap();
2373 let code = result.to_string();
2374
2375 assert!(code.contains("name"));
2376 assert!(code.contains("to_string"));
2377 }
2378
2379 #[test]
2380 fn test_button_with_handler() {
2381 let xml = r#"<column><button label="Click" on_click="handle_click" /></column>"#;
2382 let doc = parse(xml).unwrap();
2383
2384 let result = generate_view(&doc, "Model", "Message").unwrap();
2385 let code = result.to_string();
2386
2387 assert!(code.contains("button"));
2388 assert!(code.contains("HandleClick"));
2389 }
2390
2391 #[test]
2392 fn test_container_with_children() {
2393 let xml = r#"<column spacing="10"><text value="A" /><text value="B" /></column>"#;
2394 let doc = parse(xml).unwrap();
2395
2396 let result = generate_view(&doc, "Model", "Message").unwrap();
2397 let code = result.to_string();
2398
2399 assert!(code.contains("column"));
2400 assert!(code.contains("spacing"));
2401 }
2402
2403 #[test]
2404 fn test_button_with_inline_style() {
2405 use crate::ir::node::WidgetNode;
2406 use crate::ir::style::{Background, Color, StyleProperties};
2407 use std::collections::HashMap;
2408
2409 let button_node = WidgetNode {
2411 kind: WidgetKind::Button,
2412 id: None,
2413 attributes: {
2414 let mut attrs = HashMap::new();
2415 attrs.insert(
2416 "label".to_string(),
2417 AttributeValue::Static("Test".to_string()),
2418 );
2419 attrs
2420 },
2421 events: vec![],
2422 children: vec![],
2423 span: Default::default(),
2424 style: Some(StyleProperties {
2425 background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
2426 color: Some(Color::from_rgb8(255, 255, 255)),
2427 border: None,
2428 shadow: None,
2429 opacity: None,
2430 transform: None,
2431 }),
2432 layout: None,
2433 theme_ref: None,
2434 classes: vec![],
2435 breakpoint_attributes: HashMap::new(),
2436 inline_state_variants: HashMap::new(),
2437 };
2438
2439 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2440 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2441 let style_classes = HashMap::new();
2442
2443 let result =
2444 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
2445 let code = result.to_string();
2446
2447 assert!(code.contains("style"));
2449 assert!(code.contains("button :: Status"));
2450 assert!(code.contains("button :: Style"));
2451 assert!(code.contains("background"));
2452 assert!(code.contains("text_color"));
2453 }
2454
2455 #[test]
2456 fn test_button_with_css_class() {
2457 use crate::ir::node::WidgetNode;
2458 use crate::ir::theme::StyleClass;
2459 use std::collections::HashMap;
2460
2461 let button_node = WidgetNode {
2463 kind: WidgetKind::Button,
2464 id: None,
2465 attributes: {
2466 let mut attrs = HashMap::new();
2467 attrs.insert(
2468 "label".to_string(),
2469 AttributeValue::Static("Test".to_string()),
2470 );
2471 attrs
2472 },
2473 events: vec![],
2474 children: vec![],
2475 span: Default::default(),
2476 style: None,
2477 layout: None,
2478 theme_ref: None,
2479 classes: vec!["primary-button".to_string()],
2480 breakpoint_attributes: HashMap::new(),
2481 inline_state_variants: HashMap::new(),
2482 };
2483
2484 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2485 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2486 let style_classes: HashMap<String, StyleClass> = HashMap::new();
2487
2488 let result =
2489 generate_button(&button_node, &model_ident, &message_ident, &style_classes).unwrap();
2490 let code = result.to_string();
2491
2492 assert!(code.contains("style"));
2494 assert!(code.contains("style_primary_button"));
2495 }
2496
2497 #[test]
2498 fn test_container_with_inline_style() {
2499 use crate::ir::node::WidgetNode;
2500 use crate::ir::style::{
2501 Background, Border, BorderRadius, BorderStyle, Color, StyleProperties,
2502 };
2503 use crate::ir::theme::StyleClass;
2504 use std::collections::HashMap;
2505
2506 let container_node = WidgetNode {
2507 kind: WidgetKind::Container,
2508 id: None,
2509 attributes: HashMap::new(),
2510 events: vec![],
2511 children: vec![],
2512 span: Default::default(),
2513 style: Some(StyleProperties {
2514 background: Some(Background::Color(Color::from_rgb8(240, 240, 240))),
2515 color: None,
2516 border: Some(Border {
2517 width: 2.0,
2518 color: Color::from_rgb8(200, 200, 200),
2519 radius: BorderRadius {
2520 top_left: 8.0,
2521 top_right: 8.0,
2522 bottom_right: 8.0,
2523 bottom_left: 8.0,
2524 },
2525 style: BorderStyle::Solid,
2526 }),
2527 shadow: None,
2528 opacity: None,
2529 transform: None,
2530 }),
2531 layout: None,
2532 theme_ref: None,
2533 classes: vec![],
2534 breakpoint_attributes: HashMap::new(),
2535 inline_state_variants: HashMap::new(),
2536 };
2537
2538 let model_ident = syn::Ident::new("model", proc_macro2::Span::call_site());
2539 let message_ident = syn::Ident::new("Message", proc_macro2::Span::call_site());
2540 let style_classes: HashMap<String, StyleClass> = HashMap::new();
2541
2542 let result = generate_container(
2543 &container_node,
2544 "container",
2545 &model_ident,
2546 &message_ident,
2547 &style_classes,
2548 )
2549 .unwrap();
2550 let code = result.to_string();
2551
2552 assert!(code.contains("style"));
2554 assert!(code.contains("container :: Style"));
2555 assert!(code.contains("background"));
2556 assert!(code.contains("border"));
2557 }
2558}