Skip to main content

agg_gui/widgets/
primitives.rs

1//! Primitive layout widgets: Stack, Padding, SizedBox. (Spacer/Separator → `spacers`.)
2
3use crate::draw_ctx::DrawCtx;
4use crate::event::{Event, EventResult};
5use crate::geometry::{Rect, Size};
6use crate::layout_props::{resolve_fit_or_stretch, HAnchor, Insets, VAnchor, WidgetBase};
7use crate::widget::Widget;
8
9// ---------------------------------------------------------------------------
10// Stack — overlays children at the same position (first = back, last = front)
11// ---------------------------------------------------------------------------
12
13/// Stacks children on top of each other.
14///
15/// Paint order: first child is drawn first (furthest back). The last child
16/// appears on top. Hit testing also follows paint order (reverse).
17///
18/// Children added with [`add`](Stack::add) are stretched to fill the stack's
19/// area (the classic behaviour). Children added with
20/// [`add_aligned`](Stack::add_aligned) are laid out at their *natural* size
21/// and positioned within the stack using their own `h_anchor` / `v_anchor`
22/// (plus margin) — like a floating overlay panel. Because an aligned child's
23/// bounds only cover the panel itself, pointer events outside it fall through
24/// to the stretched layer(s) beneath, so an aligned control panel doesn't
25/// block interaction with a full-bleed background child.
26pub struct Stack {
27    bounds: Rect,
28    children: Vec<Box<dyn Widget>>,
29    /// Parallel to `children`: `true` = placed at natural size by anchor
30    /// (overlay), `false` = stretched to fill (default).
31    aligned: Vec<bool>,
32    base: WidgetBase,
33}
34
35impl Stack {
36    pub fn new() -> Self {
37        Self {
38            bounds: Rect::default(),
39            children: Vec::new(),
40            aligned: Vec::new(),
41            base: WidgetBase::new(),
42        }
43    }
44
45    /// Add a child stretched to fill the stack's full area.
46    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
47        self.children.push(child);
48        self.aligned.push(false);
49        self
50    }
51
52    /// Add a floating child laid out at its natural size and positioned by
53    /// its `h_anchor` / `v_anchor` (respecting margin). Points outside the
54    /// child fall through to lower layers.
55    pub fn add_aligned(mut self, child: Box<dyn Widget>) -> Self {
56        self.children.push(child);
57        self.aligned.push(true);
58        self
59    }
60
61    pub fn with_margin(mut self, m: Insets) -> Self {
62        self.base.margin = m;
63        self
64    }
65    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
66        self.base.h_anchor = h;
67        self
68    }
69    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
70        self.base.v_anchor = v;
71        self
72    }
73    pub fn with_min_size(mut self, s: Size) -> Self {
74        self.base.min_size = s;
75        self
76    }
77    pub fn with_max_size(mut self, s: Size) -> Self {
78        self.base.max_size = s;
79        self
80    }
81}
82
83impl Default for Stack {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Widget for Stack {
90    fn type_name(&self) -> &'static str {
91        "Stack"
92    }
93    fn bounds(&self) -> Rect {
94        self.bounds
95    }
96    fn set_bounds(&mut self, b: Rect) {
97        self.bounds = b;
98    }
99    fn children(&self) -> &[Box<dyn Widget>] {
100        &self.children
101    }
102    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
103        &mut self.children
104    }
105
106    fn margin(&self) -> Insets {
107        self.base.margin
108    }
109    fn widget_base(&self) -> Option<&WidgetBase> {
110        Some(&self.base)
111    }
112    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
113        Some(&mut self.base)
114    }
115    fn h_anchor(&self) -> HAnchor {
116        self.base.h_anchor
117    }
118    fn v_anchor(&self) -> VAnchor {
119        self.base.v_anchor
120    }
121    fn min_size(&self) -> Size {
122        self.base.min_size
123    }
124    fn max_size(&self) -> Size {
125        self.base.max_size
126    }
127
128    fn layout(&mut self, available: Size) -> Size {
129        for idx in 0..self.children.len() {
130            if self.aligned.get(idx).copied().unwrap_or(false) {
131                let child = &mut self.children[idx];
132                // Measure natural size, then re-layout at that size so nested
133                // content (e.g. a FlexColumn) places its children within the
134                // box we actually assign rather than the full stack height.
135                let desired = child.layout(available);
136                // Margins are logical units — DPI is applied once at the App
137                // paint boundary, never during layout.
138                let m = child.margin();
139                // The margin slot bounds the child even when its own max_size
140                // doesn't: an overlay must keep its margins on a viewport
141                // narrower than max_size, not go full-bleed off the edges.
142                let slot_w = (available.width - m.left - m.right).max(0.0);
143                let slot_h = (available.height - m.top - m.bottom).max(0.0);
144                let w = desired
145                    .width
146                    .clamp(child.min_size().width, child.max_size().width)
147                    .min(slot_w);
148                let h = desired
149                    .height
150                    .clamp(child.min_size().height, child.max_size().height)
151                    .min(slot_h);
152                child.layout(Size::new(w, h));
153
154                let ha = child.h_anchor();
155                let x = if ha.contains(HAnchor::RIGHT) && !ha.contains(HAnchor::LEFT) {
156                    (available.width - m.right - w).max(0.0)
157                } else if ha.contains(HAnchor::CENTER) && !ha.is_stretch() {
158                    m.left + (available.width - m.left - m.right - w) * 0.5
159                } else {
160                    m.left
161                };
162
163                // Y-up: BOTTOM = low Y, TOP = high Y.
164                let va = child.v_anchor();
165                let y = if va.contains(VAnchor::TOP) && !va.contains(VAnchor::BOTTOM) {
166                    (available.height - m.top - h).max(0.0)
167                } else if va.contains(VAnchor::CENTER) && !va.is_stretch() {
168                    m.bottom + (available.height - m.bottom - m.top - h) * 0.5
169                } else {
170                    m.bottom
171                };
172
173                child.set_bounds(Rect::new(x, y, w, h));
174            } else {
175                let child = &mut self.children[idx];
176                child.layout(available);
177                child.set_bounds(Rect::new(0.0, 0.0, available.width, available.height));
178            }
179        }
180
181        // Bring-to-front pass — **after** children.layout on purpose.
182        //
183        // A raise can be requested from two places:
184        //   1. Widget input handlers (e.g. `Window::on_event` firing on a
185        //      MouseDown inside the window — "click to raise").  These run
186        //      BEFORE the frame's layout pass, so the flag is already set
187        //      by the time we get here.
188        //   2. Widget `layout()` itself (e.g. `Window` detects the
189        //      `visible_cell` false→true rising edge at layout time, so
190        //      toggling a demo on from the sidebar raises its window).
191        //      These set the flag DURING this very layout pass.
192        //
193        // Draining the flags AFTER children.layout catches both cases in
194        // the same frame — no one-frame visual delay.  The reactive-mode
195        // event loop only renders once per event, so a one-frame delay
196        // means the raise is invisible until the next unrelated event
197        // arrives, which is what the user reported (sidebar-opened windows
198        // appearing in the back).
199        let mut i = 0;
200        let mut raised: Vec<(Box<dyn Widget>, bool)> = Vec::new();
201        while i < self.children.len() {
202            if self.children[i].take_raise_request() {
203                let child = self.children.remove(i);
204                // `aligned` can be shorter than `children` when the tree is
205                // mutated through `children_mut()` (e.g. the app pushes a
206                // full-canvas overlay directly, or reorders windows for
207                // z-order restore). Mirror the read path's tolerance above
208                // (`self.aligned.get(idx).unwrap_or(false)`) instead of
209                // assuming a strict 1:1 length — otherwise a raise landing on
210                // the overhang index panics in `Vec::remove`.
211                let aligned = if i < self.aligned.len() {
212                    self.aligned.remove(i)
213                } else {
214                    false
215                };
216                raised.push((child, aligned));
217                // Don't advance `i` — the list just shortened.
218            } else {
219                i += 1;
220            }
221        }
222        for (r, a) in raised {
223            self.children.push(r);
224            self.aligned.push(a);
225        }
226
227        available
228    }
229
230    fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
231
232    fn on_event(&mut self, _: &Event) -> EventResult {
233        EventResult::Ignored
234    }
235}
236
237// ---------------------------------------------------------------------------
238// Padding — wraps one child with per-side insets
239// ---------------------------------------------------------------------------
240
241/// Surrounds a single child with configurable per-side padding.
242pub struct Padding {
243    bounds: Rect,
244    children: Vec<Box<dyn Widget>>,
245    base: WidgetBase,
246    insets: Insets,
247}
248
249impl Padding {
250    /// Explicit per-side padding.
251    pub fn new(insets: Insets, child: Box<dyn Widget>) -> Self {
252        Self {
253            bounds: Rect::default(),
254            children: vec![child],
255            base: WidgetBase::new(),
256            insets,
257        }
258    }
259
260    /// Uniform padding on all four sides.
261    pub fn uniform(amount: f64, child: Box<dyn Widget>) -> Self {
262        Self::new(Insets::all(amount), child)
263    }
264
265    pub fn with_margin(mut self, m: Insets) -> Self {
266        self.base.margin = m;
267        self
268    }
269    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
270        self.base.h_anchor = h;
271        self
272    }
273    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
274        self.base.v_anchor = v;
275        self
276    }
277    pub fn with_min_size(mut self, s: Size) -> Self {
278        self.base.min_size = s;
279        self
280    }
281    pub fn with_max_size(mut self, s: Size) -> Self {
282        self.base.max_size = s;
283        self
284    }
285}
286
287impl Widget for Padding {
288    fn type_name(&self) -> &'static str {
289        "Padding"
290    }
291    fn bounds(&self) -> Rect {
292        self.bounds
293    }
294    fn set_bounds(&mut self, b: Rect) {
295        self.bounds = b;
296    }
297    fn children(&self) -> &[Box<dyn Widget>] {
298        &self.children
299    }
300    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
301        &mut self.children
302    }
303
304    fn margin(&self) -> Insets {
305        self.base.margin
306    }
307    fn widget_base(&self) -> Option<&WidgetBase> {
308        Some(&self.base)
309    }
310    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
311        Some(&mut self.base)
312    }
313    fn h_anchor(&self) -> HAnchor {
314        self.base.h_anchor
315    }
316    fn v_anchor(&self) -> VAnchor {
317        self.base.v_anchor
318    }
319    fn min_size(&self) -> Size {
320        self.base.min_size
321    }
322    fn max_size(&self) -> Size {
323        self.base.max_size
324    }
325
326    fn layout(&mut self, available: Size) -> Size {
327        let p = &self.insets;
328        let inner = Size::new(
329            (available.width - p.left - p.right).max(0.0),
330            (available.height - p.top - p.bottom).max(0.0),
331        );
332        if let Some(child) = self.children.first_mut() {
333            let desired = child.layout(inner);
334            // In Y-up coordinates: origin of the child content is at (left, bottom).
335            child.set_bounds(Rect::new(p.left, p.bottom, desired.width, desired.height));
336        }
337        // Report total size including insets.
338        let content_w = self.children.first().map_or(0.0, |c| c.bounds().width);
339        let content_h = self.children.first().map_or(0.0, |c| c.bounds().height);
340        Size::new(content_w + p.left + p.right, content_h + p.top + p.bottom)
341    }
342
343    fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
344
345    fn on_event(&mut self, _: &Event) -> EventResult {
346        EventResult::Ignored
347    }
348}
349
350// ---------------------------------------------------------------------------
351// SizedBox — forces specific width and/or height, with anchor-aware child placement
352// ---------------------------------------------------------------------------
353
354/// Forces a specific size on its optional child.
355///
356/// If `width` or `height` is `None`, the available size on that axis is passed
357/// through unchanged.  The child is placed within the box using its own
358/// `h_anchor` and `v_anchor`, respecting its `margin`.
359pub struct SizedBox {
360    bounds: Rect,
361    children: Vec<Box<dyn Widget>>,
362    base: WidgetBase,
363    pub width: Option<f64>,
364    pub height: Option<f64>,
365}
366
367impl SizedBox {
368    pub fn new() -> Self {
369        Self {
370            bounds: Rect::default(),
371            children: Vec::new(),
372            base: WidgetBase::new(),
373            width: None,
374            height: None,
375        }
376    }
377
378    pub fn with_width(mut self, w: f64) -> Self {
379        self.width = Some(w);
380        self
381    }
382    pub fn with_height(mut self, h: f64) -> Self {
383        self.height = Some(h);
384        self
385    }
386
387    pub fn with_child(mut self, child: Box<dyn Widget>) -> Self {
388        self.children.clear();
389        self.children.push(child);
390        self
391    }
392
393    /// Create a fixed-size empty box (gap / spacer with exact dimensions).
394    pub fn fixed(width: f64, height: f64) -> Self {
395        Self::new().with_width(width).with_height(height)
396    }
397
398    pub fn with_margin(mut self, m: Insets) -> Self {
399        self.base.margin = m;
400        self
401    }
402    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
403        self.base.h_anchor = h;
404        self
405    }
406    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
407        self.base.v_anchor = v;
408        self
409    }
410    pub fn with_min_size(mut self, s: Size) -> Self {
411        self.base.min_size = s;
412        self
413    }
414    pub fn with_max_size(mut self, s: Size) -> Self {
415        self.base.max_size = s;
416        self
417    }
418}
419
420impl Default for SizedBox {
421    fn default() -> Self {
422        Self::new()
423    }
424}
425
426impl Widget for SizedBox {
427    fn type_name(&self) -> &'static str {
428        "SizedBox"
429    }
430    fn bounds(&self) -> Rect {
431        self.bounds
432    }
433    fn set_bounds(&mut self, b: Rect) {
434        self.bounds = b;
435    }
436    fn children(&self) -> &[Box<dyn Widget>] {
437        &self.children
438    }
439    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
440        &mut self.children
441    }
442
443    fn margin(&self) -> Insets {
444        self.base.margin
445    }
446    fn widget_base(&self) -> Option<&WidgetBase> {
447        Some(&self.base)
448    }
449    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
450        Some(&mut self.base)
451    }
452    fn h_anchor(&self) -> HAnchor {
453        self.base.h_anchor
454    }
455    fn v_anchor(&self) -> VAnchor {
456        self.base.v_anchor
457    }
458    fn min_size(&self) -> Size {
459        self.base.min_size
460    }
461    fn max_size(&self) -> Size {
462        self.base.max_size
463    }
464
465    /// Mirror the height [`layout`] resolves to, so an ancestor
466    /// `Window::with_tight_content_fit` measures the box correctly instead
467    /// of the trait-default `0`:
468    /// - explicit `with_height` → that height;
469    /// - else a child → the child's required height plus its vertical margin;
470    /// - else (a pure horizontal spacer) → `0`.
471    fn measure_min_height(&self, available_w: f64) -> f64 {
472        if let Some(h) = self.height {
473            return h;
474        }
475        if let Some(child) = self.children.first() {
476            let m = child.margin();
477            let w = self.width.unwrap_or(available_w);
478            let slot_w = (w - m.left - m.right).max(0.0);
479            return (child.measure_min_height(slot_w) + m.vertical())
480                .clamp(self.base.min_size.height, self.base.max_size.height);
481        }
482        self.base.min_size.height
483    }
484
485    fn layout(&mut self, available: Size) -> Size {
486        // Fall back to the available axis only for dimensions that haven't been
487        // explicitly set AND don't have a child to size to; otherwise use the
488        // child's natural size on that axis so the SizedBox reports a sensible
489        // height when only a width was supplied (e.g. narrow DragValue wrapper).
490        //
491        // When neither height nor child is present (pure horizontal spacer,
492        // e.g. `SizedBox::new().with_width(8.0)`), default the height to zero
493        // so the spacer doesn't inflate the parent row/column to the full
494        // available axis — which would otherwise push sibling widgets off
495        // screen.
496        let w = self.width.unwrap_or(available.width);
497        let mut h = self.height.unwrap_or_else(|| {
498            if self.children.is_empty() {
499                0.0
500            } else {
501                available.height
502            }
503        });
504
505        if let Some(child) = self.children.first_mut() {
506            let m = child.margin();
507            let slot_w = (w - m.left - m.right).max(0.0);
508            let slot_h = (h - m.top - m.bottom).max(0.0);
509
510            let desired = child.layout(Size::new(slot_w, slot_h));
511
512            // If the caller didn't pin the height, shrink to the child's
513            // natural height plus its vertical margin.
514            if self.height.is_none() {
515                h = (desired.height + m.vertical())
516                    .clamp(self.base.min_size.height, self.base.max_size.height);
517            }
518
519            // Horizontal placement within the box (margin already limits slot).
520            let h_anchor = child.h_anchor();
521            let min_w = child.min_size().width;
522            let max_w = child.max_size().width;
523            let child_w = if h_anchor.is_stretch() {
524                slot_w.clamp(min_w, max_w)
525            } else if h_anchor == HAnchor::MAX_FIT_OR_STRETCH {
526                resolve_fit_or_stretch(desired.width, slot_w, true).clamp(min_w, max_w)
527            } else if h_anchor == HAnchor::MIN_FIT_OR_STRETCH {
528                resolve_fit_or_stretch(desired.width, slot_w, false).clamp(min_w, max_w)
529            } else {
530                desired.width.clamp(min_w, max_w)
531            };
532
533            let child_x = if h_anchor.contains(HAnchor::RIGHT) && !h_anchor.contains(HAnchor::LEFT)
534            {
535                (w - m.right - child_w).max(0.0)
536            } else if h_anchor.contains(HAnchor::CENTER) && !h_anchor.is_stretch() {
537                m.left + (slot_w - child_w) * 0.5
538            } else {
539                m.left
540            };
541
542            // Vertical placement (Y-up: BOTTOM = low Y).
543            let v_anchor = child.v_anchor();
544            let min_h = child.min_size().height;
545            let max_h = child.max_size().height;
546            let child_h = if v_anchor.is_stretch() {
547                slot_h.clamp(min_h, max_h)
548            } else if v_anchor == VAnchor::MAX_FIT_OR_STRETCH {
549                resolve_fit_or_stretch(desired.height, slot_h, true).clamp(min_h, max_h)
550            } else if v_anchor == VAnchor::MIN_FIT_OR_STRETCH {
551                resolve_fit_or_stretch(desired.height, slot_h, false).clamp(min_h, max_h)
552            } else {
553                desired.height.clamp(min_h, max_h)
554            };
555
556            // When a dimension is explicitly pinned, the child must fit —
557            // otherwise a child whose `layout` ignores the slot budget (e.g.
558            // `TextField` returning a font-derived natural height) paints
559            // outside the SizedBox and clips into siblings above it.  Widgets
560            // re-read `self.bounds` during paint, so shrinking here propagates
561            // cleanly.
562            let child_w = if self.width.is_some() {
563                child_w.min(slot_w)
564            } else {
565                child_w
566            };
567            let child_h = if self.height.is_some() {
568                child_h.min(slot_h)
569            } else {
570                child_h
571            };
572
573            let child_y = if v_anchor.contains(VAnchor::TOP) && !v_anchor.contains(VAnchor::BOTTOM)
574            {
575                (h - m.top - child_h).max(0.0)
576            } else if v_anchor.contains(VAnchor::CENTER) && !v_anchor.is_stretch() {
577                m.bottom + (slot_h - child_h) * 0.5
578            } else {
579                m.bottom
580            };
581
582            child.set_bounds(Rect::new(child_x, child_y, child_w, child_h));
583        }
584
585        Size::new(w, h)
586    }
587
588    fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
589
590    fn on_event(&mut self, _: &Event) -> EventResult {
591        EventResult::Ignored
592    }
593}