1use serde::{Deserialize, Serialize};
35
36use crate::{Element, ElementKind, ImageFormat, Transform};
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct A2UITree {
44 pub root: A2UINode,
46 #[serde(default)]
48 pub data_model: serde_json::Value,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
53pub struct A2UIStyle {
54 #[serde(default)]
56 pub font_size: Option<f32>,
57 #[serde(default)]
59 pub color: Option<String>,
60 #[serde(default)]
62 pub background: Option<String>,
63 #[serde(default)]
65 pub width: Option<f32>,
66 #[serde(default)]
68 pub height: Option<f32>,
69 #[serde(default)]
71 pub padding: Option<f32>,
72 #[serde(default)]
74 pub margin: Option<f32>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81#[serde(tag = "component", rename_all = "snake_case")]
82pub enum A2UINode {
83 Container {
85 children: Vec<A2UINode>,
87 #[serde(default = "default_layout")]
89 layout: String,
90 #[serde(default)]
92 style: Option<A2UIStyle>,
93 },
94
95 Text {
97 content: String,
99 #[serde(default)]
101 style: Option<A2UIStyle>,
102 },
103
104 Image {
106 src: String,
108 #[serde(default)]
110 alt: Option<String>,
111 #[serde(default)]
113 style: Option<A2UIStyle>,
114 },
115
116 Button {
118 label: String,
120 action: String,
122 #[serde(default)]
124 style: Option<A2UIStyle>,
125 },
126
127 Chart {
129 chart_type: String,
131 data: serde_json::Value,
133 #[serde(default)]
135 style: Option<A2UIStyle>,
136 },
137
138 VideoFeed {
140 stream_id: String,
142 #[serde(default)]
144 mirror: bool,
145 #[serde(default)]
147 style: Option<A2UIStyle>,
148 },
149}
150
151fn default_layout() -> String {
152 "vertical".to_string()
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
157pub enum Layout {
158 #[default]
160 Column,
161 Row,
163 Grid {
165 columns: u32,
167 },
168 Stack,
170}
171
172impl Layout {
173 #[must_use]
181 pub fn parse(s: &str) -> Self {
182 let lower = s.to_lowercase();
183 if lower == "horizontal" || lower == "row" {
184 Self::Row
185 } else if lower.starts_with("grid") {
186 let columns = lower
188 .trim_start_matches("grid")
189 .trim_start_matches(':')
190 .trim_start_matches('-')
191 .parse()
192 .unwrap_or(2);
193 Self::Grid { columns }
194 } else if lower == "stack" || lower == "overlay" {
195 Self::Stack
196 } else {
197 Self::Column
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct ConversionResult {
206 pub elements: Vec<Element>,
208 pub warnings: Vec<String>,
210}
211
212impl A2UITree {
213 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
219 serde_json::from_str(json)
220 }
221
222 #[must_use]
227 pub fn to_elements(&self) -> ConversionResult {
228 let mut converter = A2UIConverter::new();
229 let elements = converter.convert_node(&self.root, 0.0, 0.0);
230 ConversionResult {
231 elements,
232 warnings: converter.warnings,
233 }
234 }
235}
236
237struct A2UIConverter {
239 warnings: Vec<String>,
241 z_index: i32,
243}
244
245impl A2UIConverter {
246 fn new() -> Self {
247 Self {
248 warnings: Vec::new(),
249 z_index: 0,
250 }
251 }
252
253 fn next_z_index(&mut self) -> i32 {
254 let z = self.z_index;
255 self.z_index += 1;
256 z
257 }
258
259 fn convert_node(&mut self, node: &A2UINode, x: f32, y: f32) -> Vec<Element> {
260 match node {
261 A2UINode::Container {
262 children,
263 layout,
264 style,
265 } => self.convert_container(children, layout, style.as_ref(), x, y),
266
267 A2UINode::Text { content, style } => {
268 vec![self.convert_text(content, style.as_ref(), x, y)]
269 }
270
271 A2UINode::Image { src, style, .. } => {
272 vec![self.convert_image(src, style.as_ref(), x, y)]
273 }
274
275 A2UINode::Button { label, style, .. } => {
276 let mut element = self.convert_text(label, style.as_ref(), x, y);
278 element.interactive = true;
279 vec![element]
280 }
281
282 A2UINode::Chart {
283 chart_type,
284 data,
285 style,
286 } => {
287 vec![self.convert_chart(chart_type, data, style.as_ref(), x, y)]
288 }
289
290 A2UINode::VideoFeed {
291 stream_id,
292 mirror,
293 style,
294 } => {
295 vec![self.convert_video(stream_id, *mirror, style.as_ref(), x, y)]
296 }
297 }
298 }
299
300 fn convert_container(
301 &mut self,
302 children: &[A2UINode],
303 layout_str: &str,
304 style: Option<&A2UIStyle>,
305 start_x: f32,
306 start_y: f32,
307 ) -> Vec<Element> {
308 let layout = Layout::parse(layout_str);
309 let padding = style.and_then(|s| s.padding).unwrap_or(10.0);
310 let margin = style.and_then(|s| s.margin).unwrap_or(0.0);
311 let spacing = 10.0; let base_x = start_x + padding + margin;
314 let base_y = start_y + padding + margin;
315
316 match layout {
317 Layout::Column => self.layout_column(children, base_x, base_y, spacing),
318 Layout::Row => self.layout_row(children, base_x, base_y, spacing),
319 Layout::Grid { columns } => {
320 self.layout_grid(children, base_x, base_y, spacing, columns)
321 }
322 Layout::Stack => self.layout_stack(children, base_x, base_y),
323 }
324 }
325
326 fn layout_column(
328 &mut self,
329 children: &[A2UINode],
330 start_x: f32,
331 start_y: f32,
332 spacing: f32,
333 ) -> Vec<Element> {
334 let mut elements = Vec::new();
335 let mut current_y = start_y;
336
337 for child in children {
338 let child_elements = self.convert_node(child, start_x, current_y);
339 let (_, child_height) = Self::calculate_bounds(&child_elements);
340 elements.extend(child_elements);
341 current_y += child_height + spacing;
342 }
343
344 elements
345 }
346
347 fn layout_row(
349 &mut self,
350 children: &[A2UINode],
351 start_x: f32,
352 start_y: f32,
353 spacing: f32,
354 ) -> Vec<Element> {
355 let mut elements = Vec::new();
356 let mut current_x = start_x;
357
358 for child in children {
359 let child_elements = self.convert_node(child, current_x, start_y);
360 let (child_width, _) = Self::calculate_bounds(&child_elements);
361 elements.extend(child_elements);
362 current_x += child_width + spacing;
363 }
364
365 elements
366 }
367
368 fn layout_grid(
370 &mut self,
371 children: &[A2UINode],
372 start_x: f32,
373 start_y: f32,
374 spacing: f32,
375 columns: u32,
376 ) -> Vec<Element> {
377 let mut elements = Vec::new();
378 let mut current_x = start_x;
379 let mut current_y = start_y;
380 let mut row_height: f32 = 0.0;
381 let columns = columns.max(1) as usize; for (i, child) in children.iter().enumerate() {
384 if i > 0 && i % columns == 0 {
386 current_x = start_x;
387 current_y += row_height + spacing;
388 row_height = 0.0;
389 }
390
391 let child_elements = self.convert_node(child, current_x, current_y);
392 let (child_width, child_height) = Self::calculate_bounds(&child_elements);
393 row_height = row_height.max(child_height);
394 elements.extend(child_elements);
395 current_x += child_width + spacing;
396 }
397
398 elements
399 }
400
401 fn layout_stack(&mut self, children: &[A2UINode], start_x: f32, start_y: f32) -> Vec<Element> {
403 let mut elements = Vec::new();
404
405 for child in children {
406 let child_elements = self.convert_node(child, start_x, start_y);
407 elements.extend(child_elements);
408 }
409
410 elements
411 }
412
413 fn detect_image_format(src: &str) -> ImageFormat {
414 use std::path::Path;
415 let ext = Path::new(src)
416 .extension()
417 .and_then(|e| e.to_str())
418 .map(str::to_lowercase);
419
420 match ext.as_deref() {
421 Some("jpg" | "jpeg") => ImageFormat::Jpeg,
422 Some("svg") => ImageFormat::Svg,
423 Some("webp") => ImageFormat::WebP,
424 _ => ImageFormat::Png,
425 }
426 }
427
428 fn calculate_bounds(elements: &[Element]) -> (f32, f32) {
429 elements.iter().fold((0.0, 0.0), |(w, h), el| {
430 (w.max(el.transform.width), h.max(el.transform.height))
431 })
432 }
433
434 fn convert_text(
435 &mut self,
436 content: &str,
437 style: Option<&A2UIStyle>,
438 x: f32,
439 y: f32,
440 ) -> Element {
441 let font_size = style.and_then(|s| s.font_size).unwrap_or(16.0);
442 let color = style
443 .and_then(|s| s.color.clone())
444 .unwrap_or_else(|| "#000000".to_string());
445 let width = style.and_then(|s| s.width).unwrap_or(200.0);
446 let height = style.and_then(|s| s.height).unwrap_or(font_size * 1.5);
447
448 Element::new(ElementKind::Text {
449 content: content.to_string(),
450 font_size,
451 color,
452 })
453 .with_transform(Transform {
454 x,
455 y,
456 width,
457 height,
458 rotation: 0.0,
459 z_index: self.next_z_index(),
460 })
461 }
462
463 fn convert_image(&mut self, src: &str, style: Option<&A2UIStyle>, x: f32, y: f32) -> Element {
464 let width = style.and_then(|s| s.width).unwrap_or(200.0);
465 let height = style.and_then(|s| s.height).unwrap_or(200.0);
466
467 let format = Self::detect_image_format(src);
469
470 Element::new(ElementKind::Image {
471 src: src.to_string(),
472 format,
473 })
474 .with_transform(Transform {
475 x,
476 y,
477 width,
478 height,
479 rotation: 0.0,
480 z_index: self.next_z_index(),
481 })
482 }
483
484 fn convert_chart(
485 &mut self,
486 chart_type: &str,
487 data: &serde_json::Value,
488 style: Option<&A2UIStyle>,
489 x: f32,
490 y: f32,
491 ) -> Element {
492 let width = style.and_then(|s| s.width).unwrap_or(400.0);
493 let height = style.and_then(|s| s.height).unwrap_or(300.0);
494
495 Element::new(ElementKind::Chart {
496 chart_type: chart_type.to_string(),
497 data: data.clone(),
498 })
499 .with_transform(Transform {
500 x,
501 y,
502 width,
503 height,
504 rotation: 0.0,
505 z_index: self.next_z_index(),
506 })
507 }
508
509 fn convert_video(
510 &mut self,
511 stream_id: &str,
512 mirror: bool,
513 style: Option<&A2UIStyle>,
514 x: f32,
515 y: f32,
516 ) -> Element {
517 let width = style.and_then(|s| s.width).unwrap_or(640.0);
518 let height = style.and_then(|s| s.height).unwrap_or(480.0);
519
520 Element::new(ElementKind::Video {
521 stream_id: stream_id.to_string(),
522 is_live: true,
523 mirror,
524 crop: None,
525 media_config: None,
526 })
527 .with_transform(Transform {
528 x,
529 y,
530 width,
531 height,
532 rotation: 0.0,
533 z_index: self.next_z_index(),
534 })
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
547 fn test_parse_simple_text_node() {
548 let json = r#"{
549 "root": {
550 "component": "text",
551 "content": "Hello World"
552 }
553 }"#;
554
555 let tree = A2UITree::from_json(json).expect("should parse");
556
557 match &tree.root {
558 A2UINode::Text { content, style } => {
559 assert_eq!(content, "Hello World");
560 assert!(style.is_none());
561 }
562 _ => panic!("Expected Text node"),
563 }
564 }
565
566 #[test]
567 fn test_parse_text_with_style() {
568 let json = r##"{
569 "root": {
570 "component": "text",
571 "content": "Styled Text",
572 "style": {
573 "font_size": 24.0,
574 "color": "#FF0000"
575 }
576 }
577 }"##;
578
579 let tree = A2UITree::from_json(json).expect("should parse");
580
581 match &tree.root {
582 A2UINode::Text { content, style } => {
583 assert_eq!(content, "Styled Text");
584 let s = style.as_ref().expect("should have style");
585 assert_eq!(s.font_size, Some(24.0));
586 assert_eq!(s.color.as_deref(), Some("#FF0000"));
587 }
588 _ => panic!("Expected Text node"),
589 }
590 }
591
592 #[test]
593 fn test_parse_container_with_children() {
594 let json = r#"{
595 "root": {
596 "component": "container",
597 "layout": "vertical",
598 "children": [
599 { "component": "text", "content": "First" },
600 { "component": "text", "content": "Second" }
601 ]
602 }
603 }"#;
604
605 let tree = A2UITree::from_json(json).expect("should parse");
606
607 match &tree.root {
608 A2UINode::Container {
609 children, layout, ..
610 } => {
611 assert_eq!(layout, "vertical");
612 assert_eq!(children.len(), 2);
613 }
614 _ => panic!("Expected Container node"),
615 }
616 }
617
618 #[test]
619 fn test_parse_horizontal_container() {
620 let json = r#"{
621 "root": {
622 "component": "container",
623 "layout": "horizontal",
624 "children": []
625 }
626 }"#;
627
628 let tree = A2UITree::from_json(json).expect("should parse");
629
630 match &tree.root {
631 A2UINode::Container { layout, .. } => {
632 assert_eq!(layout, "horizontal");
633 }
634 _ => panic!("Expected Container node"),
635 }
636 }
637
638 #[test]
639 fn test_parse_image() {
640 let json = r#"{
641 "root": {
642 "component": "image",
643 "src": "https://example.com/image.png",
644 "alt": "Example image"
645 }
646 }"#;
647
648 let tree = A2UITree::from_json(json).expect("should parse");
649
650 match &tree.root {
651 A2UINode::Image { src, alt, .. } => {
652 assert_eq!(src, "https://example.com/image.png");
653 assert_eq!(alt.as_deref(), Some("Example image"));
654 }
655 _ => panic!("Expected Image node"),
656 }
657 }
658
659 #[test]
660 fn test_parse_button() {
661 let json = r#"{
662 "root": {
663 "component": "button",
664 "label": "Click Me",
665 "action": "submit_form"
666 }
667 }"#;
668
669 let tree = A2UITree::from_json(json).expect("should parse");
670
671 match &tree.root {
672 A2UINode::Button { label, action, .. } => {
673 assert_eq!(label, "Click Me");
674 assert_eq!(action, "submit_form");
675 }
676 _ => panic!("Expected Button node"),
677 }
678 }
679
680 #[test]
681 fn test_parse_chart() {
682 let json = r#"{
683 "root": {
684 "component": "chart",
685 "chart_type": "bar",
686 "data": {
687 "labels": ["A", "B", "C"],
688 "values": [10, 20, 15]
689 }
690 }
691 }"#;
692
693 let tree = A2UITree::from_json(json).expect("should parse");
694
695 match &tree.root {
696 A2UINode::Chart {
697 chart_type, data, ..
698 } => {
699 assert_eq!(chart_type, "bar");
700 assert!(data.get("labels").is_some());
701 assert!(data.get("values").is_some());
702 }
703 _ => panic!("Expected Chart node"),
704 }
705 }
706
707 #[test]
708 fn test_parse_video_feed() {
709 let json = r#"{
710 "root": {
711 "component": "video_feed",
712 "stream_id": "local",
713 "mirror": true
714 }
715 }"#;
716
717 let tree = A2UITree::from_json(json).expect("should parse");
718
719 match &tree.root {
720 A2UINode::VideoFeed {
721 stream_id, mirror, ..
722 } => {
723 assert_eq!(stream_id, "local");
724 assert!(*mirror);
725 }
726 _ => panic!("Expected VideoFeed node"),
727 }
728 }
729
730 #[test]
731 fn test_parse_with_data_model() {
732 let json = r#"{
733 "root": {
734 "component": "text",
735 "content": "Data bound"
736 },
737 "data_model": {
738 "user": "Alice",
739 "count": 42
740 }
741 }"#;
742
743 let tree = A2UITree::from_json(json).expect("should parse");
744
745 assert_eq!(tree.data_model["user"], "Alice");
746 assert_eq!(tree.data_model["count"], 42);
747 }
748
749 #[test]
750 fn test_parse_invalid_json() {
751 let json = "{ invalid json }";
752 let result = A2UITree::from_json(json);
753 assert!(result.is_err());
754 }
755
756 #[test]
757 fn test_parse_missing_component_tag() {
758 let json = r#"{
759 "root": {
760 "content": "No component tag"
761 }
762 }"#;
763
764 let result = A2UITree::from_json(json);
765 assert!(result.is_err());
766 }
767
768 #[test]
773 fn test_convert_text_to_element() {
774 let json = r#"{
775 "root": {
776 "component": "text",
777 "content": "Hello World"
778 }
779 }"#;
780
781 let tree = A2UITree::from_json(json).expect("should parse");
782 let result = tree.to_elements();
783
784 assert_eq!(result.elements.len(), 1);
785 assert!(result.warnings.is_empty());
786
787 match &result.elements[0].kind {
788 ElementKind::Text {
789 content, font_size, ..
790 } => {
791 assert_eq!(content, "Hello World");
792 assert!((font_size - 16.0).abs() < f32::EPSILON); }
794 _ => panic!("Expected Text element"),
795 }
796 }
797
798 #[test]
799 fn test_convert_text_with_style() {
800 let json = r##"{
801 "root": {
802 "component": "text",
803 "content": "Styled",
804 "style": {
805 "font_size": 32.0,
806 "color": "#00FF00"
807 }
808 }
809 }"##;
810
811 let tree = A2UITree::from_json(json).expect("should parse");
812 let result = tree.to_elements();
813
814 match &result.elements[0].kind {
815 ElementKind::Text {
816 font_size, color, ..
817 } => {
818 assert!((font_size - 32.0).abs() < f32::EPSILON);
819 assert_eq!(color, "#00FF00");
820 }
821 _ => panic!("Expected Text element"),
822 }
823 }
824
825 #[test]
826 fn test_convert_container_vertical_layout() {
827 let json = r#"{
828 "root": {
829 "component": "container",
830 "layout": "vertical",
831 "children": [
832 { "component": "text", "content": "First" },
833 { "component": "text", "content": "Second" }
834 ]
835 }
836 }"#;
837
838 let tree = A2UITree::from_json(json).expect("should parse");
839 let result = tree.to_elements();
840
841 assert_eq!(result.elements.len(), 2);
842
843 let y1 = result.elements[0].transform.y;
845 let y2 = result.elements[1].transform.y;
846 assert!(y2 > y1, "Second element should be below first");
847 }
848
849 #[test]
850 fn test_convert_container_horizontal_layout() {
851 let json = r#"{
852 "root": {
853 "component": "container",
854 "layout": "horizontal",
855 "children": [
856 { "component": "text", "content": "Left" },
857 { "component": "text", "content": "Right" }
858 ]
859 }
860 }"#;
861
862 let tree = A2UITree::from_json(json).expect("should parse");
863 let result = tree.to_elements();
864
865 assert_eq!(result.elements.len(), 2);
866
867 let x1 = result.elements[0].transform.x;
869 let x2 = result.elements[1].transform.x;
870 assert!(x2 > x1, "Second element should be right of first");
871 }
872
873 #[test]
874 fn test_convert_image() {
875 let json = r#"{
876 "root": {
877 "component": "image",
878 "src": "test.png"
879 }
880 }"#;
881
882 let tree = A2UITree::from_json(json).expect("should parse");
883 let result = tree.to_elements();
884
885 assert_eq!(result.elements.len(), 1);
886
887 match &result.elements[0].kind {
888 ElementKind::Image { src, format } => {
889 assert_eq!(src, "test.png");
890 assert_eq!(*format, ImageFormat::Png);
891 }
892 _ => panic!("Expected Image element"),
893 }
894 }
895
896 #[test]
897 fn test_convert_image_jpeg() {
898 let json = r#"{
899 "root": {
900 "component": "image",
901 "src": "photo.jpg"
902 }
903 }"#;
904
905 let tree = A2UITree::from_json(json).expect("should parse");
906 let result = tree.to_elements();
907
908 match &result.elements[0].kind {
909 ElementKind::Image { format, .. } => {
910 assert_eq!(*format, ImageFormat::Jpeg);
911 }
912 _ => panic!("Expected Image element"),
913 }
914 }
915
916 #[test]
917 fn test_convert_button_is_interactive() {
918 let json = r#"{
919 "root": {
920 "component": "button",
921 "label": "Click Me",
922 "action": "do_something"
923 }
924 }"#;
925
926 let tree = A2UITree::from_json(json).expect("should parse");
927 let result = tree.to_elements();
928
929 assert_eq!(result.elements.len(), 1);
930 assert!(
931 result.elements[0].interactive,
932 "Button should be interactive"
933 );
934 }
935
936 #[test]
937 fn test_convert_chart() {
938 let json = r#"{
939 "root": {
940 "component": "chart",
941 "chart_type": "pie",
942 "data": { "values": [25, 50, 25] }
943 }
944 }"#;
945
946 let tree = A2UITree::from_json(json).expect("should parse");
947 let result = tree.to_elements();
948
949 assert_eq!(result.elements.len(), 1);
950
951 match &result.elements[0].kind {
952 ElementKind::Chart {
953 chart_type, data, ..
954 } => {
955 assert_eq!(chart_type, "pie");
956 assert!(data.get("values").is_some());
957 }
958 _ => panic!("Expected Chart element"),
959 }
960 }
961
962 #[test]
963 fn test_convert_video_feed() {
964 let json = r#"{
965 "root": {
966 "component": "video_feed",
967 "stream_id": "camera_1",
968 "mirror": true
969 }
970 }"#;
971
972 let tree = A2UITree::from_json(json).expect("should parse");
973 let result = tree.to_elements();
974
975 assert_eq!(result.elements.len(), 1);
976
977 match &result.elements[0].kind {
978 ElementKind::Video {
979 stream_id,
980 is_live,
981 mirror,
982 ..
983 } => {
984 assert_eq!(stream_id, "camera_1");
985 assert!(*is_live);
986 assert!(*mirror);
987 }
988 _ => panic!("Expected Video element"),
989 }
990 }
991
992 #[test]
993 fn test_convert_nested_containers() {
994 let json = r#"{
995 "root": {
996 "component": "container",
997 "layout": "vertical",
998 "children": [
999 {
1000 "component": "container",
1001 "layout": "horizontal",
1002 "children": [
1003 { "component": "text", "content": "A" },
1004 { "component": "text", "content": "B" }
1005 ]
1006 },
1007 { "component": "text", "content": "C" }
1008 ]
1009 }
1010 }"#;
1011
1012 let tree = A2UITree::from_json(json).expect("should parse");
1013 let result = tree.to_elements();
1014
1015 assert_eq!(result.elements.len(), 3);
1017 }
1018
1019 #[test]
1020 fn test_convert_z_index_ordering() {
1021 let json = r#"{
1022 "root": {
1023 "component": "container",
1024 "layout": "vertical",
1025 "children": [
1026 { "component": "text", "content": "First" },
1027 { "component": "text", "content": "Second" },
1028 { "component": "text", "content": "Third" }
1029 ]
1030 }
1031 }"#;
1032
1033 let tree = A2UITree::from_json(json).expect("should parse");
1034 let result = tree.to_elements();
1035
1036 let z0 = result.elements[0].transform.z_index;
1038 let z1 = result.elements[1].transform.z_index;
1039 let z2 = result.elements[2].transform.z_index;
1040
1041 assert!(z1 > z0, "Second should be above first");
1042 assert!(z2 > z1, "Third should be above second");
1043 }
1044
1045 #[test]
1046 fn test_convert_style_dimensions() {
1047 let json = r#"{
1048 "root": {
1049 "component": "image",
1050 "src": "test.png",
1051 "style": {
1052 "width": 300.0,
1053 "height": 150.0
1054 }
1055 }
1056 }"#;
1057
1058 let tree = A2UITree::from_json(json).expect("should parse");
1059 let result = tree.to_elements();
1060
1061 let transform = &result.elements[0].transform;
1062 assert!((transform.width - 300.0).abs() < f32::EPSILON);
1063 assert!((transform.height - 150.0).abs() < f32::EPSILON);
1064 }
1065
1066 #[test]
1071 fn test_layout_parse() {
1072 assert_eq!(Layout::parse("vertical"), Layout::Column);
1073 assert_eq!(Layout::parse("column"), Layout::Column);
1074 assert_eq!(Layout::parse("horizontal"), Layout::Row);
1075 assert_eq!(Layout::parse("row"), Layout::Row);
1076 assert_eq!(Layout::parse("grid:3"), Layout::Grid { columns: 3 });
1077 assert_eq!(Layout::parse("grid-2"), Layout::Grid { columns: 2 });
1078 assert_eq!(Layout::parse("stack"), Layout::Stack);
1079 assert_eq!(Layout::parse("overlay"), Layout::Stack);
1080 assert_eq!(Layout::parse("unknown"), Layout::Column); }
1082
1083 #[test]
1084 fn test_convert_grid_layout() {
1085 let json = r#"{
1086 "root": {
1087 "component": "container",
1088 "layout": "grid:2",
1089 "children": [
1090 { "component": "text", "content": "A" },
1091 { "component": "text", "content": "B" },
1092 { "component": "text", "content": "C" },
1093 { "component": "text", "content": "D" }
1094 ]
1095 }
1096 }"#;
1097
1098 let tree = A2UITree::from_json(json).expect("should parse");
1099 let result = tree.to_elements();
1100
1101 assert_eq!(result.elements.len(), 4);
1102
1103 let a = &result.elements[0].transform;
1107 let b = &result.elements[1].transform;
1108 let c = &result.elements[2].transform;
1109 let d = &result.elements[3].transform;
1110
1111 assert!(
1113 (a.y - b.y).abs() < f32::EPSILON,
1114 "A and B should be on same row"
1115 );
1116 assert!(b.x > a.x, "B should be right of A");
1118 assert!(c.y > a.y, "C should be below A (new row)");
1120 assert!(
1122 (c.y - d.y).abs() < f32::EPSILON,
1123 "C and D should be on same row"
1124 );
1125 }
1126
1127 #[test]
1128 fn test_convert_stack_layout() {
1129 let json = r#"{
1130 "root": {
1131 "component": "container",
1132 "layout": "stack",
1133 "children": [
1134 { "component": "text", "content": "Background" },
1135 { "component": "text", "content": "Foreground" }
1136 ]
1137 }
1138 }"#;
1139
1140 let tree = A2UITree::from_json(json).expect("should parse");
1141 let result = tree.to_elements();
1142
1143 assert_eq!(result.elements.len(), 2);
1144
1145 let bg = &result.elements[0].transform;
1146 let fg = &result.elements[1].transform;
1147
1148 assert!((bg.x - fg.x).abs() < f32::EPSILON, "X should be same");
1150 assert!((bg.y - fg.y).abs() < f32::EPSILON, "Y should be same");
1151 assert!(
1152 fg.z_index > bg.z_index,
1153 "Foreground should have higher z-index"
1154 );
1155 }
1156
1157 #[test]
1158 fn test_layout_with_margin() {
1159 let json = r#"{
1160 "root": {
1161 "component": "container",
1162 "layout": "vertical",
1163 "style": {
1164 "margin": 20.0,
1165 "padding": 5.0
1166 },
1167 "children": [
1168 { "component": "text", "content": "Test" }
1169 ]
1170 }
1171 }"#;
1172
1173 let tree = A2UITree::from_json(json).expect("should parse");
1174 let result = tree.to_elements();
1175
1176 let text = &result.elements[0].transform;
1178 assert!(text.x >= 25.0, "X should include margin + padding");
1179 assert!(text.y >= 25.0, "Y should include margin + padding");
1180 }
1181
1182 #[test]
1183 fn test_grid_with_varying_sizes() {
1184 let json = r#"{
1185 "root": {
1186 "component": "container",
1187 "layout": "grid:2",
1188 "children": [
1189 { "component": "text", "content": "Small", "style": { "height": 20.0 } },
1190 { "component": "text", "content": "Tall", "style": { "height": 50.0 } },
1191 { "component": "text", "content": "Below" }
1192 ]
1193 }
1194 }"#;
1195
1196 let tree = A2UITree::from_json(json).expect("should parse");
1197 let result = tree.to_elements();
1198
1199 let small = &result.elements[0].transform;
1200 let below = &result.elements[2].transform;
1202
1203 assert!(
1206 below.y > small.y + 50.0 - 1.0,
1207 "Third element should be below the tallest in row 1"
1208 );
1209 }
1210
1211 #[test]
1212 fn test_roundtrip_serialize_deserialize() {
1213 let original = A2UITree {
1214 root: A2UINode::Container {
1215 children: vec![
1216 A2UINode::Text {
1217 content: "Hello".to_string(),
1218 style: None,
1219 },
1220 A2UINode::Button {
1221 label: "Click".to_string(),
1222 action: "submit".to_string(),
1223 style: None,
1224 },
1225 ],
1226 layout: "vertical".to_string(),
1227 style: None,
1228 },
1229 data_model: serde_json::json!({"key": "value"}),
1230 };
1231
1232 let json = serde_json::to_string(&original).expect("should serialize");
1233 let parsed: A2UITree = serde_json::from_str(&json).expect("should deserialize");
1234
1235 assert_eq!(original, parsed);
1236 }
1237
1238 #[test]
1243 fn test_empty_container() {
1244 let json = r#"{
1245 "root": {
1246 "component": "container",
1247 "layout": "vertical",
1248 "children": []
1249 }
1250 }"#;
1251
1252 let tree = A2UITree::from_json(json).expect("should parse empty container");
1253
1254 match &tree.root {
1255 A2UINode::Container { children, .. } => {
1256 assert!(children.is_empty());
1257 }
1258 _ => panic!("Expected Container node"),
1259 }
1260
1261 let result = tree.to_elements();
1263 assert!(result.elements.is_empty());
1264 }
1265
1266 #[test]
1267 fn test_deeply_nested_containers() {
1268 let json = r#"{
1270 "root": {
1271 "component": "container",
1272 "layout": "vertical",
1273 "children": [{
1274 "component": "container",
1275 "layout": "horizontal",
1276 "children": [{
1277 "component": "container",
1278 "layout": "vertical",
1279 "children": [{
1280 "component": "container",
1281 "layout": "horizontal",
1282 "children": [{
1283 "component": "container",
1284 "layout": "vertical",
1285 "children": [{
1286 "component": "text",
1287 "content": "Deep"
1288 }]
1289 }]
1290 }]
1291 }]
1292 }]
1293 }
1294 }"#;
1295
1296 let tree = A2UITree::from_json(json).expect("should parse deep nesting");
1297
1298 let depth = {
1300 let mut max_depth = 0;
1301 let mut stack: Vec<(&A2UINode, usize)> = vec![(&tree.root, 1)];
1302 while let Some((node, d)) = stack.pop() {
1303 max_depth = max_depth.max(d);
1304 if let A2UINode::Container { children, .. } = node {
1305 for child in children {
1306 stack.push((child, d + 1));
1307 }
1308 }
1309 }
1310 max_depth
1311 };
1312 assert_eq!(depth, 6); let result = tree.to_elements();
1316 assert_eq!(result.elements.len(), 1); }
1318
1319 #[test]
1320 fn test_container_with_mixed_children() {
1321 let json = r#"{
1322 "root": {
1323 "component": "container",
1324 "layout": "vertical",
1325 "children": [
1326 { "component": "text", "content": "Title" },
1327 { "component": "button", "label": "Action", "action": "click" },
1328 { "component": "container", "layout": "horizontal", "children": [
1329 { "component": "text", "content": "Nested" }
1330 ]},
1331 { "component": "image", "src": "test.png", "alt": "Test" }
1332 ]
1333 }
1334 }"#;
1335
1336 let tree = A2UITree::from_json(json).expect("should parse mixed children");
1337
1338 match &tree.root {
1339 A2UINode::Container { children, .. } => {
1340 assert_eq!(children.len(), 4);
1341 }
1342 _ => panic!("Expected Container node"),
1343 }
1344
1345 let result = tree.to_elements();
1346 assert_eq!(result.elements.len(), 4);
1348 }
1349
1350 #[test]
1351 fn test_invalid_json_returns_error() {
1352 let invalid_json = r"{ not valid json }";
1353 let result = A2UITree::from_json(invalid_json);
1354 assert!(result.is_err());
1355 }
1356
1357 #[test]
1358 fn test_missing_required_fields_returns_error() {
1359 let json = r#"{
1361 "root": {
1362 "component": "text"
1363 }
1364 }"#;
1365
1366 let result = A2UITree::from_json(json);
1367 assert!(result.is_err());
1368 }
1369
1370 #[test]
1371 fn test_unknown_component_type() {
1372 let json = r#"{
1374 "root": {
1375 "component": "unknown_widget",
1376 "value": "test"
1377 }
1378 }"#;
1379
1380 let result = A2UITree::from_json(json);
1381 assert!(result.is_err());
1382 }
1383
1384 #[test]
1385 fn test_text_with_empty_content() {
1386 let json = r#"{
1387 "root": {
1388 "component": "text",
1389 "content": ""
1390 }
1391 }"#;
1392
1393 let tree = A2UITree::from_json(json).expect("should parse empty text");
1394
1395 match &tree.root {
1396 A2UINode::Text { content, .. } => {
1397 assert!(content.is_empty());
1398 }
1399 _ => panic!("Expected Text node"),
1400 }
1401 }
1402
1403 #[test]
1404 fn test_container_single_child() {
1405 let json = r#"{
1406 "root": {
1407 "component": "container",
1408 "layout": "vertical",
1409 "children": [
1410 { "component": "text", "content": "Only child" }
1411 ]
1412 }
1413 }"#;
1414
1415 let tree = A2UITree::from_json(json).expect("should parse");
1416
1417 match &tree.root {
1418 A2UINode::Container { children, .. } => {
1419 assert_eq!(children.len(), 1);
1420 }
1421 _ => panic!("Expected Container node"),
1422 }
1423
1424 let result = tree.to_elements();
1425 assert_eq!(result.elements.len(), 1);
1426 }
1427
1428 #[test]
1429 fn test_data_model_preserved() {
1430 let json = r#"{
1431 "root": {
1432 "component": "text",
1433 "content": "Hello"
1434 },
1435 "data_model": {
1436 "user": {
1437 "name": "Alice",
1438 "age": 30
1439 },
1440 "settings": ["a", "b", "c"]
1441 }
1442 }"#;
1443
1444 let tree = A2UITree::from_json(json).expect("should parse");
1445
1446 assert_eq!(tree.data_model["user"]["name"], "Alice");
1448 assert_eq!(tree.data_model["user"]["age"], 30);
1449 assert_eq!(tree.data_model["settings"].as_array().unwrap().len(), 3);
1450 }
1451
1452 #[test]
1453 fn test_all_style_properties() {
1454 let json = r##"{
1455 "root": {
1456 "component": "text",
1457 "content": "Styled",
1458 "style": {
1459 "font_size": 18.5,
1460 "color": "#123ABC",
1461 "background": "#FFFFFF",
1462 "padding": 10.0,
1463 "margin": 5.0,
1464 "width": 200.0,
1465 "height": 50.0
1466 }
1467 }
1468 }"##;
1469
1470 let tree = A2UITree::from_json(json).expect("should parse");
1471
1472 match &tree.root {
1473 A2UINode::Text { style, .. } => {
1474 let s = style.as_ref().expect("should have style");
1475 assert!((s.font_size.unwrap() - 18.5).abs() < 0.001);
1476 assert_eq!(s.color.as_deref(), Some("#123ABC"));
1477 assert_eq!(s.background.as_deref(), Some("#FFFFFF"));
1478 assert!((s.padding.unwrap() - 10.0).abs() < 0.001);
1479 assert!((s.margin.unwrap() - 5.0).abs() < 0.001);
1480 assert!((s.width.unwrap() - 200.0).abs() < 0.001);
1481 assert!((s.height.unwrap() - 50.0).abs() < 0.001);
1482 }
1483 _ => panic!("Expected Text node"),
1484 }
1485 }
1486}