1use presentar_core::{
4 widget::{Brick, BrickAssertion, BrickBudget, BrickVerification, LayoutResult},
5 Canvas, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum StackAlignment {
14 #[default]
16 TopLeft,
17 TopCenter,
19 TopRight,
21 CenterLeft,
23 Center,
25 CenterRight,
27 BottomLeft,
29 BottomCenter,
31 BottomRight,
33}
34
35impl StackAlignment {
36 #[must_use]
38 pub const fn horizontal_ratio(&self) -> f32 {
39 match self {
40 Self::TopLeft | Self::CenterLeft | Self::BottomLeft => 0.0,
41 Self::TopCenter | Self::Center | Self::BottomCenter => 0.5,
42 Self::TopRight | Self::CenterRight | Self::BottomRight => 1.0,
43 }
44 }
45
46 #[must_use]
48 pub const fn vertical_ratio(&self) -> f32 {
49 match self {
50 Self::TopLeft | Self::TopCenter | Self::TopRight => 0.0,
51 Self::CenterLeft | Self::Center | Self::CenterRight => 0.5,
52 Self::BottomLeft | Self::BottomCenter | Self::BottomRight => 1.0,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
59pub enum StackFit {
60 #[default]
62 Loose,
63 Expand,
65}
66
67#[derive(Serialize, Deserialize)]
71pub struct Stack {
72 alignment: StackAlignment,
74 fit: StackFit,
76 #[serde(skip)]
78 children: Vec<Box<dyn Widget>>,
79 test_id_value: Option<String>,
81 #[serde(skip)]
83 bounds: Rect,
84}
85
86impl Default for Stack {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Stack {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 alignment: StackAlignment::TopLeft,
98 fit: StackFit::Loose,
99 children: Vec::new(),
100 test_id_value: None,
101 bounds: Rect::default(),
102 }
103 }
104
105 #[must_use]
107 pub const fn alignment(mut self, alignment: StackAlignment) -> Self {
108 self.alignment = alignment;
109 self
110 }
111
112 #[must_use]
114 pub const fn fit(mut self, fit: StackFit) -> Self {
115 self.fit = fit;
116 self
117 }
118
119 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
121 self.children.push(Box::new(widget));
122 self
123 }
124
125 #[must_use]
127 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
128 self.test_id_value = Some(id.into());
129 self
130 }
131
132 #[must_use]
134 pub const fn get_alignment(&self) -> StackAlignment {
135 self.alignment
136 }
137
138 #[must_use]
140 pub const fn get_fit(&self) -> StackFit {
141 self.fit
142 }
143}
144
145impl Widget for Stack {
146 fn type_id(&self) -> TypeId {
147 TypeId::of::<Self>()
148 }
149
150 fn measure(&self, constraints: Constraints) -> Size {
151 if self.children.is_empty() {
152 return match self.fit {
153 StackFit::Loose => Size::ZERO,
154 StackFit::Expand => Size::new(constraints.max_width, constraints.max_height),
155 };
156 }
157
158 let mut max_width = 0.0f32;
159 let mut max_height = 0.0f32;
160
161 for child in &self.children {
163 let child_size = child.measure(constraints);
164 max_width = max_width.max(child_size.width);
165 max_height = max_height.max(child_size.height);
166 }
167
168 match self.fit {
169 StackFit::Loose => constraints.constrain(Size::new(max_width, max_height)),
170 StackFit::Expand => Size::new(constraints.max_width, constraints.max_height),
171 }
172 }
173
174 fn layout(&mut self, bounds: Rect) -> LayoutResult {
175 self.bounds = bounds;
176
177 for child in &mut self.children {
179 let child_constraints = Constraints::loose(bounds.size());
180 let child_size = child.measure(child_constraints);
181
182 let x = (bounds.width - child_size.width)
184 .mul_add(self.alignment.horizontal_ratio(), bounds.x);
185 let y = (bounds.height - child_size.height)
186 .mul_add(self.alignment.vertical_ratio(), bounds.y);
187
188 let child_bounds = Rect::new(x, y, child_size.width, child_size.height);
189 child.layout(child_bounds);
190 }
191
192 LayoutResult {
193 size: bounds.size(),
194 }
195 }
196
197 fn paint(&self, canvas: &mut dyn Canvas) {
198 for child in &self.children {
200 child.paint(canvas);
201 }
202 }
203
204 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
205 for child in self.children.iter_mut().rev() {
207 if let Some(msg) = child.event(event) {
208 return Some(msg);
209 }
210 }
211 None
212 }
213
214 fn children(&self) -> &[Box<dyn Widget>] {
215 &self.children
216 }
217
218 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
219 &mut self.children
220 }
221
222 fn test_id(&self) -> Option<&str> {
223 self.test_id_value.as_deref()
224 }
225}
226
227impl Brick for Stack {
229 fn brick_name(&self) -> &'static str {
230 "Stack"
231 }
232
233 fn assertions(&self) -> &[BrickAssertion] {
234 &[BrickAssertion::MaxLatencyMs(16)]
235 }
236
237 fn budget(&self) -> BrickBudget {
238 BrickBudget::uniform(16)
239 }
240
241 fn verify(&self) -> BrickVerification {
242 BrickVerification {
243 passed: self.assertions().to_vec(),
244 failed: vec![],
245 verification_time: Duration::from_micros(10),
246 }
247 }
248
249 fn to_html(&self) -> String {
250 r#"<div class="brick-stack"></div>"#.to_string()
251 }
252
253 fn to_css(&self) -> String {
254 ".brick-stack { display: block; position: relative; }".to_string()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use presentar_core::Widget;
262
263 #[test]
268 fn test_stack_alignment_default() {
269 assert_eq!(StackAlignment::default(), StackAlignment::TopLeft);
270 }
271
272 #[test]
273 fn test_stack_alignment_horizontal_ratio() {
274 assert_eq!(StackAlignment::TopLeft.horizontal_ratio(), 0.0);
276 assert_eq!(StackAlignment::CenterLeft.horizontal_ratio(), 0.0);
277 assert_eq!(StackAlignment::BottomLeft.horizontal_ratio(), 0.0);
278
279 assert_eq!(StackAlignment::TopCenter.horizontal_ratio(), 0.5);
281 assert_eq!(StackAlignment::Center.horizontal_ratio(), 0.5);
282 assert_eq!(StackAlignment::BottomCenter.horizontal_ratio(), 0.5);
283
284 assert_eq!(StackAlignment::TopRight.horizontal_ratio(), 1.0);
286 assert_eq!(StackAlignment::CenterRight.horizontal_ratio(), 1.0);
287 assert_eq!(StackAlignment::BottomRight.horizontal_ratio(), 1.0);
288 }
289
290 #[test]
291 fn test_stack_alignment_vertical_ratio() {
292 assert_eq!(StackAlignment::TopLeft.vertical_ratio(), 0.0);
294 assert_eq!(StackAlignment::TopCenter.vertical_ratio(), 0.0);
295 assert_eq!(StackAlignment::TopRight.vertical_ratio(), 0.0);
296
297 assert_eq!(StackAlignment::CenterLeft.vertical_ratio(), 0.5);
299 assert_eq!(StackAlignment::Center.vertical_ratio(), 0.5);
300 assert_eq!(StackAlignment::CenterRight.vertical_ratio(), 0.5);
301
302 assert_eq!(StackAlignment::BottomLeft.vertical_ratio(), 1.0);
304 assert_eq!(StackAlignment::BottomCenter.vertical_ratio(), 1.0);
305 assert_eq!(StackAlignment::BottomRight.vertical_ratio(), 1.0);
306 }
307
308 #[test]
313 fn test_stack_fit_default() {
314 assert_eq!(StackFit::default(), StackFit::Loose);
315 }
316
317 #[test]
322 fn test_stack_new() {
323 let stack = Stack::new();
324 assert_eq!(stack.get_alignment(), StackAlignment::TopLeft);
325 assert_eq!(stack.get_fit(), StackFit::Loose);
326 assert!(stack.children().is_empty());
327 }
328
329 #[test]
330 fn test_stack_default() {
331 let stack = Stack::default();
332 assert_eq!(stack.get_alignment(), StackAlignment::TopLeft);
333 assert_eq!(stack.get_fit(), StackFit::Loose);
334 }
335
336 #[test]
337 fn test_stack_builder() {
338 let stack = Stack::new()
339 .alignment(StackAlignment::Center)
340 .fit(StackFit::Expand)
341 .with_test_id("my-stack");
342
343 assert_eq!(stack.get_alignment(), StackAlignment::Center);
344 assert_eq!(stack.get_fit(), StackFit::Expand);
345 assert_eq!(Widget::test_id(&stack), Some("my-stack"));
346 }
347
348 #[test]
353 fn test_stack_empty_loose() {
354 let stack = Stack::new().fit(StackFit::Loose);
355 let size = stack.measure(Constraints::loose(Size::new(100.0, 100.0)));
356 assert_eq!(size, Size::ZERO);
357 }
358
359 #[test]
360 fn test_stack_empty_expand() {
361 let stack = Stack::new().fit(StackFit::Expand);
362 let size = stack.measure(Constraints::loose(Size::new(100.0, 100.0)));
363 assert_eq!(size, Size::new(100.0, 100.0));
364 }
365
366 #[test]
371 fn test_stack_type_id() {
372 let stack = Stack::new();
373 let type_id = Widget::type_id(&stack);
374 assert_eq!(type_id, TypeId::of::<Stack>());
375 }
376
377 #[test]
378 fn test_stack_test_id_none() {
379 let stack = Stack::new();
380 assert_eq!(Widget::test_id(&stack), None);
381 }
382
383 #[test]
384 fn test_stack_test_id_some() {
385 let stack = Stack::new().with_test_id("test-stack");
386 assert_eq!(Widget::test_id(&stack), Some("test-stack"));
387 }
388
389 #[test]
394 fn test_stack_alignment_horizontal_ratios() {
395 assert_eq!(StackAlignment::TopLeft.horizontal_ratio(), 0.0);
396 assert_eq!(StackAlignment::TopCenter.horizontal_ratio(), 0.5);
397 assert_eq!(StackAlignment::TopRight.horizontal_ratio(), 1.0);
398 assert_eq!(StackAlignment::Center.horizontal_ratio(), 0.5);
399 assert_eq!(StackAlignment::BottomRight.horizontal_ratio(), 1.0);
400 }
401
402 #[test]
403 fn test_stack_alignment_vertical_ratios() {
404 assert_eq!(StackAlignment::TopLeft.vertical_ratio(), 0.0);
405 assert_eq!(StackAlignment::CenterLeft.vertical_ratio(), 0.5);
406 assert_eq!(StackAlignment::BottomLeft.vertical_ratio(), 1.0);
407 assert_eq!(StackAlignment::Center.vertical_ratio(), 0.5);
408 assert_eq!(StackAlignment::BottomRight.vertical_ratio(), 1.0);
409 }
410
411 #[test]
412 fn test_stack_alignment_default_is_top_left() {
413 let align = StackAlignment::default();
414 assert_eq!(align, StackAlignment::TopLeft);
415 }
416
417 #[test]
418 fn test_stack_fit_default_is_loose() {
419 let fit = StackFit::default();
420 assert_eq!(fit, StackFit::Loose);
421 }
422
423 #[test]
424 fn test_stack_layout_sets_bounds() {
425 let mut stack = Stack::new();
426 let result = stack.layout(Rect::new(10.0, 20.0, 100.0, 80.0));
427 assert_eq!(result.size, Size::new(100.0, 80.0));
428 assert_eq!(stack.bounds, Rect::new(10.0, 20.0, 100.0, 80.0));
429 }
430
431 #[test]
432 fn test_stack_children_empty() {
433 let stack = Stack::new();
434 assert!(stack.children().is_empty());
435 }
436
437 #[test]
438 fn test_stack_event_no_children() {
439 let mut stack = Stack::new();
440 stack.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
441 let result = stack.event(&Event::MouseEnter);
442 assert!(result.is_none());
443 }
444
445 #[test]
450 fn test_stack_brick_name() {
451 let stack = Stack::new();
452 assert_eq!(stack.brick_name(), "Stack");
453 }
454
455 #[test]
456 fn test_stack_brick_assertions() {
457 let stack = Stack::new();
458 let assertions = stack.assertions();
459 assert!(!assertions.is_empty());
460 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
461 }
462
463 #[test]
464 fn test_stack_brick_budget() {
465 let stack = Stack::new();
466 let budget = stack.budget();
467 assert!(budget.layout_ms > 0);
469 assert!(budget.paint_ms > 0);
470 }
471
472 #[test]
473 fn test_stack_brick_verify() {
474 let stack = Stack::new();
475 let verification = stack.verify();
476 assert!(!verification.passed.is_empty());
477 assert!(verification.failed.is_empty());
478 }
479
480 #[test]
481 fn test_stack_brick_to_html() {
482 let stack = Stack::new();
483 let html = stack.to_html();
484 assert!(html.contains("brick-stack"));
485 }
486
487 #[test]
488 fn test_stack_brick_to_css() {
489 let stack = Stack::new();
490 let css = stack.to_css();
491 assert!(css.contains(".brick-stack"));
492 assert!(css.contains("display: block"));
493 assert!(css.contains("position: relative"));
494 }
495
496 #[test]
501 fn test_stack_alignment_all_variants() {
502 let alignments = [
503 StackAlignment::TopLeft,
504 StackAlignment::TopCenter,
505 StackAlignment::TopRight,
506 StackAlignment::CenterLeft,
507 StackAlignment::Center,
508 StackAlignment::CenterRight,
509 StackAlignment::BottomLeft,
510 StackAlignment::BottomCenter,
511 StackAlignment::BottomRight,
512 ];
513 assert_eq!(alignments.len(), 9);
514 }
515
516 #[test]
517 fn test_stack_alignment_debug() {
518 let alignment = StackAlignment::Center;
519 let debug_str = format!("{:?}", alignment);
520 assert!(debug_str.contains("Center"));
521 }
522
523 #[test]
524 fn test_stack_alignment_eq() {
525 assert_eq!(StackAlignment::Center, StackAlignment::Center);
526 assert_ne!(StackAlignment::TopLeft, StackAlignment::BottomRight);
527 }
528
529 #[test]
530 fn test_stack_alignment_clone() {
531 let alignment = StackAlignment::BottomCenter;
532 let cloned = alignment;
533 assert_eq!(cloned, StackAlignment::BottomCenter);
534 }
535
536 #[test]
541 fn test_stack_fit_eq() {
542 assert_eq!(StackFit::Loose, StackFit::Loose);
543 assert_ne!(StackFit::Loose, StackFit::Expand);
544 }
545
546 #[test]
547 fn test_stack_fit_debug() {
548 let fit = StackFit::Expand;
549 let debug_str = format!("{:?}", fit);
550 assert!(debug_str.contains("Expand"));
551 }
552
553 #[test]
554 fn test_stack_fit_clone() {
555 let fit = StackFit::Expand;
556 let cloned = fit;
557 assert_eq!(cloned, StackFit::Expand);
558 }
559
560 struct MockWidget {
566 size: Size,
567 }
568
569 impl Brick for MockWidget {
570 fn brick_name(&self) -> &'static str {
571 "MockWidget"
572 }
573
574 fn assertions(&self) -> &[BrickAssertion] {
575 &[]
576 }
577
578 fn budget(&self) -> BrickBudget {
579 BrickBudget::uniform(16)
580 }
581
582 fn verify(&self) -> BrickVerification {
583 BrickVerification {
584 passed: vec![],
585 failed: vec![],
586 verification_time: Duration::from_micros(1),
587 }
588 }
589
590 fn to_html(&self) -> String {
591 String::new()
592 }
593
594 fn to_css(&self) -> String {
595 String::new()
596 }
597 }
598
599 impl Widget for MockWidget {
600 fn type_id(&self) -> TypeId {
601 TypeId::of::<Self>()
602 }
603
604 fn measure(&self, _constraints: Constraints) -> Size {
605 self.size
606 }
607
608 fn layout(&mut self, _bounds: Rect) -> LayoutResult {
609 LayoutResult { size: self.size }
610 }
611
612 fn paint(&self, _canvas: &mut dyn Canvas) {}
613
614 fn event(&mut self, _event: &Event) -> Option<Box<dyn std::any::Any + Send>> {
615 None
616 }
617
618 fn children(&self) -> &[Box<dyn Widget>] {
619 &[]
620 }
621
622 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
623 &mut []
624 }
625 }
626
627 #[test]
628 fn test_stack_measure_with_children_loose() {
629 let stack = Stack::new()
630 .fit(StackFit::Loose)
631 .child(MockWidget {
632 size: Size::new(50.0, 30.0),
633 })
634 .child(MockWidget {
635 size: Size::new(100.0, 60.0),
636 });
637
638 let size = stack.measure(Constraints::loose(Size::new(500.0, 500.0)));
639 assert_eq!(size.width, 100.0);
641 assert_eq!(size.height, 60.0);
642 }
643
644 #[test]
645 fn test_stack_measure_with_children_expand() {
646 let stack = Stack::new().fit(StackFit::Expand).child(MockWidget {
647 size: Size::new(50.0, 30.0),
648 });
649
650 let size = stack.measure(Constraints::loose(Size::new(500.0, 400.0)));
651 assert_eq!(size.width, 500.0);
653 assert_eq!(size.height, 400.0);
654 }
655
656 #[test]
657 fn test_stack_layout_with_children_top_left() {
658 let mut stack = Stack::new()
659 .alignment(StackAlignment::TopLeft)
660 .child(MockWidget {
661 size: Size::new(50.0, 30.0),
662 });
663
664 stack.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
665
666 assert_eq!(stack.bounds, Rect::new(0.0, 0.0, 200.0, 150.0));
669 }
670
671 #[test]
672 fn test_stack_layout_with_children_center() {
673 let mut stack = Stack::new()
674 .alignment(StackAlignment::Center)
675 .child(MockWidget {
676 size: Size::new(50.0, 30.0),
677 });
678
679 let result = stack.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
680 assert_eq!(result.size, Size::new(200.0, 150.0));
681 }
682
683 #[test]
684 fn test_stack_layout_with_children_bottom_right() {
685 let mut stack = Stack::new()
686 .alignment(StackAlignment::BottomRight)
687 .child(MockWidget {
688 size: Size::new(50.0, 30.0),
689 });
690
691 let result = stack.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
692 assert_eq!(result.size, Size::new(200.0, 150.0));
693 }
694
695 #[test]
696 fn test_stack_children_count() {
697 let stack = Stack::new()
698 .child(MockWidget {
699 size: Size::new(50.0, 30.0),
700 })
701 .child(MockWidget {
702 size: Size::new(100.0, 60.0),
703 })
704 .child(MockWidget {
705 size: Size::new(75.0, 45.0),
706 });
707
708 assert_eq!(stack.children().len(), 3);
709 }
710
711 #[test]
712 fn test_stack_children_mut_count() {
713 let mut stack = Stack::new()
714 .child(MockWidget {
715 size: Size::new(50.0, 30.0),
716 })
717 .child(MockWidget {
718 size: Size::new(100.0, 60.0),
719 });
720
721 assert_eq!(stack.children_mut().len(), 2);
722 }
723
724 #[test]
729 fn test_stack_widget_test_id() {
730 let stack = Stack::new().with_test_id("stack-1");
731 assert_eq!(Brick::test_id(&stack), None); }
733
734 #[test]
735 fn test_stack_debug() {
736 let stack = Stack::new();
737 let _ = stack;
739 }
740
741 #[test]
746 fn test_stack_default_impl() {
747 let stack = Stack::default();
748 assert_eq!(stack.get_alignment(), StackAlignment::TopLeft);
749 assert_eq!(stack.get_fit(), StackFit::Loose);
750 assert!(stack.children().is_empty());
751 }
752
753 struct EventCapturingWidget {
758 size: Size,
759 }
760
761 impl Brick for EventCapturingWidget {
762 fn brick_name(&self) -> &'static str {
763 "EventCapturingWidget"
764 }
765
766 fn assertions(&self) -> &[BrickAssertion] {
767 &[]
768 }
769
770 fn budget(&self) -> BrickBudget {
771 BrickBudget::uniform(16)
772 }
773
774 fn verify(&self) -> BrickVerification {
775 BrickVerification {
776 passed: vec![],
777 failed: vec![],
778 verification_time: Duration::from_micros(1),
779 }
780 }
781
782 fn to_html(&self) -> String {
783 String::new()
784 }
785
786 fn to_css(&self) -> String {
787 String::new()
788 }
789 }
790
791 impl Widget for EventCapturingWidget {
792 fn type_id(&self) -> TypeId {
793 TypeId::of::<Self>()
794 }
795
796 fn measure(&self, _constraints: Constraints) -> Size {
797 self.size
798 }
799
800 fn layout(&mut self, _bounds: Rect) -> LayoutResult {
801 LayoutResult { size: self.size }
802 }
803
804 fn paint(&self, _canvas: &mut dyn Canvas) {}
805
806 fn event(&mut self, _event: &Event) -> Option<Box<dyn std::any::Any + Send>> {
807 Some(Box::new("handled".to_string()))
809 }
810
811 fn children(&self) -> &[Box<dyn Widget>] {
812 &[]
813 }
814
815 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
816 &mut []
817 }
818 }
819
820 #[test]
821 fn test_stack_event_propagates_to_children() {
822 let mut stack = Stack::new().child(EventCapturingWidget {
823 size: Size::new(50.0, 30.0),
824 });
825
826 stack.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
827 let result = stack.event(&Event::MouseEnter);
828
829 assert!(result.is_some());
831 }
832
833 #[test]
834 fn test_stack_event_reverse_order() {
835 let mut stack = Stack::new()
837 .child(MockWidget {
838 size: Size::new(50.0, 30.0),
839 })
840 .child(EventCapturingWidget {
841 size: Size::new(50.0, 30.0),
842 });
843
844 stack.layout(Rect::new(0.0, 0.0, 200.0, 150.0));
845 let result = stack.event(&Event::MouseEnter);
846
847 assert!(result.is_some());
849 }
850
851 #[test]
856 fn test_stack_measure_loose_with_constraints() {
857 let stack = Stack::new().fit(StackFit::Loose).child(MockWidget {
858 size: Size::new(500.0, 400.0),
859 });
860
861 let size = stack.measure(Constraints {
863 min_width: 0.0,
864 min_height: 0.0,
865 max_width: 200.0,
866 max_height: 150.0,
867 });
868
869 assert_eq!(size.width, 200.0);
871 assert_eq!(size.height, 150.0);
872 }
873
874 #[test]
875 fn test_stack_measure_multiple_children_different_sizes() {
876 let stack = Stack::new()
877 .fit(StackFit::Loose)
878 .child(MockWidget {
879 size: Size::new(50.0, 100.0),
880 })
881 .child(MockWidget {
882 size: Size::new(100.0, 50.0),
883 })
884 .child(MockWidget {
885 size: Size::new(75.0, 75.0),
886 });
887
888 let size = stack.measure(Constraints::loose(Size::new(500.0, 500.0)));
889 assert_eq!(size.width, 100.0);
891 assert_eq!(size.height, 100.0);
892 }
893}