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`].
80///
81/// Every child paints, so every child receives keyboard broadcasts
82/// ([`InputEvent::Shortcut`], [`InputEvent::Key`] by focus).
83pub struct StackWidget {
84    children: Vec<ChildPod>,
85}
86
87impl<State: 'static> View<State> for StackView<State> {
88    type Element = StackWidget;
89
90    fn build(&self, ctx: &mut BuildCtx<'_>) -> StackWidget {
91        StackWidget {
92            children: self
93                .children
94                .iter()
95                .map(|view| crate::authoring::build_child(view, ctx))
96                .collect(),
97        }
98    }
99
100    fn rebuild(
101        &self,
102        prev: &Self,
103        element: &mut StackWidget,
104        ctx: &mut BuildCtx<'_>,
105    ) -> ChangeFlags {
106        // Stack children are plain `AnyView`s with no key storage, so every child
107        // reports `None` and reconciliation stays positional. Keyed stacks are a
108        // future extension (they would need a keyed child descriptor like Flex's).
109        crate::authoring::rebuild_children(
110            &prev.children,
111            &self.children,
112            &mut element.children,
113            ctx,
114            |view| view,
115            |_| None,
116        )
117    }
118
119    fn teardown(&self, element: &mut StackWidget, ctx: &mut BuildCtx<'_>) {
120        for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
121            crate::authoring::teardown_child(view, pod, ctx);
122        }
123    }
124}
125
126impl Widget for StackWidget {
127    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
128        let loose = bc.loosen();
129        let mut max = Size::ZERO;
130        for pod in &mut self.children {
131            let size = pod.layout_child(ctx, &loose);
132            pod.set_origin(Point::ZERO);
133            max = Size::new(max.width.max(size.width), max.height.max(size.height));
134        }
135        bc.constrain(max)
136    }
137
138    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
139        for pod in &mut self.children {
140            pod.paint_child(ctx, scene);
141        }
142    }
143
144    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
145        crate::authoring::route_event(&mut self.children, ctx, event)
146    }
147
148    fn semantics(&self, ctx: &mut SemanticsCtx) {
149        // Transparent overlay container: forward each child (bottom-to-top).
150        for pod in &self.children {
151            pod.semantics_child(ctx);
152        }
153    }
154
155    crate::authoring::visit_children!(children);
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::test_support::{leaf_any, probe};
162    use frust_core::{BuildCtx, LayoutCtx, PointerButton, PointerEvent, PointerPhase};
163
164    fn build<S: 'static>(view: &StackView<S>) -> StackWidget {
165        let mut counter = 0u64;
166        view.build(&mut BuildCtx::new(&mut counter))
167    }
168
169    fn down(x: f64, y: f64) -> InputEvent {
170        InputEvent::Pointer(PointerEvent {
171            phase: PointerPhase::Down,
172            position: Point::new(x, y),
173            button: PointerButton::Primary,
174        })
175    }
176
177    #[test]
178    fn sizes_to_largest_child() {
179        let view: StackView<()> = stack()
180            .child(leaf_any(30.0, 60.0))
181            .child(leaf_any(80.0, 20.0));
182        let mut w = build(&view);
183        let mut lctx = LayoutCtx::new();
184        // Widest child is 80 (child 1); tallest is 60 (child 0).
185        let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
186        assert_eq!(size, Size::new(80.0, 60.0));
187        // Children overlap at the origin.
188        assert_eq!(w.children[0].origin(), Point::ZERO);
189        assert_eq!(w.children[1].origin(), Point::ZERO);
190    }
191
192    #[test]
193    fn hit_test_prefers_topmost_child() {
194        // Two fully-overlapping probes; the last (topmost) must consume the event.
195        let view: StackView<Vec<u32>> = stack()
196            .child(probe(0).into_any())
197            .child(probe(1).into_any());
198        let mut w = build(&view);
199        let mut lctx = LayoutCtx::new();
200        w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
201
202        let mut log: Vec<u32> = Vec::new();
203        let mut ectx = frust_core::EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
204        let result = w.event(&mut ectx, &down(50.0, 50.0));
205
206        assert_eq!(result, EventResult::Handled);
207        // Only the topmost probe (id 1) saw the event — reverse-order hit test.
208        assert_eq!(log, vec![1]);
209    }
210}