Skip to main content

frust_core/
overlay.rs

1//! The overlay portal: how a widget anywhere in the tree floats a pod above the
2//! whole app (menus, popovers, tooltips, selection toolbars).
3//!
4//! The owner builds, lays out and keeps the pod and registers an [`OverlayEntry`]
5//! from its `paint`; the root paints every entry after the main tree, unclipped.
6//!
7//! Per-pass registry contract: the root clears the registry every paint, so an
8//! owner re-registers each frame to stay up. The root hit-tests registered rects
9//! first; a hit is an [`InputEvent::Overlay`](crate::event::InputEvent::Overlay)
10//! broadcast that only the owner holding the key acts on.
11//!
12//! A key the focus path declines goes to the focus session's forward pod, then (an Escape) to
13//! the topmost [`OverlayKeys::Modal`] pod, both as `OverlayEventKind::Key`, then to the shortcut
14//! table, masked to that modal pod's own declarations while one is registered.
15//!
16//! # Not in v1
17//!
18//! * No focus restore: registering a modal pod ends a main-tree session; none returns on close.
19//! * Not seen by [`inspect`](crate::app::RenderRoot::inspect) unless the owner visits the pod.
20//! * No nesting: a registration made from inside a floated pod's paint is dropped;
21//!   register a submenu from the same owner, in the main tree's paint.
22
23use std::cell::{Cell, RefCell};
24use std::fmt;
25use std::rc::Rc;
26
27use kurbo::Rect;
28
29use crate::event::PassBracket;
30use crate::insets::WindowInsets;
31use crate::widget::ChildPod;
32
33thread_local! {
34    /// The next [`OverlayKey::next`] value; starts at `1` so `0` can mean "none".
35    static NEXT_OVERLAY_KEY: Cell<u64> = const { Cell::new(1) };
36
37    /// Entries registered so far in this thread's open paint pass, in order.
38    static OVERLAY_REGISTRY: Cell<Vec<OverlayEntry>> = const { Cell::new(Vec::new()) };
39
40    static OVERLAY_PASS_OPEN: Cell<bool> = const { Cell::new(false) };
41}
42
43/// An overlay owner's identity, quoted back by every overlay event routed to it.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct OverlayKey(u64);
46
47impl OverlayKey {
48    /// Allocate a key unique on this thread. Call once when the owner is built.
49    pub fn next() -> Self {
50        OverlayKey(NEXT_OVERLAY_KEY.with(|next| {
51            let id = next.get();
52            next.set(id.wrapping_add(1));
53            id
54        }))
55    }
56
57    /// The raw integer, for map keys; allocation order is not a contract.
58    pub fn value(self) -> u64 {
59        self.0
60    }
61}
62
63/// A surface's z-band: `Floating` paints below `Tooltip`, which hit-tests first.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub enum OverlayBand {
66    /// Menus, popovers, dropdowns, selection toolbars.
67    Floating,
68    /// Tooltips and other chrome above everything else.
69    Tooltip,
70}
71
72/// Whether a registered surface takes pointer input.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub enum OverlayInput {
75    /// Hit-tested: a pointer inside the rect goes to the owner, not the main tree.
76    Interactive,
77    /// Painted but never hit-tested, and never sent any overlay event.
78    Transparent,
79}
80
81/// What a surface hears about a press that landed on nothing floated.
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
83pub enum OutsideTap {
84    /// Nothing.
85    Ignore,
86    /// Send `OverlayEventKind::OutsideDown` on a primary press outside every rect.
87    Notify {
88        /// Whether the press stops there instead of also reaching the main tree.
89        consume: bool,
90    },
91}
92
93/// Whether a registered surface claims the keys the focus path declines.
94#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
95pub enum OverlayKeys {
96    #[default]
97    Pass,
98    /// Masks main-tree shortcuts and takes a declined Escape (implied by a consuming `Notify`).
99    Modal,
100}
101
102/// The shared handle the owner and the root reach one floated pod through.
103pub type OverlayPod = Rc<RefCell<ChildPod>>;
104
105/// One floated surface, registered by its owner for the paint pass in progress.
106#[derive(Clone)]
107pub struct OverlayEntry {
108    pub key: OverlayKey,
109    pub band: OverlayBand,
110    pub input: OverlayInput,
111    pub outside_tap: OutsideTap,
112    pub keys: OverlayKeys,
113    /// Absolute logical window rect (from `PaintCtx::origin`): where the pod
114    /// paints and what routing hit-tests.
115    pub window_rect: Rect,
116    /// The pod the root paints; the owner lays it out but must not paint it.
117    pub pod: OverlayPod,
118    /// The insets the pod was laid out under; paint installs the same value.
119    pub insets: WindowInsets,
120}
121
122impl fmt::Debug for OverlayEntry {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("OverlayEntry")
125            .field("key", &self.key)
126            .field("band", &self.band)
127            .field("input", &self.input)
128            .field("outside_tap", &self.outside_tap)
129            .field("keys", &self.keys)
130            .field("window_rect", &self.window_rect)
131            .field("insets", &self.insets)
132            .field("pod", &"<pod>")
133            .finish()
134    }
135}
136
137/// An [`OverlayEntry`]'s routing half, kept between passes; no pod, so no co-ownership.
138#[derive(Clone, Copy, Debug, PartialEq)]
139pub struct OverlayHit {
140    pub key: OverlayKey,
141    pub band: OverlayBand,
142    pub input: OverlayInput,
143    pub outside_tap: OutsideTap,
144    pub keys: OverlayKeys,
145    pub window_rect: Rect,
146}
147
148impl OverlayHit {
149    pub(crate) fn of(entry: &OverlayEntry) -> Self {
150        Self {
151            key: entry.key,
152            band: entry.band,
153            input: entry.input,
154            outside_tap: entry.outside_tap,
155            keys: entry.keys,
156            window_rect: entry.window_rect,
157        }
158    }
159
160    /// Interactive and [`OverlayKeys::Modal`], or consuming outside taps.
161    pub(crate) fn is_modal(&self) -> bool {
162        self.input == OverlayInput::Interactive
163            && (self.keys == OverlayKeys::Modal
164                || self.outside_tap == OutsideTap::Notify { consume: true })
165    }
166
167    /// Half-open on the far edges, so surfaces sharing an edge never both hit.
168    pub(crate) fn contains(&self, point: kurbo::Point) -> bool {
169        point.x >= self.window_rect.x0
170            && point.x < self.window_rect.x1
171            && point.y >= self.window_rect.y0
172            && point.y < self.window_rect.y1
173    }
174}
175
176/// The [`PassBracket`] around one registration pass: enter clears, `take` drains.
177pub(crate) struct OverlayPaintPass(PassBracket<Vec<OverlayEntry>>);
178
179impl OverlayPaintPass {
180    pub(crate) fn enter() -> Self {
181        OverlayPaintPass(PassBracket::enter(&OVERLAY_REGISTRY, &OVERLAY_PASS_OPEN))
182    }
183
184    pub(crate) fn take(&self) -> Vec<OverlayEntry> {
185        self.0.take()
186    }
187}
188
189/// Record `entry` in this thread's open pass (behind `PaintCtx::register_overlay`).
190pub(crate) fn register(entry: OverlayEntry) {
191    OVERLAY_REGISTRY.with(|registry| {
192        let mut entries = registry.take();
193        entries.push(entry);
194        registry.set(entries);
195    });
196}
197
198/// Stable sort by band: later registrations paint above earlier; reverse to hit-test.
199pub(crate) fn sort_into_paint_order(entries: &mut [OverlayEntry]) {
200    entries.sort_by_key(|entry| entry.band);
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::layout::BoxConstraints;
207    use crate::widget::{LayoutCtx, PaintCtx, PaintScene, Widget};
208    use kurbo::{Point, Size};
209
210    /// A do-nothing leaf, so an entry can carry a real [`ChildPod`].
211    struct StubWidget;
212    impl Widget for StubWidget {
213        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
214            Size::ZERO
215        }
216        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
217    }
218
219    fn entry(key: OverlayKey, band: OverlayBand, rect: Rect) -> OverlayEntry {
220        OverlayEntry {
221            key,
222            band,
223            input: OverlayInput::Interactive,
224            outside_tap: OutsideTap::Ignore,
225            keys: OverlayKeys::Pass,
226            window_rect: rect,
227            pod: Rc::new(RefCell::new(ChildPod::new(Box::new(StubWidget)))),
228            insets: WindowInsets::default(),
229        }
230    }
231
232    #[test]
233    fn keys_are_distinct_and_stable() {
234        let first = OverlayKey::next();
235        let second = OverlayKey::next();
236        assert_ne!(first, second, "two owners never share an identity");
237        // An owner stores its key; copying it around must not re-allocate.
238        let stored = first;
239        assert_eq!(stored, first);
240        assert_ne!(stored, second);
241    }
242
243    #[test]
244    fn paint_order_puts_floating_below_tooltip_and_is_stable_within_a_band() {
245        let (a, b, c, d) = (
246            OverlayKey::next(),
247            OverlayKey::next(),
248            OverlayKey::next(),
249            OverlayKey::next(),
250        );
251        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
252        // Registered tooltip-first and interleaved, which is exactly what the
253        // sort has to survive: the band decides, registration order only breaks
254        // ties inside one.
255        let mut entries = vec![
256            entry(a, OverlayBand::Tooltip, rect),
257            entry(b, OverlayBand::Floating, rect),
258            entry(c, OverlayBand::Tooltip, rect),
259            entry(d, OverlayBand::Floating, rect),
260        ];
261        sort_into_paint_order(&mut entries);
262        assert_eq!(
263            entries.iter().map(|e| e.key).collect::<Vec<_>>(),
264            vec![b, d, a, c],
265            "Floating first (in registration order), then Tooltip (in registration order)"
266        );
267    }
268
269    #[test]
270    fn a_hit_rect_is_half_open_on_its_far_edges() {
271        let hit = OverlayHit::of(&entry(
272            OverlayKey::next(),
273            OverlayBand::Floating,
274            Rect::new(10.0, 20.0, 110.0, 60.0),
275        ));
276        assert!(
277            hit.contains(Point::new(10.0, 20.0)),
278            "near edges are inside"
279        );
280        assert!(hit.contains(Point::new(109.9, 59.9)));
281        // Half-open like `ChildPod::contains`, so two surfaces sharing an edge
282        // cannot both claim the same pixel.
283        assert!(!hit.contains(Point::new(110.0, 40.0)));
284        assert!(!hit.contains(Point::new(60.0, 60.0)));
285        assert!(!hit.contains(Point::new(9.9, 40.0)));
286    }
287
288    #[test]
289    fn a_hit_keeps_the_routing_fields_and_drops_the_pod() {
290        let key = OverlayKey::next();
291        let mut source = entry(key, OverlayBand::Tooltip, Rect::new(1.0, 2.0, 3.0, 4.0));
292        source.input = OverlayInput::Transparent;
293        source.outside_tap = OutsideTap::Notify { consume: true };
294        let hit = OverlayHit::of(&source);
295        assert_eq!(hit.key, key);
296        assert_eq!(hit.band, OverlayBand::Tooltip);
297        assert_eq!(hit.input, OverlayInput::Transparent);
298        assert_eq!(hit.outside_tap, OutsideTap::Notify { consume: true });
299        assert_eq!(hit.window_rect, source.window_rect);
300        // The pod stayed with its owner: the entry still holds the only two
301        // clones (the owner's and this local one), never a third in the root.
302        assert_eq!(Rc::strong_count(&source.pod), 1);
303    }
304
305    #[test]
306    fn a_hit_is_modal_when_interactive_and_modal_or_consuming_outside_taps() {
307        let mut source = entry(OverlayKey::next(), OverlayBand::Floating, Rect::ZERO);
308        assert!(!OverlayHit::of(&source).is_modal());
309        source.keys = OverlayKeys::Modal;
310        assert!(OverlayHit::of(&source).is_modal());
311        source.keys = OverlayKeys::Pass;
312        source.outside_tap = OutsideTap::Notify { consume: true };
313        assert!(OverlayHit::of(&source).is_modal());
314        source.input = OverlayInput::Transparent;
315        assert!(
316            !OverlayHit::of(&source).is_modal(),
317            "a transparent pod never takes keys"
318        );
319    }
320
321    #[test]
322    fn the_registry_is_pass_scoped_and_preserves_registration_order() {
323        let (first, second) = (OverlayKey::next(), OverlayKey::next());
324        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
325        let pass = OverlayPaintPass::enter();
326        assert!(pass.take().is_empty(), "a fresh pass starts empty");
327
328        register(entry(first, OverlayBand::Floating, rect));
329        register(entry(second, OverlayBand::Floating, rect));
330        let drained = pass.take();
331        assert_eq!(
332            drained.iter().map(|e| e.key).collect::<Vec<_>>(),
333            vec![first, second],
334            "registration order is preserved"
335        );
336        assert!(pass.take().is_empty(), "and the drain empties the registry");
337        drop(pass);
338
339        // A registration made with no pass open (a leaf unit test painting a bare
340        // `PaintCtx`) is dropped by the next pass's clear, never leaked into it —
341        // which is what makes "the registry is empty at the start of every paint"
342        // true without anyone unregistering.
343        register(entry(first, OverlayBand::Floating, rect));
344        let next = OverlayPaintPass::enter();
345        assert!(
346            next.take().is_empty(),
347            "a stray registration does not survive into the next pass"
348        );
349    }
350
351    #[test]
352    fn a_nested_pass_hands_the_enclosing_passs_registrations_back() {
353        let (outer_key, inner_key) = (OverlayKey::next(), OverlayKey::next());
354        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
355        let outer = OverlayPaintPass::enter();
356        register(entry(outer_key, OverlayBand::Floating, rect));
357        {
358            let inner = OverlayPaintPass::enter();
359            assert!(inner.take().is_empty(), "the inner pass starts empty");
360            register(entry(inner_key, OverlayBand::Floating, rect));
361            assert_eq!(
362                inner.take().iter().map(|e| e.key).collect::<Vec<_>>(),
363                vec![inner_key]
364            );
365        }
366        assert_eq!(
367            outer.take().iter().map(|e| e.key).collect::<Vec<_>>(),
368            vec![outer_key],
369            "the enclosing pass's registrations are handed back intact"
370        );
371    }
372
373    #[test]
374    fn debug_prints_the_routing_fields_without_the_pod() {
375        let rendered = format!(
376            "{:?}",
377            entry(
378                OverlayKey::next(),
379                OverlayBand::Floating,
380                Rect::new(0.0, 0.0, 10.0, 10.0)
381            )
382        );
383        assert!(rendered.contains("Floating"), "{rendered}");
384        assert!(rendered.contains("Interactive"), "{rendered}");
385        assert!(rendered.contains("<pod>"), "{rendered}");
386    }
387}