Skip to main content

rosace_widgets/tree/
row.rs

1use std::sync::Mutex;
2use rosace_core::types::Size;
3use rosace_layout::{Constraints, CrossAxisAlignment, MainAxisAlignment, layout_row};
4use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w, avail_h, offset, rect_at};
5use super::padding::EdgeInsets;
6
7/// Horizontal flex container. Children are arranged left-to-right.
8///
9/// [`Expanded`] children automatically receive leftover horizontal space.
10///
11/// [`Expanded`]: super::spacer::Expanded
12pub struct Row {
13    children: Vec<BoxedWidget>,
14    spacing: f32,
15    main_axis_alignment: MainAxisAlignment,
16    cross_axis_alignment: CrossAxisAlignment,
17    padding: EdgeInsets,
18    measure_cache: Mutex<Option<(Constraints, Vec<Size>)>>,
19}
20
21impl Row {
22    pub fn new() -> Self {
23        Self {
24            children: Vec::new(),
25            spacing: 0.0,
26            main_axis_alignment: MainAxisAlignment::Start,
27            cross_axis_alignment: CrossAxisAlignment::Center,
28            padding: EdgeInsets::default(),
29            measure_cache: Mutex::new(None),
30        }
31    }
32
33    pub fn spacing(mut self, s: f32) -> Self { self.spacing = s; self }
34    pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
35    pub fn main_axis_alignment(mut self, a: MainAxisAlignment) -> Self { self.main_axis_alignment = a; self }
36    pub fn cross_axis_alignment(mut self, a: CrossAxisAlignment) -> Self { self.cross_axis_alignment = a; self }
37
38    pub fn child(mut self, w: impl Widget + 'static) -> Self {
39        self.children.push(Box::new(w)); self
40    }
41    pub fn children(mut self, ws: Vec<BoxedWidget>) -> Self {
42        self.children.extend(ws); self
43    }
44
45    /// Wrap this flex container in a ScrollView scrolling horizontally
46    /// (D101: position is implicit per-node state — zero wiring).
47    /// Expanded children are ignored on the unbounded scroll axis.
48    pub fn scrollable(self) -> super::ScrollView {
49        super::ScrollView::horizontal(self)
50    }
51
52    fn measure(&self, ctx: &LayoutCtx) -> Vec<Size> {
53        let c = ctx.constraints;
54        {
55            let cache = self.measure_cache.lock().unwrap();
56            if let Some((cached_c, ref sizes)) = *cache {
57                if cached_c == c { return sizes.clone(); }
58            }
59        }
60
61        let max_w = (avail_w(c) - self.padding.total_h()).max(0.0);
62        let max_h = (avail_h(c) - self.padding.total_v()).max(0.0);
63        let n = self.children.len();
64        let gap_total = if n > 1 { self.spacing * (n - 1) as f32 } else { 0.0 };
65
66        let total_flex: f32 = self.children.iter().map(|c| c.flex_factor()).sum();
67        // Unbounded-axis doctrine (API_DESIGN §6): flex needs a finite main
68        // axis. Inside a horizontal ScrollView, Expanded children size to
69        // content instead of erroring.
70        let flex_enabled = total_flex > 0.0 && max_w.is_finite();
71        #[cfg(debug_assertions)]
72        if total_flex > 0.0 && !flex_enabled {
73            static WARNED: std::sync::Once = std::sync::Once::new();
74            WARNED.call_once(|| {
75                eprintln!(
76                    "[ROSACE] Row: Expanded child inside an unbounded width \
77                     (e.g. a horizontal ScrollView) — flex is ignored, the child \
78                     sizes to its content. Give the Row a bounded width to flex."
79                );
80            });
81        }
82        let fixed_w: f32 = self.children.iter()
83            .filter(|c| !flex_enabled || c.flex_factor() == 0.0)
84            .map(|c| c.layout(&ctx.with_constraints(Constraints::loose(max_w, max_h))).width)
85            .sum::<f32>() + gap_total;
86
87        let flex_pool = (max_w - fixed_w).max(0.0);
88
89        let sizes: Vec<Size> = self.children.iter().map(|c| {
90            let ff = c.flex_factor();
91            if ff > 0.0 && flex_enabled {
92                let w = flex_pool * ff / total_flex;
93                c.layout(&ctx.with_constraints(Constraints::tight(w, max_h)))
94            } else {
95                c.layout(&ctx.with_constraints(Constraints::loose(max_w, max_h)))
96            }
97        }).collect();
98
99        *self.measure_cache.lock().unwrap() = Some((c, sizes.clone()));
100        sizes
101    }
102
103    /// Paint-path sizes: reuse whatever layout() measured this frame — see
104    /// Column::layout_sizes for why paint must never re-measure.
105    fn layout_sizes(&self, ctx: &LayoutCtx) -> Vec<Size> {
106        if let Some((_, sizes)) = &*self.measure_cache.lock().unwrap() {
107            return sizes.clone();
108        }
109        self.measure(ctx)
110    }
111}
112
113impl Default for Row {
114    fn default() -> Self { Self::new() }
115}
116
117impl Widget for Row {
118    fn layout(&self, ctx: &LayoutCtx) -> Size {
119        let sizes = self.measure(ctx);
120        let c = ctx.constraints;
121        // Preserve incoming minimums (unbounded-axis doctrine) so alignment
122        // can distribute against a viewport-sized minimum.
123        let (pad_h, pad_v) = (self.padding.total_h(), self.padding.total_v());
124        let inner_c = Constraints {
125            min_width:  (c.min_width - pad_h).max(0.0),
126            max_width:  super::shrink_axis(c.max_width, pad_h),
127            min_height: (c.min_height - pad_v).max(0.0),
128            max_height: super::shrink_axis(c.max_height, pad_v),
129        };
130        let result = layout_row(inner_c, &sizes,
131            self.main_axis_alignment, self.cross_axis_alignment, self.spacing);
132        self.padding.grow(result.size)
133    }
134
135    fn paint(&self, ctx: &mut PaintCtx) {
136        let inner_rect = self.padding.shrink(ctx.rect);
137        // Tight to the allotted rect — measure and paint agree on extra space.
138        let inner_c = Constraints::tight(inner_rect.size.width, inner_rect.size.height);
139        let lctx = ctx.layout_ctx(inner_c);
140        let sizes = self.layout_sizes(&lctx);
141        let result = layout_row(inner_c, &sizes,
142            self.main_axis_alignment, self.cross_axis_alignment, self.spacing);
143        for (i, child) in self.children.iter().enumerate() {
144            let pos = result.child_positions[i];
145            let child_rect = rect_at(offset(inner_rect.origin, pos.x, pos.y), sizes[i]);
146            child.paint(&mut ctx.child(child_rect));
147        }
148    }
149}