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