presentar_widgets/
column.rs

1//! Column widget for vertical layout.
2
3use presentar_core::{
4    widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
5    Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11use crate::row::{CrossAxisAlignment, MainAxisAlignment};
12
13/// Column widget for vertical layout of children.
14#[derive(Serialize, Deserialize)]
15pub struct Column {
16    /// Main axis (vertical) alignment
17    main_axis_alignment: MainAxisAlignment,
18    /// Cross axis (horizontal) alignment
19    cross_axis_alignment: CrossAxisAlignment,
20    /// Gap between children
21    gap: f32,
22    /// Children widgets
23    #[serde(skip)]
24    children: Vec<Box<dyn Widget>>,
25    /// Test ID
26    test_id_value: Option<String>,
27    /// Cached bounds
28    #[serde(skip)]
29    bounds: Rect,
30    /// Cached child positions
31    #[serde(skip)]
32    child_bounds: Vec<Rect>,
33}
34
35impl Default for Column {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Column {
42    /// Create a new empty column.
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            main_axis_alignment: MainAxisAlignment::Start,
47            cross_axis_alignment: CrossAxisAlignment::Center,
48            gap: 0.0,
49            children: Vec::new(),
50            test_id_value: None,
51            bounds: Rect::default(),
52            child_bounds: Vec::new(),
53        }
54    }
55
56    /// Set main axis alignment.
57    #[must_use]
58    pub const fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
59        self.main_axis_alignment = alignment;
60        self
61    }
62
63    /// Set cross axis alignment.
64    #[must_use]
65    pub const fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
66        self.cross_axis_alignment = alignment;
67        self
68    }
69
70    /// Set gap between children.
71    #[must_use]
72    pub const fn gap(mut self, gap: f32) -> Self {
73        self.gap = gap;
74        self
75    }
76
77    /// Add a child widget.
78    pub fn child(mut self, widget: impl Widget + 'static) -> Self {
79        self.children.push(Box::new(widget));
80        self
81    }
82
83    /// Set test ID.
84    #[must_use]
85    pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
86        self.test_id_value = Some(id.into());
87        self
88    }
89}
90
91impl Widget for Column {
92    fn type_id(&self) -> TypeId {
93        TypeId::of::<Self>()
94    }
95
96    fn measure(&self, constraints: Constraints) -> Size {
97        if self.children.is_empty() {
98            return Size::ZERO;
99        }
100
101        let mut max_width = 0.0f32;
102        let mut total_height = 0.0f32;
103
104        // Measure all children
105        for (i, child) in self.children.iter().enumerate() {
106            let child_constraints = Constraints::new(
107                0.0,
108                constraints.max_width,
109                0.0,
110                (constraints.max_height - total_height).max(0.0),
111            );
112
113            let child_size = child.measure(child_constraints);
114            max_width = max_width.max(child_size.width);
115            total_height += child_size.height;
116
117            if i < self.children.len() - 1 {
118                total_height += self.gap;
119            }
120        }
121
122        constraints.constrain(Size::new(max_width, total_height))
123    }
124
125    fn layout(&mut self, bounds: Rect) -> LayoutResult {
126        self.bounds = bounds;
127        self.child_bounds.clear();
128
129        if self.children.is_empty() {
130            return LayoutResult { size: Size::ZERO };
131        }
132
133        // First pass: measure children
134        let mut child_sizes: Vec<Size> = Vec::with_capacity(self.children.len());
135        let mut total_height = 0.0f32;
136
137        for child in &self.children {
138            let child_constraints = Constraints::loose(bounds.size());
139            let size = child.measure(child_constraints);
140            total_height += size.height;
141            child_sizes.push(size);
142        }
143
144        let gaps_height = self.gap * (self.children.len() - 1).max(0) as f32;
145        let content_height = total_height + gaps_height;
146        let remaining_space = (bounds.height - content_height).max(0.0);
147
148        // Calculate starting position based on alignment
149        let (mut y, extra_gap) = match self.main_axis_alignment {
150            MainAxisAlignment::Start => (bounds.y, 0.0),
151            MainAxisAlignment::End => (bounds.y + remaining_space, 0.0),
152            MainAxisAlignment::Center => (bounds.y + remaining_space / 2.0, 0.0),
153            MainAxisAlignment::SpaceBetween => {
154                if self.children.len() > 1 {
155                    (bounds.y, remaining_space / (self.children.len() - 1) as f32)
156                } else {
157                    (bounds.y, 0.0)
158                }
159            }
160            MainAxisAlignment::SpaceAround => {
161                let gap = remaining_space / self.children.len() as f32;
162                (bounds.y + gap / 2.0, gap)
163            }
164            MainAxisAlignment::SpaceEvenly => {
165                let gap = remaining_space / (self.children.len() + 1) as f32;
166                (bounds.y + gap, gap)
167            }
168        };
169
170        // Second pass: position children
171        let num_children = self.children.len();
172        for (i, (child, size)) in self.children.iter_mut().zip(child_sizes.iter()).enumerate() {
173            let x = match self.cross_axis_alignment {
174                CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => bounds.x,
175                CrossAxisAlignment::End => bounds.x + bounds.width - size.width,
176                CrossAxisAlignment::Center => bounds.x + (bounds.width - size.width) / 2.0,
177            };
178
179            let width = if self.cross_axis_alignment == CrossAxisAlignment::Stretch {
180                bounds.width
181            } else {
182                size.width
183            };
184
185            let child_bounds = Rect::new(x, y, width, size.height);
186            child.layout(child_bounds);
187            self.child_bounds.push(child_bounds);
188
189            // Move y for next child
190            if i < num_children - 1 {
191                y += size.height;
192                if self.main_axis_alignment == MainAxisAlignment::SpaceBetween {
193                    // SpaceBetween uses only extra_gap (no regular gap)
194                    y += extra_gap;
195                } else {
196                    y += self.gap + extra_gap;
197                }
198            }
199        }
200
201        LayoutResult {
202            size: bounds.size(),
203        }
204    }
205
206    fn paint(&self, canvas: &mut dyn Canvas) {
207        for child in &self.children {
208            child.paint(canvas);
209        }
210    }
211
212    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
213        for child in &mut self.children {
214            if let Some(msg) = child.event(event) {
215                return Some(msg);
216            }
217        }
218        None
219    }
220
221    fn children(&self) -> &[Box<dyn Widget>] {
222        &self.children
223    }
224
225    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
226        &mut self.children
227    }
228
229    fn test_id(&self) -> Option<&str> {
230        self.test_id_value.as_deref()
231    }
232}
233
234// PROBAR-SPEC-009: Brick Architecture - Tests define interface
235impl Brick for Column {
236    fn brick_name(&self) -> &'static str {
237        "Column"
238    }
239
240    fn assertions(&self) -> &[BrickAssertion] {
241        &[BrickAssertion::MaxLatencyMs(16)]
242    }
243
244    fn budget(&self) -> BrickBudget {
245        BrickBudget::uniform(16)
246    }
247
248    fn verify(&self) -> BrickVerification {
249        BrickVerification {
250            passed: self.assertions().to_vec(),
251            failed: vec![],
252            verification_time: Duration::from_micros(10),
253        }
254    }
255
256    fn to_html(&self) -> String {
257        let test_id = self.test_id_value.as_deref().unwrap_or("column");
258        format!(r#"<div class="brick-column" data-testid="{test_id}"></div>"#)
259    }
260
261    fn to_css(&self) -> String {
262        ".brick-column { display: flex; flex-direction: column; }".into()
263    }
264
265    fn test_id(&self) -> Option<&str> {
266        self.test_id_value.as_deref()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use presentar_core::widget::AccessibleRole;
274    use presentar_core::{Brick, BrickBudget, BrickVerification, Widget};
275    use std::time::Duration;
276
277    // Test widget with fixed size for layout testing
278    struct FixedWidget {
279        size: Size,
280    }
281
282    impl FixedWidget {
283        fn new(width: f32, height: f32) -> Self {
284            Self {
285                size: Size::new(width, height),
286            }
287        }
288    }
289
290    impl Brick for FixedWidget {
291        fn brick_name(&self) -> &'static str {
292            "FixedWidget"
293        }
294
295        fn assertions(&self) -> &[presentar_core::BrickAssertion] {
296            &[]
297        }
298
299        fn budget(&self) -> BrickBudget {
300            BrickBudget::uniform(16)
301        }
302
303        fn verify(&self) -> BrickVerification {
304            BrickVerification {
305                passed: vec![],
306                failed: vec![],
307                verification_time: Duration::from_micros(1),
308            }
309        }
310
311        fn to_html(&self) -> String {
312            String::new()
313        }
314
315        fn to_css(&self) -> String {
316            String::new()
317        }
318    }
319
320    impl Widget for FixedWidget {
321        fn type_id(&self) -> TypeId {
322            TypeId::of::<Self>()
323        }
324
325        fn measure(&self, constraints: Constraints) -> Size {
326            constraints.constrain(self.size)
327        }
328
329        fn layout(&mut self, _bounds: Rect) -> LayoutResult {
330            LayoutResult { size: self.size }
331        }
332
333        fn paint(&self, _canvas: &mut dyn Canvas) {}
334
335        fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
336            None
337        }
338
339        fn children(&self) -> &[Box<dyn Widget>] {
340            &[]
341        }
342
343        fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
344            &mut []
345        }
346
347        fn accessible_role(&self) -> AccessibleRole {
348            AccessibleRole::Generic
349        }
350    }
351
352    // ===== Basic Tests =====
353
354    #[test]
355    fn test_column_empty() {
356        let col = Column::new();
357        let size = col.measure(Constraints::loose(Size::new(100.0, 100.0)));
358        assert_eq!(size, Size::ZERO);
359    }
360
361    #[test]
362    fn test_column_builder() {
363        let col = Column::new()
364            .gap(10.0)
365            .main_axis_alignment(MainAxisAlignment::Center)
366            .cross_axis_alignment(CrossAxisAlignment::Start)
367            .with_test_id("my-column");
368
369        assert_eq!(col.gap, 10.0);
370        assert_eq!(col.main_axis_alignment, MainAxisAlignment::Center);
371        assert_eq!(col.cross_axis_alignment, CrossAxisAlignment::Start);
372        assert_eq!(Widget::test_id(&col), Some("my-column"));
373    }
374
375    #[test]
376    fn test_column_default() {
377        let col = Column::default();
378        assert_eq!(col.main_axis_alignment, MainAxisAlignment::Start);
379        assert_eq!(col.cross_axis_alignment, CrossAxisAlignment::Center);
380        assert_eq!(col.gap, 0.0);
381    }
382
383    #[test]
384    fn test_column_type_id() {
385        let col = Column::new();
386        assert_eq!(Widget::type_id(&col), TypeId::of::<Column>());
387    }
388
389    #[test]
390    fn test_column_children() {
391        let col = Column::new()
392            .child(FixedWidget::new(50.0, 30.0))
393            .child(FixedWidget::new(50.0, 30.0));
394        assert_eq!(col.children().len(), 2);
395    }
396
397    // ===== Measure Tests =====
398
399    #[test]
400    fn test_column_measure_single_child() {
401        let col = Column::new().child(FixedWidget::new(50.0, 30.0));
402        let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
403        assert_eq!(size, Size::new(50.0, 30.0));
404    }
405
406    #[test]
407    fn test_column_measure_multiple_children() {
408        let col = Column::new()
409            .child(FixedWidget::new(50.0, 30.0))
410            .child(FixedWidget::new(60.0, 40.0));
411        let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
412        assert_eq!(size, Size::new(60.0, 70.0)); // max width, sum heights
413    }
414
415    #[test]
416    fn test_column_measure_with_gap() {
417        let col = Column::new()
418            .gap(10.0)
419            .child(FixedWidget::new(50.0, 30.0))
420            .child(FixedWidget::new(50.0, 30.0));
421        let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
422        assert_eq!(size, Size::new(50.0, 70.0)); // 30 + 10 + 30
423    }
424
425    #[test]
426    fn test_column_measure_constrained() {
427        let col = Column::new()
428            .child(FixedWidget::new(100.0, 100.0))
429            .child(FixedWidget::new(100.0, 100.0));
430        let size = col.measure(Constraints::tight(Size::new(80.0, 150.0)));
431        assert_eq!(size, Size::new(80.0, 150.0)); // Constrained to tight
432    }
433
434    // ===== MainAxisAlignment Tests =====
435
436    #[test]
437    fn test_column_alignment_start() {
438        let mut col = Column::new()
439            .main_axis_alignment(MainAxisAlignment::Start)
440            .child(FixedWidget::new(30.0, 20.0))
441            .child(FixedWidget::new(30.0, 20.0));
442
443        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
444
445        assert_eq!(col.child_bounds.len(), 2);
446        assert_eq!(col.child_bounds[0].y, 0.0);
447        assert_eq!(col.child_bounds[1].y, 20.0);
448    }
449
450    #[test]
451    fn test_column_alignment_end() {
452        let mut col = Column::new()
453            .main_axis_alignment(MainAxisAlignment::End)
454            .child(FixedWidget::new(30.0, 20.0))
455            .child(FixedWidget::new(30.0, 20.0));
456
457        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
458
459        // 200 - 40 = 160 remaining, children at 160 and 180
460        assert_eq!(col.child_bounds[0].y, 160.0);
461        assert_eq!(col.child_bounds[1].y, 180.0);
462    }
463
464    #[test]
465    fn test_column_alignment_center() {
466        let mut col = Column::new()
467            .main_axis_alignment(MainAxisAlignment::Center)
468            .child(FixedWidget::new(30.0, 20.0))
469            .child(FixedWidget::new(30.0, 20.0));
470
471        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
472
473        // 200 - 40 = 160 remaining, offset = 80
474        assert_eq!(col.child_bounds[0].y, 80.0);
475        assert_eq!(col.child_bounds[1].y, 100.0);
476    }
477
478    #[test]
479    fn test_column_alignment_space_between() {
480        let mut col = Column::new()
481            .main_axis_alignment(MainAxisAlignment::SpaceBetween)
482            .child(FixedWidget::new(30.0, 20.0))
483            .child(FixedWidget::new(30.0, 20.0));
484
485        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
486
487        // First at start, last at end
488        assert_eq!(col.child_bounds[0].y, 0.0);
489        assert_eq!(col.child_bounds[1].y, 180.0); // 200 - 20
490    }
491
492    #[test]
493    fn test_column_alignment_space_between_single_child() {
494        let mut col = Column::new()
495            .main_axis_alignment(MainAxisAlignment::SpaceBetween)
496            .child(FixedWidget::new(30.0, 20.0));
497
498        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
499
500        // Single child should be at start
501        assert_eq!(col.child_bounds[0].y, 0.0);
502    }
503
504    #[test]
505    fn test_column_alignment_space_between_three_children() {
506        let mut col = Column::new()
507            .main_axis_alignment(MainAxisAlignment::SpaceBetween)
508            .child(FixedWidget::new(30.0, 20.0))
509            .child(FixedWidget::new(30.0, 20.0))
510            .child(FixedWidget::new(30.0, 20.0));
511
512        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
513
514        // 200 - 60 = 140 remaining, gap = 70
515        assert_eq!(col.child_bounds[0].y, 0.0);
516        assert_eq!(col.child_bounds[1].y, 90.0); // 20 + 70
517        assert_eq!(col.child_bounds[2].y, 180.0); // 200 - 20
518    }
519
520    #[test]
521    fn test_column_alignment_space_around() {
522        let mut col = Column::new()
523            .main_axis_alignment(MainAxisAlignment::SpaceAround)
524            .child(FixedWidget::new(30.0, 40.0))
525            .child(FixedWidget::new(30.0, 40.0));
526
527        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
528
529        // 200 - 80 = 120 remaining, gap = 60, half-gap = 30
530        // First at 30, second at 30 + 40 + 60 = 130
531        assert_eq!(col.child_bounds[0].y, 30.0);
532        assert_eq!(col.child_bounds[1].y, 130.0);
533    }
534
535    #[test]
536    fn test_column_alignment_space_evenly() {
537        let mut col = Column::new()
538            .main_axis_alignment(MainAxisAlignment::SpaceEvenly)
539            .child(FixedWidget::new(30.0, 40.0))
540            .child(FixedWidget::new(30.0, 40.0));
541
542        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
543
544        // 200 - 80 = 120 remaining, 3 gaps (n+1), gap = 40
545        // First at 40, second at 40 + 40 + 40 = 120
546        assert_eq!(col.child_bounds[0].y, 40.0);
547        assert_eq!(col.child_bounds[1].y, 120.0);
548    }
549
550    // ===== CrossAxisAlignment Tests =====
551
552    #[test]
553    fn test_column_cross_alignment_start() {
554        let mut col = Column::new()
555            .cross_axis_alignment(CrossAxisAlignment::Start)
556            .child(FixedWidget::new(30.0, 20.0));
557
558        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
559
560        assert_eq!(col.child_bounds[0].x, 0.0);
561        assert_eq!(col.child_bounds[0].width, 30.0);
562    }
563
564    #[test]
565    fn test_column_cross_alignment_end() {
566        let mut col = Column::new()
567            .cross_axis_alignment(CrossAxisAlignment::End)
568            .child(FixedWidget::new(30.0, 20.0));
569
570        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
571
572        assert_eq!(col.child_bounds[0].x, 70.0); // 100 - 30
573        assert_eq!(col.child_bounds[0].width, 30.0);
574    }
575
576    #[test]
577    fn test_column_cross_alignment_center() {
578        let mut col = Column::new()
579            .cross_axis_alignment(CrossAxisAlignment::Center)
580            .child(FixedWidget::new(30.0, 20.0));
581
582        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
583
584        assert_eq!(col.child_bounds[0].x, 35.0); // (100 - 30) / 2
585        assert_eq!(col.child_bounds[0].width, 30.0);
586    }
587
588    #[test]
589    fn test_column_cross_alignment_stretch() {
590        let mut col = Column::new()
591            .cross_axis_alignment(CrossAxisAlignment::Stretch)
592            .child(FixedWidget::new(30.0, 20.0));
593
594        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
595
596        assert_eq!(col.child_bounds[0].x, 0.0);
597        assert_eq!(col.child_bounds[0].width, 100.0); // Stretched to container
598    }
599
600    // ===== Gap Tests =====
601
602    #[test]
603    fn test_column_gap_single_child() {
604        let mut col = Column::new().gap(20.0).child(FixedWidget::new(30.0, 20.0));
605
606        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
607
608        // Single child: no gap applied
609        assert_eq!(col.child_bounds[0].y, 0.0);
610    }
611
612    #[test]
613    fn test_column_gap_multiple_children() {
614        let mut col = Column::new()
615            .gap(15.0)
616            .child(FixedWidget::new(30.0, 20.0))
617            .child(FixedWidget::new(30.0, 20.0))
618            .child(FixedWidget::new(30.0, 20.0));
619
620        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
621
622        assert_eq!(col.child_bounds[0].y, 0.0);
623        assert_eq!(col.child_bounds[1].y, 35.0); // 20 + 15
624        assert_eq!(col.child_bounds[2].y, 70.0); // 35 + 20 + 15
625    }
626
627    #[test]
628    fn test_column_gap_with_alignment_center() {
629        let mut col = Column::new()
630            .gap(10.0)
631            .main_axis_alignment(MainAxisAlignment::Center)
632            .child(FixedWidget::new(30.0, 20.0))
633            .child(FixedWidget::new(30.0, 20.0));
634
635        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
636
637        // Total: 20 + 10 + 20 = 50, remaining = 150, offset = 75
638        assert_eq!(col.child_bounds[0].y, 75.0);
639        assert_eq!(col.child_bounds[1].y, 105.0); // 75 + 20 + 10
640    }
641
642    // ===== Edge Cases =====
643
644    #[test]
645    fn test_column_layout_empty() {
646        let mut col = Column::new();
647        let result = col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
648        assert_eq!(result.size, Size::ZERO);
649    }
650
651    #[test]
652    fn test_column_content_larger_than_bounds() {
653        let mut col = Column::new()
654            .child(FixedWidget::new(30.0, 100.0))
655            .child(FixedWidget::new(30.0, 100.0))
656            .child(FixedWidget::new(30.0, 100.0));
657
658        // Container only 200 tall, content is 300
659        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
660
661        // Children still placed sequentially (overflow)
662        assert_eq!(col.child_bounds[0].y, 0.0);
663        assert_eq!(col.child_bounds[1].y, 100.0);
664        assert_eq!(col.child_bounds[2].y, 200.0);
665    }
666
667    #[test]
668    fn test_column_with_offset_bounds() {
669        let mut col = Column::new()
670            .child(FixedWidget::new(30.0, 20.0))
671            .child(FixedWidget::new(30.0, 20.0));
672
673        col.layout(Rect::new(25.0, 50.0, 100.0, 200.0));
674
675        // Children should be offset by bounds origin
676        assert_eq!(col.child_bounds[0].y, 50.0);
677        assert_eq!(col.child_bounds[0].x, 60.0); // 25 + (100-30)/2
678        assert_eq!(col.child_bounds[1].y, 70.0);
679    }
680
681    #[test]
682    fn test_column_varying_child_widths() {
683        let mut col = Column::new()
684            .cross_axis_alignment(CrossAxisAlignment::Center)
685            .child(FixedWidget::new(20.0, 30.0))
686            .child(FixedWidget::new(60.0, 30.0))
687            .child(FixedWidget::new(40.0, 30.0));
688
689        col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
690
691        // All centered differently based on their widths
692        assert_eq!(col.child_bounds[0].x, 40.0); // (100-20)/2
693        assert_eq!(col.child_bounds[1].x, 20.0); // (100-60)/2
694        assert_eq!(col.child_bounds[2].x, 30.0); // (100-40)/2
695    }
696}