presentar_widgets/
row.rs

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