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, Widget,
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#[allow(non_snake_case)]
22pub fn Stack<State: 'static>(children: Vec<AnyView<State>>) -> StackView<State> {
23    StackView { children }
24}
25
26/// The retained widget for a [`StackView`].
27pub struct StackWidget {
28    children: Vec<ChildPod>,
29}
30
31impl<State: 'static> View<State> for StackView<State> {
32    type Element = StackWidget;
33
34    fn build(&self, ctx: &mut BuildCtx<'_>) -> StackWidget {
35        StackWidget {
36            children: self
37                .children
38                .iter()
39                .map(|view| crate::authoring::build_child(view, ctx))
40                .collect(),
41        }
42    }
43
44    fn rebuild(
45        &self,
46        prev: &Self,
47        element: &mut StackWidget,
48        ctx: &mut BuildCtx<'_>,
49    ) -> ChangeFlags {
50        // Stack children are plain `AnyView`s with no key storage, so every child
51        // reports `None` and reconciliation stays positional. Keyed stacks are a
52        // future extension (they would need a keyed child descriptor like Flex's).
53        crate::authoring::rebuild_children(
54            &prev.children,
55            &self.children,
56            &mut element.children,
57            ctx,
58            |view| view,
59            |_| None,
60        )
61    }
62
63    fn teardown(&self, element: &mut StackWidget, ctx: &mut BuildCtx<'_>) {
64        for (view, pod) in self.children.iter().zip(element.children.iter_mut()) {
65            crate::authoring::teardown_child(view, pod, ctx);
66        }
67    }
68}
69
70impl Widget for StackWidget {
71    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
72        let loose = bc.loosen();
73        let mut max = Size::ZERO;
74        for pod in &mut self.children {
75            let size = pod.layout_child(ctx, &loose);
76            pod.set_origin(Point::ZERO);
77            max = Size::new(max.width.max(size.width), max.height.max(size.height));
78        }
79        bc.constrain(max)
80    }
81
82    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
83        for pod in &mut self.children {
84            pod.paint_child(ctx, scene);
85        }
86    }
87
88    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
89        crate::authoring::route_event(&mut self.children, ctx, event)
90    }
91
92    fn semantics(&self, ctx: &mut SemanticsCtx) {
93        // Transparent overlay container: forward each child (bottom-to-top).
94        for pod in &self.children {
95            pod.semantics_child(ctx);
96        }
97    }
98
99    crate::authoring::visit_children!(children);
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::test_support::{leaf_any, probe};
106    use frust_core::{BuildCtx, LayoutCtx, PointerButton, PointerEvent, PointerPhase};
107
108    fn build<S: 'static>(view: &StackView<S>) -> StackWidget {
109        let mut counter = 0u64;
110        view.build(&mut BuildCtx::new(&mut counter))
111    }
112
113    fn down(x: f64, y: f64) -> InputEvent {
114        InputEvent::Pointer(PointerEvent {
115            phase: PointerPhase::Down,
116            position: Point::new(x, y),
117            button: PointerButton::Primary,
118        })
119    }
120
121    #[test]
122    fn sizes_to_largest_child() {
123        let view: StackView<()> = Stack(vec![leaf_any(30.0, 60.0), leaf_any(80.0, 20.0)]);
124        let mut w = build(&view);
125        let mut lctx = LayoutCtx::new();
126        // Widest child is 80 (child 1); tallest is 60 (child 0).
127        let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
128        assert_eq!(size, Size::new(80.0, 60.0));
129        // Children overlap at the origin.
130        assert_eq!(w.children[0].origin(), Point::ZERO);
131        assert_eq!(w.children[1].origin(), Point::ZERO);
132    }
133
134    #[test]
135    fn hit_test_prefers_topmost_child() {
136        // Two fully-overlapping probes; the last (topmost) must consume the event.
137        let view: StackView<Vec<u32>> = Stack(vec![probe(0).into_any(), probe(1).into_any()]);
138        let mut w = build(&view);
139        let mut lctx = LayoutCtx::new();
140        w.layout(&mut lctx, &BoxConstraints::tight(Size::new(100.0, 100.0)));
141
142        let mut log: Vec<u32> = Vec::new();
143        let mut ectx = frust_core::EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
144        let result = w.event(&mut ectx, &down(50.0, 50.0));
145
146        assert_eq!(result, EventResult::Handled);
147        // Only the topmost probe (id 1) saw the event — reverse-order hit test.
148        assert_eq!(log, vec![1]);
149    }
150}