Skip to main content

frust_widgets/
stack.rs

1//! Stack layout container: a z-ordered overlay of children.
2//!
3//! [`StackView`]/[`StackWidget`] lay every child under the same loose
4//! constraints, positioned at the stack origin so they overlap. The stack sizes
5//! itself to the largest child (clamped to its own constraints). Children paint
6//! in order (first = bottom-most) and hit-test in reverse order (last = topmost),
7//! the same z-order convention [`crate::FlexWidget`] establishes.
8
9use frust_core::{
10    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
11    LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, ViewSeq, Widget, any,
12};
13use kurbo::{Point, Size};
14
15/// A declarative z-ordered stack. See the [module docs](self).
16pub struct StackView<State: 'static> {
17    children: Vec<AnyView<State>>,
18}
19
20/// Overlay `children` in a z-order stack (first child at the bottom).
21///
22/// `children` is any [`ViewSeq`]: a `Vec`/array of views, a tuple of
23/// mixed view types (positional, up to 12 and nestable), an `Option` (`None`
24/// drops the slot), or `views(iter)` for an iterator. Every element is erased
25/// once, here.
26///
27/// ```
28/// use frust_widgets::{Stack, StackView, text};
29/// # fn demo() -> (StackView<()>, StackView<()>) {
30/// // Same type: a `Vec`. Mixed types: a tuple, no `any(..)` needed.
31/// let layers = Stack(vec![text("under"), text("over")]);
32/// let mixed = Stack((text("under"), Stack((text("over"), text("badge")))));
33/// # (layers, mixed)
34/// # }
35/// # let _ = demo();
36/// ```
37#[allow(non_snake_case)]
38pub fn Stack<State: 'static, M>(children: impl ViewSeq<State, M>) -> StackView<State> {
39    stack().children(children)
40}
41
42/// An empty z-order stack, ready for fluent children (no `any(..)` wrapping).
43pub fn stack<State: 'static>() -> StackView<State> {
44    StackView {
45        children: Vec::new(),
46    }
47}
48
49impl<State: 'static> StackView<State> {
50    /// Append a child on top of the existing ones. Accepts any [`View`].
51    pub fn child<V: View<State>>(mut self, view: V) -> Self {
52        self.children.push(any(view));
53        self
54    }
55
56    /// Append every element of `children` on top of the existing ones, in order.
57    ///
58    /// Accepts any [`ViewSeq`] (`Vec`, array, tuple, `Option`, nested, or
59    /// `views(iter)`); tuples are positional and a `None` element drops its slot.
60    pub fn children<M>(mut self, children: impl ViewSeq<State, M>) -> Self {
61        children.extend_views(&mut self.children);
62        self
63    }
64
65    /// Apply `f` to the builder only when `cond` is true.
66    pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
67        if cond { f(self) } else { self }
68    }
69
70    /// Apply `f` with the contained value when `opt` is `Some`.
71    pub fn when_some<T>(self, opt: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
72        match opt {
73            Some(value) => f(self, value),
74            None => self,
75        }
76    }
77}
78
79/// The retained widget for a [`StackView`].
80pub struct StackWidget {
81    children: Vec<ChildPod>,
82}
83
84impl<State: 'static> View<State> for StackView<State> {
85    type Element = StackWidget;
86
87    fn build(&self, ctx: &mut BuildCtx<'_>) -> StackWidget {
88        StackWidget {
89            children: self
90                .children
91                .iter()
92                .map(|view| crate::authoring::build_child(view, ctx))
93                .collect(),
94        }
95    }
96
97    fn rebuild(
98        &self,
99        prev: &Self,
100        element: &mut StackWidget,
101        ctx: &mut BuildCtx<'_>,
102    ) -> ChangeFlags {
103        // Stack children are plain `AnyView`s with no key storage, so every child
104        // reports `None` and reconciliation stays positional. Keyed stacks are a
105        // future extension (they would need a keyed child descriptor like Flex's).
106        crate::authoring::rebuild_children(
107            &prev.children,
108            &self.children,
109            &mut element.children,
110            ctx,
111            |view| view,
112            |_| None,
113        )
114    }
115
116    fn teardown(&self, element: &mut StackWidget, ctx: &mut BuildCtx<'_>) {
117        for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
118            crate::authoring::teardown_child(view, pod, ctx);
119        }
120    }
121}
122
123impl Widget for StackWidget {
124    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
125        let loose = bc.loosen();
126        let mut max = Size::ZERO;
127        for pod in &mut self.children {
128            let size = pod.layout_child(ctx, &loose);
129            pod.set_origin(Point::ZERO);
130            max = Size::new(max.width.max(size.width), max.height.max(size.height));
131        }
132        bc.constrain(max)
133    }
134
135    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
136        for pod in &mut self.children {
137            pod.paint_child(ctx, scene);
138        }
139    }
140
141    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
142        crate::authoring::route_event(&mut self.children, ctx, event)
143    }
144
145    fn semantics(&self, ctx: &mut SemanticsCtx) {
146        // Transparent overlay container: forward each child (bottom-to-top).
147        for pod in &self.children {
148            pod.semantics_child(ctx);
149        }
150    }
151
152    crate::authoring::visit_children!(children);
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::test_support::{leaf_any, probe};
159    use frust_core::{BuildCtx, LayoutCtx, PointerButton, PointerEvent, PointerPhase};
160
161    fn build<S: 'static>(view: &StackView<S>) -> StackWidget {
162        let mut counter = 0u64;
163        view.build(&mut BuildCtx::new(&mut counter))
164    }
165
166    fn down(x: f64, y: f64) -> InputEvent {
167        InputEvent::Pointer(PointerEvent {
168            phase: PointerPhase::Down,
169            position: Point::new(x, y),
170            button: PointerButton::Primary,
171        })
172    }
173
174    #[test]
175    fn sizes_to_largest_child() {
176        let view: StackView<()> = stack()
177            .child(leaf_any(30.0, 60.0))
178            .child(leaf_any(80.0, 20.0));
179        let mut w = build(&view);
180        let mut lctx = LayoutCtx::new();
181        // Widest child is 80 (child 1); tallest is 60 (child 0).
182        let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
183        assert_eq!(size, Size::new(80.0, 60.0));
184        // Children overlap at the origin.
185        assert_eq!(w.children[0].origin(), Point::ZERO);
186        assert_eq!(w.children[1].origin(), Point::ZERO);
187    }
188
189    #[test]
190    fn hit_test_prefers_topmost_child() {
191        // Two fully-overlapping probes; the last (topmost) must consume the event.
192        let view: StackView<Vec<u32>> = stack()
193            .child(probe(0).into_any())
194            .child(probe(1).into_any());
195        let mut w = build(&view);
196        let mut lctx = LayoutCtx::new();
197        w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
198
199        let mut log: Vec<u32> = Vec::new();
200        let mut ectx = frust_core::EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
201        let result = w.event(&mut ectx, &down(50.0, 50.0));
202
203        assert_eq!(result, EventResult::Handled);
204        // Only the topmost probe (id 1) saw the event — reverse-order hit test.
205        assert_eq!(log, vec![1]);
206    }
207}