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::event::{Event, EventResult};
38use crate::geometry::{Rect, Size};
39use crate::draw_ctx::DrawCtx;
40use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase, resolve_fit_or_stretch};
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}
159
160impl FlexColumn {
161    pub fn new() -> Self {
162        Self {
163            bounds: Rect::default(),
164            children: Vec::new(),
165            flex_factors: Vec::new(),
166            base: WidgetBase::new(),
167            gap: 0.0,
168            inner_padding: Insets::ZERO,
169            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
170            use_panel_bg: false,
171        }
172    }
173
174    pub fn with_gap(mut self, gap: f64) -> Self { self.gap = gap; self }
175    pub fn with_padding(mut self, p: f64) -> Self { self.inner_padding = Insets::all(p); self }
176    pub fn with_inner_padding(mut self, p: Insets) -> Self { self.inner_padding = p; self }
177    pub fn with_background(mut self, c: Color) -> Self { self.background = c; self }
178    /// Use `ctx.visuals().panel_fill` as background instead of the stored color.
179    pub fn with_panel_bg(mut self) -> Self { self.use_panel_bg = true; self }
180
181    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
182    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
183    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
184    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
185    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
186
187    /// Add a fixed-size child (flex = 0).
188    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
189        self.children.push(child);
190        self.flex_factors.push(0.0);
191        self
192    }
193
194    /// Add a flex child that expands proportionally.
195    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
196        self.children.push(child);
197        self.flex_factors.push(flex.max(0.0));
198        self
199    }
200
201    /// Push a child directly (for use without builder chaining).
202    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
203        self.children.push(child);
204        self.flex_factors.push(flex.max(0.0));
205    }
206}
207
208impl Default for FlexColumn { fn default() -> Self { Self::new() } }
209
210impl Widget for FlexColumn {
211    fn type_name(&self) -> &'static str { "FlexColumn" }
212    fn bounds(&self) -> Rect { self.bounds }
213    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
214    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
215    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
216
217    fn margin(&self)   -> Insets  { self.base.margin }
218    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
219    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
220    fn min_size(&self) -> Size    { self.base.min_size }
221    fn max_size(&self) -> Size    { self.base.max_size }
222
223    fn layout(&mut self, available: Size) -> Size {
224        let pad_l = self.inner_padding.left;
225        let pad_r = self.inner_padding.right;
226        let pad_t = self.inner_padding.top;
227        let pad_b = self.inner_padding.bottom;
228        let gap   = self.gap;
229        let n     = self.children.len();
230        if n == 0 { return available; }
231
232        let inner_w = (available.width  - pad_l - pad_r).max(0.0);
233        let inner_h = (available.height - pad_t - pad_b).max(0.0);
234
235        // Scaled margins for all children (physical units).
236        let scale    = device_scale();
237        let margins: Vec<Insets> = self.children.iter()
238            .map(|c| c.margin().scale(scale))
239            .collect();
240
241        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
242
243        // -------------------------------------------------------------------
244        // Step 1: measure fixed children on the main (vertical) axis.
245        //
246        // The slot for each fixed child = content_h + margin_top + margin_bottom.
247        // Flex children contribute only their margins to the space budget.
248        // -------------------------------------------------------------------
249        let mut content_heights         = vec![0.0f64; n];
250        let mut total_fixed_with_margins = 0.0f64;
251        let mut total_flex               = 0.0f64;
252        let mut total_flex_margin_v      = 0.0f64;
253
254        for i in 0..n {
255            let m     = &margins[i];
256            let slot_w = (inner_w - m.left - m.right).max(0.0);
257            if self.flex_factors[i] == 0.0 {
258                // Measure at natural height; pass inner_h as the available
259                // height so the child can self-report its natural size.
260                let desired    = self.children[i].layout(Size::new(slot_w, inner_h));
261                let clamped_h  = desired.height
262                    .clamp(self.children[i].min_size().height,
263                           self.children[i].max_size().height);
264                content_heights[i]       = clamped_h;
265                total_fixed_with_margins += clamped_h + m.vertical();
266            } else {
267                total_flex          += self.flex_factors[i];
268                total_flex_margin_v += m.vertical();
269            }
270        }
271
272        // -------------------------------------------------------------------
273        // Step 2: distribute remaining space to flex children.
274        // -------------------------------------------------------------------
275        let remaining = (inner_h
276            - total_fixed_with_margins
277            - total_gap
278            - total_flex_margin_v)
279            .max(0.0);
280        let flex_unit = if total_flex > 0.0 { remaining / total_flex } else { 0.0 };
281
282        for i in 0..n {
283            if self.flex_factors[i] > 0.0 {
284                let raw = self.flex_factors[i] * flex_unit;
285                content_heights[i] = raw
286                    .clamp(self.children[i].min_size().height,
287                           self.children[i].max_size().height);
288            }
289        }
290
291        // Natural content height (all-fixed case) determines the column's
292        // reported size when there are no flex children.
293        let natural_content_h = total_fixed_with_margins + total_gap;
294        let effective_h = if total_flex > 0.0 { inner_h } else { natural_content_h };
295
296        // -------------------------------------------------------------------
297        // Step 3: place children top-to-bottom.
298        //
299        // In Y-up coordinates "top" = high Y.  The cursor starts at the top
300        // of the inner area and decrements for each child.
301        // -------------------------------------------------------------------
302        let mut cursor_y = pad_b + effective_h;
303
304        for i in 0..n {
305            let m          = &margins[i];
306            let slot_w     = (inner_w - m.left - m.right).max(0.0);
307            let content_h  = content_heights[i];
308
309            // Subtract top margin first (moves cursor toward lower Y = downward).
310            cursor_y -= m.top;
311            let child_bottom = cursor_y - content_h;
312
313            // Layout child to obtain its natural width for cross-axis placement.
314            let desired   = self.children[i].layout(Size::new(slot_w, content_h));
315            let natural_w = desired.width;
316            let h_anchor  = self.children[i].h_anchor();
317            let min_w     = self.children[i].min_size().width;
318            let max_w     = self.children[i].max_size().width;
319
320            let (child_x, child_w) = place_cross_h(
321                h_anchor, pad_l, inner_w, m.left, m.right, natural_w, min_w, max_w,
322            );
323
324            // Round to integers so bitmap content (cached text, images) lands on
325            // exact pixel boundaries and isn't sub-pixel sampled into blur.
326            self.children[i].set_bounds(Rect::new(
327                child_x.round(), child_bottom.round(), child_w.round(), content_h.round(),
328            ));
329
330            // Advance cursor past bottom margin and inter-child gap.
331            cursor_y = child_bottom - m.bottom - gap;
332        }
333
334        // Return natural size for all-fixed layouts so ScrollView can read
335        // the true content height from layout()'s return value.
336        if total_flex > 0.0 {
337            available
338        } else {
339            Size::new(available.width, natural_content_h + pad_t + pad_b)
340        }
341    }
342
343    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
344        let bg = if self.use_panel_bg {
345            Some(ctx.visuals().panel_fill)
346        } else if self.background.a > 0.001 {
347            Some(self.background)
348        } else {
349            None
350        };
351        if let Some(color) = bg {
352            let w = self.bounds.width;
353            let h = self.bounds.height;
354            ctx.set_fill_color(color);
355            ctx.begin_path();
356            ctx.rect(0.0, 0.0, w, h);
357            ctx.fill();
358        }
359    }
360
361    fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
362}
363
364// ---------------------------------------------------------------------------
365// FlexRow
366// ---------------------------------------------------------------------------
367
368/// Arranges children left-to-right (first child = leftmost).
369pub struct FlexRow {
370    bounds: Rect,
371    children: Vec<Box<dyn Widget>>,
372    flex_factors: Vec<f64>,
373    base: WidgetBase,
374    pub gap: f64,
375    pub inner_padding: Insets,
376    pub background: Color,
377}
378
379impl FlexRow {
380    pub fn new() -> Self {
381        Self {
382            bounds: Rect::default(),
383            children: Vec::new(),
384            flex_factors: Vec::new(),
385            base: WidgetBase::new(),
386            gap: 0.0,
387            inner_padding: Insets::ZERO,
388            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
389        }
390    }
391
392    pub fn with_gap(mut self, gap: f64) -> Self { self.gap = gap; self }
393    pub fn with_padding(mut self, p: f64) -> Self { self.inner_padding = Insets::all(p); self }
394    pub fn with_inner_padding(mut self, p: Insets) -> Self { self.inner_padding = p; self }
395    pub fn with_background(mut self, c: Color) -> Self { self.background = c; self }
396
397    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
398    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
399    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
400    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
401    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
402
403    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
404        self.children.push(child);
405        self.flex_factors.push(0.0);
406        self
407    }
408
409    pub fn add_flex(mut self, child: Box<dyn Widget>, flex: f64) -> Self {
410        self.children.push(child);
411        self.flex_factors.push(flex.max(0.0));
412        self
413    }
414
415    pub fn push(&mut self, child: Box<dyn Widget>, flex: f64) {
416        self.children.push(child);
417        self.flex_factors.push(flex.max(0.0));
418    }
419}
420
421impl Default for FlexRow { fn default() -> Self { Self::new() } }
422
423impl Widget for FlexRow {
424    fn type_name(&self) -> &'static str { "FlexRow" }
425    fn bounds(&self) -> Rect { self.bounds }
426    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
427    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
428    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
429
430    fn margin(&self)   -> Insets  { self.base.margin }
431    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
432    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
433    fn min_size(&self) -> Size    { self.base.min_size }
434    fn max_size(&self) -> Size    { self.base.max_size }
435
436    fn layout(&mut self, available: Size) -> Size {
437        let pad_l = self.inner_padding.left;
438        let pad_r = self.inner_padding.right;
439        let pad_t = self.inner_padding.top;
440        let pad_b = self.inner_padding.bottom;
441        let gap   = self.gap;
442        let n     = self.children.len();
443        if n == 0 { return available; }
444
445        let inner_w = (available.width  - pad_l - pad_r).max(0.0);
446        let inner_h = (available.height - pad_t - pad_b).max(0.0);
447
448        let scale   = device_scale();
449        let margins: Vec<Insets> = self.children.iter()
450            .map(|c| c.margin().scale(scale))
451            .collect();
452
453        let total_gap = if n > 1 { gap * (n - 1) as f64 } else { 0.0 };
454
455        // -------------------------------------------------------------------
456        // Step 1: measure fixed children on the main (horizontal) axis.
457        // -------------------------------------------------------------------
458        let mut content_widths           = vec![0.0f64; n];
459        let mut total_fixed_with_margins  = 0.0f64;
460        let mut total_flex               = 0.0f64;
461        let mut total_flex_margin_h      = 0.0f64;
462
463        for i in 0..n {
464            let m      = &margins[i];
465            let slot_h = (inner_h - m.bottom - m.top).max(0.0);
466            if self.flex_factors[i] == 0.0 {
467                // Pass inner_w as available width so the child can report its
468                // natural width.
469                let desired   = self.children[i].layout(Size::new(inner_w, slot_h));
470                let clamped_w = desired.width
471                    .clamp(self.children[i].min_size().width,
472                           self.children[i].max_size().width);
473                content_widths[i]          = clamped_w;
474                total_fixed_with_margins   += clamped_w + m.horizontal();
475            } else {
476                total_flex          += self.flex_factors[i];
477                total_flex_margin_h += m.horizontal();
478            }
479        }
480
481        // -------------------------------------------------------------------
482        // Step 2: distribute remaining space to flex children.
483        // -------------------------------------------------------------------
484        let remaining = (inner_w
485            - total_fixed_with_margins
486            - total_gap
487            - total_flex_margin_h)
488            .max(0.0);
489        let flex_unit = if total_flex > 0.0 { remaining / total_flex } else { 0.0 };
490
491        for i in 0..n {
492            if self.flex_factors[i] > 0.0 {
493                let raw = self.flex_factors[i] * flex_unit;
494                content_widths[i] = raw
495                    .clamp(self.children[i].min_size().width,
496                           self.children[i].max_size().width);
497            }
498        }
499
500        // -------------------------------------------------------------------
501        // Step 3: place children left-to-right with cross-axis anchoring.
502        // -------------------------------------------------------------------
503        let mut cursor_x         = pad_l;
504        let mut max_slot_h       = 0.0f64; // tallest slot (content + margins)
505
506        for i in 0..n {
507            let m          = &margins[i];
508            let slot_h     = (inner_h - m.bottom - m.top).max(0.0);
509            let content_w  = content_widths[i];
510
511            // Advance past left margin.
512            cursor_x += m.left;
513
514            // Layout child to get natural height for cross-axis placement.
515            let desired   = self.children[i].layout(Size::new(content_w, slot_h));
516            let natural_h = desired.height;
517            let v_anchor  = self.children[i].v_anchor();
518            let min_h     = self.children[i].min_size().height;
519            let max_h     = self.children[i].max_size().height;
520
521            let (child_y, child_h) = place_cross_v(
522                v_anchor, pad_b, inner_h, m.bottom, m.top, natural_h, min_h, max_h,
523            );
524
525            // Round to integers — same reason as FlexColumn (pixel-perfect blits).
526            self.children[i].set_bounds(Rect::new(
527                cursor_x.round(), child_y.round(), content_w.round(), child_h.round(),
528            ));
529            max_slot_h = max_slot_h.max(child_h + m.vertical());
530
531            // Advance past content width, right margin, and inter-child gap.
532            cursor_x += content_w + m.right + gap;
533        }
534
535        // Return the natural (intrinsic) height to avoid propagating huge
536        // heights from ScrollView (which passes f64::MAX/2) through fixed rows.
537        let natural_h = max_slot_h + pad_t + pad_b;
538        Size::new(available.width, natural_h)
539    }
540
541    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
542        if self.background.a > 0.001 {
543            let w = self.bounds.width;
544            let h = self.bounds.height;
545            ctx.set_fill_color(self.background);
546            ctx.begin_path();
547            ctx.rect(0.0, 0.0, w, h);
548            ctx.fill();
549        }
550    }
551
552    fn on_event(&mut self, _: &Event) -> EventResult { EventResult::Ignored }
553}