Skip to main content

frust_core/
overlay.rs

1//! The overlay portal: the mechanism a widget anywhere in the tree floats a pod
2//! above the whole app with.
3//!
4//! # The contract: owner-hosted, root-painted, root-routed
5//!
6//! A floating surface (a popover, a menu, a selection toolbar, a tooltip) has to
7//! escape its owner's bounds in three different ways at once, and this module
8//! splits the three between the owner and the root:
9//!
10//! * **Owner-hosted.** The pod is a plain [`ChildPod`] the *owner* builds, lays
11//!   out (against [`LayoutCtx::window_size`](crate::widget::LayoutCtx::window_size),
12//!   loosely) and keeps — so it retains its own widget state and sits inside the
13//!   owner's reactive context, exactly like any other child. Nothing is
14//!   re-parented and no second tree exists: the pod stays a logical child of its
15//!   call site.
16//! * **Root-painted.** The owner does **not** paint the pod. It hands the root a
17//!   registration ([`OverlayEntry`]) from its own `paint`, through
18//!   [`PaintCtx::register_overlay`](crate::widget::PaintCtx::register_overlay),
19//!   and [`RenderRoot::paint`](crate::app::RenderRoot::paint) paints every
20//!   registered pod *after* the main tree — which is the only way a pod escapes
21//!   both its owner's paint order and every ancestor's clip. Because the entry
22//!   carries an absolute [`OverlayEntry::window_rect`] the owner computed from
23//!   its own [`PaintCtx::origin`](crate::widget::PaintCtx::origin), an anchor
24//!   follows the owner for free: the owner re-registers every frame from
25//!   wherever it now paints. `window_rect` is just a rect either way, so
26//!   nothing here cares whether the owner derived it from its own bounds or
27//!   from an absolute window-space point it is tracking instead (a drag
28//!   ghost following the pointer, in `frust-widgets`' `OverlaySlot`) — the
29//!   mechanism is the same registration, repainted on whatever cadence the
30//!   owner now needs.
31//! * **Root-routed.** Painting last is not enough, because hit testing is
32//!   bounds-gated: a pointer over the floated pod lands on whatever the *main*
33//!   tree has at that position. So the root hit-tests the registered rects
34//!   **first** (topmost band first) and, on a hit, dispatches
35//!   [`InputEvent::Overlay`](crate::event::InputEvent::Overlay) as a broadcast
36//!   instead of the original event. The broadcast reaches the owner wherever it
37//!   sits, and the owner forwards the window-space payload into its pod. Nothing
38//!   is hit-tested against the owner's own bounds, and — because a broadcast is
39//!   not user input at the root — the main tree's focus session is untouched:
40//!   tapping a popover does not blur the field that opened it.
41//!
42//! # Per-pass entries
43//!
44//! The registry is **per paint pass**: it is cleared when
45//! [`RenderRoot::paint`](crate::app::RenderRoot::paint) begins and drained when
46//! the main tree has finished painting. An owner keeps a surface alive by
47//! registering it again every frame; an owner that stops registering disappears
48//! from the root's routing table after the next paint, with nothing to
49//! unregister and no way to leak an entry whose owner has been unmounted. A
50//! kept-mounted exit animation is therefore just "keep registering while the
51//! animation runs".
52//!
53//! The routing table the root retains between passes
54//! ([`OverlayHit`]) deliberately carries **no pod handle** — only the key, the
55//! band, the input class, the outside-tap policy and the rect. The owner holds
56//! the pod; the root never does, so a pod can never outlive its owner because
57//! the root kept a clone of it, and no [`RefCell`] borrow is ever held across a
58//! pass boundary.
59//!
60//! # Not in v1
61//!
62//! * **No focus trap.** Focus inside a pod behaves exactly like focus anywhere
63//!   else; nothing confines traversal to the pod or restores it on dismiss.
64//! * **No declined-key forwarding.** Key/IME/edit-command events stay
65//!   focus-routed and are never re-offered to an overlay owner that did not take
66//!   focus.
67//! * **Not visible to [`inspect`](crate::app::RenderRoot::inspect).** An overlay
68//!   pod is not reachable through [`Widget::visit_children`](crate::widget::Widget::visit_children)
69//!   unless its owner chooses to visit it, so devtools sees the owner, not the
70//!   floated surface.
71//! * **No nesting.** The registry is drained once, after the main tree paints, so
72//!   a registration made from *inside* a floated pod's own paint is not painted
73//!   this pass — and, because the pass's bracket clears the slot as it closes, it
74//!   is dropped rather than leaked into the next one. A surface that itself needs
75//!   a floated surface (a menu opening a submenu) registers both from the one
76//!   owner, in the main tree's paint.
77
78use std::cell::{Cell, RefCell};
79use std::fmt;
80use std::rc::Rc;
81
82use kurbo::Rect;
83
84use crate::event::PassBracket;
85use crate::insets::WindowInsets;
86use crate::widget::ChildPod;
87
88thread_local! {
89    /// The next value [`OverlayKey::next`] hands out.
90    ///
91    /// Thread-local and UI-thread-affine, mirroring
92    /// [`ChildPod`]'s own `NEXT_INSPECT_ID` allocator: the widget tree is
93    /// single-threaded, and an owner allocates its key while building itself
94    /// with no root in scope to ask. Two threads each running a tree therefore
95    /// hand out the same integers, which is harmless — a key is only ever
96    /// compared against the entries of the one root that painted them.
97    ///
98    /// Starts at `1` so `0` stays available as "no overlay" for a consumer that
99    /// wants a niche-free sentinel.
100    static NEXT_OVERLAY_KEY: Cell<u64> = const { Cell::new(1) };
101
102    /// The entries registered so far in the paint pass currently running on this
103    /// thread — written by
104    /// [`PaintCtx::register_overlay`](crate::widget::PaintCtx::register_overlay)
105    /// and drained by [`RenderRoot::paint`](crate::app::RenderRoot::paint).
106    ///
107    /// A side channel for the *routing* reason
108    /// [`EventCtx::set_cursor`](crate::event::EventCtx::set_cursor)'s slot is
109    /// one, not for a missing-handle reason: an entry means nothing to any
110    /// container between the owner and the root, so bubbling it pod by pod would
111    /// widen every container's paint absorb to carry a payload no container
112    /// reads. Unlike the cursor this is a `Vec` rather than a last-writer-wins
113    /// slot — any number of owners may float a surface in one pass, and
114    /// registration order is what orders them within a band.
115    ///
116    /// **Pass-scoped**: cleared when a pass opens and drained when it closes, so
117    /// an entry registered by a widget painted with no root above it (a leaf
118    /// unit test) is dropped by the next pass's clear rather than leaking into
119    /// it.
120    static OVERLAY_REGISTRY: Cell<Vec<OverlayEntry>> = const { Cell::new(Vec::new()) };
121
122    /// Whether an overlay registration pass is open on this thread — owned by
123    /// [`OverlayPaintPass`] alone (see [`PassBracket`]).
124    static OVERLAY_PASS_OPEN: Cell<bool> = const { Cell::new(false) };
125}
126
127/// An overlay owner's identity, allocated once by the owner and quoted back to
128/// it by every [`InputEvent::Overlay`](crate::event::InputEvent::Overlay) the
129/// root routes into its surface.
130///
131/// Stable for the owner's lifetime: an owner allocates one key when it is built
132/// and stores it, re-registering under the same key every paint. The root
133/// therefore never has to recognise an owner by position, and an owner that
134/// floats two surfaces simply holds two keys.
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
136pub struct OverlayKey(u64);
137
138impl OverlayKey {
139    /// Allocate a fresh key, distinct from every other key handed out on this
140    /// thread.
141    ///
142    /// An owner calls this **once**, when it is built, and stores the result —
143    /// calling it per frame would hand the root a new identity every paint and
144    /// break the routing it exists to enable.
145    pub fn next() -> Self {
146        OverlayKey(NEXT_OVERLAY_KEY.with(|next| {
147            let id = next.get();
148            next.set(id.wrapping_add(1));
149            id
150        }))
151    }
152
153    /// The underlying integer, for a consumer that needs to key a map by it.
154    /// Diagnostic and bookkeeping only — never derive routing from the *value*
155    /// (allocation order is not a contract).
156    pub fn value(self) -> u64 {
157        self.0
158    }
159}
160
161/// Which z-band a registered surface paints and hit-tests in.
162///
163/// Two bands, ordered `Floating` **below** `Tooltip`: a tooltip explaining a
164/// menu item must never be painted under the menu, and — because hit testing
165/// walks the bands in reverse — must never steal the pointer from it either
166/// (a tooltip is normally registered [`OverlayInput::Transparent`] anyway).
167/// Registration order breaks ties *within* a band; the band itself always wins.
168#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
169pub enum OverlayBand {
170    /// Menus, popovers, dropdowns, selection toolbars — anything the user can
171    /// interact with. Painted first, hit-tested last.
172    Floating,
173    /// Tooltips and other explanatory chrome that must sit above everything
174    /// else. Painted last, hit-tested first.
175    Tooltip,
176}
177
178/// Whether a registered surface takes pointer input at all.
179///
180/// [`OverlayInput::Transparent`] is the rule egui's `Order::Tooltip` encodes: a
181/// surface that is painted above the app but never hit-tested, so the pointer
182/// passes straight through it to whatever the main tree has underneath. A
183/// transparent entry is skipped by the root's pre-pass entirely — it receives no
184/// [`InputEvent::Overlay`](crate::event::InputEvent::Overlay), including no
185/// [`OverlayEventKind::OutsideDown`](crate::event::OverlayEventKind::OutsideDown).
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
187pub enum OverlayInput {
188    /// The surface hit-tests: a pointer inside its rect is routed to its owner
189    /// and never reaches the main tree.
190    Interactive,
191    /// The surface is painted but never hit-tested — pointer input passes
192    /// through as though it were not there.
193    Transparent,
194}
195
196/// What a registered surface wants to hear about a press that landed on
197/// *nothing* floated — the light-dismiss policy, a per-surface boolean
198/// everywhere this pattern exists.
199#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
200pub enum OutsideTap {
201    /// Say nothing. A surface that dismisses some other way (a menu closed by
202    /// its own item, a toolbar that follows a selection) wants this.
203    Ignore,
204    /// Deliver
205    /// [`OverlayEventKind::OutsideDown`](crate::event::OverlayEventKind::OutsideDown)
206    /// to the owner on a primary press outside every registered rect.
207    Notify {
208        /// Whether the press is **consumed** by the notification.
209        ///
210        /// `true` is the modal light-dismiss shape: the press closes the surface
211        /// and the main tree never sees it, so the tap that dismisses a menu
212        /// does not also activate the button under it. `false` is the
213        /// pass-through shape: the owner is told, *and* the press continues into
214        /// the main tree as usual.
215        consume: bool,
216    },
217}
218
219/// The shared handle an owner and the root address one floated pod through.
220///
221/// `Rc<RefCell<_>>` rather than a borrow because the two sides run in different
222/// passes: the owner keeps one clone for the layout/event side, the registration
223/// carries the other so the root can paint it after the owner's own paint has
224/// returned. The root never *retains* a clone past the paint it was registered
225/// for (see [`OverlayHit`]), so the pod's lifetime stays the owner's.
226pub type OverlayPod = Rc<RefCell<ChildPod>>;
227
228/// One floated surface, registered by its owner for the paint pass in progress.
229///
230/// Every field is a D7 rule the root reads back: `band` and registration order
231/// decide paint and hit order, `input` decides whether the surface is hit-tested
232/// at all, `outside_tap` decides what a press elsewhere delivers, and
233/// `window_rect` is both where the pod is painted and what the root hit-tests.
234#[derive(Clone)]
235pub struct OverlayEntry {
236    /// The owner's identity, quoted back on every routed
237    /// [`InputEvent::Overlay`](crate::event::InputEvent::Overlay) so the owner
238    /// recognises its own surface's input and every other widget ignores it.
239    pub key: OverlayKey,
240    /// Which z-band the surface paints and hit-tests in (`Floating` below
241    /// `Tooltip`).
242    pub band: OverlayBand,
243    /// Whether the surface takes pointer input or is painted through.
244    pub input: OverlayInput,
245    /// What a primary press outside every registered rect delivers here.
246    pub outside_tap: OutsideTap,
247    /// Where the surface sits, in **absolute logical window space** — the one
248    /// coordinate space both the root's paint origin and the root's hit test use.
249    /// An owner computes it in `paint` from
250    /// [`PaintCtx::origin`](crate::widget::PaintCtx::origin), the only absolute
251    /// anchor a widget has, which is what makes an anchored surface follow its
252    /// owner with no subscription of any kind.
253    pub window_rect: Rect,
254    /// The pod the root paints. The owner holds the other clone and lays this
255    /// out itself, against the window size; the owner must **not** paint it —
256    /// painting it as well would draw the surface twice, once clipped in place
257    /// and once floated.
258    ///
259    /// The root paints it with `window_rect.origin()` as the absolute origin, and
260    /// [`ChildPod::paint_child`](crate::widget::ChildPod::paint_child) then adds
261    /// the pod's **own** origin on top as it does for any child — so leave that at
262    /// [`Point::ZERO`](kurbo::Point::ZERO) unless you mean an offset the hit test
263    /// will not know about (routing tests `window_rect` alone).
264    pub pod: OverlayPod,
265    /// The window insets the pod was laid out under (its owner's `LayoutCtx`
266    /// view); paint installs the same value so paint-time reads agree with
267    /// layout-time reads.
268    pub insets: WindowInsets,
269}
270
271impl fmt::Debug for OverlayEntry {
272    /// Prints every routing field and elides the pod: a [`ChildPod`] is not
273    /// [`Debug`], and what a diagnostic wants from an entry is where it sits and
274    /// how it routes, not what it contains.
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.debug_struct("OverlayEntry")
277            .field("key", &self.key)
278            .field("band", &self.band)
279            .field("input", &self.input)
280            .field("outside_tap", &self.outside_tap)
281            .field("window_rect", &self.window_rect)
282            .field("insets", &self.insets)
283            .field("pod", &"<pod>")
284            .finish()
285    }
286}
287
288/// What the root retains from an [`OverlayEntry`] to route the *next* pass's
289/// input with: everything except the pod.
290///
291/// Dropping the pod handle is the point. The root holds these between paints, so
292/// retaining an [`OverlayPod`] here would make the root a co-owner of a pod whose
293/// owner may have been unmounted since, and would invite a borrow held across a
294/// pass boundary. Routing needs none of it: the root decides *which key* the
295/// event belongs to and broadcasts that key, and the owner — which does hold the
296/// pod — forwards it in.
297#[derive(Clone, Copy, Debug, PartialEq)]
298pub struct OverlayHit {
299    /// The owner's identity, broadcast with the routed event.
300    pub key: OverlayKey,
301    /// The band, which orders the hit test (`Tooltip` tested before `Floating`).
302    pub band: OverlayBand,
303    /// Whether this entry is hit-tested at all.
304    pub input: OverlayInput,
305    /// What a press outside every rect delivers here.
306    pub outside_tap: OutsideTap,
307    /// The absolute logical rect the hit test runs against.
308    pub window_rect: Rect,
309}
310
311impl OverlayHit {
312    /// Take the routing half of `entry`, leaving the pod behind.
313    pub(crate) fn of(entry: &OverlayEntry) -> Self {
314        Self {
315            key: entry.key,
316            band: entry.band,
317            input: entry.input,
318            outside_tap: entry.outside_tap,
319            window_rect: entry.window_rect,
320        }
321    }
322
323    /// Whether `point` (absolute logical window space) lies inside this entry.
324    ///
325    /// Half-open on the far edges, exactly like
326    /// [`ChildPod::contains`](crate::widget::ChildPod::contains), so two
327    /// surfaces sharing an edge cannot both claim the same pixel.
328    pub(crate) fn contains(&self, point: kurbo::Point) -> bool {
329        point.x >= self.window_rect.x0
330            && point.x < self.window_rect.x1
331            && point.y >= self.window_rect.y0
332            && point.y < self.window_rect.y1
333    }
334}
335
336/// The open/close bracket around one overlay registration pass.
337///
338/// [`RenderRoot::paint`](crate::app::RenderRoot::paint) holds one for the length
339/// of the pass: entering clears the registry (so nothing from a previous pass —
340/// or from a widget painted with no root above it — can survive into this one),
341/// [`OverlayPaintPass::take`] drains what this pass registered, and `Drop` hands
342/// an enclosing pass its own in-progress registrations back.
343///
344/// The mechanism is [`PassBracket`]'s, shared with the event pass's request
345/// slots; only the channel and the open flag differ.
346pub(crate) struct OverlayPaintPass(PassBracket<Vec<OverlayEntry>>);
347
348impl OverlayPaintPass {
349    /// Open an overlay registration pass, starting from "nobody has registered
350    /// anything".
351    pub(crate) fn enter() -> Self {
352        OverlayPaintPass(PassBracket::enter(&OVERLAY_REGISTRY, &OVERLAY_PASS_OPEN))
353    }
354
355    /// Take what *this* pass registered, in registration order, leaving the
356    /// registry empty.
357    pub(crate) fn take(&self) -> Vec<OverlayEntry> {
358        self.0.take()
359    }
360}
361
362/// Record `entry` in the pass currently painting on this thread.
363///
364/// The implementation behind
365/// [`PaintCtx::register_overlay`](crate::widget::PaintCtx::register_overlay),
366/// which is the API an owner calls; this is the module-private half so the slot
367/// stays owned here.
368pub(crate) fn register(entry: OverlayEntry) {
369    OVERLAY_REGISTRY.with(|registry| {
370        // Move the `Vec` out, push, move it back: a `Cell` rather than a
371        // `RefCell` because the slot is bracketed by the shared [`PassBracket`]
372        // machinery, which is written against `Cell`'s `take`/`set`. The move is
373        // three words; the allocation is reused across the whole pass.
374        let mut entries = registry.take();
375        entries.push(entry);
376        registry.set(entries);
377    });
378}
379
380/// Sort `entries` into paint order in place: `Floating` first, then `Tooltip`,
381/// preserving registration order within each band.
382///
383/// A **stable** sort, which is the whole rule — the band is the only key, so
384/// stability is what makes "later registration paints above earlier" true within
385/// a band. Reversing the result gives the hit-test order (topmost first).
386pub(crate) fn sort_into_paint_order(entries: &mut [OverlayEntry]) {
387    entries.sort_by_key(|entry| entry.band);
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use crate::layout::BoxConstraints;
394    use crate::widget::{LayoutCtx, PaintCtx, PaintScene, Widget};
395    use kurbo::{Point, Size};
396
397    /// A do-nothing leaf, so an entry can carry a real [`ChildPod`].
398    struct StubWidget;
399    impl Widget for StubWidget {
400        fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
401            Size::ZERO
402        }
403        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
404    }
405
406    fn entry(key: OverlayKey, band: OverlayBand, rect: Rect) -> OverlayEntry {
407        OverlayEntry {
408            key,
409            band,
410            input: OverlayInput::Interactive,
411            outside_tap: OutsideTap::Ignore,
412            window_rect: rect,
413            pod: Rc::new(RefCell::new(ChildPod::new(Box::new(StubWidget)))),
414            insets: WindowInsets::default(),
415        }
416    }
417
418    #[test]
419    fn keys_are_distinct_and_stable() {
420        let first = OverlayKey::next();
421        let second = OverlayKey::next();
422        assert_ne!(first, second, "two owners never share an identity");
423        // An owner stores its key; copying it around must not re-allocate.
424        let stored = first;
425        assert_eq!(stored, first);
426        assert_ne!(stored, second);
427    }
428
429    #[test]
430    fn paint_order_puts_floating_below_tooltip_and_is_stable_within_a_band() {
431        let (a, b, c, d) = (
432            OverlayKey::next(),
433            OverlayKey::next(),
434            OverlayKey::next(),
435            OverlayKey::next(),
436        );
437        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
438        // Registered tooltip-first and interleaved, which is exactly what the
439        // sort has to survive: the band decides, registration order only breaks
440        // ties inside one.
441        let mut entries = vec![
442            entry(a, OverlayBand::Tooltip, rect),
443            entry(b, OverlayBand::Floating, rect),
444            entry(c, OverlayBand::Tooltip, rect),
445            entry(d, OverlayBand::Floating, rect),
446        ];
447        sort_into_paint_order(&mut entries);
448        assert_eq!(
449            entries.iter().map(|e| e.key).collect::<Vec<_>>(),
450            vec![b, d, a, c],
451            "Floating first (in registration order), then Tooltip (in registration order)"
452        );
453    }
454
455    #[test]
456    fn a_hit_rect_is_half_open_on_its_far_edges() {
457        let hit = OverlayHit::of(&entry(
458            OverlayKey::next(),
459            OverlayBand::Floating,
460            Rect::new(10.0, 20.0, 110.0, 60.0),
461        ));
462        assert!(
463            hit.contains(Point::new(10.0, 20.0)),
464            "near edges are inside"
465        );
466        assert!(hit.contains(Point::new(109.9, 59.9)));
467        // Half-open like `ChildPod::contains`, so two surfaces sharing an edge
468        // cannot both claim the same pixel.
469        assert!(!hit.contains(Point::new(110.0, 40.0)));
470        assert!(!hit.contains(Point::new(60.0, 60.0)));
471        assert!(!hit.contains(Point::new(9.9, 40.0)));
472    }
473
474    #[test]
475    fn a_hit_keeps_the_routing_fields_and_drops_the_pod() {
476        let key = OverlayKey::next();
477        let mut source = entry(key, OverlayBand::Tooltip, Rect::new(1.0, 2.0, 3.0, 4.0));
478        source.input = OverlayInput::Transparent;
479        source.outside_tap = OutsideTap::Notify { consume: true };
480        let hit = OverlayHit::of(&source);
481        assert_eq!(hit.key, key);
482        assert_eq!(hit.band, OverlayBand::Tooltip);
483        assert_eq!(hit.input, OverlayInput::Transparent);
484        assert_eq!(hit.outside_tap, OutsideTap::Notify { consume: true });
485        assert_eq!(hit.window_rect, source.window_rect);
486        // The pod stayed with its owner: the entry still holds the only two
487        // clones (the owner's and this local one), never a third in the root.
488        assert_eq!(Rc::strong_count(&source.pod), 1);
489    }
490
491    #[test]
492    fn the_registry_is_pass_scoped_and_preserves_registration_order() {
493        let (first, second) = (OverlayKey::next(), OverlayKey::next());
494        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
495        let pass = OverlayPaintPass::enter();
496        assert!(pass.take().is_empty(), "a fresh pass starts empty");
497
498        register(entry(first, OverlayBand::Floating, rect));
499        register(entry(second, OverlayBand::Floating, rect));
500        let drained = pass.take();
501        assert_eq!(
502            drained.iter().map(|e| e.key).collect::<Vec<_>>(),
503            vec![first, second],
504            "registration order is preserved"
505        );
506        assert!(pass.take().is_empty(), "and the drain empties the registry");
507        drop(pass);
508
509        // A registration made with no pass open (a leaf unit test painting a bare
510        // `PaintCtx`) is dropped by the next pass's clear, never leaked into it —
511        // which is what makes "the registry is empty at the start of every paint"
512        // true without anyone unregistering.
513        register(entry(first, OverlayBand::Floating, rect));
514        let next = OverlayPaintPass::enter();
515        assert!(
516            next.take().is_empty(),
517            "a stray registration does not survive into the next pass"
518        );
519    }
520
521    #[test]
522    fn a_nested_pass_hands_the_enclosing_passs_registrations_back() {
523        let (outer_key, inner_key) = (OverlayKey::next(), OverlayKey::next());
524        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
525        let outer = OverlayPaintPass::enter();
526        register(entry(outer_key, OverlayBand::Floating, rect));
527        {
528            let inner = OverlayPaintPass::enter();
529            assert!(inner.take().is_empty(), "the inner pass starts empty");
530            register(entry(inner_key, OverlayBand::Floating, rect));
531            assert_eq!(
532                inner.take().iter().map(|e| e.key).collect::<Vec<_>>(),
533                vec![inner_key]
534            );
535        }
536        assert_eq!(
537            outer.take().iter().map(|e| e.key).collect::<Vec<_>>(),
538            vec![outer_key],
539            "the enclosing pass's registrations are handed back intact"
540        );
541    }
542
543    #[test]
544    fn debug_prints_the_routing_fields_without_the_pod() {
545        let rendered = format!(
546            "{:?}",
547            entry(
548                OverlayKey::next(),
549                OverlayBand::Floating,
550                Rect::new(0.0, 0.0, 10.0, 10.0)
551            )
552        );
553        assert!(rendered.contains("Floating"), "{rendered}");
554        assert!(rendered.contains("Interactive"), "{rendered}");
555        assert!(rendered.contains("<pod>"), "{rendered}");
556    }
557}