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