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