Skip to main content

agg_gui/widgets/
flex.rs

1//! Flex layout widgets: `FlexColumn` (vertical) and `FlexRow` (horizontal).
2//!
3//! # Y-up layout convention
4//!
5//! `FlexColumn` stacks children **top to bottom** visually, which in Y-up
6//! coordinates means the *first* child gets the *highest* Y values. The layout
7//! cursor starts at the top of the available area and moves downward.
8//!
9//! `FlexRow` stacks children **left to right**, as expected.
10//!
11//! # Flex algorithm
12//!
13//! Each child has a `flex` factor (stored in a parallel `Vec<f64>`):
14//! - `flex = 0.0` → "fixed": the child is laid out at its natural size on
15//!   the main axis.
16//! - `flex > 0.0` → "growing": the child receives a proportional share of
17//!   the remaining space after all fixed children are measured.
18//!
19//! Children with equal `flex` values split remaining space equally.
20//!
21//! # Child margin support
22//!
23//! Each child's `margin()` (scaled by `device_scale`) contributes to the slot
24//! size on the main axis and is respected for cross-axis placement.
25//! Margins are **additive** — child A's `margin.top` and child B's
26//! `margin.bottom` both contribute gap space between those children (in
27//! addition to `self.gap`).
28//!
29//! # Cross-axis anchoring
30//!
31//! `FlexColumn` reads each child's `h_anchor()` to place it horizontally
32//! within the column's inner width.  `FlexRow` reads `v_anchor()` to place
33//! children vertically within the row's inner height.
34
35use crate::color::Color;
36use crate::device_scale::device_scale;
37use crate::draw_ctx::DrawCtx;
38use crate::event::{Event, EventResult};
39use crate::geometry::{Rect, Size};
40use crate::layout_props::{resolve_fit_or_stretch, HAnchor, Insets, VAnchor, WidgetBase};
41use crate::widget::Widget;
42
43// ---------------------------------------------------------------------------
44// Cross-axis placement helpers
45// ---------------------------------------------------------------------------
46
47/// Compute `(x, actual_width)` for a child in a `FlexColumn` (horizontal
48/// cross-axis placement).
49///
50/// - `pad_l`     — column's left inner-padding offset.
51/// - `inner_w`   — column's usable width (after padding, before margins).
52/// - `margin_l/r` — child's scaled left/right margins.
53/// - `natural_w` — width returned by `child.layout()`.
54/// - `min_w/max_w` — child's min/max width constraints.
55fn place_cross_h(
56    anchor: HAnchor,
57    pad_l: f64,
58    inner_w: f64,
59    margin_l: f64,
60    margin_r: f64,
61    natural_w: f64,
62    min_w: f64,
63    max_w: f64,
64) -> (f64, f64) {
65    let slot_w = (inner_w - margin_l - margin_r).max(0.0);
66
67    // Determine width.
68    let actual_w = if anchor.is_stretch() {
69        // LEFT | RIGHT → fill slot
70        slot_w.clamp(min_w, max_w)
71    } else if anchor == HAnchor::MAX_FIT_OR_STRETCH {
72        resolve_fit_or_stretch(natural_w, slot_w, true).clamp(min_w, max_w)
73    } else if anchor == HAnchor::MIN_FIT_OR_STRETCH {
74        resolve_fit_or_stretch(natural_w, slot_w, false).clamp(min_w, max_w)
75    } else {
76        // FIT, LEFT, RIGHT, CENTER, ABSOLUTE — use natural width.
77        natural_w.clamp(min_w, max_w)
78    };
79
80    // Determine x position.
81    let x = if anchor.contains(HAnchor::RIGHT) && !anchor.contains(HAnchor::LEFT) {
82        // RIGHT only (not stretch): right-align within margin slot.
83        (pad_l + inner_w - margin_r - actual_w).max(pad_l)
84    } else if anchor.contains(HAnchor::CENTER) && !anchor.is_stretch() {
85        // CENTER: center within margin slot.
86        pad_l + margin_l + (slot_w - actual_w) * 0.5
87    } else {
88        // LEFT, STRETCH, FIT, ABSOLUTE, MIN/MAX_FIT_OR_STRETCH — left-align.
89        pad_l + margin_l
90    };
91
92    (x, actual_w)
93}
94
95/// Compute `(y, actual_height)` for a child in a `FlexRow` (vertical
96/// cross-axis placement, Y-up).
97///
98/// - `pad_b`     — row's bottom inner-padding offset.
99/// - `inner_h`   — row's usable height (after padding, before margins).
100/// - `margin_b/t` — child's scaled bottom/top margins.
101/// - `natural_h` — height returned by `child.layout()`.
102/// - `min_h/max_h` — child's min/max height constraints.
103fn place_cross_v(
104    anchor: VAnchor,
105    pad_b: f64,
106    inner_h: f64,
107    margin_b: f64,
108    margin_t: f64,
109    natural_h: f64,
110    min_h: f64,
111    max_h: f64,
112) -> (f64, f64) {
113    let slot_h = (inner_h - margin_b - margin_t).max(0.0);
114
115    // Determine height.
116    let actual_h = if anchor.is_stretch() {
117        slot_h.clamp(min_h, max_h)
118    } else if anchor == VAnchor::MAX_FIT_OR_STRETCH {
119        resolve_fit_or_stretch(natural_h, slot_h, true).clamp(min_h, max_h)
120    } else if anchor == VAnchor::MIN_FIT_OR_STRETCH {
121        resolve_fit_or_stretch(natural_h, slot_h, false).clamp(min_h, max_h)
122    } else {
123        natural_h.clamp(min_h, max_h)
124    };
125
126    // Determine y position (Y-up: BOTTOM = low Y, TOP = high Y).
127    let y = if anchor.contains(VAnchor::TOP) && !anchor.contains(VAnchor::BOTTOM) {
128        // TOP only: top-align in slot.
129        (pad_b + inner_h - margin_t - actual_h).max(pad_b)
130    } else if anchor.contains(VAnchor::CENTER) && !anchor.is_stretch() {
131        // CENTER: center within margin slot.
132        pad_b + margin_b + (slot_h - actual_h) * 0.5
133    } else {
134        // BOTTOM, STRETCH, FIT, ABSOLUTE — bottom-align.
135        pad_b + margin_b
136    };
137
138    (y, actual_h)
139}
140
141// ---------------------------------------------------------------------------
142// FlexColumn
143// ---------------------------------------------------------------------------
144
145/// Stacks children top-to-bottom (first child = visually topmost).
146pub struct FlexColumn {
147    bounds: Rect,
148    children: Vec<Box<dyn Widget>>,
149    /// Parallel to `children`. 0.0 = fixed; >0 = flex fraction.
150    flex_factors: Vec<f64>,
151    base: WidgetBase,
152    pub gap: f64,
153    pub inner_padding: Insets,
154    pub background: Color,
155    /// When `true`, paint background using `ctx.visuals().panel_fill`
156    /// regardless of the stored `background` colour.
157    pub use_panel_bg: bool,
158    /// When `true`, `layout` reports the column's natural content
159    /// width (max over children, + horizontal padding) instead of the
160    /// full `available.width`.  Used by auto-sized ancestors that
161    /// want the column to shrink-to-content rather than stretch.
162    /// Off by default for backward compatibility.
163    pub fit_width: bool,
164    /// When `true`, children are anchored to the TOP of the column's
165    /// inner area, with any extra height appearing as whitespace at
166    /// the BOTTOM.  Off by default — legacy callers (e.g. ScrollView
167    /// content) rely on the natural-anchored layout where children
168    /// occupy the BOTTOM of their slot when oversized.
169    pub top_anchor: bool,
170}
171
172impl FlexColumn {
173    pub fn new() -> Self {
174        Self {
175            bounds: Rect::default(),
176            children: Vec::new(),
177            flex_factors: Vec::new(),
178            base: WidgetBase::new(),
179            gap: 0.0,
180            inner_padding: Insets::ZERO,
181            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
182            use_panel_bg: false,
183            fit_width: false,
184            top_anchor: false,
185        }
186    }
187
188    pub fn with_gap(mut self, gap: f64) -> Self {
189        self.gap = gap;
190        self
191    }
192    pub fn with_padding(mut self, p: f64) -> Self {
193        self.inner_padding = Insets::all(p);
194        self
195    }
196    pub fn with_inner_padding(mut self, p: Insets) -> Self {
197        self.inner_padding = p;
198        self
199    }
200    pub fn with_background(mut self, c: Color) -> Self {
201        self.background = c;
202        self
203    }
204    /// Use `ctx.visuals().panel_fill` as background instead of the stored color.
205    pub fn with_panel_bg(mut self) -> Self {
206        self.use_panel_bg = true;
207        self
208    }
209
210    /// Opt into content-fit width — `layout` reports the widest
211    /// child's natural width (+ horizontal padding) instead of the
212    /// full available width.  Required when this column is the
213    /// content of an auto-sized `Window`; without it, wrapped Labels
214    /// claim the full available width and the window grows to the
215    /// canvas.  Matches egui's per-column shrink-to-content option.
216    pub fn with_fit_width(mut self, fit: bool) -> Self {
217        self.fit_width = fit;
218        self
219    }
220
221    /// Anchor children to the TOP of the inner area rather than the
222    /// bottom of the natural content extent.  Default is bottom (the
223    /// classic Y-up "natural-anchored" placement) so callers like
224    /// `ScrollView` whose layout pass uses `available.height ≈ ∞`
225    /// keep working — they need cursor_y to be derived from natural
226    /// extent, not from the supplied (huge) available.  Opt in for
227    /// containers placed inside a `Resize` widget or other oversized
228    /// slot where you want the visible content to start at the top
229    /// of the frame and any extra space to appear below.
230    pub fn with_top_anchor(mut self, on: bool) -> Self {
231        self.top_anchor = on;
232        self
233    }
234
235    pub fn with_margin(mut self, m: Insets) -> Self {
236        self.base.margin = m;
237        self
238    }
239    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
240        self.base.h_anchor = h;
241        self
242    }
243    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
244        self.base.v_anchor = v;
245        self
246    }
247    pub fn with_min_size(mut self, s: Size) -> Self {
248        self.base.min_size = s;
249        self
250    }
251    pub fn with_max_size(mut self, s: Size) -> Self {
252        self.base.max_size = s;
253        self
254    }
255
256    /// Add a fixed-size child (flex = 0).
257    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
258        self.children.push(child);
259        self.flex_factors.push(0.0);
260        self
261    }
262
263    /// Add a flex child that expands proportionally.
264    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
265        self.children.push(child);
266        self.flex_factors.push(flex.max(0.0));
267        self
268    }
269
270    /// Push a child directly (for use without builder chaining).
271    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
272        self.children.push(child);
273        self.flex_factors.push(flex.max(0.0));
274    }
275}
276
277impl Default for FlexColumn {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283impl Widget for FlexColumn {
284    fn type_name(&self) -> &'static str {
285        "FlexColumn"
286    }
287    fn bounds(&self) -> Rect {
288        self.bounds
289    }
290    fn set_bounds(&mut self, b: Rect) {
291        self.bounds = b;
292    }
293    fn children(&self) -> &[Box<dyn Widget>] {
294        &self.children
295    }
296    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
297        &mut self.children
298    }
299
300    fn margin(&self) -> Insets {
301        self.base.margin
302    }
303    fn h_anchor(&self) -> HAnchor {
304        self.base.h_anchor
305    }
306    fn v_anchor(&self) -> VAnchor {
307        self.base.v_anchor
308    }
309    fn min_size(&self) -> Size {
310        self.base.min_size
311    }
312    fn max_size(&self) -> Size {
313        self.base.max_size
314    }
315
316    fn measure_min_height(&self, available_w: f64) -> f64 {
317        // Sum each child's required height (recursing through any
318        // FlexColumn / TextArea / Container chains) plus our own
319        // padding and inter-child gaps.  Used by ancestor
320        // `Window::tight_content_fit` to compute a content-bound
321        // height even when one of our children is a flex-fill widget
322        // whose `layout` would just return the available slot.
323        let pad_l = self.inner_padding.left;
324        let pad_r = self.inner_padding.right;
325        let pad_t = self.inner_padding.top;
326        let pad_b = self.inner_padding.bottom;
327        let inner_w = (available_w - pad_l - pad_r).max(0.0);
328        let scale = device_scale();
329        let n = self.children.len();
330        let mut total = 0.0_f64;
331        for child in self.children.iter() {
332            let m = child.margin().scale(scale);
333            let slot_w = (inner_w - m.left - m.right).max(0.0);
334            total += child.measure_min_height(slot_w) + m.vertical();
335        }
336        total += pad_t + pad_b;
337        if n > 1 {
338            total += self.gap * (n - 1) as f64;
339        }
340        total.max(self.base.min_size.height)
341    }
342
343    fn layout(&mut self, available: Size) -> Size {
344        let pad_l = self.inner_padding.left;
345        let pad_r = self.inner_padding.right;
346        let pad_t = self.inner_padding.top;
347        let pad_b = self.inner_padding.bottom;
348        let gap = self.gap;
349        let n = self.children.len();
350        if n == 0 {
351            return available;
352        }
353
354        let inner_w = (available.width - pad_l - pad_r).max(0.0);
355        let inner_h = (available.height - pad_t - pad_b).max(0.0);
356
357        // Scaled margins for all children (physical units).
358        let scale = device_scale();
359        let margins: Vec<Insets> = self
360            .children
361            .iter()
362            .map(|c| c.margin().scale(scale))
363            .collect();
364
365        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
366
367        // -------------------------------------------------------------------
368        // Step 1: measure fixed children on the main (vertical) axis.
369        //
370        // The slot for each fixed child = content_h + margin_top + margin_bottom.
371        // Flex children contribute only their margins to the space budget.
372        // -------------------------------------------------------------------
373        let mut content_heights = vec![0.0f64; n];
374        let mut total_fixed_with_margins = 0.0f64;
375        let mut total_flex = 0.0f64;
376        let mut total_flex_margin_v = 0.0f64;
377        let mut max_child_natural_w = 0.0f64;
378
379        for i in 0..n {
380            let m = &margins[i];
381            let slot_w = (inner_w - m.left - m.right).max(0.0);
382            if self.flex_factors[i] == 0.0 {
383                // Measure at natural height; pass inner_h as the available
384                // height so the child can self-report its natural size.
385                let desired = self.children[i].layout(Size::new(slot_w, inner_h));
386                let clamped_h = desired.height.clamp(
387                    self.children[i].min_size().height,
388                    self.children[i].max_size().height,
389                );
390                content_heights[i] = clamped_h;
391                total_fixed_with_margins += clamped_h + m.vertical();
392                max_child_natural_w = max_child_natural_w.max(desired.width + m.horizontal());
393            } else {
394                total_flex += self.flex_factors[i];
395                total_flex_margin_v += m.vertical();
396            }
397        }
398
399        // -------------------------------------------------------------------
400        // Step 2: distribute remaining space to flex children.
401        // -------------------------------------------------------------------
402        let remaining =
403            (inner_h - total_fixed_with_margins - total_gap - total_flex_margin_v).max(0.0);
404        let flex_unit = if total_flex > 0.0 {
405            remaining / total_flex
406        } else {
407            0.0
408        };
409
410        for i in 0..n {
411            if self.flex_factors[i] > 0.0 {
412                let raw = self.flex_factors[i] * flex_unit;
413                content_heights[i] = raw.clamp(
414                    self.children[i].min_size().height,
415                    self.children[i].max_size().height,
416                );
417            }
418        }
419
420        // Natural content height (all-fixed case) determines the column's
421        // reported size when there are no flex children.
422        let natural_content_h = total_fixed_with_margins + total_gap;
423        let effective_h = if total_flex > 0.0 {
424            inner_h
425        } else {
426            natural_content_h
427        };
428
429        // -------------------------------------------------------------------
430        // Step 3: place children top-to-bottom.
431        //
432        // In Y-up coordinates "top" = high Y.  Two cursor seeds:
433        //
434        //   - **Default**: start at the top of the finite inner area,
435        //     matching egui's top-down layout.  When a parent passes a
436        //     deliberately huge height to measure natural content (the
437        //     `ScrollView` path), fall back to the natural-content extent
438        //     so children keep finite y-coordinates.
439        //
440        //   - **`top_anchor=true`**: always start at the top of the inner
441        //     area, even for very tall measurement slots.
442        let measuring_natural_height = available.height > 1.0e9;
443        let mut cursor_y = if self.top_anchor || !measuring_natural_height {
444            available.height - pad_t
445        } else {
446            pad_b + effective_h
447        };
448
449        for i in 0..n {
450            let m = &margins[i];
451            let slot_w = (inner_w - m.left - m.right).max(0.0);
452            let content_h = content_heights[i];
453
454            // Subtract top margin first (moves cursor toward lower Y = downward).
455            cursor_y -= m.top;
456            let child_bottom = cursor_y - content_h;
457
458            // Layout child to obtain its natural width for cross-axis placement.
459            let desired = self.children[i].layout(Size::new(slot_w, content_h));
460            let natural_w = desired.width;
461            let h_anchor = self.children[i].h_anchor();
462            let min_w = self.children[i].min_size().width;
463            let max_w = self.children[i].max_size().width;
464
465            let (child_x, child_w) = place_cross_h(
466                h_anchor, pad_l, inner_w, m.left, m.right, natural_w, min_w, max_w,
467            );
468
469            // Round to integers so bitmap content (cached text, images) lands on
470            // exact pixel boundaries and isn't sub-pixel sampled into blur.
471            self.children[i].set_bounds(Rect::new(
472                child_x.round(),
473                child_bottom.round(),
474                child_w.round(),
475                content_h.round(),
476            ));
477
478            // Advance cursor past bottom margin and inter-child gap.
479            cursor_y = child_bottom - m.bottom - gap;
480        }
481
482        // Return natural size for all-fixed layouts so ScrollView can read
483        // the true content height from layout()'s return value.
484        //
485        // Width: by default we report the full available width (legacy
486        // behaviour many callers rely on).  `fit_width(true)` opts in
487        // to reporting the widest non-flex child's natural width +
488        // padding — NOT clamped to `available.width` so the parent
489        // (typically an auto-sized `Window`) can grow to fit content
490        // that exceeds the current slot.
491        let reported_w = if self.fit_width {
492            max_child_natural_w + pad_l + pad_r
493        } else {
494            available.width
495        };
496        if total_flex > 0.0 {
497            Size::new(reported_w, available.height)
498        } else {
499            Size::new(reported_w, natural_content_h + pad_t + pad_b)
500        }
501    }
502
503    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
504        let bg = if self.use_panel_bg {
505            Some(ctx.visuals().panel_fill)
506        } else if self.background.a > 0.001 {
507            Some(self.background)
508        } else {
509            None
510        };
511        if let Some(color) = bg {
512            let w = self.bounds.width;
513            let h = self.bounds.height;
514            ctx.set_fill_color(color);
515            ctx.begin_path();
516            ctx.rect(0.0, 0.0, w, h);
517            ctx.fill();
518        }
519    }
520
521    fn on_event(&mut self, _: &Event) -> EventResult {
522        EventResult::Ignored
523    }
524}
525
526// ---------------------------------------------------------------------------
527// FlexRow
528// ---------------------------------------------------------------------------
529
530/// Arranges children left-to-right (first child = leftmost).
531pub struct FlexRow {
532    bounds: Rect,
533    children: Vec<Box<dyn Widget>>,
534    flex_factors: Vec<f64>,
535    base: WidgetBase,
536    pub gap: f64,
537    pub inner_padding: Insets,
538    pub background: Color,
539}
540
541impl FlexRow {
542    pub fn new() -> Self {
543        Self {
544            bounds: Rect::default(),
545            children: Vec::new(),
546            flex_factors: Vec::new(),
547            base: WidgetBase::new(),
548            gap: 0.0,
549            inner_padding: Insets::ZERO,
550            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
551        }
552    }
553
554    pub fn with_gap(mut self, gap: f64) -> Self {
555        self.gap = gap;
556        self
557    }
558    pub fn with_padding(mut self, p: f64) -> Self {
559        self.inner_padding = Insets::all(p);
560        self
561    }
562    pub fn with_inner_padding(mut self, p: Insets) -> Self {
563        self.inner_padding = p;
564        self
565    }
566    pub fn with_background(mut self, c: Color) -> Self {
567        self.background = c;
568        self
569    }
570
571    pub fn with_margin(mut self, m: Insets) -> Self {
572        self.base.margin = m;
573        self
574    }
575    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
576        self.base.h_anchor = h;
577        self
578    }
579    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
580        self.base.v_anchor = v;
581        self
582    }
583    pub fn with_min_size(mut self, s: Size) -> Self {
584        self.base.min_size = s;
585        self
586    }
587    pub fn with_max_size(mut self, s: Size) -> Self {
588        self.base.max_size = s;
589        self
590    }
591
592    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
593        self.children.push(child);
594        self.flex_factors.push(0.0);
595        self
596    }
597
598    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
599        self.children.push(child);
600        self.flex_factors.push(flex.max(0.0));
601        self
602    }
603
604    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
605        self.children.push(child);
606        self.flex_factors.push(flex.max(0.0));
607    }
608}
609
610impl Default for FlexRow {
611    fn default() -> Self {
612        Self::new()
613    }
614}
615
616impl Widget for FlexRow {
617    fn type_name(&self) -> &'static str {
618        "FlexRow"
619    }
620    fn bounds(&self) -> Rect {
621        self.bounds
622    }
623    fn set_bounds(&mut self, b: Rect) {
624        self.bounds = b;
625    }
626    fn children(&self) -> &[Box<dyn Widget>] {
627        &self.children
628    }
629    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
630        &mut self.children
631    }
632
633    fn margin(&self) -> Insets {
634        self.base.margin
635    }
636    fn h_anchor(&self) -> HAnchor {
637        self.base.h_anchor
638    }
639    fn v_anchor(&self) -> VAnchor {
640        self.base.v_anchor
641    }
642    fn min_size(&self) -> Size {
643        self.base.min_size
644    }
645    fn max_size(&self) -> Size {
646        self.base.max_size
647    }
648
649    fn layout(&mut self, available: Size) -> Size {
650        let pad_l = self.inner_padding.left;
651        let pad_r = self.inner_padding.right;
652        let pad_t = self.inner_padding.top;
653        let pad_b = self.inner_padding.bottom;
654        let gap = self.gap;
655        let n = self.children.len();
656        if n == 0 {
657            return available;
658        }
659
660        let inner_w = (available.width - pad_l - pad_r).max(0.0);
661        let inner_h = (available.height - pad_t - pad_b).max(0.0);
662
663        let scale = device_scale();
664        let margins: Vec<Insets> = self
665            .children
666            .iter()
667            .map(|c| c.margin().scale(scale))
668            .collect();
669
670        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
671
672        // -------------------------------------------------------------------
673        // Step 1: measure fixed children on the main (horizontal) axis.
674        // -------------------------------------------------------------------
675        let mut content_widths = vec![0.0f64; n];
676        let mut total_fixed_with_margins = 0.0f64;
677        let mut total_flex = 0.0f64;
678        let mut total_flex_margin_h = 0.0f64;
679
680        for i in 0..n {
681            let m = &margins[i];
682            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
683            if self.flex_factors[i] == 0.0 {
684                // Pass inner_w as available width so the child can report its
685                // natural width.
686                let desired = self.children[i].layout(Size::new(inner_w, slot_h));
687                let clamped_w = desired.width.clamp(
688                    self.children[i].min_size().width,
689                    self.children[i].max_size().width,
690                );
691                content_widths[i] = clamped_w;
692                total_fixed_with_margins += clamped_w + m.horizontal();
693            } else {
694                total_flex += self.flex_factors[i];
695                total_flex_margin_h += m.horizontal();
696            }
697        }
698
699        // -------------------------------------------------------------------
700        // Step 2: distribute remaining space to flex children.
701        // -------------------------------------------------------------------
702        let remaining =
703            (inner_w - total_fixed_with_margins - total_gap - total_flex_margin_h).max(0.0);
704        let flex_unit = if total_flex > 0.0 {
705            remaining / total_flex
706        } else {
707            0.0
708        };
709
710        for i in 0..n {
711            if self.flex_factors[i] > 0.0 {
712                let raw = self.flex_factors[i] * flex_unit;
713                content_widths[i] = raw.clamp(
714                    self.children[i].min_size().width,
715                    self.children[i].max_size().width,
716                );
717            }
718        }
719
720        // -------------------------------------------------------------------
721        // Step 3: place children left-to-right with cross-axis anchoring.
722        // -------------------------------------------------------------------
723        let mut cursor_x = pad_l;
724        let mut max_slot_h = 0.0f64; // tallest slot (content + margins)
725
726        for i in 0..n {
727            let m = &margins[i];
728            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
729            let content_w = content_widths[i];
730
731            // Advance past left margin.
732            cursor_x += m.left;
733
734            // Layout child to get natural height for cross-axis placement.
735            let desired = self.children[i].layout(Size::new(content_w, slot_h));
736            let natural_h = desired.height;
737            let v_anchor = self.children[i].v_anchor();
738            let min_h = self.children[i].min_size().height;
739            let max_h = self.children[i].max_size().height;
740
741            let (child_y, child_h) = place_cross_v(
742                v_anchor, pad_b, inner_h, m.bottom, m.top, natural_h, min_h, max_h,
743            );
744
745            // Round to integers — same reason as FlexColumn (pixel-perfect blits).
746            self.children[i].set_bounds(Rect::new(
747                cursor_x.round(),
748                child_y.round(),
749                content_w.round(),
750                child_h.round(),
751            ));
752            max_slot_h = max_slot_h.max(child_h + m.vertical());
753
754            // Advance past content width, right margin, and inter-child gap.
755            cursor_x += content_w + m.right + gap;
756        }
757
758        // Return the natural (intrinsic) height to avoid propagating huge
759        // heights from ScrollView (which passes f64::MAX/2) through fixed rows.
760        let natural_h = max_slot_h + pad_t + pad_b;
761        Size::new(available.width, natural_h)
762    }
763
764    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
765        if self.background.a > 0.001 {
766            let w = self.bounds.width;
767            let h = self.bounds.height;
768            ctx.set_fill_color(self.background);
769            ctx.begin_path();
770            ctx.rect(0.0, 0.0, w, h);
771            ctx.fill();
772        }
773    }
774
775    fn on_event(&mut self, _: &Event) -> EventResult {
776        EventResult::Ignored
777    }
778}