presentar_widgets/
column.rs

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