Skip to main content

frust_widgets/
overlay.rs

1//! The widget-author face of the overlay portal: where a floated surface is
2//! placed, the slot a widget that hosts its own surface keeps, and the
3//! declarative wrapper for the common case.
4//!
5//! [`frust_core::overlay`] owns the *mechanism* — a per-paint registry the
6//! render root drains, paints above the whole main tree and hit-tests before
7//! it. This module owns the three things every caller of that mechanism would
8//! otherwise write for itself:
9//!
10//! * [`place`] — the anchored placement geometry (a side, a cross-axis
11//!   alignment, a gap, a collision flip and a shift-back-inside clamp). Pure,
12//!   total and usable on its own.
13//! * [`OverlaySlot`] — the owner's half of the portal: it holds the pod, lays
14//!   it out against the window, computes its window rect and registers it every
15//!   paint, and routes the broadcast the root sends back into it. A widget that
16//!   floats a surface of its own (a field hosting a selection toolbar, a menu
17//!   button) holds one of these and forwards four calls to it.
18//! * [`overlay_portal`] — the declarative wrapper for the common case: a child,
19//!   an optional overlay view anchored to that child's bounds, and the portal
20//!   does the rest.
21//!
22//! # Coordinate spaces
23//!
24//! Three spaces meet here, and every bug in a floating surface is a confusion
25//! between two of them:
26//!
27//! * **Window space** — absolute logical pixels. The registered
28//!   [`OverlayEntry::window_rect`](frust_core::OverlayEntry::window_rect) is in
29//!   it, the root hit-tests in it, and the payload of an
30//!   [`InputEvent::Overlay`] is in it. A widget only ever learns its own window
31//!   position in `paint`, from
32//!   [`PaintCtx::origin`](frust_core::PaintCtx::origin), which is why an
33//!   `Owner`/`Rect` placement is computed there and nowhere else — the one
34//!   exception is [`OverlayAnchor::Window`], which is already stated in this
35//!   space by its caller and is instead updated from `event` (see
36//!   [`OverlaySlot::set_window_anchor`]), because the pointer position a drag
37//!   ghost tracks is itself only ever known there.
38//! * **Owner-local space** — what the owner's own `event` sees: every container
39//!   between the root and the owner has already subtracted its origin. An
40//!   ordinary pointer event routed to the owner by a live capture arrives here,
41//!   so the slot adds the owner origin it recorded at paint time to get back to
42//!   window space.
43//! * **Pod space** — the floated pod's own local space, which is window space
44//!   minus the placed rect's origin. That single subtraction is the whole
45//!   translation into the surface.
46//!
47//! # Capture lifetime
48//!
49//! A pod that captures the pointer records the ordinary
50//! [`frust_core::ChildPod::is_active`] link, and [`OverlaySlot`] clears it on
51//! the `Up`/`Cancel` that ends the gesture which opened it — the same rule the
52//! authoring toolkit's `route_event_single` and the component boundary's own
53//! router apply to every other captured child. The slot dispatches into its pod
54//! directly rather than through either helper (the substituted route below
55//! needs a context it builds itself), so it owns that half of the container
56//! contract too.
57//!
58//! The alternative — leaving the link latched and reading the capture off the
59//! dispatch's own flag instead — was not taken. Both of the slot's capture
60//! mirrors read the *edge* into that link rather than its level, so a link that
61//! is set once and never cleared makes the edge observable once per pod
62//! instance rather than once per gesture, and a surface that stays mounted
63//! loses every gesture after the first. The link's *level* has a reader of its
64//! own besides — a pod holding it is refused a hover claim — which a latch that
65//! never falls would strand just as permanently. Restoring the link's lifetime
66//! answers both; re-deriving one mirror would leave the latch standing for the
67//! other. The slot's own "these owner-local pointer events are the surface's"
68//! flag is then derived from the link rather than latched beside it, so the two
69//! cannot drift apart.
70//!
71//! # Focus lifetime
72//!
73//! A pod's focus link has the same problem the capture link above had, one level
74//! up: a container clears a child's `focused` flag from its own
75//! blur-on-outside-tap sweep, and no container holds this pod, so no sweep ever
76//! reaches it. Nothing here fixes that by hand, and deliberately so — a fix that
77//! lived in this module would cover the surfaces `OverlaySlot` owns and leave
78//! every hand-written owner (and every direct `PaintCtx::register_overlay`
79//! caller) with the original defect.
80//!
81//! `frust-core` gives the link a lifetime instead, where every claim already
82//! passes: a recorded link carries the identity of the focus session it was
83//! recorded for, and the render root moves that identity on whenever a dispatch
84//! records a new claim or ends the session. A link whose stamp names an older
85//! session is retired by arithmetic, with no pass having to visit the branch it
86//! belongs to — and the root retires the flag itself the next time it holds the
87//! pod, during the paint that floats it
88//! ([`ChildPod::retire_stale_focus_link`](frust_core::ChildPod::retire_stale_focus_link)).
89//!
90//! What this module owes that mechanism is to ask the right question:
91//! [`OverlaySlot::pod_has_focus`] reports a link on the **live** session, not a
92//! flag that was once set, and every routing decision here reads it.
93//!
94//! # Tab traversal
95//!
96//! The root never walks a floated pod on its own. [`OverlayPortalWidget`] visits
97//! its pod through [`OverlaySlot::visit_focus_pod`] after its child, so a
98//! non-modal pod's focusable content is reached right after its owner's stops.
99//! A modal pod is the exception: the root walks it alone.
100//!
101//! # Not in v1
102//!
103//! * **The pod contributes no semantics.** A pod's nodes would attach under the
104//!   owner's own accessibility node, at the owner's position rather than the
105//!   floated rect's, so nothing is published rather than something wrong. An
106//!   assistive-technology user reaches a floated surface through the owner
107//!   (a field's own actions, a trigger's own node), not through the surface.
108//! * **No hover inside a pod.** The root marks a hover pass on a hit-tested
109//!   pointer event only, and an overlay event is a broadcast, so
110//!   [`EventCtx::claim_hover`](frust_core::EventCtx::claim_hover) inside a
111//!   floated surface records nothing.
112//! * **A substituted pod publishes no IME surface** (see
113//!   [`OverlaySlot::event`]); a pod over the ambient application state
114//!   ([`OverlaySlot::event_ambient`], the route [`overlay_portal`] takes)
115//!   publishes normally.
116//! * **No focus trap and no nesting**, per [`frust_core::overlay`]'s own list.
117
118use std::any::Any;
119use std::cell::RefCell;
120use std::marker::PhantomData;
121use std::rc::Rc;
122
123use frust_core::overlay::OverlayKeys;
124use frust_core::{
125    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
126    Key, LayoutCtx, NamedKey, OutsideTap, OverlayBand, OverlayEntry, OverlayEventKind,
127    OverlayInput, OverlayKey, PaintCtx, PaintScene, PointerEvent, SemanticsCtx, View, Widget, any,
128};
129use kurbo::{Point, Rect, Size, Vec2};
130
131use crate::authoring::{ErasedCallback, erase_callback, releases_capture, route_event_single};
132
133// ---------------------------------------------------------------------------
134// Placement
135// ---------------------------------------------------------------------------
136
137/// The side of the anchor a floated surface opens on.
138///
139/// The web vocabulary every anchored-overlay pattern in this workspace already
140/// speaks (Radix's `side`, and the four-sided tooltip variants each catalog
141/// grew on top of it).
142#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
143pub enum OverlaySide {
144    /// Above the anchor.
145    Top,
146    /// To the trailing side of the anchor.
147    Right,
148    /// Below the anchor — the default.
149    #[default]
150    Bottom,
151    /// To the leading side of the anchor.
152    Left,
153}
154
155impl OverlaySide {
156    /// The side a collision flip lands on.
157    pub const fn opposite(self) -> Self {
158        match self {
159            OverlaySide::Top => OverlaySide::Bottom,
160            OverlaySide::Bottom => OverlaySide::Top,
161            OverlaySide::Left => OverlaySide::Right,
162            OverlaySide::Right => OverlaySide::Left,
163        }
164    }
165
166    /// Whether this side stacks the surface vertically (`Top`/`Bottom`).
167    pub const fn is_vertical(self) -> bool {
168        matches!(self, OverlaySide::Top | OverlaySide::Bottom)
169    }
170}
171
172/// How a floated surface lines up with its anchor on the cross axis.
173#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
174pub enum OverlayAlign {
175    /// Leading edges flush (left edges for a `Top`/`Bottom` side, top edges for
176    /// a `Left`/`Right` one).
177    Start,
178    /// Centres flush — the default.
179    #[default]
180    Center,
181    /// Trailing edges flush.
182    End,
183}
184
185/// A resolved placement request: which side, how it lines up, how far off the
186/// anchor it sits, and how it may move to stay inside the area.
187#[derive(Clone, Copy, Debug, PartialEq)]
188pub struct OverlayPlacement {
189    /// The preferred side.
190    pub side: OverlaySide,
191    /// Cross-axis alignment.
192    pub align: OverlayAlign,
193    /// Gap between the anchor and the surface, in logical px.
194    pub offset: f64,
195    /// Flip to [`OverlaySide::opposite`] when the preferred side does not fit
196    /// and the opposite one does.
197    pub flip: bool,
198    /// Shift the placed rect back inside the padded area when it overflows.
199    pub clamp: bool,
200    /// The margin the surface keeps from every edge of the area, in logical px.
201    /// Both the fit test and the clamp read the area inset by it.
202    pub padding: f64,
203}
204
205/// The default gap between an anchor and the surface placed against it, in
206/// logical px.
207///
208/// The small neutral gap two of the three anchored hosts in this workspace
209/// already default to; a design system that wants a wider one (a panel with a
210/// visible neck) sets [`OverlayPlacement::offset`] in its own wrapper rather
211/// than changing this.
212pub const DEFAULT_OFFSET: f64 = 4.0;
213
214/// The default margin a placed surface keeps from every edge of the area, in
215/// logical px — the viewport padding the collision clamp works against.
216pub const DEFAULT_PADDING: f64 = 8.0;
217
218impl Default for OverlayPlacement {
219    /// Below the anchor, centred, at [`DEFAULT_OFFSET`], flipping and clamping
220    /// inside [`DEFAULT_PADDING`] of the area's edges.
221    fn default() -> Self {
222        OverlayPlacement {
223            side: OverlaySide::default(),
224            align: OverlayAlign::default(),
225            offset: DEFAULT_OFFSET,
226            flip: true,
227            clamp: true,
228            padding: DEFAULT_PADDING,
229        }
230    }
231}
232
233impl OverlayPlacement {
234    /// A placement on `side`, everything else defaulted.
235    pub fn on(side: OverlaySide) -> Self {
236        OverlayPlacement {
237            side,
238            ..Self::default()
239        }
240    }
241
242    /// Set the cross-axis alignment.
243    pub const fn align(mut self, align: OverlayAlign) -> Self {
244        self.align = align;
245        self
246    }
247
248    /// Set the anchor gap, in logical px.
249    pub const fn offset(mut self, offset: f64) -> Self {
250        self.offset = offset;
251        self
252    }
253
254    /// Enable or disable the collision flip.
255    pub const fn flip(mut self, flip: bool) -> Self {
256        self.flip = flip;
257        self
258    }
259
260    /// Enable or disable the shift-back-inside clamp.
261    pub const fn clamp(mut self, clamp: bool) -> Self {
262        self.clamp = clamp;
263        self
264    }
265
266    /// Set the margin kept from the area's edges, in logical px.
267    pub const fn padding(mut self, padding: f64) -> Self {
268        self.padding = padding;
269        self
270    }
271}
272
273/// The region a surface may occupy: `area` inset by `padding` on every side.
274///
275/// Falls back to `area` itself when the inset would invert it — a window
276/// narrower than twice the padding still has to place its surface somewhere,
277/// and an inverted rect would make the clamp below meaningless.
278fn field(area: Rect, padding: f64) -> Rect {
279    let inset = area.inset(-padding);
280    if inset.width() > 0.0 && inset.height() > 0.0 {
281        inset
282    } else {
283        area
284    }
285}
286
287/// Whether a `content`-sized surface fits on `side` of `anchor` inside `field`.
288fn fits(side: OverlaySide, anchor: Rect, content: Size, field: Rect, offset: f64) -> bool {
289    match side {
290        OverlaySide::Top => anchor.y0 - offset - content.height >= field.y0,
291        OverlaySide::Bottom => anchor.y1 + offset + content.height <= field.y1,
292        OverlaySide::Left => anchor.x0 - offset - content.width >= field.x0,
293        OverlaySide::Right => anchor.x1 + offset + content.width <= field.x1,
294    }
295}
296
297/// The cross-axis start coordinate for `align`, given the anchor's own span
298/// `[a0, a1]` and the surface's `extent` along that axis.
299fn align_start(align: OverlayAlign, a0: f64, a1: f64, extent: f64) -> f64 {
300    match align {
301        OverlayAlign::Start => a0,
302        OverlayAlign::Center => (a0 + a1) / 2.0 - extent / 2.0,
303        OverlayAlign::End => a1 - extent,
304    }
305}
306
307/// Shift `rect` back inside `field`, keeping its size.
308///
309/// The start edge wins when the surface is larger than the field (`min` before
310/// `max`): a too-wide panel hangs off the trailing edge rather than the leading
311/// one, where its content starts.
312fn clamp_into(rect: Rect, field: Rect) -> Rect {
313    let x = rect.x0.min(field.x1 - rect.width()).max(field.x0);
314    let y = rect.y0.min(field.y1 - rect.height()).max(field.y0);
315    Rect::from_origin_size(Point::new(x, y), rect.size())
316}
317
318/// Place a `content`-sized surface against `anchor` inside `area`.
319///
320/// All three rects are in one coordinate space — window space, for every caller
321/// inside this module. The returned rect is the surface's placed bounds:
322///
323/// 1. inset `area` by `placement.padding` — every step below works in that
324///    field;
325/// 2. pick the side: the preferred one, or its opposite when `flip` is on, the
326///    preferred one does not fit and the opposite one does;
327/// 3. offset off that edge of the anchor by `offset`, lined up on the cross axis
328///    per `align`;
329/// 4. shift back inside the field when `clamp` is on.
330///
331/// Pure and total: it allocates nothing, reads no context, and is defined for a
332/// degenerate anchor (a zero-size rect places against that point) and for
333/// content larger than the field (which pins to the field's *start* edge).
334///
335/// This is the one copy of geometry each anchored-overlay host in the design
336/// systems had derived for itself (`plugins/shadcn`, `plugins/beui` and
337/// `plugins/material`'s `overlay::anchored`), promoted here so a widget, a
338/// catalog and an app all place a surface the same way.
339pub fn place(anchor: Rect, content: Size, area: Rect, placement: OverlayPlacement) -> Rect {
340    let field = field(area, placement.padding);
341    let mut side = placement.side;
342    if placement.flip
343        && !fits(side, anchor, content, field, placement.offset)
344        && fits(side.opposite(), anchor, content, field, placement.offset)
345    {
346        side = side.opposite();
347    }
348    let origin = match side {
349        OverlaySide::Top => Point::new(
350            align_start(placement.align, anchor.x0, anchor.x1, content.width),
351            anchor.y0 - placement.offset - content.height,
352        ),
353        OverlaySide::Bottom => Point::new(
354            align_start(placement.align, anchor.x0, anchor.x1, content.width),
355            anchor.y1 + placement.offset,
356        ),
357        OverlaySide::Left => Point::new(
358            anchor.x0 - placement.offset - content.width,
359            align_start(placement.align, anchor.y0, anchor.y1, content.height),
360        ),
361        OverlaySide::Right => Point::new(
362            anchor.x1 + placement.offset,
363            align_start(placement.align, anchor.y0, anchor.y1, content.height),
364        ),
365    };
366    let rect = Rect::from_origin_size(origin, content);
367    if placement.clamp {
368        clamp_into(rect, field)
369    } else {
370        rect
371    }
372}
373
374// ---------------------------------------------------------------------------
375// The owner's slot
376// ---------------------------------------------------------------------------
377
378/// What a slot places its surface against.
379///
380/// `Owner` and `Rect` resolve to a **window-space** rect in the owner's
381/// `paint`, where the owner's absolute origin is finally known; nothing is
382/// subscribed to and nothing is cached across frames, so an anchor follows
383/// its owner across scroll, relayout and animation for free. `Window` is
384/// already window-space and skips that resolution entirely — see its own
385/// docs for why it is updated from `event` instead.
386#[derive(Clone, Copy, Debug, Default, PartialEq)]
387pub enum OverlayAnchor {
388    /// The owner's own bounds — a trigger floating a menu under itself.
389    #[default]
390    Owner,
391    /// A rect in the **owner's local space**: a caret, a selection's bounding
392    /// box, a press point, one row of a list. Translated by the owner's paint
393    /// origin, so a caller states it in the same coordinates its `layout` and
394    /// `event` already speak.
395    Rect(Rect),
396    /// An absolute **window-space** point the surface's top-left tracks
397    /// directly — a drag ghost following the pointer rather than an owner's
398    /// bounds.
399    ///
400    /// The placed rect is always `point + offset`, sized to the pod's own
401    /// content size: no side, alignment or collision flip applies, because
402    /// there is no anchor rect to place against, only a point — so none of
403    /// [`OverlayPlacement`]'s side/align/flip/clamp fields apply to it either
404    /// (`place` is never called for this variant). The rect is clamped into
405    /// the window area when [`OverlaySlot::set_clamp_to_window`] has turned
406    /// that on (default `false`, so the ghost may trail off the window's edge
407    /// with the pointer).
408    ///
409    /// Set once (with whatever `offset` the gesture's grab point needs) via
410    /// [`OverlaySlot::set_anchor`], then updated every pointer `Move`/`Down`
411    /// with [`OverlaySlot::set_window_anchor`], which keeps `offset` and only
412    /// moves `point` — see that method for why the update belongs in the
413    /// owner's `event` rather than waiting for the next `paint`.
414    Window {
415        /// The absolute window-space point the rect's top-left tracks.
416        point: Point,
417        /// A fixed offset from `point` to the rect's actual top-left — the
418        /// grab point within a dragged ghost, say.
419        offset: Vec2,
420    },
421}
422
423/// One floated surface an owner hosts: the pod, where it goes, and the routing
424/// of the input the root sends back to it.
425///
426/// # The four calls
427///
428/// An owner widget forwards four of its own lifecycle calls here, and the slot
429/// does nothing on its own:
430///
431/// 1. [`rebuild`](Self::rebuild) from the owner's `View::rebuild`, with the
432///    overlay view it wants mounted (or `None` to drop it).
433/// 2. [`layout`](Self::layout) from the owner's `Widget::layout` — the pod is
434///    laid out loosely against the **window**, never the owner's own
435///    constraints, because it escapes the owner's box entirely.
436/// 3. [`paint`](Self::paint) from the owner's `Widget::paint`, which computes
437///    the placement and registers the pod. It paints nothing: the root paints
438///    every registered pod after the main tree, which is the only way a surface
439///    escapes its owner's paint order and every ancestor's clip.
440/// 4. [`event`](Self::event) (or [`event_ambient`](Self::event_ambient)) from
441///    the owner's `Widget::event`, **before** the owner routes to its own
442///    children. `None` means "not mine" and the owner carries on.
443///
444/// # `PodState`
445///
446/// The state type the floated view is diffed against. Two shapes exist and the
447/// dispatch call differs between them:
448///
449/// * the pod is built over the **ambient application state** (what
450///   [`overlay_portal`] does): use [`event_ambient`](Self::event_ambient), which
451///   forwards through the owner's own [`EventCtx`] so focus, capture, hover and
452///   IME all bubble exactly as they do for any other child;
453/// * the pod is built over a **different** state — `()` for a
454///   framework-built surface whose callbacks carry their own handles: use
455///   [`event`](Self::event) and hand it `&mut PodState`, which dispatches over a
456///   substituted context (see that method for what does and does not bubble
457///   through the substitution).
458pub struct OverlaySlot<PodState: 'static> {
459    /// This surface's identity, allocated once and quoted back by every routed
460    /// event. Stable for the slot's life — re-allocating per frame would hand
461    /// the root a new identity every paint.
462    key: OverlayKey,
463    /// The mounted pod, shared with the registration the root paints. `None`
464    /// while the surface is closed.
465    pod: Option<Rc<RefCell<ChildPod>>>,
466    band: OverlayBand,
467    input: OverlayInput,
468    outside_tap: OutsideTap,
469    keys: OverlayKeys,
470    placement: OverlayPlacement,
471    anchor: OverlayAnchor,
472    /// Whether an [`OverlayAnchor::Window`] rect is clamped back inside the
473    /// window area. No effect on any other anchor, which is placed (and, per
474    /// its own `clamp`, possibly shifted back) by [`place`] instead. See
475    /// [`set_clamp_to_window`](Self::set_clamp_to_window).
476    clamp_to_window: bool,
477    /// The window size the last layout pass saw — the area the placement is
478    /// computed against, recorded in `layout` because a paint context carries
479    /// no window size of its own.
480    window: Size,
481    /// Where the last paint placed the surface, in window space: what was
482    /// registered, what the root hit-tests, and what a routed position is made
483    /// pod-local against.
484    window_rect: Rect,
485    /// The owner's own absolute origin as of the last paint — what an
486    /// owner-local pointer position (a captured drag) is lifted into window
487    /// space with.
488    owner_origin: Point,
489    /// Whether the pod holds the pointer capture, so ordinary pointer events
490    /// routed to the owner by the capture path belong to the surface.
491    ///
492    /// Recomputed from the pod's own recorded link by every dispatch through
493    /// `forward` — the one writer while a pod is mounted — rather than latched
494    /// alongside it. Mounting, dropping or replacing the pod resets it for the
495    /// same reason: the link it mirrors goes with the widget that held it.
496    captured: bool,
497    /// A press landed outside every floated surface and this one asked to hear
498    /// about it; drained by [`take_outside_down`](Self::take_outside_down).
499    outside_down_pending: bool,
500    /// A declined Escape reached the surface and the pod did not consume it; drained by
501    /// [`take_escape`](Self::take_escape).
502    escape_pending: bool,
503    _state: PhantomData<fn(&mut PodState)>,
504}
505
506impl<PodState: 'static> Default for OverlaySlot<PodState> {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512impl<PodState: 'static> OverlaySlot<PodState> {
513    /// A closed slot with a fresh identity: `Floating`, interactive, ignoring
514    /// outside taps, anchored to its owner's own bounds.
515    ///
516    /// Built **once**, when the owner widget is built, and kept: the key is the
517    /// whole addressing mechanism between the root and this surface.
518    pub fn new() -> Self {
519        Self {
520            key: OverlayKey::next(),
521            pod: None,
522            band: OverlayBand::Floating,
523            input: OverlayInput::Interactive,
524            outside_tap: OutsideTap::Ignore,
525            keys: OverlayKeys::Pass,
526            placement: OverlayPlacement::default(),
527            anchor: OverlayAnchor::Owner,
528            clamp_to_window: false,
529            window: Size::ZERO,
530            window_rect: Rect::ZERO,
531            owner_origin: Point::ZERO,
532            captured: false,
533            outside_down_pending: false,
534            escape_pending: false,
535            _state: PhantomData,
536        }
537    }
538
539    /// This surface's identity.
540    pub fn key(&self) -> OverlayKey {
541        self.key
542    }
543
544    /// Whether a pod is currently mounted.
545    pub fn is_open(&self) -> bool {
546        self.pod.is_some()
547    }
548
549    /// Where the last paint placed the surface, in window space.
550    ///
551    /// [`Rect::ZERO`] before the first paint of an open slot — a surface that
552    /// has never been painted has never been registered, so nothing routes to
553    /// it either.
554    pub fn window_rect(&self) -> Rect {
555        self.window_rect
556    }
557
558    /// Whether the pod holds a focus link on the **live** session — the read
559    /// every routing decision here makes.
560    ///
561    /// Not the raw recorded flag: a claim made from inside a floated surface
562    /// reaches no container's blur sweep, so a link this surface recorded
563    /// survives the session moving away from it (see the module docs' *Focus
564    /// lifetime*). Asking the raw flag would keep routing the keyboard into a
565    /// surface the user has left.
566    pub fn pod_has_focus(&self) -> bool {
567        self.pod
568            .as_ref()
569            .is_some_and(|pod| pod.borrow().holds_live_focus())
570    }
571
572    /// Visit the mounted pod for the focus walk, if one is mounted and not
573    /// currently borrowed. The owner calls this from its
574    /// `Widget::visit_focus_children`, after its own children, so a floated
575    /// pod's stops follow its owner's. The root's walk decides what the pod
576    /// contributes; a closed slot visits nothing.
577    pub fn visit_focus_pod(&self, visitor: &mut dyn FnMut(&ChildPod)) {
578        if let Some(pod) = &self.pod
579            && let Ok(pod) = pod.try_borrow()
580        {
581            visitor(&pod);
582        }
583    }
584
585    /// Drop the pod's recorded focus link, so focus-routed events stop reaching
586    /// it — the owner's half of "the surface asked for focus, and the owner
587    /// declined on its behalf".
588    ///
589    /// An owner's *decision*, and the only reason this exists: a link the
590    /// session has merely moved away from needs no call, since its stamp retires
591    /// it on its own (see the module docs' *Focus lifetime*).
592    pub fn withdraw_pod_focus(&mut self) {
593        if let Some(pod) = &self.pod {
594            pod.borrow_mut().set_focused(false);
595        }
596    }
597
598    /// Take the pending outside-press notification, clearing it.
599    ///
600    /// `true` exactly once per press that landed outside every floated surface
601    /// while this one was registered [`OutsideTap::Notify`] — the light-dismiss
602    /// signal an owner closes on.
603    pub fn take_outside_down(&mut self) -> bool {
604        std::mem::take(&mut self.outside_down_pending)
605    }
606
607    /// Take the pending Escape notification, clearing it.
608    ///
609    /// `true` once per declined Escape the root delivered here as
610    /// [`OverlayEventKind::Key`] (a [modal](OverlayKeys::Modal) surface, or the
611    /// forward target of a focused editable) that the pod itself did not consume
612    /// — the close signal. An owner that acts on it calls
613    /// [`EventCtx::consume_overlay_key`] so the key stops there.
614    pub fn take_escape(&mut self) -> bool {
615        std::mem::take(&mut self.escape_pending)
616    }
617
618    /// Which z-band the surface paints and hit-tests in.
619    pub fn set_band(&mut self, band: OverlayBand) {
620        self.band = band;
621    }
622
623    /// Whether the surface takes pointer input at all.
624    pub fn set_input(&mut self, input: OverlayInput) {
625        self.input = input;
626    }
627
628    /// What a press outside every floated surface delivers here.
629    pub fn set_outside_tap(&mut self, outside_tap: OutsideTap) {
630        self.outside_tap = outside_tap;
631    }
632
633    /// Whether the surface claims declined keys: [`OverlayKeys::Modal`] masks
634    /// main-tree shortcuts and takes a declined Escape (see [`frust_core::overlay`]).
635    pub fn set_keys(&mut self, keys: OverlayKeys) {
636        self.keys = keys;
637    }
638
639    /// Where the surface sits relative to its anchor.
640    pub fn set_placement(&mut self, placement: OverlayPlacement) {
641        self.placement = placement;
642    }
643
644    /// What the surface is placed against.
645    pub fn set_anchor(&mut self, anchor: OverlayAnchor) {
646        self.anchor = anchor;
647    }
648
649    /// Move an [`OverlayAnchor::Window`] anchor's point, keeping whatever
650    /// `offset` it already carries (or [`Vec2::ZERO`] if the slot was not
651    /// already `Window`-anchored) — the owner's call from its own `event`
652    /// handler on every pointer `Move`/`Down`, so a pointer-following pod (a
653    /// drag ghost) tracks the pointer without waiting for its own paint to
654    /// recompute a bounds-derived anchor the way [`Owner`](OverlayAnchor::Owner)
655    /// and [`Rect`](OverlayAnchor::Rect) do: both of those are only ever
656    /// re-derived from the owner's own `paint` (`ctx.origin()`, read there and
657    /// nowhere else), which is fine when the anchor only ever moves because
658    /// the owner itself moved or repainted for some unrelated reason, but a
659    /// ghost has to repaint at frame cadence as the pointer moves, which a
660    /// call from `event` is the only door onto.
661    ///
662    /// So, unlike [`set_anchor`](Self::set_anchor) (a plain field write the
663    /// next `paint` happens to pick up), this also requests a **repaint
664    /// only** ([`EventCtx::request_redraw`]) — never a relayout — of the
665    /// owner: `self.window` (the area) and the pod's own size are unaffected
666    /// by where the pointer now is, so the usual `ChangeFlags::LAYOUT` round
667    /// trip through `rebuild` would be wasted work the pointer cannot wait
668    /// for anyway.
669    pub fn set_window_anchor(&mut self, ctx: &mut EventCtx<'_>, point: Point) {
670        let offset = match self.anchor {
671            OverlayAnchor::Window { offset, .. } => offset,
672            _ => Vec2::ZERO,
673        };
674        self.anchor = OverlayAnchor::Window { point, offset };
675        ctx.request_redraw();
676    }
677
678    /// Whether an [`OverlayAnchor::Window`] rect is clamped back inside the
679    /// window area (default `false` — a drag ghost is allowed to trail off
680    /// the window's edge with the pointer). No effect on an `Owner`/`Rect`
681    /// anchor, whose own [`OverlayPlacement::clamp`] already governs it.
682    pub fn set_clamp_to_window(&mut self, clamp_to_window: bool) {
683        self.clamp_to_window = clamp_to_window;
684    }
685
686    /// Mount, reconcile or drop the floated view — the owner's `View::rebuild`
687    /// half.
688    ///
689    /// `prev`/`next` are the previous and current frame's overlay views, in the
690    /// shape [`rebuild_child`](crate::authoring::rebuild_child) itself takes: a
691    /// `None` → `Some` transition builds the pod, `Some` → `Some` reconciles it
692    /// in place (so a kept-open surface keeps its own widget state), `Some` →
693    /// `None` tears it down, and `None` → `None` does nothing. An owner that
694    /// mounts a view it builds itself (from a process-global builder, say)
695    /// keeps the previous one and hands both in.
696    ///
697    /// Dropping the pod also drops any capture it held: the widget that was
698    /// mid-gesture no longer exists, so there is nothing to unwind and nothing
699    /// to route follow-ups to.
700    pub fn rebuild(
701        &mut self,
702        prev: Option<&AnyView<PodState>>,
703        next: Option<&AnyView<PodState>>,
704        ctx: &mut BuildCtx<'_>,
705    ) -> ChangeFlags {
706        match (prev, next) {
707            (Some(prev), Some(next)) => match &self.pod {
708                Some(pod) => {
709                    let mut pod = pod.borrow_mut();
710                    crate::authoring::rebuild_child(prev, next, &mut pod, ctx)
711                }
712                // The view stayed mounted but the pod did not: build a fresh
713                // one rather than route into nothing.
714                None => self.mount(next, ctx),
715            },
716            (None, Some(next)) => {
717                // Nothing to reconcile against — an existing pod here belongs to
718                // a view the owner no longer has, so it is replaced rather than
719                // diffed (a torn-down widget's state dies with it either way).
720                self.drop_pod();
721                self.mount(next, ctx)
722            }
723            (Some(prev), None) => match self.pod.take() {
724                Some(pod) => {
725                    {
726                        let mut pod = pod.borrow_mut();
727                        crate::authoring::teardown_child(prev, &mut pod, ctx);
728                    }
729                    self.captured = false;
730                    ChangeFlags::LAYOUT
731                }
732                None => ChangeFlags::NONE,
733            },
734            (None, None) => {
735                if self.pod.is_some() {
736                    self.drop_pod();
737                    ChangeFlags::LAYOUT
738                } else {
739                    ChangeFlags::NONE
740                }
741            }
742        }
743    }
744
745    /// Build `view` into a fresh pod, replacing whatever was mounted.
746    fn mount(&mut self, view: &AnyView<PodState>, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
747        let pod = crate::authoring::build_child(view, ctx);
748        self.pod = Some(Rc::new(RefCell::new(pod)));
749        self.captured = false;
750        ChangeFlags::LAYOUT
751    }
752
753    /// Drop the pod with no view to tear it down through — the recovery arm for
754    /// a slot whose mounted view vanished without one.
755    fn drop_pod(&mut self) {
756        self.pod = None;
757        self.captured = false;
758    }
759
760    /// Lay the pod out against the window — the owner's `Widget::layout` half.
761    ///
762    /// Loose constraints against
763    /// [`LayoutCtx::window_size`](frust_core::LayoutCtx::window_size), never the
764    /// owner's own `bc`: the surface escapes the owner's box, so the owner's
765    /// constraints say nothing about how much room it has. The pod's own origin
766    /// stays [`Point::ZERO`] — the root paints it at the registered rect's
767    /// origin and adds the pod's origin on top, and routing tests the registered
768    /// rect alone, so any other value would desynchronize paint from hit test.
769    pub fn layout(&mut self, ctx: &mut LayoutCtx) {
770        self.window = ctx.window_size();
771        if let Some(pod) = &self.pod {
772            let bc = BoxConstraints::loose(self.window);
773            let mut pod = pod.borrow_mut();
774            pod.layout_child(ctx, &bc);
775            pod.set_origin(Point::ZERO);
776        }
777    }
778
779    /// Place and register the pod — the owner's `Widget::paint` half.
780    ///
781    /// Computes the anchor rect in window space from
782    /// [`PaintCtx::origin`](frust_core::PaintCtx::origin) (plus the local rect,
783    /// for [`OverlayAnchor::Rect`]), places the pod against it with [`place`],
784    /// records the result and hands the root a registration. **It paints
785    /// nothing**: an owner that also painted the pod would draw the surface
786    /// twice, once clipped in place and once floated.
787    ///
788    /// Registration is per paint pass, so a surface stays alive exactly while
789    /// its owner keeps painting — an owner that is culled, unmounted or simply
790    /// stops registering disappears from the routing table after the next paint
791    /// with nothing to unregister.
792    pub fn paint(&mut self, ctx: &mut PaintCtx, owner_size: Size) {
793        let Some(pod) = &self.pod else {
794            return;
795        };
796        self.owner_origin = ctx.origin();
797        let area = Rect::from_origin_size(Point::ZERO, self.window);
798        let content = pod.borrow().size();
799        self.window_rect = match self.anchor {
800            OverlayAnchor::Owner => {
801                let anchor = Rect::from_origin_size(ctx.origin(), owner_size);
802                place(anchor, content, area, self.placement)
803            }
804            OverlayAnchor::Rect(local) => {
805                let anchor = local + ctx.origin().to_vec2();
806                place(anchor, content, area, self.placement)
807            }
808            // No side, alignment or collision flip applies to a single point:
809            // the rect is stated directly and only `self.clamp_to_window` — a
810            // separate knob from `place`'s own `self.placement.clamp` — may
811            // pull it back inside the (unpadded) window area.
812            OverlayAnchor::Window { point, offset } => {
813                let rect = Rect::from_origin_size(point + offset, content);
814                if self.clamp_to_window {
815                    clamp_into(rect, area)
816                } else {
817                    rect
818                }
819            }
820        };
821        ctx.register_overlay(OverlayEntry {
822            key: self.key,
823            band: self.band,
824            input: self.input,
825            outside_tap: self.outside_tap,
826            keys: self.keys,
827            window_rect: self.window_rect,
828            pod: Rc::clone(pod),
829            insets: ctx.window_insets(),
830        });
831    }
832
833    /// Route an event into the surface over a **substituted** state — the
834    /// owner's `Widget::event` half for a pod whose `PodState` is not the
835    /// ambient application state (a `()`-typed, framework-built surface).
836    ///
837    /// `Some(_)` means the slot owned the event and the owner must not route it
838    /// on; `None` means it belongs to the owner's ordinary routing.
839    ///
840    /// # What crosses the substitution
841    ///
842    /// The pod runs over a fresh [`EventCtx`] built on `state`, exactly as a
843    /// component boundary runs its subtree over its own local state, and the
844    /// results are mirrored back onto the owner's context: a redraw request, a
845    /// pointer capture, and a focus claim or release (observed through the pod's
846    /// own recorded link). A published IME surface and a hover claim do **not**
847    /// cross — an IME publish has no route back through a substituted context,
848    /// and an overlay event is a broadcast, which records no hover anywhere. A
849    /// surface that needs either is built over the ambient state instead (see
850    /// [`event_ambient`](Self::event_ambient)).
851    ///
852    /// Edit commands do cross, by a different road: they ride a pass-scoped
853    /// queue rather than the context, so a pod that calls
854    /// [`EventCtx::dispatch_edit_command`](frust_core::EventCtx::dispatch_edit_command)
855    /// is drained by the owner's
856    /// [`EventCtx::take_edit_commands`](frust_core::EventCtx::take_edit_commands)
857    /// in the same pass.
858    pub fn event(
859        &mut self,
860        ctx: &mut EventCtx<'_>,
861        event: &InputEvent,
862        state: &mut PodState,
863    ) -> Option<EventResult> {
864        let substitute: &mut dyn Any = state;
865        self.route(ctx, event, Some(substitute))
866    }
867
868    /// Route an event into the surface over the **ambient** application state —
869    /// the owner's `Widget::event` half for a pod built over the same state the
870    /// owner itself is diffed against.
871    ///
872    /// The pod is dispatched through the owner's own [`EventCtx`], so
873    /// everything a child normally bubbles (redraw, capture, focus, a published
874    /// IME surface) reaches the root unchanged, and the pod's callbacks reach
875    /// the same application state every other widget sees. This is the route
876    /// [`overlay_portal`] takes.
877    pub fn event_ambient(
878        &mut self,
879        ctx: &mut EventCtx<'_>,
880        event: &InputEvent,
881    ) -> Option<EventResult> {
882        self.route(ctx, event, None)
883    }
884
885    /// The shared body of [`event`](Self::event)/
886    /// [`event_ambient`](Self::event_ambient): decide whether this event belongs
887    /// to the surface and, if it does, translate it into pod space and forward
888    /// it.
889    fn route(
890        &mut self,
891        ctx: &mut EventCtx<'_>,
892        event: &InputEvent,
893        substitute: Option<&mut dyn Any>,
894    ) -> Option<EventResult> {
895        // A closed slot owns nothing: every event belongs to the owner.
896        self.pod.as_ref()?;
897        let origin = self.window_rect.origin().to_vec2();
898        match event {
899            // A floated surface's own input, broadcast to the whole tree so it
900            // reaches this owner wherever it sits. The key comparison is the
901            // entire addressing mechanism: another owner's surface falls
902            // through untouched.
903            InputEvent::Overlay(overlay) if overlay.key == self.key => {
904                match &overlay.kind {
905                    OverlayEventKind::Pointer(pointer) => {
906                        let local = InputEvent::Pointer(PointerEvent {
907                            position: pointer.position - origin,
908                            ..*pointer
909                        });
910                        self.forward(ctx, &local, substitute);
911                    }
912                    OverlayEventKind::Scroll { position, delta } => {
913                        let local = InputEvent::Scroll {
914                            position: *position - origin,
915                            delta: *delta,
916                        };
917                        self.forward(ctx, &local, substitute);
918                    }
919                    // A scale gesture is routed on the same terms as a scroll:
920                    // only its focal point needs lifting into surface space.
921                    OverlayEventKind::Scale {
922                        focal,
923                        phase,
924                        scale_delta,
925                        velocity,
926                    } => {
927                        let local = InputEvent::Scale(frust_core::event::ScaleEvent {
928                            phase: *phase,
929                            scale_delta: *scale_delta,
930                            focal: *focal - origin,
931                            velocity: *velocity,
932                        });
933                        self.forward(ctx, &local, substitute);
934                    }
935                    // The press landed on nothing floated: the surface never saw
936                    // it, so nothing is forwarded — the owner reads the
937                    // notification and decides whether to close.
938                    OverlayEventKind::OutsideDown => self.outside_down_pending = true,
939                    // A shortcut a widget inside the pod declared: delivered there as the
940                    // plain broadcast its declarer recognises.
941                    OverlayEventKind::Shortcut(id) => {
942                        self.forward(ctx, &InputEvent::Shortcut(*id), substitute);
943                    }
944                    // A key the focus path declined: offered to the pod first; an
945                    // unconsumed Escape is left for the owner to close on.
946                    OverlayEventKind::Key(key) => {
947                        let local = InputEvent::Key(key.clone());
948                        if self.forward(ctx, &local, substitute) == EventResult::Handled {
949                            ctx.consume_overlay_key();
950                        } else if key.key == Key::Named(NamedKey::Escape)
951                            && key.modifiers == frust_core::Modifiers::default()
952                        {
953                            self.escape_pending = true;
954                        }
955                    }
956                }
957                // A broadcast is never consumed, whatever the pod returned.
958                Some(EventResult::Ignored)
959            }
960            // A gesture that began inside the surface: the capture it opened
961            // short-circuits the root's overlay pre-pass, so its follow-ups
962            // arrive here as ordinary pointer events in the OWNER's local space
963            // and have to be lifted back into window space first.
964            InputEvent::Pointer(pointer) if self.captured => {
965                let local = InputEvent::Pointer(PointerEvent {
966                    position: pointer.position + self.owner_origin.to_vec2() - origin,
967                    ..*pointer
968                });
969                // The release that ends the gesture is handled where the pod
970                // is dispatched, so it lands whichever door the event came in
971                // by — this one, or the broadcast arm above.
972                Some(self.forward(ctx, &local, substitute))
973            }
974            // Keyboard, IME and the clipboard verbs: focus-routed, so they only
975            // arrive here at all because the owner is on the recorded focus
976            // chain — and they belong to the surface exactly when the surface is
977            // what claimed focus.
978            event if event.is_focus_routed() && self.pod_has_focus() => {
979                Some(self.forward(ctx, event, substitute))
980            }
981            _ => None,
982        }
983    }
984
985    /// Dispatch `local` (already in pod space) into the pod, updating the
986    /// slot's own capture bookkeeping and, for a substituted state, mirroring
987    /// the pod's results onto the owner's context.
988    fn forward(
989        &mut self,
990        ctx: &mut EventCtx<'_>,
991        local: &InputEvent,
992        substitute: Option<&mut dyn Any>,
993    ) -> EventResult {
994        let Some(pod) = self.pod.clone() else {
995            return EventResult::Ignored;
996        };
997        let mut pod = pod.borrow_mut();
998        let was_active = pod.is_active();
999        let was_focused = pod.is_focused();
1000        let result = match substitute {
1001            // The ambient route: the pod is a child like any other, and
1002            // `event_child` bubbles its redraw/capture/focus/IME flags into the
1003            // owner's context on its own.
1004            None => pod.event_child(ctx, local),
1005            Some(state) => {
1006                let (result, needs_redraw, needs_layout) = {
1007                    let mut inner =
1008                        EventCtx::new(state, self.window_rect.origin(), self.window_rect.size());
1009                    let result = pod.event_child(&mut inner, local);
1010                    (result, inner.needs_redraw(), inner.needs_layout())
1011                };
1012                if needs_redraw {
1013                    ctx.request_redraw();
1014                }
1015                // A layout request crosses the substituted context like the
1016                // redraw does, so the root's pending flags carry LAYOUT and a
1017                // shell gating layout on them runs the pass. (The floated pod
1018                // itself was marked dirty by `event_child`; the root reaches it
1019                // through its dirty-surface check.)
1020                if needs_layout {
1021                    ctx.request_layout();
1022                }
1023                // Capture and focus are mirrored through the pod's own recorded
1024                // links rather than the substituted context's flags: the pod
1025                // records exactly what its widget asked for, and reading it here
1026                // keeps one mirror instead of two.
1027                if pod.is_active() && !was_active {
1028                    ctx.capture_pointer();
1029                }
1030                match (was_focused, pod.is_focused()) {
1031                    (false, true) => ctx.request_focus(),
1032                    (true, false) => ctx.release_focus(),
1033                    _ => {}
1034                }
1035                result
1036            }
1037        };
1038        // Give the link the lifetime every other container gives a captured
1039        // child. `event_child` only ever sets it, and this is the one dispatch
1040        // path that does not run through a routing helper, so the release has
1041        // to happen here or never — and until it does, the two `was_active`
1042        // edges above are edges into a latch that never falls.
1043        if was_active && releases_capture(local) {
1044            pod.set_active(false);
1045        }
1046        self.captured = pod.is_active();
1047        result
1048    }
1049}
1050
1051// ---------------------------------------------------------------------------
1052// The declarative portal
1053// ---------------------------------------------------------------------------
1054
1055/// Float `overlay` above the whole app, anchored to `child`'s bounds — the
1056/// declarative half of the portal.
1057///
1058/// The child is laid out, painted and routed exactly as it would be without the
1059/// wrapper: the portal adds nothing to its geometry and consumes none of its
1060/// input. The overlay is mounted while [`OverlayPortalView::overlay`] is
1061/// `Some`, which is also how an exit animation is expressed — keep handing the
1062/// same view in while the surface ramps out, and hand `None` once it has.
1063///
1064/// ```
1065/// use frust_widgets::{OverlayPlacement, OverlaySide, overlay_portal, text};
1066///
1067/// struct App {
1068///     hovering: bool,
1069/// }
1070///
1071/// fn tip(state: &mut App) -> impl frust_core::View<App> + use<> {
1072///     overlay_portal(text("save"))
1073///         .overlay(state.hovering.then(|| text("Save the document")))
1074///         .placement(OverlayPlacement::on(OverlaySide::Top))
1075/// }
1076/// # let _ = tip;
1077/// ```
1078pub fn overlay_portal<State: 'static, V: View<State>>(child: V) -> OverlayPortalView<State> {
1079    OverlayPortalView {
1080        child: any(child),
1081        overlay: None,
1082        placement: OverlayPlacement::default(),
1083        band: OverlayBand::Floating,
1084        input: OverlayInput::Interactive,
1085        outside_tap: OutsideTap::Ignore,
1086        keys: OverlayKeys::Pass,
1087        on_outside_tap: None,
1088        on_escape: None,
1089        preserve_focus: false,
1090    }
1091}
1092
1093/// The light-dismiss callback a portal holds before it is erased.
1094type OnOutsideTap<State> = Rc<dyn Fn(&mut State)>;
1095
1096/// A declarative overlay portal. See [`overlay_portal`].
1097pub struct OverlayPortalView<State: 'static> {
1098    child: AnyView<State>,
1099    overlay: Option<AnyView<State>>,
1100    placement: OverlayPlacement,
1101    band: OverlayBand,
1102    input: OverlayInput,
1103    outside_tap: OutsideTap,
1104    keys: OverlayKeys,
1105    on_outside_tap: Option<OnOutsideTap<State>>,
1106    on_escape: Option<OnOutsideTap<State>>,
1107    preserve_focus: bool,
1108}
1109
1110impl<State: 'static> OverlayPortalView<State> {
1111    /// The floated surface: `Some` mounts it, `None` drops it after the next
1112    /// paint.
1113    ///
1114    /// The view is diffed against the **same** application state the portal
1115    /// itself is, so the surface reads and writes app state exactly like the
1116    /// child does — it is a logical child of this call site that happens to be
1117    /// painted elsewhere.
1118    ///
1119    /// A bare `None` needs the surface's view type spelled out, e.g.
1120    /// `.overlay(None::<AnyView<State>>)`.
1121    pub fn overlay<V: View<State>>(mut self, overlay: Option<V>) -> Self {
1122        self.overlay = overlay.map(any);
1123        self
1124    }
1125
1126    /// Where the surface sits relative to the child's bounds (default: below,
1127    /// centred — see [`OverlayPlacement::default`]).
1128    pub fn placement(mut self, placement: OverlayPlacement) -> Self {
1129        self.placement = placement;
1130        self
1131    }
1132
1133    /// Which z-band the surface paints and hit-tests in (default
1134    /// [`OverlayBand::Floating`]; a tooltip belongs in
1135    /// [`OverlayBand::Tooltip`]).
1136    pub fn band(mut self, band: OverlayBand) -> Self {
1137        self.band = band;
1138        self
1139    }
1140
1141    /// Whether the surface takes pointer input (default
1142    /// [`OverlayInput::Interactive`]; explanatory chrome the pointer passes
1143    /// through is [`OverlayInput::Transparent`]).
1144    pub fn input(mut self, input: OverlayInput) -> Self {
1145        self.input = input;
1146        self
1147    }
1148
1149    /// What a press landing outside every floated surface delivers here
1150    /// (default [`OutsideTap::Ignore`]).
1151    pub fn outside_tap(mut self, outside_tap: OutsideTap) -> Self {
1152        self.outside_tap = outside_tap;
1153        self
1154    }
1155
1156    /// Run `callback` when a press lands outside every floated surface — the
1157    /// light-dismiss hook.
1158    ///
1159    /// Setting it also opts the surface into the notification
1160    /// ([`OutsideTap::Notify`] with `consume: true`, the modal shape) unless an
1161    /// explicit [`outside_tap`](Self::outside_tap) says otherwise, so the tap
1162    /// that dismisses a menu does not also activate what sits under it. Pass
1163    /// `OutsideTap::Notify { consume: false }` for the pass-through shape.
1164    pub fn on_outside_tap(mut self, callback: impl Fn(&mut State) + 'static) -> Self {
1165        self.on_outside_tap = Some(Rc::new(callback));
1166        if self.outside_tap == OutsideTap::Ignore {
1167            self.outside_tap = OutsideTap::Notify { consume: true };
1168        }
1169        self
1170    }
1171
1172    /// Whether the surface claims declined keys (default [`OverlayKeys::Pass`];
1173    /// see [`OverlaySlot::set_keys`]).
1174    pub fn keys(mut self, keys: OverlayKeys) -> Self {
1175        self.keys = keys;
1176        self
1177    }
1178
1179    /// Run `callback` when a declined Escape reaches the surface (see
1180    /// [`OverlaySlot::take_escape`]) — the close hook of a modal surface, which
1181    /// then consumes the key.
1182    pub fn on_escape(mut self, callback: impl Fn(&mut State) + 'static) -> Self {
1183        self.on_escape = Some(Rc::new(callback));
1184        self
1185    }
1186
1187    /// Keep the child's focus session when the surface claims focus (default
1188    /// `false`).
1189    ///
1190    /// A surface that is *about* the child rather than a place to type — a
1191    /// selection toolbar over a field — must not take the field's focus, or the
1192    /// selection it acts on disappears the moment it is touched. With this set,
1193    /// a focus claim from inside the surface is withdrawn again whenever the
1194    /// child held the focus link, and focus-routed events keep reaching the
1195    /// child. Left `false`, a surface that claims focus gets it and the child's
1196    /// link inside this portal is dropped, which is what a popover containing
1197    /// its own text field wants.
1198    pub fn preserve_focus(mut self, preserve: bool) -> Self {
1199        self.preserve_focus = preserve;
1200        self
1201    }
1202}
1203
1204/// The retained widget for an [`OverlayPortalView`].
1205pub struct OverlayPortalWidget<State: 'static> {
1206    child: ChildPod,
1207    slot: OverlaySlot<State>,
1208    on_outside_tap: Option<ErasedCallback>,
1209    on_escape: Option<ErasedCallback>,
1210    preserve_focus: bool,
1211}
1212
1213impl<State: 'static> OverlayPortalView<State> {
1214    /// Push the view's routing configuration onto the slot — the half of
1215    /// `build`/`rebuild` that is identical in both.
1216    fn configure(&self, slot: &mut OverlaySlot<State>) {
1217        slot.set_placement(self.placement);
1218        slot.set_band(self.band);
1219        slot.set_input(self.input);
1220        slot.set_outside_tap(self.outside_tap);
1221        slot.set_keys(self.keys);
1222    }
1223}
1224
1225impl<State: 'static> View<State> for OverlayPortalView<State> {
1226    type Element = OverlayPortalWidget<State>;
1227
1228    fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayPortalWidget<State> {
1229        let mut slot = OverlaySlot::new();
1230        self.configure(&mut slot);
1231        slot.rebuild(None, self.overlay.as_ref(), ctx);
1232        OverlayPortalWidget {
1233            child: crate::authoring::build_child(&self.child, ctx),
1234            slot,
1235            on_outside_tap: self.on_outside_tap.as_ref().map(erase_callback),
1236            on_escape: self.on_escape.as_ref().map(erase_callback),
1237            preserve_focus: self.preserve_focus,
1238        }
1239    }
1240
1241    fn rebuild(
1242        &self,
1243        prev: &Self,
1244        element: &mut OverlayPortalWidget<State>,
1245        ctx: &mut BuildCtx<'_>,
1246    ) -> ChangeFlags {
1247        let mut flags = ChangeFlags::NONE;
1248        if prev.placement != self.placement
1249            || prev.band != self.band
1250            || prev.input != self.input
1251            || prev.outside_tap != self.outside_tap
1252            || prev.keys != self.keys
1253        {
1254            // Placement and the routing fields are re-read from the slot on the
1255            // next paint, which is also when the new rect is registered.
1256            flags |= ChangeFlags::PAINT;
1257        }
1258        self.configure(&mut element.slot);
1259        element.preserve_focus = self.preserve_focus;
1260        // Closures are not comparable, so the adapter is reinstalled
1261        // unconditionally — it is cheap.
1262        element.on_outside_tap = self.on_outside_tap.as_ref().map(erase_callback);
1263        element.on_escape = self.on_escape.as_ref().map(erase_callback);
1264        flags |= element
1265            .slot
1266            .rebuild(prev.overlay.as_ref(), self.overlay.as_ref(), ctx);
1267        flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
1268        flags
1269    }
1270
1271    fn teardown(&self, element: &mut OverlayPortalWidget<State>, ctx: &mut BuildCtx<'_>) {
1272        element.slot.rebuild(self.overlay.as_ref(), None, ctx);
1273        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
1274    }
1275}
1276
1277impl<State: 'static> Widget for OverlayPortalWidget<State> {
1278    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1279        let size = self.child.layout_child(ctx, bc);
1280        self.child.set_origin(Point::ZERO);
1281        // The floated pod is sized against the window, not against `bc` — it
1282        // escapes this widget's box entirely.
1283        self.slot.layout(ctx);
1284        bc.constrain(size)
1285    }
1286
1287    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1288        self.child.paint_child(ctx, scene);
1289        // Registered, never painted here: the root paints it after the whole
1290        // main tree, which is what puts it above a later sibling.
1291        let size = ctx.size();
1292        self.slot.paint(ctx, size);
1293    }
1294
1295    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1296        let child_focused = self.child.holds_live_focus();
1297        // A focus-routed event belongs to the child whenever the child holds the
1298        // live link. Both links can be live at once only through
1299        // `preserve_focus`, which deliberately re-records the child's against the
1300        // same session it hands back; otherwise exactly one of them is, and this
1301        // check simply prefers the main tree when both are.
1302        if !(event.is_focus_routed() && child_focused) {
1303            let pod_focused_before = self.slot.pod_has_focus();
1304            if let Some(result) = self.slot.event_ambient(ctx, event) {
1305                // Drained unconditionally: a notification with no callback
1306                // installed is still spent, not left standing for a later pass.
1307                if self.slot.take_outside_down()
1308                    && let Some(callback) = &mut self.on_outside_tap
1309                {
1310                    callback(ctx);
1311                    ctx.request_redraw();
1312                }
1313                if self.slot.take_escape()
1314                    && let Some(callback) = &mut self.on_escape
1315                {
1316                    callback(ctx);
1317                    ctx.consume_overlay_key();
1318                    ctx.request_redraw();
1319                }
1320                if !pod_focused_before && self.slot.pod_has_focus() {
1321                    if self.preserve_focus && child_focused {
1322                        // Hand the session back to the child: the surface's own
1323                        // claim is withdrawn, and re-asserting the request keeps
1324                        // every ancestor's recorded chain pointing here.
1325                        //
1326                        // The child's own link is re-recorded rather than left
1327                        // alone. The surface's claim opened a new focus session,
1328                        // and a link recorded against the session before it is
1329                        // not a link on this one — so handing the session back
1330                        // means saying so on the child's own record, not merely
1331                        // not clearing it.
1332                        self.slot.withdraw_pod_focus();
1333                        self.child.set_focused(true);
1334                        ctx.request_focus();
1335                    } else {
1336                        // The surface took the session, so the child's link
1337                        // inside this portal is stale — an overlay press never
1338                        // reaches the root's own blur rule to clear it.
1339                        self.child.set_focused(false);
1340                    }
1341                }
1342                return result;
1343            }
1344        }
1345        route_event_single(&mut self.child, ctx, event)
1346    }
1347
1348    fn semantics(&self, ctx: &mut SemanticsCtx) {
1349        // The child only: a floated pod's nodes would attach at this widget's
1350        // position rather than the surface's (see the module docs).
1351        self.child.semantics_child(ctx);
1352    }
1353
1354    crate::authoring::visit_children!(child);
1355
1356    // The pod is visited after the child, so Tab reaches the owner's own stops
1357    // first. Not `is_focusable` forwarding: the walk would then stop on the
1358    // portal and its child for one control, and the phantom stop's grant
1359    // would leave focus where it was.
1360    fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1361        visitor(&self.child);
1362        self.slot.visit_focus_pod(visitor);
1363    }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369    use crate::test_support::RecordingScene;
1370    use crate::{SizedBox, StackView, column, scroll_view, stack};
1371    use frust_core::{
1372        EditCommand, FrameTime, Key, KeyEvent, Modifiers, NamedKey, PointerButton, PointerPhase,
1373        RenderRoot, ScrollDelta,
1374    };
1375    use peniko::Color;
1376
1377    // -----------------------------------------------------------------------
1378    // Placement
1379    // -----------------------------------------------------------------------
1380
1381    const AREA: Rect = Rect::new(0.0, 0.0, 400.0, 600.0);
1382
1383    /// Placement with the padding switched off, so a geometry assertion reads
1384    /// against the raw area rather than the inset one.
1385    fn bare() -> OverlayPlacement {
1386        OverlayPlacement::default().padding(0.0)
1387    }
1388
1389    fn anchor_rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
1390        Rect::from_origin_size(Point::new(x, y), Size::new(w, h))
1391    }
1392
1393    #[test]
1394    fn the_default_placement_is_below_the_anchor_centred_at_the_neutral_gap() {
1395        let p = OverlayPlacement::default();
1396        assert_eq!(p.side, OverlaySide::Bottom);
1397        assert_eq!(p.align, OverlayAlign::Center);
1398        assert_eq!(p.offset, DEFAULT_OFFSET);
1399        assert_eq!(p.padding, DEFAULT_PADDING);
1400        assert!(p.flip && p.clamp);
1401    }
1402
1403    #[test]
1404    fn each_side_offsets_off_its_own_edge() {
1405        let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
1406        let content = Size::new(120.0, 60.0);
1407        // The raw side placement, with the collision passes off: `Left` would
1408        // otherwise flip (a 120px panel does not fit in the 100px to the
1409        // anchor's left), which the flip test below covers on its own.
1410        let raw = |side| OverlayPlacement::on(side).flip(false).clamp(false);
1411        let bottom = place(a, content, AREA, raw(OverlaySide::Bottom));
1412        assert_eq!(bottom.y0, a.y1 + DEFAULT_OFFSET);
1413        let top = place(a, content, AREA, raw(OverlaySide::Top));
1414        assert_eq!(top.y1, a.y0 - DEFAULT_OFFSET);
1415        let right = place(a, content, AREA, raw(OverlaySide::Right));
1416        assert_eq!(right.x0, a.x1 + DEFAULT_OFFSET);
1417        let left = place(a, content, AREA, raw(OverlaySide::Left));
1418        assert_eq!(left.x1, a.x0 - DEFAULT_OFFSET);
1419        // Sizes are never altered by placement.
1420        for r in [bottom, top, right, left] {
1421            assert_eq!(r.size(), content);
1422        }
1423        assert!(OverlaySide::Top.is_vertical() && !OverlaySide::Left.is_vertical());
1424        assert_eq!(OverlaySide::Top.opposite(), OverlaySide::Bottom);
1425        assert_eq!(OverlaySide::Left.opposite(), OverlaySide::Right);
1426    }
1427
1428    #[test]
1429    fn align_lines_up_leading_center_or_trailing_edges() {
1430        let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
1431        let content = Size::new(120.0, 60.0);
1432        let at = |side, align| place(a, content, AREA, OverlayPlacement::on(side).align(align));
1433        assert_eq!(at(OverlaySide::Bottom, OverlayAlign::Start).x0, a.x0);
1434        assert_eq!(at(OverlaySide::Bottom, OverlayAlign::End).x1, a.x1);
1435        assert_eq!(
1436            at(OverlaySide::Bottom, OverlayAlign::Center).center().x,
1437            a.center().x
1438        );
1439
1440        // The same three on a horizontal side act on the vertical axis.
1441        assert_eq!(at(OverlaySide::Right, OverlayAlign::Start).y0, a.y0);
1442        assert_eq!(at(OverlaySide::Right, OverlayAlign::End).y1, a.y1);
1443        assert_eq!(
1444            at(OverlaySide::Right, OverlayAlign::Center).center().y,
1445            a.center().y
1446        );
1447    }
1448
1449    #[test]
1450    fn a_side_that_does_not_fit_flips_to_the_opposite_one() {
1451        // An anchor near the bottom edge: `Bottom` overflows, `Top` fits.
1452        let a = anchor_rect(100.0, 560.0, 80.0, 20.0);
1453        let content = Size::new(120.0, 100.0);
1454        let flipped = place(a, content, AREA, bare());
1455        assert_eq!(
1456            flipped.y1,
1457            a.y0 - DEFAULT_OFFSET,
1458            "flipped above the anchor"
1459        );
1460
1461        // With the flip disabled it stays below and only the clamp moves it.
1462        let pinned = place(a, content, AREA, bare().flip(false));
1463        assert_eq!(pinned.y1, AREA.y1, "clamped, not flipped");
1464
1465        // Neither side fits: the preferred one is kept (and clamped).
1466        let tall = Size::new(120.0, 590.0);
1467        let kept = place(a, tall, AREA, bare());
1468        assert_eq!(kept.y1, AREA.y1);
1469    }
1470
1471    #[test]
1472    fn each_side_flips_at_the_edge_it_would_overflow() {
1473        let content = Size::new(120.0, 100.0);
1474        // Top edge: a `Top` placement flips down.
1475        let high = anchor_rect(100.0, 10.0, 80.0, 20.0);
1476        let down = place(high, content, AREA, OverlayPlacement::on(OverlaySide::Top));
1477        assert_eq!(down.y0, high.y1 + DEFAULT_OFFSET);
1478        // Left edge: a `Left` placement flips right.
1479        let leading = anchor_rect(10.0, 200.0, 20.0, 20.0);
1480        let right = place(
1481            leading,
1482            content,
1483            AREA,
1484            OverlayPlacement::on(OverlaySide::Left),
1485        );
1486        assert_eq!(right.x0, leading.x1 + DEFAULT_OFFSET);
1487        // Right edge: a `Right` placement flips left.
1488        let trailing = anchor_rect(370.0, 200.0, 20.0, 20.0);
1489        let left = place(
1490            trailing,
1491            content,
1492            AREA,
1493            OverlayPlacement::on(OverlaySide::Right),
1494        );
1495        assert_eq!(left.x1, trailing.x0 - DEFAULT_OFFSET);
1496    }
1497
1498    #[test]
1499    fn clamping_shifts_the_rect_back_inside_and_can_be_turned_off() {
1500        // An anchor at the right edge, centre-aligned: the panel overflows.
1501        let a = anchor_rect(380.0, 100.0, 20.0, 20.0);
1502        let content = Size::new(200.0, 50.0);
1503        let clamped = place(a, content, AREA, bare());
1504        assert_eq!(clamped.x1, AREA.x1);
1505        assert_eq!(
1506            clamped.y0,
1507            a.y1 + DEFAULT_OFFSET,
1508            "only the cross axis moved"
1509        );
1510
1511        let free = place(a, content, AREA, bare().clamp(false));
1512        assert!(free.x1 > AREA.x1, "unclamped placement may overflow");
1513
1514        // Content wider than the area pins to the leading edge (the start-edge
1515        // rule), not the trailing one.
1516        let huge = Size::new(600.0, 50.0);
1517        let pinned = place(a, huge, AREA, bare());
1518        assert_eq!(pinned.x0, AREA.x0);
1519    }
1520
1521    #[test]
1522    fn the_clamp_keeps_the_padding_off_every_edge() {
1523        let content = Size::new(200.0, 50.0);
1524        // Trailing overflow lands `DEFAULT_PADDING` short of the area edge.
1525        let trailing = place(
1526            anchor_rect(380.0, 100.0, 20.0, 20.0),
1527            content,
1528            AREA,
1529            OverlayPlacement::default(),
1530        );
1531        assert_eq!(trailing.x1, AREA.x1 - DEFAULT_PADDING);
1532        // …and so does a leading one.
1533        let leading = place(
1534            anchor_rect(0.0, 100.0, 20.0, 20.0),
1535            content,
1536            AREA,
1537            OverlayPlacement::default(),
1538        );
1539        assert_eq!(leading.x0, AREA.x0 + DEFAULT_PADDING);
1540        // The bottom edge is the same rule on the other axis.
1541        let low = place(
1542            anchor_rect(100.0, 560.0, 20.0, 20.0),
1543            Size::new(100.0, 300.0),
1544            AREA,
1545            OverlayPlacement::default().flip(false),
1546        );
1547        assert_eq!(low.y1, AREA.y1 - DEFAULT_PADDING);
1548    }
1549
1550    #[test]
1551    fn a_padding_larger_than_the_area_falls_back_to_the_area_itself() {
1552        // A window narrower than twice the padding still has to place its
1553        // surface somewhere; the inset would invert, so it is dropped.
1554        let tiny = Rect::new(0.0, 0.0, 10.0, 10.0);
1555        let rect = place(
1556            Rect::ZERO,
1557            Size::new(4.0, 4.0),
1558            tiny,
1559            OverlayPlacement::default().flip(false),
1560        );
1561        assert!(tiny.contains(rect.origin()));
1562        assert_eq!(rect.x0, 0.0, "clamped to the un-inset area's own edge");
1563    }
1564
1565    #[test]
1566    fn a_degenerate_anchor_places_against_that_point() {
1567        let content = Size::new(80.0, 40.0);
1568        let rect = place(Rect::ZERO, content, AREA, OverlayPlacement::default());
1569        assert_eq!(
1570            rect.x0,
1571            AREA.x0 + DEFAULT_PADDING,
1572            "clamped in from the centred overhang"
1573        );
1574        // The neutral gap is smaller than the padding, so the same clamp holds
1575        // the surface off the top edge as well.
1576        assert_eq!(rect.y0, AREA.y0 + DEFAULT_PADDING);
1577    }
1578
1579    // -----------------------------------------------------------------------
1580    // The portal, driven through a real render root
1581    // -----------------------------------------------------------------------
1582
1583    /// Everything the fixture's widgets record, in the order it happened.
1584    type Log = Rc<RefCell<Vec<String>>>;
1585
1586    const WINDOW: Size = Size::new(400.0, 600.0);
1587
1588    /// Where the `Plain` fixture's surface lands: the top-left corner of the
1589    /// padded field, because a window-sized anchor fits its content on neither
1590    /// side and the clamp takes over.
1591    const POD: Rect = Rect::new(8.0, 8.0, 88.0, 38.0);
1592
1593    /// What the floated content does when it is pressed.
1594    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1595    enum Reaction {
1596        #[default]
1597        Nothing,
1598        Capture,
1599        Focus,
1600        /// Ask for a layout pass (`EventCtx::request_layout`).
1601        Layout,
1602    }
1603
1604    /// Which tree the fixture builds.
1605    #[derive(Clone, Copy, PartialEq, Eq)]
1606    enum Shape {
1607        /// A window-filling portal child with a later, window-filling sibling.
1608        Plain,
1609        /// A small portal child partway down a scrollable column.
1610        Scrolled,
1611        /// The portal beside a focus-taking field, both inside one `Column`, so
1612        /// the container reconciling them is the ordinary
1613        /// [`route_event`](crate::authoring::route_event) one and the field is a
1614        /// SIBLING of the portal rather than its child — the arrangement in
1615        /// which a recorded focus link the container never swept still decides
1616        /// where a focus-routed event goes.
1617        Sibling,
1618    }
1619
1620    #[derive(Clone, Copy)]
1621    struct Cfg {
1622        open: bool,
1623        shape: Shape,
1624        band: OverlayBand,
1625        input: OverlayInput,
1626        /// `None` leaves the policy to `on_outside_tap`'s own default.
1627        outside_tap: Option<OutsideTap>,
1628        keys: OverlayKeys,
1629        preserve_focus: bool,
1630        reaction: Reaction,
1631        placement: OverlayPlacement,
1632    }
1633
1634    /// The corner placement the `Plain` fixture pins its surface with — chosen
1635    /// so the rect is the same however the fixture is configured.
1636    fn corner() -> OverlayPlacement {
1637        OverlayPlacement::on(OverlaySide::Top)
1638            .offset(0.0)
1639            .align(OverlayAlign::Start)
1640    }
1641
1642    impl Default for Cfg {
1643        fn default() -> Self {
1644            Cfg {
1645                open: true,
1646                shape: Shape::Plain,
1647                band: OverlayBand::Floating,
1648                input: OverlayInput::Interactive,
1649                outside_tap: Some(OutsideTap::Ignore),
1650                keys: OverlayKeys::Pass,
1651                preserve_focus: false,
1652                reaction: Reaction::Nothing,
1653                placement: corner(),
1654            }
1655        }
1656    }
1657
1658    struct App {
1659        cfg: Cfg,
1660        log: Log,
1661        presses: u32,
1662        outside_taps: u32,
1663        escapes: u32,
1664    }
1665
1666    /// A recording leaf: paints one rect at its own absolute origin, records
1667    /// every event it receives (positions in its own local space) and reacts to
1668    /// a press the way the fixture asked it to.
1669    struct Probe {
1670        tag: &'static str,
1671        /// `None` fills whatever it is offered.
1672        size: Option<Size>,
1673        reaction: Reaction,
1674        /// Whether its pointer arms report `Handled` — a `false` leaf lets a
1675        /// press fall through to the child painted under it.
1676        handles: bool,
1677        log: Log,
1678    }
1679
1680    struct ProbeWidget {
1681        tag: &'static str,
1682        size: Option<Size>,
1683        reaction: Reaction,
1684        handles: bool,
1685        log: Log,
1686    }
1687
1688    impl View<App> for Probe {
1689        type Element = ProbeWidget;
1690        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
1691            ProbeWidget {
1692                tag: self.tag,
1693                size: self.size,
1694                reaction: self.reaction,
1695                handles: self.handles,
1696                log: Rc::clone(&self.log),
1697            }
1698        }
1699        fn rebuild(
1700            &self,
1701            _prev: &Self,
1702            element: &mut ProbeWidget,
1703            _ctx: &mut BuildCtx<'_>,
1704        ) -> ChangeFlags {
1705            element.size = self.size;
1706            element.reaction = self.reaction;
1707            element.handles = self.handles;
1708            element.log = Rc::clone(&self.log);
1709            ChangeFlags::NONE
1710        }
1711    }
1712
1713    impl Widget for ProbeWidget {
1714        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1715            bc.constrain(self.size.unwrap_or_else(|| bc.max()))
1716        }
1717        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1718            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1719        }
1720        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1721            match event {
1722                InputEvent::Pointer(p) => {
1723                    self.log.borrow_mut().push(format!(
1724                        "{}:{:?}@{},{}",
1725                        self.tag, p.phase, p.position.x, p.position.y
1726                    ));
1727                    if p.phase == PointerPhase::Down {
1728                        ctx.state_mut::<App>().presses += 1;
1729                        match self.reaction {
1730                            // A press this leaf only records: the commonest
1731                            // shape, and the one that must not disturb focus.
1732                            Reaction::Nothing => {}
1733                            Reaction::Capture => ctx.capture_pointer(),
1734                            Reaction::Focus => ctx.request_focus(),
1735                            Reaction::Layout => ctx.request_layout(),
1736                        }
1737                    }
1738                    if self.handles {
1739                        EventResult::Handled
1740                    } else {
1741                        EventResult::Ignored
1742                    }
1743                }
1744                InputEvent::Key(_) => {
1745                    self.log.borrow_mut().push(format!(
1746                        "{}:key focus={}",
1747                        self.tag,
1748                        ctx.has_focus()
1749                    ));
1750                    if ctx.has_focus() {
1751                        EventResult::Handled
1752                    } else {
1753                        EventResult::Ignored
1754                    }
1755                }
1756                _ => EventResult::Ignored,
1757            }
1758        }
1759    }
1760
1761    /// The fixture's whole view tree, rebuilt from the app state every frame so
1762    /// a test can reconfigure it between frames.
1763    fn logic(state: &mut App) -> StackView<App> {
1764        let cfg = state.cfg;
1765        let log = Rc::clone(&state.log);
1766        let pod = cfg.open.then(|| {
1767            any(Probe {
1768                tag: "pod",
1769                size: Some(Size::new(80.0, 30.0)),
1770                reaction: cfg.reaction,
1771                handles: true,
1772                log: Rc::clone(&log),
1773            })
1774        });
1775        // The child claims focus on a press, so it stands in for the focused
1776        // field a surface is opened over.
1777        let child = Probe {
1778            tag: "child",
1779            size: match cfg.shape {
1780                Shape::Plain => None,
1781                Shape::Scrolled => Some(Size::new(120.0, 40.0)),
1782                Shape::Sibling => Some(Size::new(400.0, 50.0)),
1783            },
1784            reaction: Reaction::Focus,
1785            handles: true,
1786            log: Rc::clone(&log),
1787        };
1788        let mut portal = overlay_portal(child)
1789            .overlay(pod)
1790            .placement(cfg.placement)
1791            .band(cfg.band)
1792            .input(cfg.input)
1793            .preserve_focus(cfg.preserve_focus)
1794            .keys(cfg.keys)
1795            .on_escape(|state: &mut App| state.escapes += 1)
1796            .on_outside_tap(|state: &mut App| state.outside_taps += 1);
1797        if let Some(policy) = cfg.outside_tap {
1798            portal = portal.outside_tap(policy);
1799        }
1800        match cfg.shape {
1801            Shape::Plain => stack()
1802                .child(portal)
1803                // Painted after the portal and covering it, so "the pod paints
1804                // above a later sibling" is a real question. It handles nothing,
1805                // so a press falls through to the portal's own child.
1806                .child(Probe {
1807                    tag: "sib",
1808                    size: None,
1809                    reaction: Reaction::Nothing,
1810                    handles: false,
1811                    log,
1812                }),
1813            Shape::Scrolled => stack().child(scroll_view(
1814                column()
1815                    .child(SizedBox(Some(400.0), Some(200.0)))
1816                    .child(portal)
1817                    .child(SizedBox(Some(400.0), Some(1000.0))),
1818            )),
1819            // The field first, the portal second: a focus-routed event walking
1820            // the children in order meets the field's link before the portal's.
1821            Shape::Sibling => stack().child(
1822                column()
1823                    .child(Probe {
1824                        tag: "field",
1825                        size: Some(Size::new(400.0, 50.0)),
1826                        reaction: Reaction::Focus,
1827                        handles: true,
1828                        log,
1829                    })
1830                    .child(portal),
1831            ),
1832        }
1833    }
1834
1835    struct Harness {
1836        root: RenderRoot<App, StackView<App>>,
1837        state: App,
1838        clock_ms: f64,
1839    }
1840
1841    impl Harness {
1842        fn new(cfg: Cfg) -> Self {
1843            Harness {
1844                root: RenderRoot::new(),
1845                state: App {
1846                    cfg,
1847                    log: Rc::new(RefCell::new(Vec::new())),
1848                    presses: 0,
1849                    outside_taps: 0,
1850                    escapes: 0,
1851                },
1852                clock_ms: 0.0,
1853            }
1854        }
1855
1856        /// One whole frame: rebuild, layout, paint — returning what was painted,
1857        /// in paint order.
1858        fn frame(&mut self) -> RecordingScene {
1859            let mut build: fn(&mut App) -> StackView<App> = logic;
1860            self.root.rebuild(&mut build, &mut self.state);
1861            self.root.layout(WINDOW);
1862            self.clock_ms += 16.0;
1863            let mut scene = RecordingScene::default();
1864            self.root.paint(
1865                &mut scene,
1866                FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
1867            );
1868            scene
1869        }
1870
1871        fn pointer(&mut self, phase: PointerPhase, x: f64, y: f64) {
1872            self.root.event(
1873                &mut self.state,
1874                &InputEvent::Pointer(PointerEvent {
1875                    phase,
1876                    position: Point::new(x, y),
1877                    button: PointerButton::Primary,
1878                }),
1879            );
1880        }
1881
1882        fn down(&mut self, x: f64, y: f64) {
1883            self.pointer(PointerPhase::Down, x, y);
1884        }
1885
1886        fn key(&mut self) {
1887            self.root.event(
1888                &mut self.state,
1889                &InputEvent::Key(KeyEvent {
1890                    key: Key::Named(NamedKey::ArrowLeft),
1891                    modifiers: Modifiers::default(),
1892                    repeat: false,
1893                }),
1894            );
1895        }
1896
1897        fn press(&mut self, key: NamedKey) -> frust_core::EventOutcome {
1898            let event = KeyEvent {
1899                key: Key::Named(key),
1900                modifiers: Modifiers::default(),
1901                repeat: false,
1902            };
1903            self.root.event(&mut self.state, &InputEvent::Key(event))
1904        }
1905
1906        fn log(&self) -> Vec<String> {
1907            self.state.log.borrow().clone()
1908        }
1909
1910        fn clear_log(&mut self) {
1911            self.state.log.borrow_mut().clear();
1912        }
1913    }
1914
1915    /// The rects a scene recorded, so a paint-order assertion reads as geometry.
1916    fn rects(scene: &RecordingScene) -> Vec<Rect> {
1917        scene
1918            .rects
1919            .iter()
1920            .map(|(origin, size)| Rect::from_origin_size(*origin, *size))
1921            .collect()
1922    }
1923
1924    #[test]
1925    fn the_overlay_pod_paints_above_a_later_sibling() {
1926        let mut h = Harness::new(Cfg::default());
1927        let scene = h.frame();
1928        let painted = rects(&scene);
1929        assert_eq!(
1930            painted.last().copied(),
1931            Some(POD),
1932            "the floated pod paints after the whole main tree, not in place: {painted:?}"
1933        );
1934        // …and it really is covered in the main tree: the sibling painted over
1935        // the same pixels one step earlier.
1936        let sibling = painted[painted.len() - 2];
1937        assert_eq!(sibling, Rect::from_origin_size(Point::ZERO, WINDOW));
1938    }
1939
1940    #[test]
1941    fn a_press_inside_the_surface_reaches_the_pod_in_its_own_space() {
1942        let mut h = Harness::new(Cfg::default());
1943        h.frame();
1944        h.clear_log();
1945        h.down(40.0, 20.0);
1946        assert_eq!(
1947            h.log(),
1948            vec!["pod:Down@32,12".to_string()],
1949            "the press is delivered in pod space (window minus the placed origin)"
1950        );
1951        assert_eq!(
1952            h.state.presses, 1,
1953            "the pod's own callback ran on app state"
1954        );
1955    }
1956
1957    #[test]
1958    fn a_transparent_tooltip_surface_never_receives_input() {
1959        let mut h = Harness::new(Cfg {
1960            band: OverlayBand::Tooltip,
1961            input: OverlayInput::Transparent,
1962            ..Cfg::default()
1963        });
1964        let scene = h.frame();
1965        assert_eq!(
1966            rects(&scene).last().copied(),
1967            Some(POD),
1968            "a transparent surface is still painted above everything"
1969        );
1970        h.clear_log();
1971        h.down(40.0, 20.0);
1972        assert_eq!(
1973            h.log(),
1974            vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
1975            "the pointer passes through to the main tree, topmost sibling first"
1976        );
1977    }
1978
1979    #[test]
1980    fn a_drag_begun_on_the_surface_continues_into_it_through_the_capture() {
1981        // Deliberately the scrolled tree: the owner sits 200px down, so the
1982        // follow-ups — which arrive in the OWNER's local space, not the
1983        // window's — are wrong by exactly that much unless they are lifted.
1984        let mut h = Harness::new(Cfg {
1985            shape: Shape::Scrolled,
1986            placement: OverlayPlacement::default(),
1987            reaction: Reaction::Capture,
1988            ..Cfg::default()
1989        });
1990        h.frame();
1991        h.clear_log();
1992        h.down(30.0, 250.0);
1993        assert!(
1994            h.root.is_pointer_captured(),
1995            "a capture claimed from inside the surface is honoured"
1996        );
1997        // Far outside the placed rect, and outside the owner too: the capture,
1998        // not the hit test, is what routes these.
1999        h.pointer(PointerPhase::Move, 200.0, 400.0);
2000        h.pointer(PointerPhase::Up, 210.0, 410.0);
2001        assert_eq!(
2002            h.log(),
2003            vec![
2004                "pod:Down@10,6".to_string(),
2005                "pod:Move@180,156".to_string(),
2006                "pod:Up@190,166".to_string(),
2007            ]
2008        );
2009        assert!(!h.root.is_pointer_captured(), "the Up releases the capture");
2010        // The gesture is over: a further move outside the surface is nobody's.
2011        h.clear_log();
2012        h.pointer(PointerPhase::Move, 200.0, 400.0);
2013        assert!(
2014            !h.log().iter().any(|e| e.starts_with("pod:")),
2015            "a move after the release is no longer the surface's: {:?}",
2016            h.log()
2017        );
2018    }
2019
2020    /// The whole gesture, in the log shape the fixture records it in: pressed
2021    /// inside the placed rect, dragged well outside it, released there.
2022    fn one_gesture(h: &mut Harness, label: &str) {
2023        h.clear_log();
2024        h.down(40.0, 20.0);
2025        assert!(
2026            h.root.is_pointer_captured(),
2027            "{label}: the capture claimed inside the surface opened a gesture"
2028        );
2029        h.pointer(PointerPhase::Move, 300.0, 300.0);
2030        h.pointer(PointerPhase::Up, 300.0, 300.0);
2031        assert_eq!(
2032            h.log(),
2033            vec![
2034                "pod:Down@32,12".to_string(),
2035                "pod:Move@292,292".to_string(),
2036                "pod:Up@292,292".to_string(),
2037            ],
2038            "{label}: every phase of the gesture reached the surface"
2039        );
2040        assert!(
2041            !h.root.is_pointer_captured(),
2042            "{label}: the Up released the capture"
2043        );
2044    }
2045
2046    #[test]
2047    fn a_second_gesture_into_the_same_mounted_pod_routes_exactly_like_the_first() {
2048        let mut h = Harness::new(Cfg {
2049            reaction: Reaction::Capture,
2050            ..Cfg::default()
2051        });
2052        h.frame();
2053        // No frame between the two: the pod is the same widget instance, so the
2054        // second press meets whatever the first gesture left recorded on it.
2055        one_gesture(&mut h, "first gesture");
2056        one_gesture(&mut h, "second gesture");
2057    }
2058
2059    #[test]
2060    fn a_cancelled_gesture_leaves_the_surface_ready_for_the_next_one() {
2061        let mut h = Harness::new(Cfg {
2062            reaction: Reaction::Capture,
2063            ..Cfg::default()
2064        });
2065        h.frame();
2066        h.down(40.0, 20.0);
2067        h.pointer(PointerPhase::Cancel, 300.0, 300.0);
2068        assert!(
2069            !h.root.is_pointer_captured(),
2070            "a Cancel ends the gesture exactly as an Up does"
2071        );
2072        one_gesture(&mut h, "the gesture after a cancel");
2073    }
2074
2075    #[test]
2076    fn a_press_outside_notifies_the_owner_and_can_still_reach_the_main_tree() {
2077        let mut h = Harness::new(Cfg {
2078            outside_tap: Some(OutsideTap::Notify { consume: false }),
2079            ..Cfg::default()
2080        });
2081        h.frame();
2082        h.clear_log();
2083        h.down(200.0, 400.0);
2084        assert_eq!(h.state.outside_taps, 1, "the owner was told");
2085        assert_eq!(
2086            h.log(),
2087            vec![
2088                "sib:Down@200,400".to_string(),
2089                "child:Down@200,400".to_string()
2090            ],
2091            "a pass-through notification still lets the press through"
2092        );
2093    }
2094
2095    #[test]
2096    fn a_consuming_outside_press_is_swallowed() {
2097        let mut h = Harness::new(Cfg {
2098            outside_tap: Some(OutsideTap::Notify { consume: true }),
2099            ..Cfg::default()
2100        });
2101        h.frame();
2102        h.clear_log();
2103        h.down(200.0, 400.0);
2104        assert_eq!(h.state.outside_taps, 1);
2105        assert!(
2106            h.log().is_empty(),
2107            "the dismissing press never reached the main tree: {:?}",
2108            h.log()
2109        );
2110    }
2111
2112    #[test]
2113    fn an_outside_tap_callback_opts_into_the_notification_by_itself() {
2114        // No explicit policy: installing the callback is what asks to hear.
2115        let mut h = Harness::new(Cfg {
2116            outside_tap: None,
2117            ..Cfg::default()
2118        });
2119        h.frame();
2120        h.clear_log();
2121        h.down(200.0, 400.0);
2122        assert_eq!(h.state.outside_taps, 1);
2123        assert!(
2124            h.log().is_empty(),
2125            "the implied policy is the modal one (consuming): {:?}",
2126            h.log()
2127        );
2128    }
2129
2130    #[test]
2131    fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
2132        let mut h = Harness::new(Cfg::default());
2133        h.frame();
2134        h.clear_log();
2135        h.down(200.0, 400.0);
2136        assert_eq!(h.state.outside_taps, 0);
2137        assert_eq!(
2138            h.log(),
2139            vec![
2140                "sib:Down@200,400".to_string(),
2141                "child:Down@200,400".to_string()
2142            ]
2143        );
2144    }
2145
2146    #[test]
2147    fn a_declined_escape_reaches_a_modal_surfaces_on_escape_and_stops_there() {
2148        let mut h = Harness::new(Cfg {
2149            keys: OverlayKeys::Modal,
2150            ..Cfg::default()
2151        });
2152        h.frame();
2153        assert!(h.press(NamedKey::Escape).handled, "on_escape consumed it");
2154        assert_eq!(h.state.escapes, 1);
2155        assert!(!h.press(NamedKey::ArrowLeft).handled, "only Escape closes");
2156        assert_eq!(h.state.escapes, 1);
2157
2158        let mut h = Harness::new(Cfg::default());
2159        h.frame();
2160        assert!(!h.press(NamedKey::Escape).handled);
2161        assert_eq!(h.state.escapes, 0, "a non-modal surface hears nothing");
2162    }
2163
2164    #[test]
2165    fn a_surface_that_claims_focus_takes_it_from_the_child() {
2166        let mut h = Harness::new(Cfg {
2167            reaction: Reaction::Focus,
2168            preserve_focus: false,
2169            ..Cfg::default()
2170        });
2171        h.frame();
2172        // The child is focused first, exactly as a field is before its surface
2173        // opens over it.
2174        h.down(200.0, 400.0);
2175        assert!(h.root.is_focus_active());
2176        h.clear_log();
2177        h.key();
2178        assert_eq!(h.log(), vec!["child:key focus=true".to_string()]);
2179
2180        // A press inside the surface, which claims focus for itself.
2181        h.clear_log();
2182        h.down(40.0, 20.0);
2183        h.key();
2184        assert_eq!(
2185            h.log(),
2186            vec![
2187                "pod:Down@32,12".to_string(),
2188                "pod:key focus=true".to_string()
2189            ],
2190            "the keyboard follows the surface"
2191        );
2192        assert!(h.root.is_focus_active());
2193    }
2194
2195    #[test]
2196    fn preserve_focus_hands_the_session_back_to_the_child() {
2197        let mut h = Harness::new(Cfg {
2198            reaction: Reaction::Focus,
2199            preserve_focus: true,
2200            ..Cfg::default()
2201        });
2202        h.frame();
2203        h.down(200.0, 400.0);
2204        h.clear_log();
2205        h.down(40.0, 20.0);
2206        h.key();
2207        assert_eq!(
2208            h.log(),
2209            vec![
2210                "pod:Down@32,12".to_string(),
2211                "child:key focus=true".to_string()
2212            ],
2213            "the field that opened the surface keeps typing"
2214        );
2215        assert!(h.root.is_focus_active(), "and keeps its session");
2216    }
2217
2218    #[test]
2219    fn a_child_that_takes_focus_back_gets_the_keyboard_again() {
2220        let mut h = Harness::new(Cfg {
2221            reaction: Reaction::Focus,
2222            preserve_focus: false,
2223            ..Cfg::default()
2224        });
2225        h.frame();
2226        // The child is focused, the surface then takes the session…
2227        h.down(200.0, 400.0);
2228        h.down(40.0, 20.0);
2229        // …and the child is pressed again, which re-claims it.
2230        h.down(200.0, 400.0);
2231        h.clear_log();
2232        h.key();
2233        assert_eq!(
2234            h.log(),
2235            vec!["child:key focus=true".to_string()],
2236            "the surface's older link must not outrank the main tree's live one"
2237        );
2238    }
2239
2240    /// A focus link the container never swept must stop deciding where a
2241    /// focus-routed event goes.
2242    ///
2243    /// The field and the portal are siblings here, so the surface's claim
2244    /// arrives as a broadcast the container forwards to both — no hit test, and
2245    /// therefore no blur-on-outside-tap sweep to clear the field's link. Both
2246    /// children then read as focus-link holders, and a container that answers
2247    /// "which child holds focus" with the first one in child order delivers the
2248    /// keyboard to the field the user has left.
2249    #[test]
2250    fn a_stale_sibling_link_does_not_outrank_the_surface_that_took_the_session() {
2251        let mut h = Harness::new(Cfg {
2252            shape: Shape::Sibling,
2253            reaction: Reaction::Focus,
2254            placement: OverlayPlacement::on(OverlaySide::Bottom)
2255                .offset(0.0)
2256                .align(OverlayAlign::Start)
2257                .padding(0.0),
2258            ..Cfg::default()
2259        });
2260        h.frame();
2261
2262        // The field takes the session.
2263        h.down(200.0, 25.0);
2264        h.clear_log();
2265        h.key();
2266        assert_eq!(h.log(), vec!["field:key focus=true".to_string()]);
2267
2268        // A press inside the surface, which claims focus for itself. The
2269        // container sees only the broadcast, so it sweeps nothing.
2270        h.down(40.0, 110.0);
2271        h.clear_log();
2272        h.key();
2273        assert_eq!(
2274            h.log(),
2275            vec!["pod:key focus=true".to_string()],
2276            "the keyboard follows the branch that actually holds the session"
2277        );
2278    }
2279
2280    #[test]
2281    fn a_surface_that_claims_nothing_never_disturbs_the_focused_child() {
2282        let mut h = Harness::new(Cfg::default());
2283        h.frame();
2284        h.down(200.0, 400.0);
2285        h.clear_log();
2286        h.down(40.0, 20.0);
2287        h.key();
2288        assert_eq!(
2289            h.log(),
2290            vec![
2291                "pod:Down@32,12".to_string(),
2292                "child:key focus=true".to_string()
2293            ],
2294            "an overlay press is not a blur"
2295        );
2296    }
2297
2298    #[test]
2299    fn scrolling_an_ancestor_moves_the_surface_on_the_next_paint() {
2300        let mut h = Harness::new(Cfg {
2301            shape: Shape::Scrolled,
2302            placement: OverlayPlacement::default(),
2303            ..Cfg::default()
2304        });
2305        let before = rects(&h.frame());
2306        assert_eq!(
2307            before.last().copied(),
2308            Some(Rect::new(20.0, 244.0, 100.0, 274.0)),
2309            "placed under its anchor, 200px down the scrolled column"
2310        );
2311        h.root.event(
2312            &mut h.state,
2313            &InputEvent::Scroll {
2314                position: Point::new(10.0, 10.0),
2315                delta: ScrollDelta::Pixels(0.0, 50.0),
2316            },
2317        );
2318        let after = rects(&h.frame());
2319        assert_eq!(
2320            after.last().copied(),
2321            Some(Rect::new(20.0, 194.0, 100.0, 224.0)),
2322            "the anchor moved with the scroll, and so did the surface"
2323        );
2324        // …and routing followed it: the pod's own rect is where the press lands.
2325        h.clear_log();
2326        h.down(30.0, 200.0);
2327        assert_eq!(h.log(), vec!["pod:Down@10,6".to_string()]);
2328    }
2329
2330    #[test]
2331    fn clearing_the_overlay_removes_it_after_the_next_paint() {
2332        let mut h = Harness::new(Cfg::default());
2333        assert_eq!(rects(&h.frame()).last().copied(), Some(POD));
2334        h.state.cfg.open = false;
2335        let scene = h.frame();
2336        assert!(
2337            !rects(&scene).contains(&POD),
2338            "nothing floated is painted once the view is gone: {:?}",
2339            rects(&scene)
2340        );
2341        h.clear_log();
2342        h.down(40.0, 20.0);
2343        assert_eq!(
2344            h.log(),
2345            vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
2346            "and the press reaches the main tree again"
2347        );
2348    }
2349
2350    // -----------------------------------------------------------------------
2351    // A slot over a `()`-typed pod: the framework-built-surface shape
2352    // -----------------------------------------------------------------------
2353
2354    /// An owner with no children of its own that hosts one `()`-typed surface,
2355    /// builds the floated view itself (as a widget mounting a framework-built
2356    /// surface does) and drains whatever the surface dispatched.
2357    struct ToolbarHost {
2358        log: Log,
2359        reaction: Reaction,
2360    }
2361
2362    struct ToolbarHostWidget {
2363        slot: OverlaySlot<()>,
2364        /// The mounted view, kept so the next rebuild has something to
2365        /// reconcile against.
2366        view: Option<AnyView<()>>,
2367        log: Log,
2368        reaction: Reaction,
2369    }
2370
2371    impl ToolbarHost {
2372        // erasure: keep result stored in an AnyView slot and element
2373        fn pod(&self) -> AnyView<()> {
2374            any(UnitProbe {
2375                log: Rc::clone(&self.log),
2376                reaction: self.reaction,
2377            })
2378        }
2379    }
2380
2381    impl View<()> for ToolbarHost {
2382        type Element = ToolbarHostWidget;
2383        fn build(&self, ctx: &mut BuildCtx<'_>) -> ToolbarHostWidget {
2384            let mut slot = OverlaySlot::new();
2385            slot.set_placement(corner());
2386            let view = self.pod();
2387            slot.rebuild(None, Some(&view), ctx);
2388            ToolbarHostWidget {
2389                slot,
2390                view: Some(view),
2391                log: Rc::clone(&self.log),
2392                reaction: self.reaction,
2393            }
2394        }
2395        fn rebuild(
2396            &self,
2397            _prev: &Self,
2398            element: &mut ToolbarHostWidget,
2399            ctx: &mut BuildCtx<'_>,
2400        ) -> ChangeFlags {
2401            element.log = Rc::clone(&self.log);
2402            element.reaction = self.reaction;
2403            let view = self.pod();
2404            let flags = element
2405                .slot
2406                .rebuild(element.view.as_ref(), Some(&view), ctx);
2407            element.view = Some(view);
2408            flags
2409        }
2410    }
2411
2412    impl Widget for ToolbarHostWidget {
2413        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2414            self.slot.layout(ctx);
2415            bc.constrain(Size::new(100.0, 40.0))
2416        }
2417        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2418            let size = ctx.size();
2419            self.slot.paint(ctx, size);
2420        }
2421        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2422            let mut pod_state = ();
2423            let Some(result) = self.slot.event(ctx, event, &mut pod_state) else {
2424                return EventResult::Ignored;
2425            };
2426            // Drained in the same pass the surface dispatched them in — the
2427            // queue is pass-scoped and is not a mailbox.
2428            for command in ctx.take_edit_commands() {
2429                self.log.borrow_mut().push(format!("cmd:{command:?}"));
2430            }
2431            result
2432        }
2433    }
2434
2435    /// The `()`-typed floated content: it carries no application state at all,
2436    /// and speaks to its owner through the edit-command queue.
2437    struct UnitProbe {
2438        log: Log,
2439        reaction: Reaction,
2440    }
2441
2442    struct UnitProbeWidget {
2443        log: Log,
2444        reaction: Reaction,
2445    }
2446
2447    impl View<()> for UnitProbe {
2448        type Element = UnitProbeWidget;
2449        fn build(&self, _ctx: &mut BuildCtx<'_>) -> UnitProbeWidget {
2450            UnitProbeWidget {
2451                log: Rc::clone(&self.log),
2452                reaction: self.reaction,
2453            }
2454        }
2455        fn rebuild(
2456            &self,
2457            _prev: &Self,
2458            element: &mut UnitProbeWidget,
2459            _ctx: &mut BuildCtx<'_>,
2460        ) -> ChangeFlags {
2461            element.log = Rc::clone(&self.log);
2462            element.reaction = self.reaction;
2463            ChangeFlags::NONE
2464        }
2465    }
2466
2467    impl Widget for UnitProbeWidget {
2468        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2469            bc.constrain(Size::new(80.0, 30.0))
2470        }
2471        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2472        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2473            if let InputEvent::Pointer(p) = event {
2474                self.log.borrow_mut().push(format!(
2475                    "pod:{:?}@{},{}",
2476                    p.phase, p.position.x, p.position.y
2477                ));
2478                if p.phase == PointerPhase::Down {
2479                    match self.reaction {
2480                        Reaction::Nothing => {}
2481                        Reaction::Capture => ctx.capture_pointer(),
2482                        Reaction::Focus => ctx.request_focus(),
2483                        Reaction::Layout => ctx.request_layout(),
2484                    }
2485                    ctx.dispatch_edit_command(EditCommand::Copy);
2486                    ctx.request_redraw();
2487                }
2488            }
2489            EventResult::Handled
2490        }
2491    }
2492
2493    /// Drive the `()`-state host through a real root.
2494    fn unit_harness(reaction: Reaction) -> (RenderRoot<(), ToolbarHost>, Log) {
2495        let log: Log = Rc::new(RefCell::new(Vec::new()));
2496        let mut root: RenderRoot<(), ToolbarHost> = RenderRoot::new();
2497        let captured = Rc::clone(&log);
2498        let mut build = move |_: &mut ()| ToolbarHost {
2499            log: Rc::clone(&captured),
2500            reaction,
2501        };
2502        let mut state = ();
2503        root.rebuild(&mut build, &mut state);
2504        root.layout(WINDOW);
2505        root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
2506        (root, log)
2507    }
2508
2509    #[test]
2510    fn a_unit_typed_pod_is_hosted_and_its_edit_commands_reach_the_owner() {
2511        let (mut root, log) = unit_harness(Reaction::Nothing);
2512        let mut state = ();
2513        root.event(
2514            &mut state,
2515            &InputEvent::Pointer(PointerEvent {
2516                phase: PointerPhase::Down,
2517                position: Point::new(40.0, 50.0),
2518                button: PointerButton::Primary,
2519            }),
2520        );
2521        assert_eq!(
2522            log.borrow().clone(),
2523            vec!["pod:Down@32,10".to_string(), "cmd:Copy".to_string()],
2524            "the surface ran over its own `()` state and its command was drained"
2525        );
2526    }
2527
2528    #[test]
2529    fn a_focus_claim_from_a_substituted_pod_still_opens_a_session() {
2530        let (mut root, _log) = unit_harness(Reaction::Focus);
2531        let mut state = ();
2532        assert!(!root.is_focus_active());
2533        root.event(
2534            &mut state,
2535            &InputEvent::Pointer(PointerEvent {
2536                phase: PointerPhase::Down,
2537                position: Point::new(40.0, 50.0),
2538                button: PointerButton::Primary,
2539            }),
2540        );
2541        assert!(
2542            root.is_focus_active(),
2543            "the substituted context's focus claim is mirrored onto the owner"
2544        );
2545    }
2546
2547    #[test]
2548    fn a_layout_request_from_a_substituted_pod_reaches_the_pending_flags() {
2549        let (mut root, _log) = unit_harness(Reaction::Layout);
2550        root.take_change_flags();
2551        unit_pointer(&mut root, PointerPhase::Down, 40.0, 50.0);
2552        assert!(
2553            root.take_change_flags().contains(ChangeFlags::LAYOUT),
2554            "the substituted context's layout request is mirrored onto the owner, \
2555             so a shell gating layout on the pending flags runs the pass"
2556        );
2557    }
2558
2559    /// One pointer event into the `()`-state host, which carries no application
2560    /// state to thread.
2561    fn unit_pointer(root: &mut RenderRoot<(), ToolbarHost>, phase: PointerPhase, x: f64, y: f64) {
2562        let mut state = ();
2563        root.event(
2564            &mut state,
2565            &InputEvent::Pointer(PointerEvent {
2566                phase,
2567                position: Point::new(x, y),
2568                button: PointerButton::Primary,
2569            }),
2570        );
2571    }
2572
2573    #[test]
2574    fn a_second_gesture_into_a_substituted_pod_routes_exactly_like_the_first() {
2575        let (mut root, log) = unit_harness(Reaction::Capture);
2576        // The substituted route has no second road to the owner: the mirror in
2577        // `forward` is the only thing that can open the root's gesture, so a
2578        // capture it fails to report strands the follow-ups entirely.
2579        for label in ["first gesture", "second gesture"] {
2580            log.borrow_mut().clear();
2581            unit_pointer(&mut root, PointerPhase::Down, 40.0, 50.0);
2582            assert!(
2583                root.is_pointer_captured(),
2584                "{label}: the substituted pod's capture was mirrored onto the owner"
2585            );
2586            unit_pointer(&mut root, PointerPhase::Move, 300.0, 300.0);
2587            unit_pointer(&mut root, PointerPhase::Up, 300.0, 300.0);
2588            assert_eq!(
2589                log.borrow().clone(),
2590                vec![
2591                    "pod:Down@32,10".to_string(),
2592                    "cmd:Copy".to_string(),
2593                    "pod:Move@292,260".to_string(),
2594                    "pod:Up@292,260".to_string(),
2595                ],
2596                "{label}: every phase of the gesture reached the surface"
2597            );
2598            assert!(
2599                !root.is_pointer_captured(),
2600                "{label}: the Up released the capture"
2601            );
2602        }
2603    }
2604
2605    #[test]
2606    fn a_slot_starts_closed_and_hands_its_outside_press_over_once() {
2607        let mut slot: OverlaySlot<()> = OverlaySlot::new();
2608        assert!(!slot.is_open());
2609        assert_eq!(slot.window_rect(), Rect::ZERO);
2610        assert!(!slot.take_outside_down());
2611        // Two slots never share an identity, which is the whole addressing rule.
2612        let other: OverlaySlot<()> = OverlaySlot::default();
2613        assert_ne!(slot.key(), other.key());
2614    }
2615
2616    #[test]
2617    fn a_shortcut_quoting_the_slots_key_reaches_the_pod_as_a_plain_shortcut() {
2618        let mut slot: OverlaySlot<Vec<u32>> = OverlaySlot::new();
2619        let mut counter = 0u64;
2620        let view = any(crate::test_support::probe(7));
2621        slot.rebuild(None, Some(&view), &mut BuildCtx::new(&mut counter));
2622        let mut log: Vec<u32> = Vec::new();
2623        let mut ctx = EventCtx::new(&mut log, Point::ZERO, Size::new(100.0, 100.0));
2624        let id = frust_core::ShortcutId::next();
2625        let bare = InputEvent::Shortcut(id);
2626        assert_eq!(
2627            slot.event_ambient(&mut ctx, &bare),
2628            None,
2629            "never a bare broadcast"
2630        );
2631        let routed = InputEvent::Overlay(frust_core::OverlayEvent {
2632            key: slot.key(),
2633            kind: OverlayEventKind::Shortcut(id),
2634        });
2635        assert_eq!(
2636            slot.event_ambient(&mut ctx, &routed),
2637            Some(EventResult::Ignored)
2638        );
2639        assert_eq!(log, vec![7], "the pod heard it");
2640    }
2641
2642    // -----------------------------------------------------------------------
2643    // `OverlayAnchor::Window`: a pointer-following, absolute anchor
2644    // -----------------------------------------------------------------------
2645
2646    /// A `()`-typed pod that paints a rect at its own absolute origin, so a
2647    /// test can read the registered entry's placed rect back off the painted
2648    /// scene rather than reaching into state no owner exposes.
2649    struct GhostPod;
2650
2651    struct GhostPodWidget;
2652
2653    impl View<()> for GhostPod {
2654        type Element = GhostPodWidget;
2655        fn build(&self, _ctx: &mut BuildCtx<'_>) -> GhostPodWidget {
2656            GhostPodWidget
2657        }
2658        fn rebuild(
2659            &self,
2660            _prev: &Self,
2661            _element: &mut GhostPodWidget,
2662            _ctx: &mut BuildCtx<'_>,
2663        ) -> ChangeFlags {
2664            ChangeFlags::NONE
2665        }
2666    }
2667
2668    impl Widget for GhostPodWidget {
2669        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2670            bc.constrain(Size::new(80.0, 30.0))
2671        }
2672        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2673            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
2674        }
2675    }
2676
2677    /// A `()`-typed probe with a configurable tag, filling whatever room it is
2678    /// offered — the "widget beneath" a `Transparent` surface must still
2679    /// reach, wherever that surface is anchored.
2680    struct TaggedProbe {
2681        tag: &'static str,
2682        log: Log,
2683    }
2684
2685    struct TaggedProbeWidget {
2686        tag: &'static str,
2687        log: Log,
2688    }
2689
2690    impl View<()> for TaggedProbe {
2691        type Element = TaggedProbeWidget;
2692        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TaggedProbeWidget {
2693            TaggedProbeWidget {
2694                tag: self.tag,
2695                log: Rc::clone(&self.log),
2696            }
2697        }
2698        fn rebuild(
2699            &self,
2700            _prev: &Self,
2701            element: &mut TaggedProbeWidget,
2702            _ctx: &mut BuildCtx<'_>,
2703        ) -> ChangeFlags {
2704            element.log = Rc::clone(&self.log);
2705            ChangeFlags::NONE
2706        }
2707    }
2708
2709    impl Widget for TaggedProbeWidget {
2710        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2711            bc.constrain(bc.max())
2712        }
2713        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2714        fn event(&mut self, _ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2715            if let InputEvent::Pointer(p) = event {
2716                self.log.borrow_mut().push(format!(
2717                    "{}:{:?}@{},{}",
2718                    self.tag, p.phase, p.position.x, p.position.y
2719                ));
2720                EventResult::Handled
2721            } else {
2722                EventResult::Ignored
2723            }
2724        }
2725    }
2726
2727    /// An owner that fills the window and, on every pointer `Move`/`Down`,
2728    /// moves its own `Window`-anchored, `Transparent`, `Tooltip`-band surface
2729    /// to the event's position — the shape a full-window pointer-tracking
2730    /// layer takes while a drag ghost is live. Never handles the event itself,
2731    /// so whatever sits beneath it in the main tree still sees every press.
2732    struct GhostOwner {
2733        offset: Vec2,
2734    }
2735
2736    struct GhostOwnerWidget {
2737        slot: OverlaySlot<()>,
2738    }
2739
2740    impl View<()> for GhostOwner {
2741        type Element = GhostOwnerWidget;
2742        fn build(&self, ctx: &mut BuildCtx<'_>) -> GhostOwnerWidget {
2743            let mut slot = OverlaySlot::new();
2744            slot.set_band(OverlayBand::Tooltip);
2745            slot.set_input(OverlayInput::Transparent);
2746            slot.set_anchor(OverlayAnchor::Window {
2747                point: Point::ZERO,
2748                offset: self.offset,
2749            });
2750            let view: AnyView<()> = any(GhostPod);
2751            slot.rebuild(None, Some(&view), ctx);
2752            GhostOwnerWidget { slot }
2753        }
2754        fn rebuild(
2755            &self,
2756            _prev: &Self,
2757            _element: &mut GhostOwnerWidget,
2758            _ctx: &mut BuildCtx<'_>,
2759        ) -> ChangeFlags {
2760            ChangeFlags::NONE
2761        }
2762    }
2763
2764    impl Widget for GhostOwnerWidget {
2765        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2766            self.slot.layout(ctx);
2767            bc.constrain(bc.max())
2768        }
2769        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2770            let size = ctx.size();
2771            self.slot.paint(ctx, size);
2772        }
2773        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2774            if let InputEvent::Pointer(p) = event
2775                && matches!(p.phase, PointerPhase::Move | PointerPhase::Down)
2776            {
2777                self.slot.set_window_anchor(ctx, p.position);
2778            }
2779            EventResult::Ignored
2780        }
2781    }
2782
2783    /// A plain `Floating`, `Owner`-anchored, `Interactive` surface — the
2784    /// counterpart `a_window_anchored_tooltip_still_paints_above_a_floating_surface`
2785    /// checks the band sort against.
2786    struct FloatingOwner;
2787
2788    struct FloatingOwnerWidget {
2789        slot: OverlaySlot<()>,
2790    }
2791
2792    impl View<()> for FloatingOwner {
2793        type Element = FloatingOwnerWidget;
2794        fn build(&self, ctx: &mut BuildCtx<'_>) -> FloatingOwnerWidget {
2795            let mut slot = OverlaySlot::new();
2796            slot.set_placement(corner());
2797            let view: AnyView<()> = any(GhostPod);
2798            slot.rebuild(None, Some(&view), ctx);
2799            FloatingOwnerWidget { slot }
2800        }
2801        fn rebuild(
2802            &self,
2803            _prev: &Self,
2804            _element: &mut FloatingOwnerWidget,
2805            _ctx: &mut BuildCtx<'_>,
2806        ) -> ChangeFlags {
2807            ChangeFlags::NONE
2808        }
2809    }
2810
2811    impl Widget for FloatingOwnerWidget {
2812        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2813            self.slot.layout(ctx);
2814            bc.constrain(Size::new(100.0, 40.0))
2815        }
2816        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2817            let size = ctx.size();
2818            self.slot.paint(ctx, size);
2819        }
2820    }
2821
2822    /// Drive a window-filling [`GhostOwner`] over a [`TaggedProbe`] floor
2823    /// through a real root.
2824    fn ghost_harness(offset: Vec2) -> (RenderRoot<(), StackView<()>>, Log) {
2825        let beneath_log: Log = Rc::new(RefCell::new(Vec::new()));
2826        let mut root: RenderRoot<(), StackView<()>> = RenderRoot::new();
2827        let log = Rc::clone(&beneath_log);
2828        let mut build = move |_: &mut ()| {
2829            stack()
2830                .child(TaggedProbe {
2831                    tag: "beneath",
2832                    log: Rc::clone(&log),
2833                })
2834                .child(GhostOwner { offset })
2835        };
2836        let mut state = ();
2837        root.rebuild(&mut build, &mut state);
2838        root.layout(WINDOW);
2839        root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
2840        (root, beneath_log)
2841    }
2842
2843    #[test]
2844    fn a_window_anchor_paints_at_point_plus_offset() {
2845        let offset = Vec2::new(5.0, 7.0);
2846        let (mut root, _log) = ghost_harness(offset);
2847        let mut scene = RecordingScene::default();
2848        root.paint(&mut scene, FrameTime::from_nanos(16_000_000));
2849        assert_eq!(
2850            rects(&scene).last().copied(),
2851            Some(Rect::from_origin_size(
2852                Point::ZERO + offset,
2853                Size::new(80.0, 30.0)
2854            )),
2855            "a Window anchor's rect is point + offset, with no side/align geometry"
2856        );
2857    }
2858
2859    #[test]
2860    fn moving_a_window_anchor_moves_the_paint_rect_on_the_next_paint() {
2861        let offset = Vec2::new(5.0, 7.0);
2862        let (mut root, _log) = ghost_harness(offset);
2863        let mut state = ();
2864        root.event(
2865            &mut state,
2866            &InputEvent::Pointer(PointerEvent {
2867                phase: PointerPhase::Down,
2868                position: Point::new(100.0, 200.0),
2869                button: PointerButton::Primary,
2870            }),
2871        );
2872        let mut scene = RecordingScene::default();
2873        root.paint(&mut scene, FrameTime::from_nanos(16_000_000));
2874        assert_eq!(
2875            rects(&scene).last().copied(),
2876            Some(Rect::from_origin_size(
2877                Point::new(100.0, 200.0) + offset,
2878                Size::new(80.0, 30.0)
2879            )),
2880            "the event handler's set_window_anchor call moved the surface to \
2881             the pointer, picked up on the following paint"
2882        );
2883
2884        root.event(
2885            &mut state,
2886            &InputEvent::Pointer(PointerEvent {
2887                phase: PointerPhase::Move,
2888                position: Point::new(250.0, 40.0),
2889                button: PointerButton::Primary,
2890            }),
2891        );
2892        let mut scene = RecordingScene::default();
2893        root.paint(&mut scene, FrameTime::from_nanos(32_000_000));
2894        assert_eq!(
2895            rects(&scene).last().copied(),
2896            Some(Rect::from_origin_size(
2897                Point::new(250.0, 40.0) + offset,
2898                Size::new(80.0, 30.0)
2899            )),
2900            "a further Move keeps tracking the pointer"
2901        );
2902    }
2903
2904    #[test]
2905    fn a_window_anchored_transparent_surface_still_lets_a_press_through() {
2906        let (mut root, log) = ghost_harness(Vec2::new(5.0, 7.0));
2907        let mut state = ();
2908        root.event(
2909            &mut state,
2910            &InputEvent::Pointer(PointerEvent {
2911                phase: PointerPhase::Down,
2912                position: Point::new(100.0, 200.0),
2913                button: PointerButton::Primary,
2914            }),
2915        );
2916        assert_eq!(
2917            log.borrow().clone(),
2918            vec!["beneath:Down@100,200".to_string()],
2919            "`Transparent` never hit-tests, Window anchor or not — the press \
2920             reaches the widget the ghost is floating over"
2921        );
2922    }
2923
2924    #[test]
2925    fn a_window_anchored_tooltip_still_paints_above_a_floating_surface() {
2926        let offset = Vec2::new(5.0, 7.0);
2927        let mut root: RenderRoot<(), StackView<()>> = RenderRoot::new();
2928        // Registered Tooltip-first (`GhostOwner`), Floating-second
2929        // (`FloatingOwner`): exactly the order the band sort has to survive,
2930        // not merely happen to preserve.
2931        let mut build = move |_: &mut ()| stack().child(GhostOwner { offset }).child(FloatingOwner);
2932        let mut state = ();
2933        root.rebuild(&mut build, &mut state);
2934        root.layout(WINDOW);
2935        let mut scene = RecordingScene::default();
2936        root.paint(&mut scene, FrameTime::from_nanos(0));
2937        assert_eq!(
2938            rects(&scene),
2939            vec![
2940                Rect::new(8.0, 40.0, 88.0, 70.0),
2941                Rect::from_origin_size(Point::ZERO + offset, Size::new(80.0, 30.0)),
2942            ],
2943            "Floating still paints below Tooltip despite registering after it: \
2944             {:?}",
2945            rects(&scene)
2946        );
2947    }
2948
2949    // -----------------------------------------------------------------------
2950    // Tab traversal into a floated pod
2951    // -----------------------------------------------------------------------
2952
2953    /// A focusable leaf that records, on each paint, whether it holds focus.
2954    struct Stop {
2955        focused: Rc<std::cell::Cell<bool>>,
2956    }
2957
2958    struct StopWidget {
2959        focused: Rc<std::cell::Cell<bool>>,
2960    }
2961
2962    impl View<TabApp> for Stop {
2963        type Element = StopWidget;
2964        fn build(&self, _ctx: &mut BuildCtx<'_>) -> StopWidget {
2965            StopWidget {
2966                focused: Rc::clone(&self.focused),
2967            }
2968        }
2969        fn rebuild(
2970            &self,
2971            _prev: &Self,
2972            element: &mut StopWidget,
2973            _ctx: &mut BuildCtx<'_>,
2974        ) -> ChangeFlags {
2975            element.focused = Rc::clone(&self.focused);
2976            ChangeFlags::NONE
2977        }
2978    }
2979
2980    impl Widget for StopWidget {
2981        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2982            bc.constrain(Size::new(40.0, 20.0))
2983        }
2984        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2985            self.focused.set(ctx.has_focus());
2986            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
2987        }
2988        fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
2989            EventResult::Ignored
2990        }
2991        fn is_focusable(&self) -> bool {
2992            true
2993        }
2994    }
2995
2996    /// The owner's button and the floated pod's content, and whether the pod is mounted.
2997    #[derive(Default)]
2998    struct TabApp {
2999        open: bool,
3000        owner: Rc<std::cell::Cell<bool>>,
3001        pod: Rc<std::cell::Cell<bool>>,
3002    }
3003
3004    fn tab_logic(state: &mut TabApp) -> StackView<TabApp> {
3005        let surface = state.open.then(|| Stop {
3006            focused: Rc::clone(&state.pod),
3007        });
3008        stack().child(
3009            overlay_portal(Stop {
3010                focused: Rc::clone(&state.owner),
3011            })
3012            .overlay(surface),
3013        )
3014    }
3015
3016    fn tab_frame(root: &mut RenderRoot<TabApp, StackView<TabApp>>, state: &mut TabApp) {
3017        let mut build: fn(&mut TabApp) -> StackView<TabApp> = tab_logic;
3018        root.rebuild(&mut build, state);
3019        root.layout(WINDOW);
3020        root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
3021    }
3022
3023    /// One Tab (or Shift+Tab) press, then a frame so the paint reports the new focus.
3024    fn tab_press(
3025        root: &mut RenderRoot<TabApp, StackView<TabApp>>,
3026        state: &mut TabApp,
3027        shift: bool,
3028    ) {
3029        let event = KeyEvent {
3030            key: Key::Named(NamedKey::Tab),
3031            modifiers: Modifiers {
3032                shift,
3033                ..Modifiers::default()
3034            },
3035            repeat: false,
3036        };
3037        root.event(state, &InputEvent::Key(event));
3038        tab_frame(root, state);
3039    }
3040
3041    #[test]
3042    fn tab_moves_from_the_owner_into_an_open_pass_pod_and_shift_tab_comes_back() {
3043        let mut root = RenderRoot::new();
3044        let mut state = TabApp {
3045            open: true,
3046            ..TabApp::default()
3047        };
3048        tab_frame(&mut root, &mut state);
3049
3050        tab_press(&mut root, &mut state, false);
3051        assert!(state.owner.get(), "the owner's button takes the first stop");
3052        assert!(!state.pod.get());
3053
3054        tab_press(&mut root, &mut state, false);
3055        assert!(root.is_focus_active());
3056        assert!(state.pod.get(), "the pod's content takes the next stop");
3057        assert!(!state.owner.get(), "the owner gives up focus to the pod");
3058
3059        tab_press(&mut root, &mut state, true);
3060        assert!(state.owner.get(), "Shift+Tab goes back to the owner's stop");
3061        assert!(!state.pod.get());
3062    }
3063
3064    #[test]
3065    fn a_closed_pod_contributes_no_stops() {
3066        let mut root = RenderRoot::new();
3067        let mut state = TabApp::default();
3068        tab_frame(&mut root, &mut state);
3069
3070        tab_press(&mut root, &mut state, false);
3071        assert!(state.owner.get());
3072        // The owner is the only stop, so Tab wraps back onto it and the pod
3073        // never takes focus.
3074        tab_press(&mut root, &mut state, false);
3075        assert!(state.owner.get());
3076        assert!(!state.pod.get(), "an unmounted pod is never a stop");
3077    }
3078}