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