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 widget_base(&self) -> Option<&WidgetBase> {
304        Some(&self.base)
305    }
306    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
307        Some(&mut self.base)
308    }
309    fn padding(&self) -> Insets {
310        self.inner_padding
311    }
312    fn h_anchor(&self) -> HAnchor {
313        self.base.h_anchor
314    }
315    fn v_anchor(&self) -> VAnchor {
316        self.base.v_anchor
317    }
318    fn min_size(&self) -> Size {
319        self.base.min_size
320    }
321    fn max_size(&self) -> Size {
322        self.base.max_size
323    }
324
325    fn measure_min_height(&self, available_w: f64) -> f64 {
326        // Sum each child's required height (recursing through any
327        // FlexColumn / TextArea / Container chains) plus our own
328        // padding and inter-child gaps.  Used by ancestor
329        // `Window::tight_content_fit` to compute a content-bound
330        // height even when one of our children is a flex-fill widget
331        // whose `layout` would just return the available slot.
332        let pad_l = self.inner_padding.left;
333        let pad_r = self.inner_padding.right;
334        let pad_t = self.inner_padding.top;
335        let pad_b = self.inner_padding.bottom;
336        let inner_w = (available_w - pad_l - pad_r).max(0.0);
337        let scale = device_scale();
338        let n = self.children.len();
339        let mut total = 0.0_f64;
340        for child in self.children.iter() {
341            let m = child.margin().scale(scale);
342            let slot_w = (inner_w - m.left - m.right).max(0.0);
343            total += child.measure_min_height(slot_w) + m.vertical();
344        }
345        total += pad_t + pad_b;
346        if n > 1 {
347            total += self.gap * (n - 1) as f64;
348        }
349        total.max(self.base.min_size.height)
350    }
351
352    fn layout(&mut self, available: Size) -> Size {
353        let pad_l = self.inner_padding.left;
354        let pad_r = self.inner_padding.right;
355        let pad_t = self.inner_padding.top;
356        let pad_b = self.inner_padding.bottom;
357        let gap = self.gap;
358        let n = self.children.len();
359        if n == 0 {
360            return available;
361        }
362
363        let inner_w = (available.width - pad_l - pad_r).max(0.0);
364        let inner_h = (available.height - pad_t - pad_b).max(0.0);
365
366        // Scaled margins for all children (physical units).
367        let scale = device_scale();
368        let margins: Vec<Insets> = self
369            .children
370            .iter()
371            .map(|c| c.margin().scale(scale))
372            .collect();
373
374        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
375
376        // -------------------------------------------------------------------
377        // Step 1: measure fixed children on the main (vertical) axis.
378        //
379        // The slot for each fixed child = content_h + margin_top + margin_bottom.
380        // Flex children contribute only their margins to the space budget.
381        // -------------------------------------------------------------------
382        let mut content_heights = vec![0.0f64; n];
383        let mut total_fixed_with_margins = 0.0f64;
384        let mut total_flex = 0.0f64;
385        let mut total_flex_margin_v = 0.0f64;
386        let mut max_child_natural_w = 0.0f64;
387
388        for i in 0..n {
389            let m = &margins[i];
390            let slot_w = (inner_w - m.left - m.right).max(0.0);
391            if self.flex_factors[i] == 0.0 {
392                // Measure at natural height; pass inner_h as the available
393                // height so the child can self-report its natural size.
394                let desired = self.children[i].layout(Size::new(slot_w, inner_h));
395                let clamped_h = desired.height.clamp(
396                    self.children[i].min_size().height,
397                    self.children[i].max_size().height,
398                );
399                content_heights[i] = clamped_h;
400                total_fixed_with_margins += clamped_h + m.vertical();
401                max_child_natural_w = max_child_natural_w.max(desired.width + m.horizontal());
402            } else {
403                total_flex += self.flex_factors[i];
404                total_flex_margin_v += m.vertical();
405            }
406        }
407
408        // -------------------------------------------------------------------
409        // Step 2: distribute remaining space to flex children.
410        // -------------------------------------------------------------------
411        let remaining =
412            (inner_h - total_fixed_with_margins - total_gap - total_flex_margin_v).max(0.0);
413        let flex_unit = if total_flex > 0.0 {
414            remaining / total_flex
415        } else {
416            0.0
417        };
418
419        for i in 0..n {
420            if self.flex_factors[i] > 0.0 {
421                let raw = self.flex_factors[i] * flex_unit;
422                content_heights[i] = raw.clamp(
423                    self.children[i].min_size().height,
424                    self.children[i].max_size().height,
425                );
426            }
427        }
428
429        // Natural content height (all-fixed case) determines the column's
430        // reported size when there are no flex children.
431        let natural_content_h = total_fixed_with_margins + total_gap;
432        let effective_h = if total_flex > 0.0 {
433            inner_h
434        } else {
435            natural_content_h
436        };
437
438        // -------------------------------------------------------------------
439        // Step 3: place children top-to-bottom.
440        //
441        // In Y-up coordinates "top" = high Y.  Two cursor seeds:
442        //
443        //   - **Default**: start at the top of the finite inner area,
444        //     matching egui's top-down layout.  When a parent passes a
445        //     deliberately huge height to measure natural content (the
446        //     `ScrollView` path), fall back to the natural-content extent
447        //     so children keep finite y-coordinates.
448        //
449        //   - **`top_anchor=true`**: always start at the top of the inner
450        //     area, even for very tall measurement slots.
451        let measuring_natural_height = available.height > 1.0e9;
452        let mut cursor_y = if self.top_anchor || !measuring_natural_height {
453            available.height - pad_t
454        } else {
455            pad_b + effective_h
456        };
457
458        for i in 0..n {
459            let m = &margins[i];
460            let slot_w = (inner_w - m.left - m.right).max(0.0);
461            let content_h = content_heights[i];
462
463            // Subtract top margin first (moves cursor toward lower Y = downward).
464            cursor_y -= m.top;
465            let child_bottom = cursor_y - content_h;
466
467            // Layout child to obtain its natural width for cross-axis placement.
468            let desired = self.children[i].layout(Size::new(slot_w, content_h));
469            let natural_w = desired.width;
470            let h_anchor = self.children[i].h_anchor();
471            let min_w = self.children[i].min_size().width;
472            let max_w = self.children[i].max_size().width;
473
474            let (child_x, child_w) = place_cross_h(
475                h_anchor, pad_l, inner_w, m.left, m.right, natural_w, min_w, max_w,
476            );
477
478            // Round to integers so bitmap content (cached text, images) lands on
479            // exact pixel boundaries and isn't sub-pixel sampled into blur.
480            self.children[i].set_bounds(Rect::new(
481                child_x.round(),
482                child_bottom.round(),
483                child_w.round(),
484                content_h.round(),
485            ));
486
487            // Advance cursor past bottom margin and inter-child gap.
488            cursor_y = child_bottom - m.bottom - gap;
489        }
490
491        // Return natural size for all-fixed layouts so ScrollView can read
492        // the true content height from layout()'s return value.
493        //
494        // Width: by default we report the full available width (legacy
495        // behaviour many callers rely on).  `fit_width(true)` opts in
496        // to reporting the widest non-flex child's natural width +
497        // padding — NOT clamped to `available.width` so the parent
498        // (typically an auto-sized `Window`) can grow to fit content
499        // that exceeds the current slot.
500        let reported_w = if self.fit_width {
501            max_child_natural_w + pad_l + pad_r
502        } else {
503            available.width
504        };
505        if total_flex > 0.0 {
506            Size::new(reported_w, available.height)
507        } else {
508            Size::new(reported_w, natural_content_h + pad_t + pad_b)
509        }
510    }
511
512    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
513        let bg = if self.use_panel_bg {
514            Some(ctx.visuals().panel_fill)
515        } else if self.background.a > 0.001 {
516            Some(self.background)
517        } else {
518            None
519        };
520        if let Some(color) = bg {
521            let w = self.bounds.width;
522            let h = self.bounds.height;
523            ctx.set_fill_color(color);
524            ctx.begin_path();
525            ctx.rect(0.0, 0.0, w, h);
526            ctx.fill();
527        }
528    }
529
530    fn on_event(&mut self, _: &Event) -> EventResult {
531        EventResult::Ignored
532    }
533}
534
535// ---------------------------------------------------------------------------
536// FlexRow
537// ---------------------------------------------------------------------------
538
539/// Arranges children left-to-right (first child = leftmost).
540pub struct FlexRow {
541    bounds: Rect,
542    children: Vec<Box<dyn Widget>>,
543    flex_factors: Vec<f64>,
544    base: WidgetBase,
545    pub gap: f64,
546    pub inner_padding: Insets,
547    pub background: Color,
548}
549
550impl FlexRow {
551    pub fn new() -> Self {
552        Self {
553            bounds: Rect::default(),
554            children: Vec::new(),
555            flex_factors: Vec::new(),
556            base: WidgetBase::new(),
557            gap: 0.0,
558            inner_padding: Insets::ZERO,
559            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
560        }
561    }
562
563    pub fn with_gap(mut self, gap: f64) -> Self {
564        self.gap = gap;
565        self
566    }
567    pub fn with_padding(mut self, p: f64) -> Self {
568        self.inner_padding = Insets::all(p);
569        self
570    }
571    pub fn with_inner_padding(mut self, p: Insets) -> Self {
572        self.inner_padding = p;
573        self
574    }
575    pub fn with_background(mut self, c: Color) -> Self {
576        self.background = c;
577        self
578    }
579
580    pub fn with_margin(mut self, m: Insets) -> Self {
581        self.base.margin = m;
582        self
583    }
584    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
585        self.base.h_anchor = h;
586        self
587    }
588    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
589        self.base.v_anchor = v;
590        self
591    }
592    pub fn with_min_size(mut self, s: Size) -> Self {
593        self.base.min_size = s;
594        self
595    }
596    pub fn with_max_size(mut self, s: Size) -> Self {
597        self.base.max_size = s;
598        self
599    }
600
601    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
602        self.children.push(child);
603        self.flex_factors.push(0.0);
604        self
605    }
606
607    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
608        self.children.push(child);
609        self.flex_factors.push(flex.max(0.0));
610        self
611    }
612
613    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
614        self.children.push(child);
615        self.flex_factors.push(flex.max(0.0));
616    }
617}
618
619impl Default for FlexRow {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624
625impl Widget for FlexRow {
626    fn type_name(&self) -> &'static str {
627        "FlexRow"
628    }
629    fn bounds(&self) -> Rect {
630        self.bounds
631    }
632    fn set_bounds(&mut self, b: Rect) {
633        self.bounds = b;
634    }
635    fn children(&self) -> &[Box<dyn Widget>] {
636        &self.children
637    }
638    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
639        &mut self.children
640    }
641
642    fn margin(&self) -> Insets {
643        self.base.margin
644    }
645    fn widget_base(&self) -> Option<&WidgetBase> {
646        Some(&self.base)
647    }
648    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
649        Some(&mut self.base)
650    }
651    fn padding(&self) -> Insets {
652        self.inner_padding
653    }
654    fn h_anchor(&self) -> HAnchor {
655        self.base.h_anchor
656    }
657    fn v_anchor(&self) -> VAnchor {
658        self.base.v_anchor
659    }
660    fn min_size(&self) -> Size {
661        self.base.min_size
662    }
663    fn max_size(&self) -> Size {
664        self.base.max_size
665    }
666
667    fn layout(&mut self, available: Size) -> Size {
668        let pad_l = self.inner_padding.left;
669        let pad_r = self.inner_padding.right;
670        let pad_t = self.inner_padding.top;
671        let pad_b = self.inner_padding.bottom;
672        let gap = self.gap;
673        let n = self.children.len();
674        if n == 0 {
675            return available;
676        }
677
678        let inner_w = (available.width - pad_l - pad_r).max(0.0);
679        let inner_h = (available.height - pad_t - pad_b).max(0.0);
680
681        let scale = device_scale();
682        let margins: Vec<Insets> = self
683            .children
684            .iter()
685            .map(|c| c.margin().scale(scale))
686            .collect();
687
688        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
689
690        // -------------------------------------------------------------------
691        // Step 1: measure fixed children on the main (horizontal) axis.
692        // -------------------------------------------------------------------
693        let mut content_widths = vec![0.0f64; n];
694        let mut total_fixed_with_margins = 0.0f64;
695        let mut total_flex = 0.0f64;
696        let mut total_flex_margin_h = 0.0f64;
697
698        for i in 0..n {
699            let m = &margins[i];
700            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
701            if self.flex_factors[i] == 0.0 {
702                // Pass inner_w as available width so the child can report its
703                // natural width.
704                let desired = self.children[i].layout(Size::new(inner_w, slot_h));
705                let clamped_w = desired.width.clamp(
706                    self.children[i].min_size().width,
707                    self.children[i].max_size().width,
708                );
709                content_widths[i] = clamped_w;
710                total_fixed_with_margins += clamped_w + m.horizontal();
711            } else {
712                total_flex += self.flex_factors[i];
713                total_flex_margin_h += m.horizontal();
714            }
715        }
716
717        // -------------------------------------------------------------------
718        // Step 2: distribute remaining space to flex children.
719        // -------------------------------------------------------------------
720        let remaining =
721            (inner_w - total_fixed_with_margins - total_gap - total_flex_margin_h).max(0.0);
722        let flex_unit = if total_flex > 0.0 {
723            remaining / total_flex
724        } else {
725            0.0
726        };
727
728        for i in 0..n {
729            if self.flex_factors[i] > 0.0 {
730                let raw = self.flex_factors[i] * flex_unit;
731                content_widths[i] = raw.clamp(
732                    self.children[i].min_size().width,
733                    self.children[i].max_size().width,
734                );
735            }
736        }
737
738        // -------------------------------------------------------------------
739        // Step 3: place children left-to-right with cross-axis anchoring.
740        // -------------------------------------------------------------------
741        let mut cursor_x = pad_l;
742        let mut max_slot_h = 0.0f64; // tallest slot (content + margins)
743
744        for i in 0..n {
745            let m = &margins[i];
746            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
747            let content_w = content_widths[i];
748
749            // Advance past left margin.
750            cursor_x += m.left;
751
752            // Layout child to get natural height for cross-axis placement.
753            let desired = self.children[i].layout(Size::new(content_w, slot_h));
754            let natural_h = desired.height;
755            let v_anchor = self.children[i].v_anchor();
756            let min_h = self.children[i].min_size().height;
757            let max_h = self.children[i].max_size().height;
758
759            let (child_y, child_h) = place_cross_v(
760                v_anchor, pad_b, inner_h, m.bottom, m.top, natural_h, min_h, max_h,
761            );
762
763            // Round to integers — same reason as FlexColumn (pixel-perfect blits).
764            self.children[i].set_bounds(Rect::new(
765                cursor_x.round(),
766                child_y.round(),
767                content_w.round(),
768                child_h.round(),
769            ));
770            max_slot_h = max_slot_h.max(child_h + m.vertical());
771
772            // Advance past content width, right margin, and inter-child gap.
773            cursor_x += content_w + m.right + gap;
774        }
775
776        // Return the natural (intrinsic) height to avoid propagating huge
777        // heights from ScrollView (which passes f64::MAX/2) through fixed rows.
778        let natural_h = max_slot_h + pad_t + pad_b;
779        Size::new(available.width, natural_h)
780    }
781
782    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
783        if self.background.a > 0.001 {
784            let w = self.bounds.width;
785            let h = self.bounds.height;
786            ctx.set_fill_color(self.background);
787            ctx.begin_path();
788            ctx.rect(0.0, 0.0, w, h);
789            ctx.fill();
790        }
791    }
792
793    fn on_event(&mut self, _: &Event) -> EventResult {
794        EventResult::Ignored
795    }
796}