Skip to main content

agg_gui/widgets/
flex_row.rs

1//! `FlexRow`: horizontal flex layout (children left-to-right).
2//!
3//! Split out of `flex.rs` to keep both files under the workspace 800-line
4//! guardrail. Shares the flex algorithm and Y-up conventions documented on
5//! [`crate::widgets::flex`]; `FlexColumn` lives there.
6//!
7//! `FlexRow` reads each child's `v_anchor()` to place it vertically within
8//! the row's inner height (see [`place_cross_v`]).
9
10use crate::color::Color;
11use crate::draw_ctx::DrawCtx;
12use crate::event::{Event, EventResult};
13use crate::geometry::{Rect, Size};
14use crate::widgets::flex::DEFAULT_ROW_GAP;
15use crate::layout_props::{resolve_fit_or_stretch, HAnchor, Insets, VAnchor, WidgetBase};
16use crate::widget::Widget;
17
18/// Compute `(y, actual_height)` for a child in a `FlexRow` (vertical
19/// cross-axis placement, Y-up).
20///
21/// - `pad_b`     — row's bottom inner-padding offset.
22/// - `inner_h`   — row's usable height (after padding, before margins).
23/// - `margin_b/t` — child's bottom/top margins (logical units).
24/// - `natural_h` — height returned by `child.layout()`.
25/// - `min_h/max_h` — child's min/max height constraints.
26fn place_cross_v(
27    anchor: VAnchor,
28    pad_b: f64,
29    inner_h: f64,
30    margin_b: f64,
31    margin_t: f64,
32    natural_h: f64,
33    min_h: f64,
34    max_h: f64,
35) -> (f64, f64) {
36    let slot_h = (inner_h - margin_b - margin_t).max(0.0);
37
38    // Determine height.
39    let actual_h = if anchor.is_stretch() {
40        slot_h.clamp(min_h, max_h)
41    } else if anchor == VAnchor::MAX_FIT_OR_STRETCH {
42        resolve_fit_or_stretch(natural_h, slot_h, true).clamp(min_h, max_h)
43    } else if anchor == VAnchor::MIN_FIT_OR_STRETCH {
44        resolve_fit_or_stretch(natural_h, slot_h, false).clamp(min_h, max_h)
45    } else {
46        natural_h.clamp(min_h, max_h)
47    };
48
49    // Determine y position (Y-up: BOTTOM = low Y, TOP = high Y).
50    let y = if anchor.contains(VAnchor::TOP) && !anchor.contains(VAnchor::BOTTOM) {
51        // TOP only: top-align in slot.
52        (pad_b + inner_h - margin_t - actual_h).max(pad_b)
53    } else if anchor.contains(VAnchor::CENTER) && !anchor.is_stretch() {
54        // CENTER: center within margin slot.
55        pad_b + margin_b + (slot_h - actual_h) * 0.5
56    } else {
57        // BOTTOM, STRETCH, FIT, ABSOLUTE — bottom-align.
58        pad_b + margin_b
59    };
60
61    (y, actual_h)
62}
63
64/// Arranges children left-to-right (first child = leftmost).
65pub struct FlexRow {
66    bounds: Rect,
67    children: Vec<Box<dyn Widget>>,
68    flex_factors: Vec<f64>,
69    base: WidgetBase,
70    pub gap: f64,
71    pub inner_padding: Insets,
72    pub background: Color,
73    /// When `true`, `layout` reports the row's natural content width
74    /// (sum of fixed children + gaps + horizontal padding) instead of the
75    /// full `available.width`. Mirrors [`crate::widgets::FlexColumn`]'s
76    /// `fit_width` — needed when the row is floated by an auto-sized
77    /// ancestor (e.g. a `Stack` `add_aligned` overlay) that must hug the
78    /// content rather than span the whole stack. Off by default for
79    /// backward compatibility.
80    pub fit_width: bool,
81}
82
83impl FlexRow {
84    pub fn new() -> Self {
85        Self {
86            bounds: Rect::default(),
87            children: Vec::new(),
88            flex_factors: Vec::new(),
89            base: WidgetBase::new(),
90            gap: DEFAULT_ROW_GAP,
91            inner_padding: Insets::ZERO,
92            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
93            fit_width: false,
94        }
95    }
96
97    pub fn with_gap(mut self, gap: f64) -> Self {
98        self.gap = gap;
99        self
100    }
101
102    /// Opt into content-fit width — see [`FlexRow::fit_width`].
103    pub fn with_fit_width(mut self, fit: bool) -> Self {
104        self.fit_width = fit;
105        self
106    }
107    pub fn with_padding(mut self, p: f64) -> Self {
108        self.inner_padding = Insets::all(p);
109        self
110    }
111    pub fn with_inner_padding(mut self, p: Insets) -> Self {
112        self.inner_padding = p;
113        self
114    }
115    pub fn with_background(mut self, c: Color) -> Self {
116        self.background = c;
117        self
118    }
119
120    pub fn with_margin(mut self, m: Insets) -> Self {
121        self.base.margin = m;
122        self
123    }
124    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
125        self.base.h_anchor = h;
126        self
127    }
128    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
129        self.base.v_anchor = v;
130        self
131    }
132    pub fn with_min_size(mut self, s: Size) -> Self {
133        self.base.min_size = s;
134        self
135    }
136    pub fn with_max_size(mut self, s: Size) -> Self {
137        self.base.max_size = s;
138        self
139    }
140
141    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
142        self.children.push(child);
143        self.flex_factors.push(0.0);
144        self
145    }
146
147    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
148        self.children.push(child);
149        self.flex_factors.push(flex.max(0.0));
150        self
151    }
152
153    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
154        self.children.push(child);
155        self.flex_factors.push(flex.max(0.0));
156    }
157}
158
159impl Default for FlexRow {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl Widget for FlexRow {
166    fn type_name(&self) -> &'static str {
167        "FlexRow"
168    }
169    fn bounds(&self) -> Rect {
170        self.bounds
171    }
172    fn set_bounds(&mut self, b: Rect) {
173        self.bounds = b;
174    }
175    fn children(&self) -> &[Box<dyn Widget>] {
176        &self.children
177    }
178    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
179        &mut self.children
180    }
181
182    fn margin(&self) -> Insets {
183        self.base.margin
184    }
185    fn widget_base(&self) -> Option<&WidgetBase> {
186        Some(&self.base)
187    }
188    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
189        Some(&mut self.base)
190    }
191    fn padding(&self) -> Insets {
192        self.inner_padding
193    }
194    fn h_anchor(&self) -> HAnchor {
195        self.base.h_anchor
196    }
197    fn v_anchor(&self) -> VAnchor {
198        self.base.v_anchor
199    }
200    fn min_size(&self) -> Size {
201        self.base.min_size
202    }
203    fn max_size(&self) -> Size {
204        self.base.max_size
205    }
206
207    fn layout(&mut self, available: Size) -> Size {
208        let pad_l = self.inner_padding.left;
209        let pad_r = self.inner_padding.right;
210        let pad_t = self.inner_padding.top;
211        let pad_b = self.inner_padding.bottom;
212        let gap = self.gap;
213        let n = self.children.len();
214        if n == 0 {
215            return available;
216        }
217
218        let inner_w = (available.width - pad_l - pad_r).max(0.0);
219        let inner_h = (available.height - pad_t - pad_b).max(0.0);
220
221        // Child margins (logical units end-to-end; DPI applied at paint).
222        let margins: Vec<Insets> = self.children.iter().map(|c| c.margin()).collect();
223
224        // -------------------------------------------------------------------
225        // Step 1: measure fixed children on the main (horizontal) axis.
226        // -------------------------------------------------------------------
227        let mut content_widths = vec![0.0f64; n];
228        let mut total_fixed_with_margins = 0.0f64;
229        let mut total_flex = 0.0f64;
230        let mut total_flex_margin_h = 0.0f64;
231
232        for i in 0..n {
233            if self.flex_factors[i] == 0.0 {
234                let m = &margins[i];
235                let slot_h = (inner_h - m.bottom - m.top).max(0.0);
236                // Pass inner_w as available width so the child can report its
237                // natural width.
238                let desired = self.children[i].layout(Size::new(inner_w, slot_h));
239                content_widths[i] = desired.width.clamp(
240                    self.children[i].min_size().width,
241                    self.children[i].max_size().width,
242                );
243            }
244        }
245
246        // Visibility is read AFTER measuring: a child that hides itself
247        // (collapsed `Conditional`, an empty self-hiding widget) consumes no
248        // slot, margin, or gap — the contract documented on
249        // [`crate::widgets::Conditional`]. Mirrors `FlexColumn`.
250        let visible: Vec<bool> = self.children.iter().map(|c| c.is_visible()).collect();
251        let visible_n = visible.iter().filter(|v| **v).count();
252        let total_gap = if visible_n > 1 {
253            gap * (visible_n - 1) as f64
254        } else {
255            0.0
256        };
257
258        for i in 0..n {
259            if !visible[i] {
260                continue;
261            }
262            let m = &margins[i];
263            if self.flex_factors[i] == 0.0 {
264                total_fixed_with_margins += content_widths[i] + m.horizontal();
265            } else {
266                total_flex += self.flex_factors[i];
267                total_flex_margin_h += m.horizontal();
268            }
269        }
270
271        // -------------------------------------------------------------------
272        // Step 2: distribute remaining space to flex children.
273        // -------------------------------------------------------------------
274        let remaining =
275            (inner_w - total_fixed_with_margins - total_gap - total_flex_margin_h).max(0.0);
276        let flex_unit = if total_flex > 0.0 {
277            remaining / total_flex
278        } else {
279            0.0
280        };
281
282        for i in 0..n {
283            if self.flex_factors[i] > 0.0 && visible[i] {
284                let raw = self.flex_factors[i] * flex_unit;
285                content_widths[i] = raw.clamp(
286                    self.children[i].min_size().width,
287                    self.children[i].max_size().width,
288                );
289            }
290        }
291
292        // -------------------------------------------------------------------
293        // Step 3: place children left-to-right with cross-axis anchoring.
294        // -------------------------------------------------------------------
295        let mut cursor_x = pad_l;
296        let mut max_slot_h = 0.0f64; // tallest slot (content + margins)
297
298        for i in 0..n {
299            let m = &margins[i];
300            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
301            let content_w = content_widths[i];
302
303            if !visible[i] {
304                // Hidden slot: zero-size bounds at the cursor, no margins,
305                // no gap, no cursor advance — as if the child weren't there.
306                self.children[i].set_bounds(Rect::new(cursor_x, pad_b + m.bottom, 0.0, 0.0));
307                continue;
308            }
309
310            // Advance past left margin.
311            cursor_x += m.left;
312
313            // Layout child to get natural height for cross-axis placement.
314            let desired = self.children[i].layout(Size::new(content_w, slot_h));
315            let natural_h = desired.height;
316            let v_anchor = self.children[i].v_anchor();
317            let min_h = self.children[i].min_size().height;
318            let max_h = self.children[i].max_size().height;
319
320            let (child_y, child_h) = place_cross_v(
321                v_anchor, pad_b, inner_h, m.bottom, m.top, natural_h, min_h, max_h,
322            );
323
324            // Round to integers — same reason as FlexColumn (pixel-perfect blits).
325            let final_w = content_w.round();
326            let final_h = child_h.round();
327            // Re-layout at the final assigned box. The measure pass above used
328            // the full slot height, so a fit-content child (e.g. a shorter
329            // FlexColumn) top-anchored its own children for that taller slot.
330            // Without this, those grandchildren keep their tall-slot positions
331            // and fall outside the child's now-shorter bounds → clipped away
332            // (the "flyout opens but its buttons never paint" bug).
333            if (final_h - slot_h).abs() > 0.5 || (final_w - content_w).abs() > 0.5 {
334                self.children[i].layout(Size::new(final_w, final_h));
335            }
336            self.children[i].set_bounds(Rect::new(
337                cursor_x.round(),
338                child_y.round(),
339                final_w,
340                final_h,
341            ));
342            max_slot_h = max_slot_h.max(child_h + m.vertical());
343
344            // Advance past content width, right margin, and inter-child gap.
345            cursor_x += content_w + m.right + gap;
346        }
347
348        // Return the natural (intrinsic) height to avoid propagating huge
349        // heights from ScrollView (which passes f64::MAX/2) through fixed rows.
350        let natural_h = max_slot_h + pad_t + pad_b;
351        // Width: full available by default (legacy). `fit_width` reports the
352        // content extent so an auto-sized parent can hug the row.
353        let reported_w = if self.fit_width {
354            pad_l + pad_r + total_fixed_with_margins + total_gap
355        } else {
356            available.width
357        };
358        Size::new(reported_w, natural_h)
359    }
360
361    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
362        if self.background.a > 0.001 {
363            let w = self.bounds.width;
364            let h = self.bounds.height;
365            ctx.set_fill_color(self.background);
366            ctx.begin_path();
367            ctx.rect(0.0, 0.0, w, h);
368            ctx.fill();
369        }
370    }
371
372    fn on_event(&mut self, _: &Event) -> EventResult {
373        EventResult::Ignored
374    }
375}