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