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) = {
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())
949                };
950                if needs_redraw {
951                    ctx.request_redraw();
952                }
953                // Capture and focus are mirrored through the pod's own recorded
954                // links rather than the substituted context's flags: the pod
955                // records exactly what its widget asked for, and reading it here
956                // keeps one mirror instead of two.
957                if pod.is_active() && !was_active {
958                    ctx.capture_pointer();
959                }
960                match (was_focused, pod.is_focused()) {
961                    (false, true) => ctx.request_focus(),
962                    (true, false) => ctx.release_focus(),
963                    _ => {}
964                }
965                result
966            }
967        };
968        // Give the link the lifetime every other container gives a captured
969        // child. `event_child` only ever sets it, and this is the one dispatch
970        // path that does not run through a routing helper, so the release has
971        // to happen here or never — and until it does, the two `was_active`
972        // edges above are edges into a latch that never falls.
973        if was_active && releases_capture(local) {
974            pod.set_active(false);
975        }
976        self.captured = pod.is_active();
977        result
978    }
979}
980
981// ---------------------------------------------------------------------------
982// The declarative portal
983// ---------------------------------------------------------------------------
984
985/// Float `overlay` above the whole app, anchored to `child`'s bounds — the
986/// declarative half of the portal.
987///
988/// The child is laid out, painted and routed exactly as it would be without the
989/// wrapper: the portal adds nothing to its geometry and consumes none of its
990/// input. The overlay is mounted while [`OverlayPortalView::overlay`] is
991/// `Some`, which is also how an exit animation is expressed — keep handing the
992/// same view in while the surface ramps out, and hand `None` once it has.
993///
994/// ```
995/// use frust_widgets::{OverlayPlacement, OverlaySide, overlay_portal, text};
996///
997/// struct App {
998///     hovering: bool,
999/// }
1000///
1001/// fn tip(state: &mut App) -> impl frust_core::View<App> + use<> {
1002///     overlay_portal(text("save"))
1003///         .overlay(state.hovering.then(|| text("Save the document")))
1004///         .placement(OverlayPlacement::on(OverlaySide::Top))
1005/// }
1006/// # let _ = tip;
1007/// ```
1008pub fn overlay_portal<State: 'static, V: View<State>>(child: V) -> OverlayPortalView<State> {
1009    OverlayPortalView {
1010        child: any(child),
1011        overlay: None,
1012        placement: OverlayPlacement::default(),
1013        band: OverlayBand::Floating,
1014        input: OverlayInput::Interactive,
1015        outside_tap: OutsideTap::Ignore,
1016        on_outside_tap: None,
1017        preserve_focus: false,
1018    }
1019}
1020
1021/// The light-dismiss callback a portal holds before it is erased.
1022type OnOutsideTap<State> = Rc<dyn Fn(&mut State)>;
1023
1024/// A declarative overlay portal. See [`overlay_portal`].
1025pub struct OverlayPortalView<State: 'static> {
1026    child: AnyView<State>,
1027    overlay: Option<AnyView<State>>,
1028    placement: OverlayPlacement,
1029    band: OverlayBand,
1030    input: OverlayInput,
1031    outside_tap: OutsideTap,
1032    on_outside_tap: Option<OnOutsideTap<State>>,
1033    preserve_focus: bool,
1034}
1035
1036impl<State: 'static> OverlayPortalView<State> {
1037    /// The floated surface: `Some` mounts it, `None` drops it after the next
1038    /// paint.
1039    ///
1040    /// The view is diffed against the **same** application state the portal
1041    /// itself is, so the surface reads and writes app state exactly like the
1042    /// child does — it is a logical child of this call site that happens to be
1043    /// painted elsewhere.
1044    ///
1045    /// A bare `None` needs the surface's view type spelled out, e.g.
1046    /// `.overlay(None::<AnyView<State>>)`.
1047    pub fn overlay<V: View<State>>(mut self, overlay: Option<V>) -> Self {
1048        self.overlay = overlay.map(any);
1049        self
1050    }
1051
1052    /// Where the surface sits relative to the child's bounds (default: below,
1053    /// centred — see [`OverlayPlacement::default`]).
1054    pub fn placement(mut self, placement: OverlayPlacement) -> Self {
1055        self.placement = placement;
1056        self
1057    }
1058
1059    /// Which z-band the surface paints and hit-tests in (default
1060    /// [`OverlayBand::Floating`]; a tooltip belongs in
1061    /// [`OverlayBand::Tooltip`]).
1062    pub fn band(mut self, band: OverlayBand) -> Self {
1063        self.band = band;
1064        self
1065    }
1066
1067    /// Whether the surface takes pointer input (default
1068    /// [`OverlayInput::Interactive`]; explanatory chrome the pointer passes
1069    /// through is [`OverlayInput::Transparent`]).
1070    pub fn input(mut self, input: OverlayInput) -> Self {
1071        self.input = input;
1072        self
1073    }
1074
1075    /// What a press landing outside every floated surface delivers here
1076    /// (default [`OutsideTap::Ignore`]).
1077    pub fn outside_tap(mut self, outside_tap: OutsideTap) -> Self {
1078        self.outside_tap = outside_tap;
1079        self
1080    }
1081
1082    /// Run `callback` when a press lands outside every floated surface — the
1083    /// light-dismiss hook.
1084    ///
1085    /// Setting it also opts the surface into the notification
1086    /// ([`OutsideTap::Notify`] with `consume: true`, the modal shape) unless an
1087    /// explicit [`outside_tap`](Self::outside_tap) says otherwise, so the tap
1088    /// that dismisses a menu does not also activate what sits under it. Pass
1089    /// `OutsideTap::Notify { consume: false }` for the pass-through shape.
1090    pub fn on_outside_tap(mut self, callback: impl Fn(&mut State) + 'static) -> Self {
1091        self.on_outside_tap = Some(Rc::new(callback));
1092        if self.outside_tap == OutsideTap::Ignore {
1093            self.outside_tap = OutsideTap::Notify { consume: true };
1094        }
1095        self
1096    }
1097
1098    /// Keep the child's focus session when the surface claims focus (default
1099    /// `false`).
1100    ///
1101    /// A surface that is *about* the child rather than a place to type — a
1102    /// selection toolbar over a field — must not take the field's focus, or the
1103    /// selection it acts on disappears the moment it is touched. With this set,
1104    /// a focus claim from inside the surface is withdrawn again whenever the
1105    /// child held the focus link, and focus-routed events keep reaching the
1106    /// child. Left `false`, a surface that claims focus gets it and the child's
1107    /// link inside this portal is dropped, which is what a popover containing
1108    /// its own text field wants.
1109    pub fn preserve_focus(mut self, preserve: bool) -> Self {
1110        self.preserve_focus = preserve;
1111        self
1112    }
1113}
1114
1115/// The retained widget for an [`OverlayPortalView`].
1116pub struct OverlayPortalWidget<State: 'static> {
1117    child: ChildPod,
1118    slot: OverlaySlot<State>,
1119    on_outside_tap: Option<ErasedCallback>,
1120    preserve_focus: bool,
1121}
1122
1123impl<State: 'static> OverlayPortalView<State> {
1124    /// Push the view's routing configuration onto the slot — the half of
1125    /// `build`/`rebuild` that is identical in both.
1126    fn configure(&self, slot: &mut OverlaySlot<State>) {
1127        slot.set_placement(self.placement);
1128        slot.set_band(self.band);
1129        slot.set_input(self.input);
1130        slot.set_outside_tap(self.outside_tap);
1131    }
1132}
1133
1134impl<State: 'static> View<State> for OverlayPortalView<State> {
1135    type Element = OverlayPortalWidget<State>;
1136
1137    fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayPortalWidget<State> {
1138        let mut slot = OverlaySlot::new();
1139        self.configure(&mut slot);
1140        slot.rebuild(None, self.overlay.as_ref(), ctx);
1141        OverlayPortalWidget {
1142            child: crate::authoring::build_child(&self.child, ctx),
1143            slot,
1144            on_outside_tap: self.on_outside_tap.as_ref().map(erase_callback),
1145            preserve_focus: self.preserve_focus,
1146        }
1147    }
1148
1149    fn rebuild(
1150        &self,
1151        prev: &Self,
1152        element: &mut OverlayPortalWidget<State>,
1153        ctx: &mut BuildCtx<'_>,
1154    ) -> ChangeFlags {
1155        let mut flags = ChangeFlags::NONE;
1156        if prev.placement != self.placement
1157            || prev.band != self.band
1158            || prev.input != self.input
1159            || prev.outside_tap != self.outside_tap
1160        {
1161            // Placement and the routing fields are re-read from the slot on the
1162            // next paint, which is also when the new rect is registered.
1163            flags |= ChangeFlags::PAINT;
1164        }
1165        self.configure(&mut element.slot);
1166        element.preserve_focus = self.preserve_focus;
1167        // Closures are not comparable, so the adapter is reinstalled
1168        // unconditionally — it is cheap.
1169        element.on_outside_tap = self.on_outside_tap.as_ref().map(erase_callback);
1170        flags |= element
1171            .slot
1172            .rebuild(prev.overlay.as_ref(), self.overlay.as_ref(), ctx);
1173        flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
1174        flags
1175    }
1176
1177    fn teardown(&self, element: &mut OverlayPortalWidget<State>, ctx: &mut BuildCtx<'_>) {
1178        element.slot.rebuild(self.overlay.as_ref(), None, ctx);
1179        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
1180    }
1181}
1182
1183impl<State: 'static> Widget for OverlayPortalWidget<State> {
1184    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1185        let size = self.child.layout_child(ctx, bc);
1186        self.child.set_origin(Point::ZERO);
1187        // The floated pod is sized against the window, not against `bc` — it
1188        // escapes this widget's box entirely.
1189        self.slot.layout(ctx);
1190        bc.constrain(size)
1191    }
1192
1193    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1194        self.child.paint_child(ctx, scene);
1195        // Registered, never painted here: the root paints it after the whole
1196        // main tree, which is what puts it above a later sibling.
1197        let size = ctx.size();
1198        self.slot.paint(ctx, size);
1199    }
1200
1201    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1202        let child_focused = self.child.holds_live_focus();
1203        // A focus-routed event belongs to the child whenever the child holds the
1204        // live link. Both links can be live at once only through
1205        // `preserve_focus`, which deliberately re-records the child's against the
1206        // same session it hands back; otherwise exactly one of them is, and this
1207        // check simply prefers the main tree when both are.
1208        if !(event.is_focus_routed() && child_focused) {
1209            let pod_focused_before = self.slot.pod_has_focus();
1210            if let Some(result) = self.slot.event_ambient(ctx, event) {
1211                // Drained unconditionally: a notification with no callback
1212                // installed is still spent, not left standing for a later pass.
1213                if self.slot.take_outside_down()
1214                    && let Some(callback) = &mut self.on_outside_tap
1215                {
1216                    callback(ctx);
1217                    ctx.request_redraw();
1218                }
1219                if !pod_focused_before && self.slot.pod_has_focus() {
1220                    if self.preserve_focus && child_focused {
1221                        // Hand the session back to the child: the surface's own
1222                        // claim is withdrawn, and re-asserting the request keeps
1223                        // every ancestor's recorded chain pointing here.
1224                        //
1225                        // The child's own link is re-recorded rather than left
1226                        // alone. The surface's claim opened a new focus session,
1227                        // and a link recorded against the session before it is
1228                        // not a link on this one — so handing the session back
1229                        // means saying so on the child's own record, not merely
1230                        // not clearing it.
1231                        self.slot.withdraw_pod_focus();
1232                        self.child.set_focused(true);
1233                        ctx.request_focus();
1234                    } else {
1235                        // The surface took the session, so the child's link
1236                        // inside this portal is stale — an overlay press never
1237                        // reaches the root's own blur rule to clear it.
1238                        self.child.set_focused(false);
1239                    }
1240                }
1241                return result;
1242            }
1243        }
1244        route_event_single(&mut self.child, ctx, event)
1245    }
1246
1247    fn semantics(&self, ctx: &mut SemanticsCtx) {
1248        // The child only: a floated pod's nodes would attach at this widget's
1249        // position rather than the surface's (see the module docs).
1250        self.child.semantics_child(ctx);
1251    }
1252
1253    crate::authoring::visit_children!(child);
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use super::*;
1259    use crate::test_support::RecordingScene;
1260    use crate::{SizedBox, StackView, column, scroll_view, stack};
1261    use frust_core::{
1262        EditCommand, FrameTime, Key, KeyEvent, Modifiers, NamedKey, PointerButton, PointerPhase,
1263        RenderRoot, ScrollDelta,
1264    };
1265    use peniko::Color;
1266
1267    // -----------------------------------------------------------------------
1268    // Placement
1269    // -----------------------------------------------------------------------
1270
1271    const AREA: Rect = Rect::new(0.0, 0.0, 400.0, 600.0);
1272
1273    /// Placement with the padding switched off, so a geometry assertion reads
1274    /// against the raw area rather than the inset one.
1275    fn bare() -> OverlayPlacement {
1276        OverlayPlacement::default().padding(0.0)
1277    }
1278
1279    fn anchor_rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
1280        Rect::from_origin_size(Point::new(x, y), Size::new(w, h))
1281    }
1282
1283    #[test]
1284    fn the_default_placement_is_below_the_anchor_centred_at_the_neutral_gap() {
1285        let p = OverlayPlacement::default();
1286        assert_eq!(p.side, OverlaySide::Bottom);
1287        assert_eq!(p.align, OverlayAlign::Center);
1288        assert_eq!(p.offset, DEFAULT_OFFSET);
1289        assert_eq!(p.padding, DEFAULT_PADDING);
1290        assert!(p.flip && p.clamp);
1291    }
1292
1293    #[test]
1294    fn each_side_offsets_off_its_own_edge() {
1295        let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
1296        let content = Size::new(120.0, 60.0);
1297        // The raw side placement, with the collision passes off: `Left` would
1298        // otherwise flip (a 120px panel does not fit in the 100px to the
1299        // anchor's left), which the flip test below covers on its own.
1300        let raw = |side| OverlayPlacement::on(side).flip(false).clamp(false);
1301        let bottom = place(a, content, AREA, raw(OverlaySide::Bottom));
1302        assert_eq!(bottom.y0, a.y1 + DEFAULT_OFFSET);
1303        let top = place(a, content, AREA, raw(OverlaySide::Top));
1304        assert_eq!(top.y1, a.y0 - DEFAULT_OFFSET);
1305        let right = place(a, content, AREA, raw(OverlaySide::Right));
1306        assert_eq!(right.x0, a.x1 + DEFAULT_OFFSET);
1307        let left = place(a, content, AREA, raw(OverlaySide::Left));
1308        assert_eq!(left.x1, a.x0 - DEFAULT_OFFSET);
1309        // Sizes are never altered by placement.
1310        for r in [bottom, top, right, left] {
1311            assert_eq!(r.size(), content);
1312        }
1313        assert!(OverlaySide::Top.is_vertical() && !OverlaySide::Left.is_vertical());
1314        assert_eq!(OverlaySide::Top.opposite(), OverlaySide::Bottom);
1315        assert_eq!(OverlaySide::Left.opposite(), OverlaySide::Right);
1316    }
1317
1318    #[test]
1319    fn align_lines_up_leading_center_or_trailing_edges() {
1320        let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
1321        let content = Size::new(120.0, 60.0);
1322        let at = |side, align| place(a, content, AREA, OverlayPlacement::on(side).align(align));
1323        assert_eq!(at(OverlaySide::Bottom, OverlayAlign::Start).x0, a.x0);
1324        assert_eq!(at(OverlaySide::Bottom, OverlayAlign::End).x1, a.x1);
1325        assert_eq!(
1326            at(OverlaySide::Bottom, OverlayAlign::Center).center().x,
1327            a.center().x
1328        );
1329
1330        // The same three on a horizontal side act on the vertical axis.
1331        assert_eq!(at(OverlaySide::Right, OverlayAlign::Start).y0, a.y0);
1332        assert_eq!(at(OverlaySide::Right, OverlayAlign::End).y1, a.y1);
1333        assert_eq!(
1334            at(OverlaySide::Right, OverlayAlign::Center).center().y,
1335            a.center().y
1336        );
1337    }
1338
1339    #[test]
1340    fn a_side_that_does_not_fit_flips_to_the_opposite_one() {
1341        // An anchor near the bottom edge: `Bottom` overflows, `Top` fits.
1342        let a = anchor_rect(100.0, 560.0, 80.0, 20.0);
1343        let content = Size::new(120.0, 100.0);
1344        let flipped = place(a, content, AREA, bare());
1345        assert_eq!(
1346            flipped.y1,
1347            a.y0 - DEFAULT_OFFSET,
1348            "flipped above the anchor"
1349        );
1350
1351        // With the flip disabled it stays below and only the clamp moves it.
1352        let pinned = place(a, content, AREA, bare().flip(false));
1353        assert_eq!(pinned.y1, AREA.y1, "clamped, not flipped");
1354
1355        // Neither side fits: the preferred one is kept (and clamped).
1356        let tall = Size::new(120.0, 590.0);
1357        let kept = place(a, tall, AREA, bare());
1358        assert_eq!(kept.y1, AREA.y1);
1359    }
1360
1361    #[test]
1362    fn each_side_flips_at_the_edge_it_would_overflow() {
1363        let content = Size::new(120.0, 100.0);
1364        // Top edge: a `Top` placement flips down.
1365        let high = anchor_rect(100.0, 10.0, 80.0, 20.0);
1366        let down = place(high, content, AREA, OverlayPlacement::on(OverlaySide::Top));
1367        assert_eq!(down.y0, high.y1 + DEFAULT_OFFSET);
1368        // Left edge: a `Left` placement flips right.
1369        let leading = anchor_rect(10.0, 200.0, 20.0, 20.0);
1370        let right = place(
1371            leading,
1372            content,
1373            AREA,
1374            OverlayPlacement::on(OverlaySide::Left),
1375        );
1376        assert_eq!(right.x0, leading.x1 + DEFAULT_OFFSET);
1377        // Right edge: a `Right` placement flips left.
1378        let trailing = anchor_rect(370.0, 200.0, 20.0, 20.0);
1379        let left = place(
1380            trailing,
1381            content,
1382            AREA,
1383            OverlayPlacement::on(OverlaySide::Right),
1384        );
1385        assert_eq!(left.x1, trailing.x0 - DEFAULT_OFFSET);
1386    }
1387
1388    #[test]
1389    fn clamping_shifts_the_rect_back_inside_and_can_be_turned_off() {
1390        // An anchor at the right edge, centre-aligned: the panel overflows.
1391        let a = anchor_rect(380.0, 100.0, 20.0, 20.0);
1392        let content = Size::new(200.0, 50.0);
1393        let clamped = place(a, content, AREA, bare());
1394        assert_eq!(clamped.x1, AREA.x1);
1395        assert_eq!(
1396            clamped.y0,
1397            a.y1 + DEFAULT_OFFSET,
1398            "only the cross axis moved"
1399        );
1400
1401        let free = place(a, content, AREA, bare().clamp(false));
1402        assert!(free.x1 > AREA.x1, "unclamped placement may overflow");
1403
1404        // Content wider than the area pins to the leading edge (the start-edge
1405        // rule), not the trailing one.
1406        let huge = Size::new(600.0, 50.0);
1407        let pinned = place(a, huge, AREA, bare());
1408        assert_eq!(pinned.x0, AREA.x0);
1409    }
1410
1411    #[test]
1412    fn the_clamp_keeps_the_padding_off_every_edge() {
1413        let content = Size::new(200.0, 50.0);
1414        // Trailing overflow lands `DEFAULT_PADDING` short of the area edge.
1415        let trailing = place(
1416            anchor_rect(380.0, 100.0, 20.0, 20.0),
1417            content,
1418            AREA,
1419            OverlayPlacement::default(),
1420        );
1421        assert_eq!(trailing.x1, AREA.x1 - DEFAULT_PADDING);
1422        // …and so does a leading one.
1423        let leading = place(
1424            anchor_rect(0.0, 100.0, 20.0, 20.0),
1425            content,
1426            AREA,
1427            OverlayPlacement::default(),
1428        );
1429        assert_eq!(leading.x0, AREA.x0 + DEFAULT_PADDING);
1430        // The bottom edge is the same rule on the other axis.
1431        let low = place(
1432            anchor_rect(100.0, 560.0, 20.0, 20.0),
1433            Size::new(100.0, 300.0),
1434            AREA,
1435            OverlayPlacement::default().flip(false),
1436        );
1437        assert_eq!(low.y1, AREA.y1 - DEFAULT_PADDING);
1438    }
1439
1440    #[test]
1441    fn a_padding_larger_than_the_area_falls_back_to_the_area_itself() {
1442        // A window narrower than twice the padding still has to place its
1443        // surface somewhere; the inset would invert, so it is dropped.
1444        let tiny = Rect::new(0.0, 0.0, 10.0, 10.0);
1445        let rect = place(
1446            Rect::ZERO,
1447            Size::new(4.0, 4.0),
1448            tiny,
1449            OverlayPlacement::default().flip(false),
1450        );
1451        assert!(tiny.contains(rect.origin()));
1452        assert_eq!(rect.x0, 0.0, "clamped to the un-inset area's own edge");
1453    }
1454
1455    #[test]
1456    fn a_degenerate_anchor_places_against_that_point() {
1457        let content = Size::new(80.0, 40.0);
1458        let rect = place(Rect::ZERO, content, AREA, OverlayPlacement::default());
1459        assert_eq!(
1460            rect.x0,
1461            AREA.x0 + DEFAULT_PADDING,
1462            "clamped in from the centred overhang"
1463        );
1464        // The neutral gap is smaller than the padding, so the same clamp holds
1465        // the surface off the top edge as well.
1466        assert_eq!(rect.y0, AREA.y0 + DEFAULT_PADDING);
1467    }
1468
1469    // -----------------------------------------------------------------------
1470    // The portal, driven through a real render root
1471    // -----------------------------------------------------------------------
1472
1473    /// Everything the fixture's widgets record, in the order it happened.
1474    type Log = Rc<RefCell<Vec<String>>>;
1475
1476    const WINDOW: Size = Size::new(400.0, 600.0);
1477
1478    /// Where the `Plain` fixture's surface lands: the top-left corner of the
1479    /// padded field, because a window-sized anchor fits its content on neither
1480    /// side and the clamp takes over.
1481    const POD: Rect = Rect::new(8.0, 8.0, 88.0, 38.0);
1482
1483    /// What the floated content does when it is pressed.
1484    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1485    enum Reaction {
1486        #[default]
1487        Nothing,
1488        Capture,
1489        Focus,
1490    }
1491
1492    /// Which tree the fixture builds.
1493    #[derive(Clone, Copy, PartialEq, Eq)]
1494    enum Shape {
1495        /// A window-filling portal child with a later, window-filling sibling.
1496        Plain,
1497        /// A small portal child partway down a scrollable column.
1498        Scrolled,
1499        /// The portal beside a focus-taking field, both inside one `Column`, so
1500        /// the container reconciling them is the ordinary
1501        /// [`route_event`](crate::authoring::route_event) one and the field is a
1502        /// SIBLING of the portal rather than its child — the arrangement in
1503        /// which a recorded focus link the container never swept still decides
1504        /// where a focus-routed event goes.
1505        Sibling,
1506    }
1507
1508    #[derive(Clone, Copy)]
1509    struct Cfg {
1510        open: bool,
1511        shape: Shape,
1512        band: OverlayBand,
1513        input: OverlayInput,
1514        /// `None` leaves the policy to `on_outside_tap`'s own default.
1515        outside_tap: Option<OutsideTap>,
1516        preserve_focus: bool,
1517        reaction: Reaction,
1518        placement: OverlayPlacement,
1519    }
1520
1521    /// The corner placement the `Plain` fixture pins its surface with — chosen
1522    /// so the rect is the same however the fixture is configured.
1523    fn corner() -> OverlayPlacement {
1524        OverlayPlacement::on(OverlaySide::Top)
1525            .offset(0.0)
1526            .align(OverlayAlign::Start)
1527    }
1528
1529    impl Default for Cfg {
1530        fn default() -> Self {
1531            Cfg {
1532                open: true,
1533                shape: Shape::Plain,
1534                band: OverlayBand::Floating,
1535                input: OverlayInput::Interactive,
1536                outside_tap: Some(OutsideTap::Ignore),
1537                preserve_focus: false,
1538                reaction: Reaction::Nothing,
1539                placement: corner(),
1540            }
1541        }
1542    }
1543
1544    struct App {
1545        cfg: Cfg,
1546        log: Log,
1547        presses: u32,
1548        outside_taps: u32,
1549    }
1550
1551    /// A recording leaf: paints one rect at its own absolute origin, records
1552    /// every event it receives (positions in its own local space) and reacts to
1553    /// a press the way the fixture asked it to.
1554    struct Probe {
1555        tag: &'static str,
1556        /// `None` fills whatever it is offered.
1557        size: Option<Size>,
1558        reaction: Reaction,
1559        /// Whether its pointer arms report `Handled` — a `false` leaf lets a
1560        /// press fall through to the child painted under it.
1561        handles: bool,
1562        log: Log,
1563    }
1564
1565    struct ProbeWidget {
1566        tag: &'static str,
1567        size: Option<Size>,
1568        reaction: Reaction,
1569        handles: bool,
1570        log: Log,
1571    }
1572
1573    impl View<App> for Probe {
1574        type Element = ProbeWidget;
1575        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
1576            ProbeWidget {
1577                tag: self.tag,
1578                size: self.size,
1579                reaction: self.reaction,
1580                handles: self.handles,
1581                log: Rc::clone(&self.log),
1582            }
1583        }
1584        fn rebuild(
1585            &self,
1586            _prev: &Self,
1587            element: &mut ProbeWidget,
1588            _ctx: &mut BuildCtx<'_>,
1589        ) -> ChangeFlags {
1590            element.size = self.size;
1591            element.reaction = self.reaction;
1592            element.handles = self.handles;
1593            element.log = Rc::clone(&self.log);
1594            ChangeFlags::NONE
1595        }
1596    }
1597
1598    impl Widget for ProbeWidget {
1599        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1600            bc.constrain(self.size.unwrap_or_else(|| bc.max()))
1601        }
1602        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1603            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1604        }
1605        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1606            match event {
1607                InputEvent::Pointer(p) => {
1608                    self.log.borrow_mut().push(format!(
1609                        "{}:{:?}@{},{}",
1610                        self.tag, p.phase, p.position.x, p.position.y
1611                    ));
1612                    if p.phase == PointerPhase::Down {
1613                        ctx.state_mut::<App>().presses += 1;
1614                        match self.reaction {
1615                            // A press this leaf only records: the commonest
1616                            // shape, and the one that must not disturb focus.
1617                            Reaction::Nothing => {}
1618                            Reaction::Capture => ctx.capture_pointer(),
1619                            Reaction::Focus => ctx.request_focus(),
1620                        }
1621                    }
1622                    if self.handles {
1623                        EventResult::Handled
1624                    } else {
1625                        EventResult::Ignored
1626                    }
1627                }
1628                InputEvent::Key(_) => {
1629                    self.log.borrow_mut().push(format!(
1630                        "{}:key focus={}",
1631                        self.tag,
1632                        ctx.has_focus()
1633                    ));
1634                    EventResult::Handled
1635                }
1636                _ => EventResult::Ignored,
1637            }
1638        }
1639    }
1640
1641    /// The fixture's whole view tree, rebuilt from the app state every frame so
1642    /// a test can reconfigure it between frames.
1643    fn logic(state: &mut App) -> StackView<App> {
1644        let cfg = state.cfg;
1645        let log = Rc::clone(&state.log);
1646        let pod = cfg.open.then(|| {
1647            any(Probe {
1648                tag: "pod",
1649                size: Some(Size::new(80.0, 30.0)),
1650                reaction: cfg.reaction,
1651                handles: true,
1652                log: Rc::clone(&log),
1653            })
1654        });
1655        // The child claims focus on a press, so it stands in for the focused
1656        // field a surface is opened over.
1657        let child = Probe {
1658            tag: "child",
1659            size: match cfg.shape {
1660                Shape::Plain => None,
1661                Shape::Scrolled => Some(Size::new(120.0, 40.0)),
1662                Shape::Sibling => Some(Size::new(400.0, 50.0)),
1663            },
1664            reaction: Reaction::Focus,
1665            handles: true,
1666            log: Rc::clone(&log),
1667        };
1668        let mut portal = overlay_portal(child)
1669            .overlay(pod)
1670            .placement(cfg.placement)
1671            .band(cfg.band)
1672            .input(cfg.input)
1673            .preserve_focus(cfg.preserve_focus)
1674            .on_outside_tap(|state: &mut App| state.outside_taps += 1);
1675        if let Some(policy) = cfg.outside_tap {
1676            portal = portal.outside_tap(policy);
1677        }
1678        match cfg.shape {
1679            Shape::Plain => stack()
1680                .child(portal)
1681                // Painted after the portal and covering it, so "the pod paints
1682                // above a later sibling" is a real question. It handles nothing,
1683                // so a press falls through to the portal's own child.
1684                .child(Probe {
1685                    tag: "sib",
1686                    size: None,
1687                    reaction: Reaction::Nothing,
1688                    handles: false,
1689                    log,
1690                }),
1691            Shape::Scrolled => stack().child(scroll_view(
1692                column()
1693                    .child(SizedBox(Some(400.0), Some(200.0)))
1694                    .child(portal)
1695                    .child(SizedBox(Some(400.0), Some(1000.0))),
1696            )),
1697            // The field first, the portal second: a focus-routed event walking
1698            // the children in order meets the field's link before the portal's.
1699            Shape::Sibling => stack().child(
1700                column()
1701                    .child(Probe {
1702                        tag: "field",
1703                        size: Some(Size::new(400.0, 50.0)),
1704                        reaction: Reaction::Focus,
1705                        handles: true,
1706                        log,
1707                    })
1708                    .child(portal),
1709            ),
1710        }
1711    }
1712
1713    struct Harness {
1714        root: RenderRoot<App, StackView<App>>,
1715        state: App,
1716        clock_ms: f64,
1717    }
1718
1719    impl Harness {
1720        fn new(cfg: Cfg) -> Self {
1721            Harness {
1722                root: RenderRoot::new(),
1723                state: App {
1724                    cfg,
1725                    log: Rc::new(RefCell::new(Vec::new())),
1726                    presses: 0,
1727                    outside_taps: 0,
1728                },
1729                clock_ms: 0.0,
1730            }
1731        }
1732
1733        /// One whole frame: rebuild, layout, paint — returning what was painted,
1734        /// in paint order.
1735        fn frame(&mut self) -> RecordingScene {
1736            let mut build: fn(&mut App) -> StackView<App> = logic;
1737            self.root.rebuild(&mut build, &mut self.state);
1738            self.root.layout(WINDOW);
1739            self.clock_ms += 16.0;
1740            let mut scene = RecordingScene::default();
1741            self.root.paint(
1742                &mut scene,
1743                FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
1744            );
1745            scene
1746        }
1747
1748        fn pointer(&mut self, phase: PointerPhase, x: f64, y: f64) {
1749            self.root.event(
1750                &mut self.state,
1751                &InputEvent::Pointer(PointerEvent {
1752                    phase,
1753                    position: Point::new(x, y),
1754                    button: PointerButton::Primary,
1755                }),
1756            );
1757        }
1758
1759        fn down(&mut self, x: f64, y: f64) {
1760            self.pointer(PointerPhase::Down, x, y);
1761        }
1762
1763        fn key(&mut self) {
1764            self.root.event(
1765                &mut self.state,
1766                &InputEvent::Key(KeyEvent {
1767                    key: Key::Named(NamedKey::ArrowLeft),
1768                    modifiers: Modifiers::default(),
1769                    repeat: false,
1770                }),
1771            );
1772        }
1773
1774        fn log(&self) -> Vec<String> {
1775            self.state.log.borrow().clone()
1776        }
1777
1778        fn clear_log(&mut self) {
1779            self.state.log.borrow_mut().clear();
1780        }
1781    }
1782
1783    /// The rects a scene recorded, so a paint-order assertion reads as geometry.
1784    fn rects(scene: &RecordingScene) -> Vec<Rect> {
1785        scene
1786            .rects
1787            .iter()
1788            .map(|(origin, size)| Rect::from_origin_size(*origin, *size))
1789            .collect()
1790    }
1791
1792    #[test]
1793    fn the_overlay_pod_paints_above_a_later_sibling() {
1794        let mut h = Harness::new(Cfg::default());
1795        let scene = h.frame();
1796        let painted = rects(&scene);
1797        assert_eq!(
1798            painted.last().copied(),
1799            Some(POD),
1800            "the floated pod paints after the whole main tree, not in place: {painted:?}"
1801        );
1802        // …and it really is covered in the main tree: the sibling painted over
1803        // the same pixels one step earlier.
1804        let sibling = painted[painted.len() - 2];
1805        assert_eq!(sibling, Rect::from_origin_size(Point::ZERO, WINDOW));
1806    }
1807
1808    #[test]
1809    fn a_press_inside_the_surface_reaches_the_pod_in_its_own_space() {
1810        let mut h = Harness::new(Cfg::default());
1811        h.frame();
1812        h.clear_log();
1813        h.down(40.0, 20.0);
1814        assert_eq!(
1815            h.log(),
1816            vec!["pod:Down@32,12".to_string()],
1817            "the press is delivered in pod space (window minus the placed origin)"
1818        );
1819        assert_eq!(
1820            h.state.presses, 1,
1821            "the pod's own callback ran on app state"
1822        );
1823    }
1824
1825    #[test]
1826    fn a_transparent_tooltip_surface_never_receives_input() {
1827        let mut h = Harness::new(Cfg {
1828            band: OverlayBand::Tooltip,
1829            input: OverlayInput::Transparent,
1830            ..Cfg::default()
1831        });
1832        let scene = h.frame();
1833        assert_eq!(
1834            rects(&scene).last().copied(),
1835            Some(POD),
1836            "a transparent surface is still painted above everything"
1837        );
1838        h.clear_log();
1839        h.down(40.0, 20.0);
1840        assert_eq!(
1841            h.log(),
1842            vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
1843            "the pointer passes through to the main tree, topmost sibling first"
1844        );
1845    }
1846
1847    #[test]
1848    fn a_drag_begun_on_the_surface_continues_into_it_through_the_capture() {
1849        // Deliberately the scrolled tree: the owner sits 200px down, so the
1850        // follow-ups — which arrive in the OWNER's local space, not the
1851        // window's — are wrong by exactly that much unless they are lifted.
1852        let mut h = Harness::new(Cfg {
1853            shape: Shape::Scrolled,
1854            placement: OverlayPlacement::default(),
1855            reaction: Reaction::Capture,
1856            ..Cfg::default()
1857        });
1858        h.frame();
1859        h.clear_log();
1860        h.down(30.0, 250.0);
1861        assert!(
1862            h.root.is_pointer_captured(),
1863            "a capture claimed from inside the surface is honoured"
1864        );
1865        // Far outside the placed rect, and outside the owner too: the capture,
1866        // not the hit test, is what routes these.
1867        h.pointer(PointerPhase::Move, 200.0, 400.0);
1868        h.pointer(PointerPhase::Up, 210.0, 410.0);
1869        assert_eq!(
1870            h.log(),
1871            vec![
1872                "pod:Down@10,6".to_string(),
1873                "pod:Move@180,156".to_string(),
1874                "pod:Up@190,166".to_string(),
1875            ]
1876        );
1877        assert!(!h.root.is_pointer_captured(), "the Up releases the capture");
1878        // The gesture is over: a further move outside the surface is nobody's.
1879        h.clear_log();
1880        h.pointer(PointerPhase::Move, 200.0, 400.0);
1881        assert!(
1882            !h.log().iter().any(|e| e.starts_with("pod:")),
1883            "a move after the release is no longer the surface's: {:?}",
1884            h.log()
1885        );
1886    }
1887
1888    /// The whole gesture, in the log shape the fixture records it in: pressed
1889    /// inside the placed rect, dragged well outside it, released there.
1890    fn one_gesture(h: &mut Harness, label: &str) {
1891        h.clear_log();
1892        h.down(40.0, 20.0);
1893        assert!(
1894            h.root.is_pointer_captured(),
1895            "{label}: the capture claimed inside the surface opened a gesture"
1896        );
1897        h.pointer(PointerPhase::Move, 300.0, 300.0);
1898        h.pointer(PointerPhase::Up, 300.0, 300.0);
1899        assert_eq!(
1900            h.log(),
1901            vec![
1902                "pod:Down@32,12".to_string(),
1903                "pod:Move@292,292".to_string(),
1904                "pod:Up@292,292".to_string(),
1905            ],
1906            "{label}: every phase of the gesture reached the surface"
1907        );
1908        assert!(
1909            !h.root.is_pointer_captured(),
1910            "{label}: the Up released the capture"
1911        );
1912    }
1913
1914    #[test]
1915    fn a_second_gesture_into_the_same_mounted_pod_routes_exactly_like_the_first() {
1916        let mut h = Harness::new(Cfg {
1917            reaction: Reaction::Capture,
1918            ..Cfg::default()
1919        });
1920        h.frame();
1921        // No frame between the two: the pod is the same widget instance, so the
1922        // second press meets whatever the first gesture left recorded on it.
1923        one_gesture(&mut h, "first gesture");
1924        one_gesture(&mut h, "second gesture");
1925    }
1926
1927    #[test]
1928    fn a_cancelled_gesture_leaves_the_surface_ready_for_the_next_one() {
1929        let mut h = Harness::new(Cfg {
1930            reaction: Reaction::Capture,
1931            ..Cfg::default()
1932        });
1933        h.frame();
1934        h.down(40.0, 20.0);
1935        h.pointer(PointerPhase::Cancel, 300.0, 300.0);
1936        assert!(
1937            !h.root.is_pointer_captured(),
1938            "a Cancel ends the gesture exactly as an Up does"
1939        );
1940        one_gesture(&mut h, "the gesture after a cancel");
1941    }
1942
1943    #[test]
1944    fn a_press_outside_notifies_the_owner_and_can_still_reach_the_main_tree() {
1945        let mut h = Harness::new(Cfg {
1946            outside_tap: Some(OutsideTap::Notify { consume: false }),
1947            ..Cfg::default()
1948        });
1949        h.frame();
1950        h.clear_log();
1951        h.down(200.0, 400.0);
1952        assert_eq!(h.state.outside_taps, 1, "the owner was told");
1953        assert_eq!(
1954            h.log(),
1955            vec![
1956                "sib:Down@200,400".to_string(),
1957                "child:Down@200,400".to_string()
1958            ],
1959            "a pass-through notification still lets the press through"
1960        );
1961    }
1962
1963    #[test]
1964    fn a_consuming_outside_press_is_swallowed() {
1965        let mut h = Harness::new(Cfg {
1966            outside_tap: Some(OutsideTap::Notify { consume: true }),
1967            ..Cfg::default()
1968        });
1969        h.frame();
1970        h.clear_log();
1971        h.down(200.0, 400.0);
1972        assert_eq!(h.state.outside_taps, 1);
1973        assert!(
1974            h.log().is_empty(),
1975            "the dismissing press never reached the main tree: {:?}",
1976            h.log()
1977        );
1978    }
1979
1980    #[test]
1981    fn an_outside_tap_callback_opts_into_the_notification_by_itself() {
1982        // No explicit policy: installing the callback is what asks to hear.
1983        let mut h = Harness::new(Cfg {
1984            outside_tap: None,
1985            ..Cfg::default()
1986        });
1987        h.frame();
1988        h.clear_log();
1989        h.down(200.0, 400.0);
1990        assert_eq!(h.state.outside_taps, 1);
1991        assert!(
1992            h.log().is_empty(),
1993            "the implied policy is the modal one (consuming): {:?}",
1994            h.log()
1995        );
1996    }
1997
1998    #[test]
1999    fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
2000        let mut h = Harness::new(Cfg::default());
2001        h.frame();
2002        h.clear_log();
2003        h.down(200.0, 400.0);
2004        assert_eq!(h.state.outside_taps, 0);
2005        assert_eq!(
2006            h.log(),
2007            vec![
2008                "sib:Down@200,400".to_string(),
2009                "child:Down@200,400".to_string()
2010            ]
2011        );
2012    }
2013
2014    #[test]
2015    fn a_surface_that_claims_focus_takes_it_from_the_child() {
2016        let mut h = Harness::new(Cfg {
2017            reaction: Reaction::Focus,
2018            preserve_focus: false,
2019            ..Cfg::default()
2020        });
2021        h.frame();
2022        // The child is focused first, exactly as a field is before its surface
2023        // opens over it.
2024        h.down(200.0, 400.0);
2025        assert!(h.root.is_focus_active());
2026        h.clear_log();
2027        h.key();
2028        assert_eq!(h.log(), vec!["child:key focus=true".to_string()]);
2029
2030        // A press inside the surface, which claims focus for itself.
2031        h.clear_log();
2032        h.down(40.0, 20.0);
2033        h.key();
2034        assert_eq!(
2035            h.log(),
2036            vec![
2037                "pod:Down@32,12".to_string(),
2038                "pod:key focus=true".to_string()
2039            ],
2040            "the keyboard follows the surface"
2041        );
2042        assert!(h.root.is_focus_active());
2043    }
2044
2045    #[test]
2046    fn preserve_focus_hands_the_session_back_to_the_child() {
2047        let mut h = Harness::new(Cfg {
2048            reaction: Reaction::Focus,
2049            preserve_focus: true,
2050            ..Cfg::default()
2051        });
2052        h.frame();
2053        h.down(200.0, 400.0);
2054        h.clear_log();
2055        h.down(40.0, 20.0);
2056        h.key();
2057        assert_eq!(
2058            h.log(),
2059            vec![
2060                "pod:Down@32,12".to_string(),
2061                "child:key focus=true".to_string()
2062            ],
2063            "the field that opened the surface keeps typing"
2064        );
2065        assert!(h.root.is_focus_active(), "and keeps its session");
2066    }
2067
2068    #[test]
2069    fn a_child_that_takes_focus_back_gets_the_keyboard_again() {
2070        let mut h = Harness::new(Cfg {
2071            reaction: Reaction::Focus,
2072            preserve_focus: false,
2073            ..Cfg::default()
2074        });
2075        h.frame();
2076        // The child is focused, the surface then takes the session…
2077        h.down(200.0, 400.0);
2078        h.down(40.0, 20.0);
2079        // …and the child is pressed again, which re-claims it.
2080        h.down(200.0, 400.0);
2081        h.clear_log();
2082        h.key();
2083        assert_eq!(
2084            h.log(),
2085            vec!["child:key focus=true".to_string()],
2086            "the surface's older link must not outrank the main tree's live one"
2087        );
2088    }
2089
2090    /// A focus link the container never swept must stop deciding where a
2091    /// focus-routed event goes.
2092    ///
2093    /// The field and the portal are siblings here, so the surface's claim
2094    /// arrives as a broadcast the container forwards to both — no hit test, and
2095    /// therefore no blur-on-outside-tap sweep to clear the field's link. Both
2096    /// children then read as focus-link holders, and a container that answers
2097    /// "which child holds focus" with the first one in child order delivers the
2098    /// keyboard to the field the user has left.
2099    #[test]
2100    fn a_stale_sibling_link_does_not_outrank_the_surface_that_took_the_session() {
2101        let mut h = Harness::new(Cfg {
2102            shape: Shape::Sibling,
2103            reaction: Reaction::Focus,
2104            placement: OverlayPlacement::on(OverlaySide::Bottom)
2105                .offset(0.0)
2106                .align(OverlayAlign::Start)
2107                .padding(0.0),
2108            ..Cfg::default()
2109        });
2110        h.frame();
2111
2112        // The field takes the session.
2113        h.down(200.0, 25.0);
2114        h.clear_log();
2115        h.key();
2116        assert_eq!(h.log(), vec!["field:key focus=true".to_string()]);
2117
2118        // A press inside the surface, which claims focus for itself. The
2119        // container sees only the broadcast, so it sweeps nothing.
2120        h.down(40.0, 110.0);
2121        h.clear_log();
2122        h.key();
2123        assert_eq!(
2124            h.log(),
2125            vec!["pod:key focus=true".to_string()],
2126            "the keyboard follows the branch that actually holds the session"
2127        );
2128    }
2129
2130    #[test]
2131    fn a_surface_that_claims_nothing_never_disturbs_the_focused_child() {
2132        let mut h = Harness::new(Cfg::default());
2133        h.frame();
2134        h.down(200.0, 400.0);
2135        h.clear_log();
2136        h.down(40.0, 20.0);
2137        h.key();
2138        assert_eq!(
2139            h.log(),
2140            vec![
2141                "pod:Down@32,12".to_string(),
2142                "child:key focus=true".to_string()
2143            ],
2144            "an overlay press is not a blur"
2145        );
2146    }
2147
2148    #[test]
2149    fn scrolling_an_ancestor_moves_the_surface_on_the_next_paint() {
2150        let mut h = Harness::new(Cfg {
2151            shape: Shape::Scrolled,
2152            placement: OverlayPlacement::default(),
2153            ..Cfg::default()
2154        });
2155        let before = rects(&h.frame());
2156        assert_eq!(
2157            before.last().copied(),
2158            Some(Rect::new(20.0, 244.0, 100.0, 274.0)),
2159            "placed under its anchor, 200px down the scrolled column"
2160        );
2161        h.root.event(
2162            &mut h.state,
2163            &InputEvent::Scroll {
2164                position: Point::new(10.0, 10.0),
2165                delta: ScrollDelta::Pixels(0.0, 50.0),
2166            },
2167        );
2168        let after = rects(&h.frame());
2169        assert_eq!(
2170            after.last().copied(),
2171            Some(Rect::new(20.0, 194.0, 100.0, 224.0)),
2172            "the anchor moved with the scroll, and so did the surface"
2173        );
2174        // …and routing followed it: the pod's own rect is where the press lands.
2175        h.clear_log();
2176        h.down(30.0, 200.0);
2177        assert_eq!(h.log(), vec!["pod:Down@10,6".to_string()]);
2178    }
2179
2180    #[test]
2181    fn clearing_the_overlay_removes_it_after_the_next_paint() {
2182        let mut h = Harness::new(Cfg::default());
2183        assert_eq!(rects(&h.frame()).last().copied(), Some(POD));
2184        h.state.cfg.open = false;
2185        let scene = h.frame();
2186        assert!(
2187            !rects(&scene).contains(&POD),
2188            "nothing floated is painted once the view is gone: {:?}",
2189            rects(&scene)
2190        );
2191        h.clear_log();
2192        h.down(40.0, 20.0);
2193        assert_eq!(
2194            h.log(),
2195            vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
2196            "and the press reaches the main tree again"
2197        );
2198    }
2199
2200    // -----------------------------------------------------------------------
2201    // A slot over a `()`-typed pod: the framework-built-surface shape
2202    // -----------------------------------------------------------------------
2203
2204    /// An owner with no children of its own that hosts one `()`-typed surface,
2205    /// builds the floated view itself (as a widget mounting a framework-built
2206    /// surface does) and drains whatever the surface dispatched.
2207    struct ToolbarHost {
2208        log: Log,
2209        reaction: Reaction,
2210    }
2211
2212    struct ToolbarHostWidget {
2213        slot: OverlaySlot<()>,
2214        /// The mounted view, kept so the next rebuild has something to
2215        /// reconcile against.
2216        view: Option<AnyView<()>>,
2217        log: Log,
2218        reaction: Reaction,
2219    }
2220
2221    impl ToolbarHost {
2222        // erasure: keep result stored in an AnyView slot and element
2223        fn pod(&self) -> AnyView<()> {
2224            any(UnitProbe {
2225                log: Rc::clone(&self.log),
2226                reaction: self.reaction,
2227            })
2228        }
2229    }
2230
2231    impl View<()> for ToolbarHost {
2232        type Element = ToolbarHostWidget;
2233        fn build(&self, ctx: &mut BuildCtx<'_>) -> ToolbarHostWidget {
2234            let mut slot = OverlaySlot::new();
2235            slot.set_placement(corner());
2236            let view = self.pod();
2237            slot.rebuild(None, Some(&view), ctx);
2238            ToolbarHostWidget {
2239                slot,
2240                view: Some(view),
2241                log: Rc::clone(&self.log),
2242                reaction: self.reaction,
2243            }
2244        }
2245        fn rebuild(
2246            &self,
2247            _prev: &Self,
2248            element: &mut ToolbarHostWidget,
2249            ctx: &mut BuildCtx<'_>,
2250        ) -> ChangeFlags {
2251            element.log = Rc::clone(&self.log);
2252            element.reaction = self.reaction;
2253            let view = self.pod();
2254            let flags = element
2255                .slot
2256                .rebuild(element.view.as_ref(), Some(&view), ctx);
2257            element.view = Some(view);
2258            flags
2259        }
2260    }
2261
2262    impl Widget for ToolbarHostWidget {
2263        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2264            self.slot.layout(ctx);
2265            bc.constrain(Size::new(100.0, 40.0))
2266        }
2267        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2268            let size = ctx.size();
2269            self.slot.paint(ctx, size);
2270        }
2271        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2272            let mut pod_state = ();
2273            let Some(result) = self.slot.event(ctx, event, &mut pod_state) else {
2274                return EventResult::Ignored;
2275            };
2276            // Drained in the same pass the surface dispatched them in — the
2277            // queue is pass-scoped and is not a mailbox.
2278            for command in ctx.take_edit_commands() {
2279                self.log.borrow_mut().push(format!("cmd:{command:?}"));
2280            }
2281            result
2282        }
2283    }
2284
2285    /// The `()`-typed floated content: it carries no application state at all,
2286    /// and speaks to its owner through the edit-command queue.
2287    struct UnitProbe {
2288        log: Log,
2289        reaction: Reaction,
2290    }
2291
2292    struct UnitProbeWidget {
2293        log: Log,
2294        reaction: Reaction,
2295    }
2296
2297    impl View<()> for UnitProbe {
2298        type Element = UnitProbeWidget;
2299        fn build(&self, _ctx: &mut BuildCtx<'_>) -> UnitProbeWidget {
2300            UnitProbeWidget {
2301                log: Rc::clone(&self.log),
2302                reaction: self.reaction,
2303            }
2304        }
2305        fn rebuild(
2306            &self,
2307            _prev: &Self,
2308            element: &mut UnitProbeWidget,
2309            _ctx: &mut BuildCtx<'_>,
2310        ) -> ChangeFlags {
2311            element.log = Rc::clone(&self.log);
2312            element.reaction = self.reaction;
2313            ChangeFlags::NONE
2314        }
2315    }
2316
2317    impl Widget for UnitProbeWidget {
2318        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2319            bc.constrain(Size::new(80.0, 30.0))
2320        }
2321        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2322        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2323            if let InputEvent::Pointer(p) = event {
2324                self.log.borrow_mut().push(format!(
2325                    "pod:{:?}@{},{}",
2326                    p.phase, p.position.x, p.position.y
2327                ));
2328                if p.phase == PointerPhase::Down {
2329                    match self.reaction {
2330                        Reaction::Nothing => {}
2331                        Reaction::Capture => ctx.capture_pointer(),
2332                        Reaction::Focus => ctx.request_focus(),
2333                    }
2334                    ctx.dispatch_edit_command(EditCommand::Copy);
2335                    ctx.request_redraw();
2336                }
2337            }
2338            EventResult::Handled
2339        }
2340    }
2341
2342    /// Drive the `()`-state host through a real root.
2343    fn unit_harness(reaction: Reaction) -> (RenderRoot<(), ToolbarHost>, Log) {
2344        let log: Log = Rc::new(RefCell::new(Vec::new()));
2345        let mut root: RenderRoot<(), ToolbarHost> = RenderRoot::new();
2346        let captured = Rc::clone(&log);
2347        let mut build = move |_: &mut ()| ToolbarHost {
2348            log: Rc::clone(&captured),
2349            reaction,
2350        };
2351        let mut state = ();
2352        root.rebuild(&mut build, &mut state);
2353        root.layout(WINDOW);
2354        root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
2355        (root, log)
2356    }
2357
2358    #[test]
2359    fn a_unit_typed_pod_is_hosted_and_its_edit_commands_reach_the_owner() {
2360        let (mut root, log) = unit_harness(Reaction::Nothing);
2361        let mut state = ();
2362        root.event(
2363            &mut state,
2364            &InputEvent::Pointer(PointerEvent {
2365                phase: PointerPhase::Down,
2366                position: Point::new(40.0, 50.0),
2367                button: PointerButton::Primary,
2368            }),
2369        );
2370        assert_eq!(
2371            log.borrow().clone(),
2372            vec!["pod:Down@32,10".to_string(), "cmd:Copy".to_string()],
2373            "the surface ran over its own `()` state and its command was drained"
2374        );
2375    }
2376
2377    #[test]
2378    fn a_focus_claim_from_a_substituted_pod_still_opens_a_session() {
2379        let (mut root, _log) = unit_harness(Reaction::Focus);
2380        let mut state = ();
2381        assert!(!root.is_focus_active());
2382        root.event(
2383            &mut state,
2384            &InputEvent::Pointer(PointerEvent {
2385                phase: PointerPhase::Down,
2386                position: Point::new(40.0, 50.0),
2387                button: PointerButton::Primary,
2388            }),
2389        );
2390        assert!(
2391            root.is_focus_active(),
2392            "the substituted context's focus claim is mirrored onto the owner"
2393        );
2394    }
2395
2396    /// One pointer event into the `()`-state host, which carries no application
2397    /// state to thread.
2398    fn unit_pointer(root: &mut RenderRoot<(), ToolbarHost>, phase: PointerPhase, x: f64, y: f64) {
2399        let mut state = ();
2400        root.event(
2401            &mut state,
2402            &InputEvent::Pointer(PointerEvent {
2403                phase,
2404                position: Point::new(x, y),
2405                button: PointerButton::Primary,
2406            }),
2407        );
2408    }
2409
2410    #[test]
2411    fn a_second_gesture_into_a_substituted_pod_routes_exactly_like_the_first() {
2412        let (mut root, log) = unit_harness(Reaction::Capture);
2413        // The substituted route has no second road to the owner: the mirror in
2414        // `forward` is the only thing that can open the root's gesture, so a
2415        // capture it fails to report strands the follow-ups entirely.
2416        for label in ["first gesture", "second gesture"] {
2417            log.borrow_mut().clear();
2418            unit_pointer(&mut root, PointerPhase::Down, 40.0, 50.0);
2419            assert!(
2420                root.is_pointer_captured(),
2421                "{label}: the substituted pod's capture was mirrored onto the owner"
2422            );
2423            unit_pointer(&mut root, PointerPhase::Move, 300.0, 300.0);
2424            unit_pointer(&mut root, PointerPhase::Up, 300.0, 300.0);
2425            assert_eq!(
2426                log.borrow().clone(),
2427                vec![
2428                    "pod:Down@32,10".to_string(),
2429                    "cmd:Copy".to_string(),
2430                    "pod:Move@292,260".to_string(),
2431                    "pod:Up@292,260".to_string(),
2432                ],
2433                "{label}: every phase of the gesture reached the surface"
2434            );
2435            assert!(
2436                !root.is_pointer_captured(),
2437                "{label}: the Up released the capture"
2438            );
2439        }
2440    }
2441
2442    #[test]
2443    fn a_slot_starts_closed_and_hands_its_outside_press_over_once() {
2444        let mut slot: OverlaySlot<()> = OverlaySlot::new();
2445        assert!(!slot.is_open());
2446        assert_eq!(slot.window_rect(), Rect::ZERO);
2447        assert!(!slot.take_outside_down());
2448        // Two slots never share an identity, which is the whole addressing rule.
2449        let other: OverlaySlot<()> = OverlaySlot::default();
2450        assert_ne!(slot.key(), other.key());
2451    }
2452
2453    // -----------------------------------------------------------------------
2454    // `OverlayAnchor::Window`: a pointer-following, absolute anchor
2455    // -----------------------------------------------------------------------
2456
2457    /// A `()`-typed pod that paints a rect at its own absolute origin, so a
2458    /// test can read the registered entry's placed rect back off the painted
2459    /// scene rather than reaching into state no owner exposes.
2460    struct GhostPod;
2461
2462    struct GhostPodWidget;
2463
2464    impl View<()> for GhostPod {
2465        type Element = GhostPodWidget;
2466        fn build(&self, _ctx: &mut BuildCtx<'_>) -> GhostPodWidget {
2467            GhostPodWidget
2468        }
2469        fn rebuild(
2470            &self,
2471            _prev: &Self,
2472            _element: &mut GhostPodWidget,
2473            _ctx: &mut BuildCtx<'_>,
2474        ) -> ChangeFlags {
2475            ChangeFlags::NONE
2476        }
2477    }
2478
2479    impl Widget for GhostPodWidget {
2480        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2481            bc.constrain(Size::new(80.0, 30.0))
2482        }
2483        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2484            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
2485        }
2486    }
2487
2488    /// A `()`-typed probe with a configurable tag, filling whatever room it is
2489    /// offered — the "widget beneath" a `Transparent` surface must still
2490    /// reach, wherever that surface is anchored.
2491    struct TaggedProbe {
2492        tag: &'static str,
2493        log: Log,
2494    }
2495
2496    struct TaggedProbeWidget {
2497        tag: &'static str,
2498        log: Log,
2499    }
2500
2501    impl View<()> for TaggedProbe {
2502        type Element = TaggedProbeWidget;
2503        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TaggedProbeWidget {
2504            TaggedProbeWidget {
2505                tag: self.tag,
2506                log: Rc::clone(&self.log),
2507            }
2508        }
2509        fn rebuild(
2510            &self,
2511            _prev: &Self,
2512            element: &mut TaggedProbeWidget,
2513            _ctx: &mut BuildCtx<'_>,
2514        ) -> ChangeFlags {
2515            element.log = Rc::clone(&self.log);
2516            ChangeFlags::NONE
2517        }
2518    }
2519
2520    impl Widget for TaggedProbeWidget {
2521        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2522            bc.constrain(bc.max())
2523        }
2524        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2525        fn event(&mut self, _ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2526            if let InputEvent::Pointer(p) = event {
2527                self.log.borrow_mut().push(format!(
2528                    "{}:{:?}@{},{}",
2529                    self.tag, p.phase, p.position.x, p.position.y
2530                ));
2531                EventResult::Handled
2532            } else {
2533                EventResult::Ignored
2534            }
2535        }
2536    }
2537
2538    /// An owner that fills the window and, on every pointer `Move`/`Down`,
2539    /// moves its own `Window`-anchored, `Transparent`, `Tooltip`-band surface
2540    /// to the event's position — the shape a full-window pointer-tracking
2541    /// layer takes while a drag ghost is live. Never handles the event itself,
2542    /// so whatever sits beneath it in the main tree still sees every press.
2543    struct GhostOwner {
2544        offset: Vec2,
2545    }
2546
2547    struct GhostOwnerWidget {
2548        slot: OverlaySlot<()>,
2549    }
2550
2551    impl View<()> for GhostOwner {
2552        type Element = GhostOwnerWidget;
2553        fn build(&self, ctx: &mut BuildCtx<'_>) -> GhostOwnerWidget {
2554            let mut slot = OverlaySlot::new();
2555            slot.set_band(OverlayBand::Tooltip);
2556            slot.set_input(OverlayInput::Transparent);
2557            slot.set_anchor(OverlayAnchor::Window {
2558                point: Point::ZERO,
2559                offset: self.offset,
2560            });
2561            let view: AnyView<()> = any(GhostPod);
2562            slot.rebuild(None, Some(&view), ctx);
2563            GhostOwnerWidget { slot }
2564        }
2565        fn rebuild(
2566            &self,
2567            _prev: &Self,
2568            _element: &mut GhostOwnerWidget,
2569            _ctx: &mut BuildCtx<'_>,
2570        ) -> ChangeFlags {
2571            ChangeFlags::NONE
2572        }
2573    }
2574
2575    impl Widget for GhostOwnerWidget {
2576        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2577            self.slot.layout(ctx);
2578            bc.constrain(bc.max())
2579        }
2580        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2581            let size = ctx.size();
2582            self.slot.paint(ctx, size);
2583        }
2584        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2585            if let InputEvent::Pointer(p) = event
2586                && matches!(p.phase, PointerPhase::Move | PointerPhase::Down)
2587            {
2588                self.slot.set_window_anchor(ctx, p.position);
2589            }
2590            EventResult::Ignored
2591        }
2592    }
2593
2594    /// A plain `Floating`, `Owner`-anchored, `Interactive` surface — the
2595    /// counterpart `a_window_anchored_tooltip_still_paints_above_a_floating_surface`
2596    /// checks the band sort against.
2597    struct FloatingOwner;
2598
2599    struct FloatingOwnerWidget {
2600        slot: OverlaySlot<()>,
2601    }
2602
2603    impl View<()> for FloatingOwner {
2604        type Element = FloatingOwnerWidget;
2605        fn build(&self, ctx: &mut BuildCtx<'_>) -> FloatingOwnerWidget {
2606            let mut slot = OverlaySlot::new();
2607            slot.set_placement(corner());
2608            let view: AnyView<()> = any(GhostPod);
2609            slot.rebuild(None, Some(&view), ctx);
2610            FloatingOwnerWidget { slot }
2611        }
2612        fn rebuild(
2613            &self,
2614            _prev: &Self,
2615            _element: &mut FloatingOwnerWidget,
2616            _ctx: &mut BuildCtx<'_>,
2617        ) -> ChangeFlags {
2618            ChangeFlags::NONE
2619        }
2620    }
2621
2622    impl Widget for FloatingOwnerWidget {
2623        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2624            self.slot.layout(ctx);
2625            bc.constrain(Size::new(100.0, 40.0))
2626        }
2627        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2628            let size = ctx.size();
2629            self.slot.paint(ctx, size);
2630        }
2631    }
2632
2633    /// Drive a window-filling [`GhostOwner`] over a [`TaggedProbe`] floor
2634    /// through a real root.
2635    fn ghost_harness(offset: Vec2) -> (RenderRoot<(), StackView<()>>, Log) {
2636        let beneath_log: Log = Rc::new(RefCell::new(Vec::new()));
2637        let mut root: RenderRoot<(), StackView<()>> = RenderRoot::new();
2638        let log = Rc::clone(&beneath_log);
2639        let mut build = move |_: &mut ()| {
2640            stack()
2641                .child(TaggedProbe {
2642                    tag: "beneath",
2643                    log: Rc::clone(&log),
2644                })
2645                .child(GhostOwner { offset })
2646        };
2647        let mut state = ();
2648        root.rebuild(&mut build, &mut state);
2649        root.layout(WINDOW);
2650        root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
2651        (root, beneath_log)
2652    }
2653
2654    #[test]
2655    fn a_window_anchor_paints_at_point_plus_offset() {
2656        let offset = Vec2::new(5.0, 7.0);
2657        let (mut root, _log) = ghost_harness(offset);
2658        let mut scene = RecordingScene::default();
2659        root.paint(&mut scene, FrameTime::from_nanos(16_000_000));
2660        assert_eq!(
2661            rects(&scene).last().copied(),
2662            Some(Rect::from_origin_size(
2663                Point::ZERO + offset,
2664                Size::new(80.0, 30.0)
2665            )),
2666            "a Window anchor's rect is point + offset, with no side/align geometry"
2667        );
2668    }
2669
2670    #[test]
2671    fn moving_a_window_anchor_moves_the_paint_rect_on_the_next_paint() {
2672        let offset = Vec2::new(5.0, 7.0);
2673        let (mut root, _log) = ghost_harness(offset);
2674        let mut state = ();
2675        root.event(
2676            &mut state,
2677            &InputEvent::Pointer(PointerEvent {
2678                phase: PointerPhase::Down,
2679                position: Point::new(100.0, 200.0),
2680                button: PointerButton::Primary,
2681            }),
2682        );
2683        let mut scene = RecordingScene::default();
2684        root.paint(&mut scene, FrameTime::from_nanos(16_000_000));
2685        assert_eq!(
2686            rects(&scene).last().copied(),
2687            Some(Rect::from_origin_size(
2688                Point::new(100.0, 200.0) + offset,
2689                Size::new(80.0, 30.0)
2690            )),
2691            "the event handler's set_window_anchor call moved the surface to \
2692             the pointer, picked up on the following paint"
2693        );
2694
2695        root.event(
2696            &mut state,
2697            &InputEvent::Pointer(PointerEvent {
2698                phase: PointerPhase::Move,
2699                position: Point::new(250.0, 40.0),
2700                button: PointerButton::Primary,
2701            }),
2702        );
2703        let mut scene = RecordingScene::default();
2704        root.paint(&mut scene, FrameTime::from_nanos(32_000_000));
2705        assert_eq!(
2706            rects(&scene).last().copied(),
2707            Some(Rect::from_origin_size(
2708                Point::new(250.0, 40.0) + offset,
2709                Size::new(80.0, 30.0)
2710            )),
2711            "a further Move keeps tracking the pointer"
2712        );
2713    }
2714
2715    #[test]
2716    fn a_window_anchored_transparent_surface_still_lets_a_press_through() {
2717        let (mut root, log) = ghost_harness(Vec2::new(5.0, 7.0));
2718        let mut state = ();
2719        root.event(
2720            &mut state,
2721            &InputEvent::Pointer(PointerEvent {
2722                phase: PointerPhase::Down,
2723                position: Point::new(100.0, 200.0),
2724                button: PointerButton::Primary,
2725            }),
2726        );
2727        assert_eq!(
2728            log.borrow().clone(),
2729            vec!["beneath:Down@100,200".to_string()],
2730            "`Transparent` never hit-tests, Window anchor or not — the press \
2731             reaches the widget the ghost is floating over"
2732        );
2733    }
2734
2735    #[test]
2736    fn a_window_anchored_tooltip_still_paints_above_a_floating_surface() {
2737        let offset = Vec2::new(5.0, 7.0);
2738        let mut root: RenderRoot<(), StackView<()>> = RenderRoot::new();
2739        // Registered Tooltip-first (`GhostOwner`), Floating-second
2740        // (`FloatingOwner`): exactly the order the band sort has to survive,
2741        // not merely happen to preserve.
2742        let mut build = move |_: &mut ()| stack().child(GhostOwner { offset }).child(FloatingOwner);
2743        let mut state = ();
2744        root.rebuild(&mut build, &mut state);
2745        root.layout(WINDOW);
2746        let mut scene = RecordingScene::default();
2747        root.paint(&mut scene, FrameTime::from_nanos(0));
2748        assert_eq!(
2749            rects(&scene),
2750            vec![
2751                Rect::new(8.0, 40.0, 88.0, 70.0),
2752                Rect::from_origin_size(Point::ZERO + offset, Size::new(80.0, 30.0)),
2753            ],
2754            "Floating still paints below Tooltip despite registering after it: \
2755             {:?}",
2756            rects(&scene)
2757        );
2758    }
2759}