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