Skip to main content

frust_widgets/drag/
draggable.rs

1//! [`draggable()`]: the drag *source* — a transparent wrapper that turns a press
2//! on its child into a [`DragCoordinator`] session, floats a ghost that follows
3//! the pointer, and marks the source while the drag is in flight.
4//!
5//! # Initiation
6//!
7//! A primary `Down` **arms** the coordinator ([`DragCoordinator::arm`]) and
8//! captures the pointer; the press then becomes a drag one of two ways, chosen
9//! per press by [`DragPolicy`]:
10//!
11//! * **distance** ([`DragPolicy::Immediate`]) — the press travels further than
12//!   the threshold ([`DraggableView::threshold`], default [`DRAG_THRESHOLD`]);
13//! * **hold** ([`DragPolicy::LongPress`]) — the press stays inside
14//!   [`TOUCH_SLOP`] for the long-press duration
15//!   ([`DraggableView::hold_threshold_ms`], default the same 500 ms every
16//!   `GestureDetector` holds for). Moving past the slop first gives the
17//!   gesture up — it was a scroll or a swipe, not a drag — and an enclosing
18//!   scroll surface is free to take it over.
19//!
20//! [`DragPolicy::Auto`] (the default) picks by the device the press came from,
21//! read off [`EventCtx::pointer_id`] at the `Down`: a
22//! [`PointerSource::Mouse`] press (a bare `InputEvent::Pointer`, which is what
23//! the desktop shells report for the mouse, the trackpad and a pen) uses the
24//! distance threshold, and a [`PointerSource::Touch`] press (an
25//! `InputEvent::PointerContact`, which is what the Android, iOS and web shells
26//! report for a finger) the long-press — a finger dragging a list item would
27//! otherwise steal every scroll.
28//!
29//! The hold is timed on the paint clock by the same timer the gesture detector
30//! uses (`crate::gesture`'s `HoldTracker`), so it fires the way a long-press
31//! does: the paint that sees the threshold crossed raises the pending-result
32//! flush, and the `Housekeeping` broadcast the next rebuild dispatches carries
33//! the `EventCtx` that starts the drag — or a live pointer event does, if one
34//! arrives first. See `crate::gesture`'s *Long-press firing semantics* for the
35//! full race.
36//!
37//! Until the drag begins the child sees the whole gesture, so a press that never
38//! becomes a drag — a click, a tap, a short touch — still reaches it exactly as
39//! it would without the wrapper. When the drag begins the child is handed a
40//! `Cancel` and hears nothing more of the gesture.
41//!
42//! # The session
43//!
44//! On begin the payload builder runs against the application state and the
45//! typed payload is handed to [`DragCoordinator::begin`]. The pointer capture
46//! taken on the arming `Down` — the only phase the root mirrors a capture on —
47//! keeps every `Move` and the final `Up`/`Cancel` coming here wherever the
48//! pointer goes; each `Move` is lifted into window space and reported with
49//! [`DragCoordinator::update_pointer`], which also resolves the drop target
50//! under it (see the [module docs](super#target-resolution)). An `Up` reports
51//! the release point the same way and calls [`DragCoordinator::drop`] — a
52//! release outside every target is therefore a cancel — and a `Cancel` calls
53//! [`DragCoordinator::cancel`]; both end in [`DraggableWidget`]'s single
54//! teardown (`on_session_end`), and the root releases the capture on that same
55//! physical `Up`/`Cancel`. A session ended from elsewhere (cancelled, or
56//! superseded by an OS file drag) is noticed on the next event or rebuild and
57//! torn down the same way, the rest of the gesture swallowed.
58//!
59//! # Keyboard
60//!
61//! `Escape` ([`NamedKey::Escape`]) while this source drags — by pointer or by
62//! keyboard — cancels the session ([`DragCoordinator::cancel`]) and runs the
63//! same teardown; for a pointer drag the pointer is still down, so the rest of
64//! the gesture — its `Move`s and its `Up` — is swallowed, never reaching the
65//! child as a stray release. For a keyboard drag (below) there is no pending
66//! gesture to swallow, so the widget lands back on its idle state at once,
67//! ready for another lift.
68//!
69//! A keyboard session can also end from outside this source entirely — a
70//! target completing the drop on its own click, a cancel raised elsewhere —
71//! noticed on the next event or rebuild rather than through `Escape` or this
72//! source's own drop chord. With no pointer gesture pending either way, that
73//! also lands the widget back on idle at once (never the pointer case's
74//! swallowing wait for a stray `Up`/`Cancel`), the focus claim left exactly
75//! as it is — still focused, ready for another lift.
76//!
77//! Key events are focus-routed, never routed to a pointer captor, so the source
78//! holds the keyboard focus for the length of each press: the arming `Down`
79//! requests focus ([`EventCtx::request_focus`]) unless the child took it on
80//! that same `Down` (a field inside the source keeps its own focus, and Escape
81//! still passes through this wrapper on its way down the focus chain). A `Down`
82//! is the only phase on which the root honours a focus claim — a drag begins on
83//! a `Move` or a housekeeping broadcast, too late to take it then. Whatever
84//! held focus elsewhere before the press is not displaced by this: a `Down`
85//! that claims nothing blurs the tree anyway. The claim is released
86//! ([`EventCtx::release_focus`]) when a drag ends — by drop, cancel or Escape —
87//! restoring the unfocused state the press would otherwise have left; **a
88//! plain tap that never became a drag instead keeps the claim**, the one
89//! deliberate exception, so a mouse/touch user can click an item and then
90//! reach for the keyboard to lift it — the same "click focuses, Space/Enter
91//! activates" contract an HTML control honours. An `Up`/`Cancel` release
92//! clears the recorded focus path at once, but the root's own focus flag is
93//! reconciled only by its next `Down` (the root adjusts its focus bookkeeping
94//! on `Down` and keyboard passes, not on a release).
95//!
96//! While a pointer drags, every enclosing scroll surface's live takeover veto
97//! (the one a pinch raises, see `crate::scroll`'s *Multi-contact veto*) is
98//! held raised — not only the nearest, since each surface checks its own
99//! cell at its own slop — so a drag inside a `ScrollView`/`ListView`, however
100//! deeply nested, is not stolen once the finger travels past any enclosing
101//! scroll's own slop. A keyboard drag has no
102//! finger to steal from, so this does not apply to it.
103//!
104//! ## Lift, cycle, drop
105//!
106//! While this source holds its own focus (not a descendant's —
107//! [`ChildPod::holds_live_focus`] on the child reads `false`) and no gesture
108//! is in flight, `Enter` or Space (`Key::Character(" ")`, there being no
109//! [`NamedKey`] for it) **lifts**: [`DragCoordinator::lift`] then
110//! [`DragCoordinator::begin`] with the same payload a pointer press would
111//! hand over, the ghost anchored at this source's own window-space bounds
112//! (there is no pointer to anchor it to yet). Once lifted, `ArrowRight`/
113//! `ArrowDown` and `ArrowLeft`/`ArrowUp` cycle the hovered target forward and
114//! backward through [`DragCoordinator::move_to_next_target`]/
115//! [`DragCoordinator::move_to_previous_target`], and the ghost's
116//! [`OverlayAnchor::Window`] point follows: every `Enter` the cycle raises
117//! repositions it to the newly hovered target's own bounds
118//! ([`DragCoordinator::target_bounds`]) rather than a pointer position that
119//! does not exist. `Enter`/Space while lifted **drops**
120//! ([`DragCoordinator::drop`]) on whatever is currently hovered, exactly like
121//! a pointer's `Up` — released with nothing hovered, a cancel, like the
122//! pointer case. `Escape` cancels from either phase (above). A primary
123//! `Down` while lifted answers the same way — see *Semantics*, below, for
124//! why a real press on the lifted item (and the accessibility seam's
125//! synthesized one) reads as the same drop-or-cancel chord rather than a
126//! stray press.
127//!
128//! A key this source does not otherwise react to — including every key while
129//! its own focus claim is the child's instead — falls through to the child
130//! untouched, the same pass-through every other phase gives it.
131//!
132//! # Ghost and source feedback
133//!
134//! The ghost floats through an [`OverlaySlot`] — band `Tooltip`, input
135//! `Transparent` (it never takes a hit, so what is under the pointer stays
136//! reachable), anchored with [`OverlayAnchor::Window`] at the pointer plus the
137//! press point's offset into the child, so the grabbed point stays under the
138//! finger. Its content is [`DraggableView::ghost`]'s view built from the state
139//! at begin, or by default a **snapshot of the child itself**: the child's
140//! retained subtree painted a second time, at the ghost's position and the
141//! child's size. Either is painted under a [`GHOST_OPACITY`] layer
142//! ([`DraggableView::ghost_opacity`]). The pod is mounted by the rebuild that
143//! follows begin (begin raises the pending-result flush, so that rebuild runs
144//! in the same frame) and torn down by the one after the session ends; it stops
145//! being registered — and so painted — the moment the session ends.
146//!
147//! [`SourceFeedback`] decides what the child looks like in place meanwhile.
148//!
149//! # Semantics
150//!
151//! [`Widget::semantics`] pushes a `Role::Button`-equivalent node — accesskit
152//! names no drag-specific role — wrapping the child as its one accesskit
153//! child. The label names a noun this source *is*, never a verb `Click`
154//! might not perform: `"Draggable"` while idle, with a short description
155//! (`"Press Enter to lift"`) naming the keyboard chord; `"Dragging"` while a
156//! *pointer* session is in flight; `"Drop"` while this source's own
157//! *keyboard* session is live — the one state in which `Click` really does
158//! drop it. The richer surface the lift/cycle/drop verbs above would ideally
159//! advertise — `accesskit::Action::CustomAction` for `lift`/`drop`/`cancel`
160//! individually — is not wired to anything: `frust-core`'s
161//! `perform_accessibility_action` (the one seam a shell's `ActionRequest`
162//! reaches a widget through) matches only `Action::Click`/`Action::Focus`,
163//! dropping every other action and the `ActionRequest::data` a custom
164//! action's id would ride in (the same gap `TextInput`'s own
165//! advertised-but-uninvocable custom actions document — see
166//! `docs/CODE_STANDARDS.md`'s *Text Selection and the Clipboard*). `Click`
167//! reaches this widget as a synthesized `Down`+`Up` pair at the node's own
168//! bounds center, routed through the normal [`event`] path exactly like a
169//! real press, with no marker distinguishing it from one — [`PointerEvent`]
170//! and [`EventCtx`] carry no synthetic-origin field, and none is needed: on
171//! an idle source the synthesized press-and-release is simply too short to
172//! cross the drag threshold — a tap — which reaches the child and (per
173//! *Keyboard*, above) leaves the source focused, same as any other plain
174//! tap. Arriving while this source drags its own *keyboard* session, a
175//! `Down` answers exactly the chord Enter/Space would
176//! ([`keyboard_drop`](DraggableWidget::keyboard_drop)) — drop onto whatever
177//! [`move_to_next_target`](DragCoordinator::move_to_next_target)/
178//! [`move_to_previous_target`](DragCoordinator::move_to_previous_target) left
179//! hovered, or cancel when nothing is — an honest reading for a real mouse
180//! press too (pressing the lifted item puts it down), so no synthetic marker
181//! is needed there either; the matching `Up` is swallowed rather than
182//! reaching the child as a stray tap. Arriving during a *pointer* session
183//! instead, it is indistinguishable from an unrelated stray press and so is
184//! handled exactly the same way every other press interrupts one: it cancels
185//! the session rather than committing a drop. The keyboard chord —
186//! `Enter`/Space to lift and drop, `ArrowRight`/`ArrowDown`/`ArrowLeft`/
187//! `ArrowUp` to cycle the hovered target, `Escape` to cancel — is the
188//! assistive-technology path through the whole lift-cycle-drop walk; `Click`
189//! reuses its drop/cancel half rather than duplicating it.
190//!
191//! # Limits
192//!
193//! * Window-space positions are the child's paint origin plus an owner-local
194//!   pointer position, so a source under a transformed ancestor (a `pan_zoom`)
195//!   reports — and floats its ghost at — an untransformed position, like any
196//!   other [`OverlayAnchor`] consumer.
197//! * The default snapshot paints the child's subtree twice a frame unless
198//!   [`SourceFeedback::Placeholder`] leaves its rest position empty. Paint-time
199//!   side effects a widget inside the child keeps (a nested floated surface, a
200//!   published platform view, a recorded window position) see that second
201//!   paint too; a child carrying any of those should name a
202//!   [`ghost`](DraggableView::ghost) of its own.
203//! * Nested draggables are unsupported: both arm on the same `Down` and the
204//!   outer arm cancels the inner one.
205//! * Every press takes keyboard focus for its length (see *Keyboard*), and a
206//!   plain tap that never became a drag keeps it afterward rather than
207//!   releasing it.
208
209use std::cell::{Cell, RefCell};
210use std::rc::Rc;
211
212use frust_core::accesskit::{Action, Role};
213use frust_core::event::PointerSource;
214use frust_core::{
215    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, CursorIcon, EventCtx, EventResult,
216    InputEvent, Key, KeyEvent, LayoutCtx, NamedKey, OverlayBand, OverlayInput, PaintCtx,
217    PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, TOUCH_SLOP, View, Widget,
218    any,
219};
220use frust_theme::Theme;
221use kurbo::{Point, Size, Vec2};
222use peniko::Color;
223
224use super::coordinator::{DragCoordinator, DragPhase, DragSourceId, DragState};
225use crate::authoring::presses;
226use crate::gesture::{HoldTracker, LONG_PRESS_MS};
227use crate::overlay::{OverlayAnchor, OverlaySlot};
228use crate::scroll::ambient_scroll_vetoes;
229
230/// How far, in logical px, a press under the distance policy travels before
231/// it becomes a drag ([`DraggableView::threshold`] overrides it).
232///
233/// **Community-approximate**: larger than [`frust_core::MOUSE_SLOP`] (3 px —
234/// a click with a little hand tremor must stay a click, since beginning a drag
235/// cancels the child's press) and inside the 4–8 px band desktop toolkits ship
236/// as their drag threshold (Windows' `SM_CXDRAG` defaults to 4, GTK's
237/// `gtk-dnd-drag-threshold` to 8). Distinct from [`TOUCH_SLOP`], which the
238/// long-press policy uses as its give-up distance instead.
239pub const DRAG_THRESHOLD: f64 = 6.0;
240
241/// The default opacity a ghost is painted at
242/// ([`DraggableView::ghost_opacity`] overrides it) — translucent enough that
243/// what it hovers over stays legible.
244pub const GHOST_OPACITY: f32 = 0.85;
245
246/// The opacity [`SourceFeedback::Dim`] paints the source at in place. No theme
247/// token carries a "dragged-away" opacity; this is the disabled-content opacity
248/// the baseline button uses for the same "present but not interactive" read.
249const DIM_OPACITY: f32 = 0.38;
250
251/// [`SourceFeedback::Placeholder`]'s fill with no theme threaded; themed, it
252/// fills with the scheme's `surface_container_highest`.
253const PLACEHOLDER_FILL: Color = Color::from_rgba8(0x00, 0x00, 0x00, 0x14);
254
255/// [`SourceFeedback::Placeholder`]'s corner radius with no theme threaded;
256/// themed, it uses the shape scale's `small` radius.
257const PLACEHOLDER_RADIUS: f64 = 8.0;
258
259/// How a press on a [`draggable()`] becomes a drag. See the [module
260/// docs](self#initiation).
261#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
262pub enum DragPolicy {
263    /// Begin once the press travels past the threshold
264    /// ([`DraggableView::threshold`]) — the desktop pointer behaviour.
265    Immediate,
266    /// Begin once the press is held still (inside [`TOUCH_SLOP`]) for the
267    /// hold duration ([`DraggableView::hold_threshold_ms`]); moving past the
268    /// slop first gives the gesture up — the touch behaviour.
269    LongPress,
270    /// [`Immediate`](Self::Immediate) for a mouse press,
271    /// [`LongPress`](Self::LongPress) for a touch press, decided per press from
272    /// [`EventCtx::pointer_id`]'s source.
273    #[default]
274    Auto,
275}
276
277/// What the source looks like in place while its drag is in flight.
278#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
279pub enum SourceFeedback {
280    /// Paint the child dimmed, so the item reads as "being moved".
281    #[default]
282    Dim,
283    /// Paint an empty, themed slot of the child's size instead of the child —
284    /// the gap the item will leave.
285    Placeholder,
286    /// Leave the child exactly as it was.
287    None,
288}
289
290/// Builds the ghost's content from the application state when a drag begins.
291type GhostBuilder<State> = Rc<dyn Fn(&State) -> AnyView<State>>;
292
293/// A declarative drag source. See the [module docs](self).
294pub struct DraggableView<State: 'static, T: 'static> {
295    child: AnyView<State>,
296    coordinator: DragCoordinator,
297    payload: Rc<dyn Fn(&State) -> T>,
298    policy: DragPolicy,
299    threshold: f64,
300    hold_threshold_ms: Option<u64>,
301    ghost: Option<GhostBuilder<State>>,
302    ghost_opacity: f32,
303    feedback: SourceFeedback,
304}
305
306/// Make `child` a drag source on `coordinator`: a press that becomes a drag
307/// (see [`DragPolicy`]) begins a session carrying `payload(state)`.
308///
309/// The child keeps every press that does not become a drag. The ghost defaults
310/// to a translucent snapshot of the child and the source dims in place; see
311/// [`DraggableView`]'s builders.
312pub fn draggable<State, V, T, F>(
313    child: V,
314    coordinator: DragCoordinator,
315    payload: F,
316) -> DraggableView<State, T>
317where
318    State: 'static,
319    V: View<State>,
320    T: 'static,
321    F: Fn(&State) -> T + 'static,
322{
323    DraggableView {
324        child: any(child),
325        coordinator,
326        payload: Rc::new(payload),
327        policy: DragPolicy::Auto,
328        threshold: DRAG_THRESHOLD,
329        hold_threshold_ms: None,
330        ghost: None,
331        ghost_opacity: GHOST_OPACITY,
332        feedback: SourceFeedback::Dim,
333    }
334}
335
336impl<State: 'static, T: 'static> DraggableView<State, T> {
337    /// How a press becomes a drag (default [`DragPolicy::Auto`]).
338    pub fn policy(mut self, policy: DragPolicy) -> Self {
339        self.policy = policy;
340        self
341    }
342
343    /// The distance, in logical px, a press travels before it becomes a drag
344    /// under the distance policy (default [`DRAG_THRESHOLD`]). Negative values
345    /// clamp to zero.
346    pub fn threshold(mut self, px: f64) -> Self {
347        self.threshold = px.max(0.0);
348        self
349    }
350
351    /// How long, in ms, a press is held before it becomes a drag under the
352    /// long-press policy (default the gesture detector's long-press duration,
353    /// 500 ms). Floored at 1 ms.
354    pub fn hold_threshold_ms(mut self, ms: u64) -> Self {
355        self.hold_threshold_ms = Some(ms);
356        self
357    }
358
359    /// Build the ghost from the application state when the drag begins,
360    /// instead of snapshotting the child. Sized by its own content, laid out
361    /// loosely against the window, and painted at
362    /// [`ghost_opacity`](Self::ghost_opacity). `ghost` may return any
363    /// [`View`]; it is erased here.
364    ///
365    /// ```
366    /// use frust_widgets::{DragCoordinator, DraggableView, draggable, text};
367    /// # fn demo(coordinator: DragCoordinator) -> DraggableView<(), u32> {
368    /// draggable(text("card"), coordinator, |_: &()| 7u32).ghost(|_: &()| text("moving"))
369    /// # }
370    /// # let _ = demo;
371    /// ```
372    pub fn ghost<F, V>(mut self, ghost: F) -> Self
373    where
374        F: Fn(&State) -> V + 'static,
375        V: View<State>,
376    {
377        self.ghost = Some(Rc::new(move |state: &State| any(ghost(state))));
378        self
379    }
380
381    /// The opacity the ghost is painted at (default [`GHOST_OPACITY`]),
382    /// clamped to `0.0..=1.0`.
383    pub fn ghost_opacity(mut self, opacity: f32) -> Self {
384        self.ghost_opacity = opacity.clamp(0.0, 1.0);
385        self
386    }
387
388    /// What the source looks like in place while it is dragged (default
389    /// [`SourceFeedback::Dim`]).
390    pub fn while_dragging(mut self, feedback: SourceFeedback) -> Self {
391        self.feedback = feedback;
392        self
393    }
394
395    fn resolved_hold_ms(&self) -> f64 {
396        self.hold_threshold_ms
397            .map(|ms| ms.max(1) as f64)
398            .unwrap_or(LONG_PRESS_MS)
399    }
400}
401
402/// Which way the current press becomes a drag — [`DragPolicy`] resolved
403/// against the pressing device.
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405enum Initiation {
406    Distance,
407    Hold,
408}
409
410/// The source's gesture state machine.
411#[derive(Clone, Copy, Debug, PartialEq)]
412enum Gesture {
413    /// No press in flight.
414    Idle,
415    /// A primary press armed the coordinator and may still become a drag.
416    /// `down`/`last` are owner-local; `elapsed` flips on the paint that sees a
417    /// hold cross its threshold (hold initiation only).
418    Pressed {
419        down: Point,
420        last: Point,
421        initiation: Initiation,
422        elapsed: bool,
423    },
424    /// The press is the child's: it never armed, or gave up (moved past the
425    /// slop before a hold, or lost its arm). The rest of the gesture is routed
426    /// to the child as if the wrapper were not there.
427    Declined,
428    /// This source's session is in flight.
429    Dragging,
430    /// A *pointer* session ended out from under the gesture (cancelled or
431    /// superseded elsewhere); the rest of the still-pressed gesture is
432    /// swallowed. A *keyboard* session ended the same way has no pressed
433    /// gesture to wait for, so it returns straight to [`Idle`](Self::Idle)
434    /// instead — see [`end_elsewhere`](DraggableWidget::end_elsewhere).
435    Ended,
436}
437
438/// The ghost pod's lifecycle across event and rebuild passes: begin (an event)
439/// stages the view, the following rebuild mounts it, and the rebuild after the
440/// session ends tears it down.
441struct Ghost<State: 'static> {
442    /// Built at begin, mounted by the next rebuild.
443    pending: Option<AnyView<State>>,
444    /// The view currently mounted in the slot — what a teardown diffs against.
445    mounted: Option<AnyView<State>>,
446    /// Whether the session it belongs to is still in flight: paint registers
447    /// the slot only while this holds.
448    live: bool,
449}
450
451/// The retained widget for a [`DraggableView`].
452pub struct DraggableWidget<State: 'static, T: 'static> {
453    /// Shared with the default ghost, which paints this same retained subtree.
454    child: Rc<RefCell<ChildPod>>,
455    coordinator: DragCoordinator,
456    payload: Rc<dyn Fn(&State) -> T>,
457    policy: DragPolicy,
458    threshold: f64,
459    ghost_builder: Option<GhostBuilder<State>>,
460    ghost_opacity: f32,
461    feedback: SourceFeedback,
462    gesture: Gesture,
463    /// The hold timer for long-press initiation, reset on every arming `Down`.
464    hold: HoldTracker,
465    /// The id this source armed the current press with — allocated per press
466    /// from the coordinator it is armed on, so it is always that coordinator's.
467    source: Option<DragSourceId>,
468    slot: OverlaySlot<State>,
469    ghost: Ghost<State>,
470    /// This widget's absolute paint origin as of the last paint: what an
471    /// owner-local pointer position is lifted into window space with.
472    window_origin: Point,
473    /// Every enclosing scroll surface's live takeover veto — the whole chain,
474    /// not only the nearest — captured on the arming `Down` and held raised
475    /// while dragging.
476    scroll_vetoes: Vec<Rc<Cell<bool>>>,
477    /// Whether the current press requested keyboard focus for this source (so
478    /// `Escape` reaches it) and has not released it yet.
479    focus_claimed: bool,
480    /// Whether the current [`Gesture::Dragging`] session was begun by
481    /// [`keyboard_lift`](Self::keyboard_lift) rather than a pointer press —
482    /// set there and by [`begin`](Self::begin), read when a session is
483    /// noticed to have ended elsewhere (see [`end_elsewhere`](Self::end_elsewhere)).
484    keyboard_drag: bool,
485}
486
487impl<State: 'static, T: 'static> View<State> for DraggableView<State, T> {
488    type Element = DraggableWidget<State, T>;
489
490    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
491        let mut slot = OverlaySlot::new();
492        slot.set_band(OverlayBand::Tooltip);
493        slot.set_input(OverlayInput::Transparent);
494        DraggableWidget {
495            child: Rc::new(RefCell::new(crate::authoring::build_child(
496                &self.child,
497                ctx,
498            ))),
499            coordinator: self.coordinator.clone(),
500            payload: Rc::clone(&self.payload),
501            policy: self.policy,
502            threshold: self.threshold,
503            ghost_builder: self.ghost.clone(),
504            ghost_opacity: self.ghost_opacity,
505            feedback: self.feedback,
506            gesture: Gesture::Idle,
507            hold: HoldTracker::new(self.resolved_hold_ms()),
508            source: None,
509            slot,
510            ghost: Ghost {
511                pending: None,
512                mounted: None,
513                live: false,
514            },
515            window_origin: Point::ZERO,
516            scroll_vetoes: Vec::new(),
517            focus_claimed: false,
518            keyboard_drag: false,
519        }
520    }
521
522    fn rebuild(
523        &self,
524        prev: &Self,
525        element: &mut Self::Element,
526        ctx: &mut BuildCtx<'_>,
527    ) -> ChangeFlags {
528        let mut flags = ChangeFlags::NONE;
529        // A press in flight keeps the coordinator it armed on; any other time
530        // the latest handle is adopted.
531        if matches!(element.gesture, Gesture::Idle) {
532            element.coordinator = self.coordinator.clone();
533        }
534        element.payload = Rc::clone(&self.payload);
535        element.policy = self.policy;
536        element.threshold = self.threshold;
537        element.hold.set_threshold_ms(self.resolved_hold_ms());
538        element.ghost_builder = self.ghost.clone();
539        if element.ghost_opacity != self.ghost_opacity || element.feedback != self.feedback {
540            element.ghost_opacity = self.ghost_opacity;
541            element.feedback = self.feedback;
542            flags |= ChangeFlags::PAINT;
543        }
544        flags |= crate::authoring::rebuild_child(
545            &prev.child,
546            &self.child,
547            &mut element.child.borrow_mut(),
548            ctx,
549        );
550        flags | element.sync_ghost(ctx)
551    }
552
553    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
554        // Nothing would ever drop or cancel a session whose source is gone.
555        if matches!(element.gesture, Gesture::Pressed { .. } | Gesture::Dragging)
556            && element.owns_session()
557        {
558            element.coordinator.cancel();
559        }
560        element.gesture = Gesture::Idle;
561        element.source = None;
562        // The pod is going away, and its focus link with it.
563        element.focus_claimed = false;
564        element.keyboard_drag = false;
565        element.release_scroll_veto();
566        element.ghost.live = false;
567        element.ghost.pending = None;
568        if let Some(mounted) = element.ghost.mounted.take() {
569            element.slot.rebuild(Some(&mounted), None, ctx);
570        }
571        crate::authoring::teardown_child(&self.child, &mut element.child.borrow_mut(), ctx);
572    }
573}
574
575impl<State: 'static, T: 'static> DraggableWidget<State, T> {
576    /// Whether the coordinator's current session (armed or dragging) is the one
577    /// this source armed.
578    fn owns_session(&self) -> bool {
579        let Some(source) = self.source else {
580            return false;
581        };
582        match self.coordinator.state() {
583            DragState::Armed { source: armed, .. } => armed == source,
584            DragState::Dragging(session) => session.source == Some(source),
585            DragState::Idle | DragState::Dropping { .. } => false,
586        }
587    }
588
589    /// Whether this source's drag is in flight right now — the gesture says so
590    /// and the coordinator agrees.
591    fn is_dragging(&self) -> bool {
592        matches!(self.gesture, Gesture::Dragging)
593            && self.coordinator.phase() == DragPhase::Dragging
594            && self.owns_session()
595    }
596
597    /// An owner-local position in window space.
598    fn to_window(&self, local: Point) -> Point {
599        local + self.window_origin.to_vec2()
600    }
601
602    fn set_scroll_veto(&self, raised: bool) {
603        for veto in &self.scroll_vetoes {
604            veto.set(raised);
605        }
606    }
607
608    fn release_scroll_veto(&mut self) {
609        self.set_scroll_veto(false);
610        self.scroll_vetoes.clear();
611    }
612
613    /// A [`Gesture::Dragging`] session noticed to have ended out from under
614    /// this source — cancelled or superseded elsewhere, not through this
615    /// widget's own end-of-session call ([`on_session_end`](Self::on_session_end)).
616    /// A pointer drag still has a physical gesture in flight (its `Move`s and
617    /// its `Up`/`Cancel` are still coming): land on [`Gesture::Ended`] and
618    /// swallow them, same as before. A keyboard drag has no physical gesture
619    /// to wait for — nothing will ever deliver the `Up`/`Cancel` that would
620    /// otherwise release it from `Ended` — so it returns straight to `Idle`,
621    /// the focus claim it already holds left exactly as it is, ready for
622    /// another lift.
623    fn end_elsewhere(&mut self) {
624        self.gesture = if self.keyboard_drag {
625            Gesture::Idle
626        } else {
627            Gesture::Ended
628        };
629        self.keyboard_drag = false;
630        self.ghost.live = false;
631        self.release_scroll_veto();
632    }
633
634    /// Mount a staged ghost, or tear down one whose session ended — the
635    /// rebuild half of the ghost's lifecycle (see [`Ghost`]).
636    fn sync_ghost(&mut self, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
637        // A session cancelled or superseded elsewhere since the last event:
638        // stop showing it now rather than on the next pointer event.
639        if matches!(self.gesture, Gesture::Dragging) && !self.is_dragging() {
640            self.end_elsewhere();
641        }
642        let mut flags = ChangeFlags::NONE;
643        if let Some(next) = self.ghost.pending.take() {
644            if let Some(prev) = self.ghost.mounted.take() {
645                flags |= self.slot.rebuild(Some(&prev), None, ctx);
646            }
647            if self.ghost.live {
648                flags |= self.slot.rebuild(None, Some(&next), ctx);
649                self.ghost.mounted = Some(next);
650            }
651        } else if !self.ghost.live
652            && let Some(prev) = self.ghost.mounted.take()
653        {
654            flags |= self.slot.rebuild(Some(&prev), None, ctx);
655        }
656        flags
657    }
658
659    /// Arm the coordinator for a primary `Down` at owner-local `at`.
660    fn press(&mut self, ctx: &mut EventCtx<'_>, event: &InputEvent, at: Point) -> EventResult {
661        let initiation = match self.policy {
662            DragPolicy::Immediate => Initiation::Distance,
663            DragPolicy::LongPress => Initiation::Hold,
664            DragPolicy::Auto => match ctx.pointer_id().source {
665                PointerSource::Mouse => Initiation::Distance,
666                PointerSource::Touch => Initiation::Hold,
667            },
668        };
669        let source = self.coordinator.new_source_id();
670        if !self.coordinator.arm(source, self.to_window(at)) {
671            // A session is already in flight elsewhere: this press is the
672            // child's alone.
673            self.gesture = Gesture::Declined;
674            return crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event);
675        }
676        self.source = Some(source);
677        self.gesture = Gesture::Pressed {
678            down: at,
679            last: at,
680            initiation,
681            elapsed: false,
682        };
683        self.hold.cancel();
684        // Captured now, while every enclosing scroll surface's `Down` forward
685        // is still on the stack (see the module docs' *The session*).
686        self.scroll_vetoes = ambient_scroll_vetoes();
687        ctx.capture_pointer();
688        // Start the hold clock on the next paint.
689        ctx.request_redraw();
690        let child_took_focus = {
691            let mut child = self.child.borrow_mut();
692            child.event_child(ctx, event);
693            child.holds_live_focus()
694        };
695        // Hold the keyboard focus for the press so `Escape` reaches this
696        // source — unless the child took it, in which case `Escape` passes
697        // through here anyway (see the module docs' *Escape*).
698        if !child_took_focus {
699            ctx.request_focus();
700            self.focus_claimed = true;
701        }
702        EventResult::Handled
703    }
704
705    /// Release the keyboard focus the current press claimed, if it did.
706    fn release_focus_claim(&mut self, ctx: &mut EventCtx<'_>) {
707        if std::mem::take(&mut self.focus_claimed) {
708            ctx.release_focus();
709        }
710    }
711
712    /// Give a press up to the child: drop this source's arm and route the rest
713    /// of the gesture through untouched.
714    fn decline(&mut self) {
715        if self.owns_session() {
716            self.coordinator.cancel();
717        }
718        self.source = None;
719        self.release_scroll_veto();
720        self.gesture = Gesture::Declined;
721    }
722
723    /// Start the drag with the pointer at owner-local `at`, the press having
724    /// gone down at `down`. Returns whether a session began.
725    fn begin(&mut self, ctx: &mut EventCtx<'_>, at: Point, down: Point) -> bool {
726        if !self.owns_session() {
727            return false;
728        }
729        let (payload, custom) = {
730            let state: &State = ctx.state_mut::<State>();
731            (
732                (self.payload)(state),
733                self.ghost_builder.as_ref().map(|build| build(state)),
734            )
735        };
736        if !self.coordinator.begin(payload) {
737            return false;
738        }
739        // From the pointer to the child's top-left, so the grabbed point stays
740        // under the pointer.
741        let offset = Point::ZERO - down;
742        let pointer = self.to_window(at);
743        self.coordinator.set_ghost_offset(offset);
744        self.coordinator.update_pointer(pointer);
745        let content = custom.unwrap_or_else(|| {
746            any(SnapshotView {
747                source: Rc::clone(&self.child),
748            })
749        });
750        self.ghost.pending = Some(any(GhostFrame {
751            content,
752            opacity: self.ghost_opacity,
753        }));
754        self.ghost.live = true;
755        self.slot.set_anchor(OverlayAnchor::Window {
756            point: pointer,
757            offset,
758        });
759        // The gesture is the drag's now: cancel whatever the child made of it.
760        {
761            let mut child = self.child.borrow_mut();
762            child.event_child(ctx, &pointer_event(PointerPhase::Cancel, at));
763            ctx.release_captured_child(&mut child);
764        }
765        self.set_scroll_veto(true);
766        self.gesture = Gesture::Dragging;
767        self.keyboard_drag = false;
768        ctx.request_redraw();
769        true
770    }
771
772    /// The local teardown every end of a session runs, whichever way it ended
773    /// (an `Up`'s drop, a `Cancel`, an `Escape`, a keyboard drop/cancel, or a
774    /// session ended elsewhere): the gesture resets, the ghost stops
775    /// registering at once and is unmounted by the next rebuild, and the
776    /// scroll veto drops. `release_focus` is the caller's call on whether the
777    /// press's focus claim goes with it — every ending but a plain tap's
778    /// releases it (see the module docs' *Keyboard* section for the one
779    /// exception). The coordinator call that ended the session, if any, is
780    /// the caller's.
781    fn on_session_end(&mut self, ctx: &mut EventCtx<'_>, release_focus: bool) {
782        self.gesture = Gesture::Idle;
783        self.source = None;
784        self.keyboard_drag = false;
785        self.hold.cancel();
786        self.release_scroll_veto();
787        if release_focus {
788            self.release_focus_claim(ctx);
789        }
790        if self.ghost.live || self.ghost.mounted.is_some() {
791            self.ghost.live = false;
792            frust_core::mark_pending_result_flush();
793        }
794        ctx.request_redraw();
795    }
796
797    fn pointer(
798        &mut self,
799        ctx: &mut EventCtx<'_>,
800        event: &InputEvent,
801        p: &PointerEvent,
802    ) -> EventResult {
803        match (p.phase, self.gesture) {
804            (PointerPhase::Down, _) => {
805                // A primary press while this source drags its own keyboard
806                // session is an honest "put it down": reuse the same
807                // drop-on-hovered/cancel-on-none chord Enter/Space answers
808                // (`keyboard_drop`) rather than treating it as just another
809                // stray press that cancels — a real mouse press on the
810                // lifted item reads the same way, and so does the
811                // accessibility seam's synthesized `Down`. The matching `Up`
812                // is swallowed rather than reaching the child, the same
813                // `Gesture::Ended` wait a stray press during a *pointer*
814                // session already uses.
815                if presses(p) && self.is_keyboard_dragging() {
816                    self.keyboard_drop(ctx);
817                    self.gesture = Gesture::Ended;
818                    return EventResult::Handled;
819                }
820                // A gesture the shell never ended: abandon it before the new
821                // press, rather than leave a session nobody will drop.
822                if !matches!(self.gesture, Gesture::Idle | Gesture::Declined) {
823                    if self.owns_session() {
824                        self.coordinator.cancel();
825                    }
826                    self.on_session_end(ctx, true);
827                }
828                self.gesture = Gesture::Idle;
829                if !presses(p) {
830                    return crate::authoring::route_event_single(
831                        &mut self.child.borrow_mut(),
832                        ctx,
833                        event,
834                    );
835                }
836                self.press(ctx, event, p.position)
837            }
838            (
839                PointerPhase::Move,
840                Gesture::Pressed {
841                    down,
842                    initiation,
843                    elapsed,
844                    ..
845                },
846            ) => {
847                let travel = (p.position - down).hypot();
848                let begins = match initiation {
849                    Initiation::Distance => travel > self.threshold,
850                    Initiation::Hold if travel > TOUCH_SLOP => {
851                        self.decline();
852                        return self.child.borrow_mut().event_child(ctx, event);
853                    }
854                    Initiation::Hold => elapsed,
855                };
856                if begins && self.begin(ctx, p.position, down) {
857                    return self.drag_move(ctx, p.position);
858                }
859                if begins {
860                    // The arm was lost before the drag could start.
861                    self.decline();
862                } else if let Gesture::Pressed { last, .. } = &mut self.gesture {
863                    *last = p.position;
864                }
865                self.child.borrow_mut().event_child(ctx, event);
866                EventResult::Handled
867            }
868            (PointerPhase::Move, Gesture::Dragging) => self.drag_move(ctx, p.position),
869            (PointerPhase::Up, Gesture::Pressed { elapsed, .. }) => {
870                // Released before it became a drag: a click or tap, the child's.
871                // A hold that crossed its threshold but had not started yet
872                // (the release beat the deferred start) was not a tap, though.
873                if self.owns_session() {
874                    self.coordinator.cancel();
875                }
876                let phase = if elapsed {
877                    PointerPhase::Cancel
878                } else {
879                    PointerPhase::Up
880                };
881                {
882                    let mut child = self.child.borrow_mut();
883                    child.event_child(ctx, &pointer_event(phase, p.position));
884                    child.set_active(false);
885                }
886                // A genuine tap (not a hold that almost began) keeps the
887                // focus claim, so a following keyboard chord can lift it
888                // (the module docs' *Keyboard* section).
889                self.on_session_end(ctx, elapsed);
890                EventResult::Handled
891            }
892            (PointerPhase::Cancel, Gesture::Pressed { .. }) => {
893                if self.owns_session() {
894                    self.coordinator.cancel();
895                }
896                {
897                    let mut child = self.child.borrow_mut();
898                    child.event_child(ctx, event);
899                    child.set_active(false);
900                }
901                self.on_session_end(ctx, true);
902                EventResult::Handled
903            }
904            (PointerPhase::Up, Gesture::Dragging) => {
905                if self.owns_session() {
906                    self.coordinator.update_pointer(self.to_window(p.position));
907                    self.coordinator.drop();
908                }
909                self.on_session_end(ctx, true);
910                EventResult::Handled
911            }
912            (PointerPhase::Cancel, Gesture::Dragging) => {
913                if self.owns_session() {
914                    self.coordinator.cancel();
915                }
916                self.on_session_end(ctx, true);
917                EventResult::Handled
918            }
919            (PointerPhase::Up | PointerPhase::Cancel, Gesture::Ended) => {
920                self.on_session_end(ctx, true);
921                EventResult::Handled
922            }
923            (PointerPhase::Move, Gesture::Ended) => EventResult::Handled,
924            (PointerPhase::Up | PointerPhase::Cancel, Gesture::Declined) => {
925                self.gesture = Gesture::Idle;
926                self.release_focus_claim(ctx);
927                crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
928            }
929            // Hover moves, stray releases, and a declined press's moves.
930            _ => crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event),
931        }
932    }
933
934    /// `Escape` while a *pointer* drags this source: cancel the session and
935    /// tear it down, swallowing the rest of the still-pressed gesture.
936    fn escape(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
937        if self.owns_session() {
938            self.coordinator.cancel();
939        }
940        self.on_session_end(ctx, true);
941        // The pointer is still down: its `Move`s and `Up` are this gesture's
942        // and must not reach the child as a fresh one.
943        self.gesture = Gesture::Ended;
944        EventResult::Handled
945    }
946
947    /// Whether this source currently drags a session it began by keyboard
948    /// ([`keyboard_lift`](Self::keyboard_lift)) — the gate the lift/cycle/
949    /// drop/cancel chords below use to tell a keyboard drag from a pointer
950    /// one in flight.
951    fn is_keyboard_dragging(&self) -> bool {
952        matches!(self.gesture, Gesture::Dragging)
953            && self.owns_session()
954            && matches!(self.coordinator.state(), DragState::Dragging(session) if session.keyboard)
955    }
956
957    /// `Escape` while a *keyboard* drag is live: cancel and tear down. Unlike
958    /// [`escape`](Self::escape) there is no still-pressed pointer gesture to
959    /// swallow, so the widget lands directly back on `Idle`, ready for
960    /// another lift.
961    fn keyboard_cancel(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
962        if self.owns_session() {
963            self.coordinator.cancel();
964        }
965        self.on_session_end(ctx, true);
966        EventResult::Handled
967    }
968
969    /// `Enter`/Space while a keyboard drag is live: drop on whatever
970    /// [`move_to_next_target`](DragCoordinator::move_to_next_target)/
971    /// [`move_to_previous_target`](DragCoordinator::move_to_previous_target)
972    /// left hovered — a cancel if nothing is, exactly like a pointer's `Up`.
973    fn keyboard_drop(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
974        if self.owns_session() {
975            self.coordinator.drop();
976        }
977        self.on_session_end(ctx, true);
978        EventResult::Handled
979    }
980
981    /// `Enter`/Space while this source holds its own focus and nothing is in
982    /// flight: lift it (see the module docs' *Lift, cycle, drop*). Returns
983    /// `Ignored` without effect when a session is already in flight
984    /// elsewhere (nothing here claims it).
985    fn keyboard_lift(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
986        let source = self.coordinator.new_source_id();
987        if !self.coordinator.lift(source) {
988            return EventResult::Ignored;
989        }
990        self.source = Some(source);
991        let (payload, custom) = {
992            let state: &State = ctx.state_mut::<State>();
993            (
994                (self.payload)(state),
995                self.ghost_builder.as_ref().map(|build| build(state)),
996            )
997        };
998        if !self.coordinator.begin(payload) {
999            self.source = None;
1000            return EventResult::Ignored;
1001        }
1002        let content = custom.unwrap_or_else(|| {
1003            any(SnapshotView {
1004                source: Rc::clone(&self.child),
1005            })
1006        });
1007        self.ghost.pending = Some(any(GhostFrame {
1008            content,
1009            opacity: self.ghost_opacity,
1010        }));
1011        self.ghost.live = true;
1012        // No pointer yet: the ghost starts over this source's own bounds.
1013        self.slot.set_anchor(OverlayAnchor::Window {
1014            point: self.window_origin,
1015            offset: Vec2::ZERO,
1016        });
1017        self.gesture = Gesture::Dragging;
1018        self.keyboard_drag = true;
1019        ctx.request_focus();
1020        self.focus_claimed = true;
1021        ctx.request_redraw();
1022        EventResult::Handled
1023    }
1024
1025    /// Reposition the ghost over whatever is now hovered, after a keyboard
1026    /// cycle step — the keyboard counterpart of [`drag_move`](Self::drag_move)
1027    /// moving it to the pointer. A no-op with nothing hovered (every target
1028    /// cycled past, or none registered).
1029    fn follow_hovered_target(&mut self, ctx: &mut EventCtx<'_>) {
1030        let DragState::Dragging(session) = self.coordinator.state() else {
1031            return;
1032        };
1033        let Some(target) = session.hovered else {
1034            return;
1035        };
1036        let Some(bounds) = self.coordinator.target_bounds(target) else {
1037            return;
1038        };
1039        self.slot.set_window_anchor(ctx, bounds.origin());
1040    }
1041
1042    /// The lift/cycle/drop chord this source answers while it — not a
1043    /// descendant — holds the live focus (see the module docs' *Lift, cycle,
1044    /// drop*). `None` falls through to the child: not our chord, a session
1045    /// (pointer or keyboard) owned elsewhere, or the child's own focus claim
1046    /// takes keys first.
1047    fn keyboard_chord(&mut self, ctx: &mut EventCtx<'_>, key: &KeyEvent) -> Option<EventResult> {
1048        if !ctx.has_focus() || self.child.borrow().holds_live_focus() {
1049            return None;
1050        }
1051        let activates = key.key == Key::Named(NamedKey::Enter)
1052            || matches!(&key.key, Key::Character(s) if s == " ");
1053        if matches!(self.gesture, Gesture::Idle) {
1054            return activates.then(|| self.keyboard_lift(ctx));
1055        }
1056        if !self.is_keyboard_dragging() {
1057            return None;
1058        }
1059        match &key.key {
1060            Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
1061                self.coordinator.move_to_next_target();
1062                self.follow_hovered_target(ctx);
1063                ctx.request_redraw();
1064                Some(EventResult::Handled)
1065            }
1066            Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
1067                self.coordinator.move_to_previous_target();
1068                self.follow_hovered_target(ctx);
1069                ctx.request_redraw();
1070                Some(EventResult::Handled)
1071            }
1072            _ if activates => Some(self.keyboard_drop(ctx)),
1073            _ => None,
1074        }
1075    }
1076
1077    /// A `Move` while this source drags: report it and move the ghost.
1078    fn drag_move(&mut self, ctx: &mut EventCtx<'_>, at: Point) -> EventResult {
1079        if !self.is_dragging() {
1080            // Ended elsewhere since the last event.
1081            self.end_elsewhere();
1082            frust_core::mark_pending_result_flush();
1083            ctx.request_redraw();
1084            return EventResult::Handled;
1085        }
1086        let pointer = self.to_window(at);
1087        self.coordinator.update_pointer(pointer);
1088        self.slot.set_window_anchor(ctx, pointer);
1089        ctx.set_cursor(CursorIcon::Grabbing);
1090        EventResult::Handled
1091    }
1092
1093    /// Paint the child in place, under whatever [`SourceFeedback`] the
1094    /// dragging state calls for.
1095    fn paint_source(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1096        let feedback = if self.is_dragging() {
1097            self.feedback
1098        } else {
1099            SourceFeedback::None
1100        };
1101        let mut child = self.child.borrow_mut();
1102        match feedback {
1103            SourceFeedback::None => child.paint_child(ctx, scene),
1104            SourceFeedback::Dim => {
1105                scene.push_layer(ctx.origin(), ctx.size(), DIM_OPACITY);
1106                child.paint_child(ctx, scene);
1107                scene.pop_layer();
1108            }
1109            SourceFeedback::Placeholder => {
1110                let (fill, radius) = match Theme::from_paint_ctx(ctx) {
1111                    Some(theme) => (theme.scheme().surface_container_highest, theme.shape.small),
1112                    None => (PLACEHOLDER_FILL, PLACEHOLDER_RADIUS),
1113                };
1114                scene.fill_rounded_rect(ctx.origin(), ctx.size(), radius, fill);
1115            }
1116        }
1117    }
1118}
1119
1120impl<State: 'static, T: 'static> Widget for DraggableWidget<State, T> {
1121    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1122        let size = {
1123            let mut child = self.child.borrow_mut();
1124            let size = child.layout_child(ctx, bc);
1125            child.set_origin(Point::ZERO);
1126            size
1127        };
1128        // After the child, so a snapshot ghost reads the child's fresh size.
1129        self.slot.layout(ctx);
1130        bc.constrain(size)
1131    }
1132
1133    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1134        self.window_origin = ctx.origin();
1135        // The hold clock (see `crate::gesture`'s long-press firing semantics):
1136        // the paint that sees the threshold crossed latches the flush whose
1137        // `Housekeeping` broadcast starts the drag, and asks for the frame that
1138        // reaches it; until then, keep frames coming so the clock advances.
1139        if let Gesture::Pressed {
1140            initiation: Initiation::Hold,
1141            elapsed,
1142            ..
1143        } = &mut self.gesture
1144            && !*elapsed
1145        {
1146            if self.hold.fired(ctx.frame_time()) {
1147                *elapsed = true;
1148                frust_core::mark_pending_result_flush();
1149            }
1150            ctx.request_frame();
1151        }
1152        self.paint_source(ctx, scene);
1153        if self.ghost.live {
1154            let size = ctx.size();
1155            self.slot.paint(ctx, size);
1156        }
1157    }
1158
1159    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1160        if event.is_broadcast() {
1161            // A hold that crossed its threshold starts here when no pointer
1162            // event beat the deferred flush to it.
1163            if let Gesture::Pressed {
1164                initiation: Initiation::Hold,
1165                elapsed: true,
1166                down,
1167                last,
1168            } = self.gesture
1169                && !self.begin(ctx, last, down)
1170            {
1171                self.decline();
1172            }
1173            self.child.borrow_mut().event_child(ctx, event);
1174            return EventResult::Ignored;
1175        }
1176        if let InputEvent::Pointer(p) = event {
1177            return self.pointer(ctx, event, p);
1178        }
1179        if let InputEvent::Key(key) = event {
1180            if key.key == Key::Named(NamedKey::Escape) && matches!(self.gesture, Gesture::Dragging)
1181            {
1182                return if self.is_keyboard_dragging() {
1183                    self.keyboard_cancel(ctx)
1184                } else {
1185                    self.escape(ctx)
1186                };
1187            }
1188            if let Some(result) = self.keyboard_chord(ctx, key) {
1189                return result;
1190            }
1191        }
1192        crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
1193    }
1194
1195    fn semantics(&self, ctx: &mut SemanticsCtx) {
1196        // No accesskit role names a drag source; `Role::Button` is the
1197        // closest activatable analog (see the module docs' *Semantics*
1198        // section for why `Click` — not a custom `lift`/`drop`/`cancel`
1199        // action — is what this node advertises, and why the label never
1200        // names a verb `Click` does not perform in the current state).
1201        let keyboard_dragging = self.is_keyboard_dragging();
1202        let dragging = self.is_dragging();
1203        let label = if keyboard_dragging {
1204            "Drop"
1205        } else if dragging {
1206            "Dragging"
1207        } else {
1208            "Draggable"
1209        };
1210        ctx.push_container(
1211            Role::Button,
1212            |node| {
1213                node.add_action(Action::Click);
1214                node.set_label(label);
1215                if !dragging {
1216                    node.set_description("Press Enter to lift");
1217                }
1218            },
1219            |ctx| self.child.borrow().semantics_child(ctx),
1220        );
1221    }
1222
1223    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1224        visitor(&self.child.borrow());
1225    }
1226}
1227
1228/// A synthesized pointer event at owner-local `at`.
1229fn pointer_event(phase: PointerPhase, at: Point) -> InputEvent {
1230    InputEvent::Pointer(PointerEvent {
1231        phase,
1232        position: at,
1233        button: PointerButton::Primary,
1234    })
1235}
1236
1237// ---------------------------------------------------------------------------
1238// The ghost
1239// ---------------------------------------------------------------------------
1240
1241/// The ghost pod's root: its content painted under one opacity layer.
1242struct GhostFrame<State: 'static> {
1243    content: AnyView<State>,
1244    opacity: f32,
1245}
1246
1247struct GhostFrameWidget {
1248    content: ChildPod,
1249    opacity: f32,
1250}
1251
1252impl<State: 'static> View<State> for GhostFrame<State> {
1253    type Element = GhostFrameWidget;
1254
1255    fn build(&self, ctx: &mut BuildCtx<'_>) -> GhostFrameWidget {
1256        GhostFrameWidget {
1257            content: crate::authoring::build_child(&self.content, ctx),
1258            opacity: self.opacity,
1259        }
1260    }
1261
1262    fn rebuild(
1263        &self,
1264        prev: &Self,
1265        element: &mut GhostFrameWidget,
1266        ctx: &mut BuildCtx<'_>,
1267    ) -> ChangeFlags {
1268        element.opacity = self.opacity;
1269        crate::authoring::rebuild_child(&prev.content, &self.content, &mut element.content, ctx)
1270    }
1271
1272    fn teardown(&self, element: &mut GhostFrameWidget, ctx: &mut BuildCtx<'_>) {
1273        crate::authoring::teardown_child(&self.content, &mut element.content, ctx);
1274    }
1275}
1276
1277impl Widget for GhostFrameWidget {
1278    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1279        let size = self.content.layout_child(ctx, bc);
1280        self.content.set_origin(Point::ZERO);
1281        bc.constrain(size)
1282    }
1283
1284    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1285        if self.opacity >= 1.0 {
1286            self.content.paint_child(ctx, scene);
1287            return;
1288        }
1289        scene.push_layer(ctx.origin(), ctx.size(), self.opacity);
1290        self.content.paint_child(ctx, scene);
1291        scene.pop_layer();
1292    }
1293
1294    crate::authoring::visit_children!(content);
1295}
1296
1297/// The default ghost content: the source's own retained child, painted again at
1298/// the ghost's position and the child's laid-out size.
1299struct SnapshotView {
1300    source: Rc<RefCell<ChildPod>>,
1301}
1302
1303struct SnapshotWidget {
1304    source: Rc<RefCell<ChildPod>>,
1305}
1306
1307impl<State: 'static> View<State> for SnapshotView {
1308    type Element = SnapshotWidget;
1309
1310    fn build(&self, _ctx: &mut BuildCtx<'_>) -> SnapshotWidget {
1311        SnapshotWidget {
1312            source: Rc::clone(&self.source),
1313        }
1314    }
1315
1316    fn rebuild(
1317        &self,
1318        _prev: &Self,
1319        element: &mut SnapshotWidget,
1320        _ctx: &mut BuildCtx<'_>,
1321    ) -> ChangeFlags {
1322        element.source = Rc::clone(&self.source);
1323        ChangeFlags::NONE
1324    }
1325}
1326
1327impl Widget for SnapshotWidget {
1328    fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1329        // The source lays itself out in the main tree; the snapshot only takes
1330        // its size, never re-runs its layout against the window.
1331        bc.constrain(self.source.borrow().size())
1332    }
1333
1334    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1335        // The source pod sits at its owner's origin, so painting it from here
1336        // lands it at the ghost's. Skipped rather than panicking should the
1337        // source ever be mid-paint itself.
1338        if let Ok(mut source) = self.source.try_borrow_mut() {
1339            source.paint_child(ctx, scene);
1340        }
1341    }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346    use super::*;
1347    use crate::test_support::RecordingScene;
1348    use crate::{EdgeInsets, Padding, StackView, stack};
1349    use frust_core::event::PointerId;
1350    use frust_core::{FrameTime, RenderRoot};
1351    use kurbo::Rect;
1352
1353    use crate::drag::{DragStateChange, DragTargetId};
1354
1355    const WINDOW: Size = Size::new(400.0, 600.0);
1356    /// Where the draggable sits in the window.
1357    const AT: Vec2 = Vec2::new(10.0, 100.0);
1358    const CHILD: Size = Size::new(100.0, 40.0);
1359
1360    type Log = Rc<RefCell<Vec<String>>>;
1361
1362    #[derive(Clone, Copy)]
1363    struct Cfg {
1364        policy: DragPolicy,
1365        feedback: SourceFeedback,
1366        custom_ghost: bool,
1367        /// Host the source inside a tall `ScrollView`.
1368        in_scroll: bool,
1369        /// Host the source inside a `ScrollView`-with-capacity nested inside
1370        /// another `ScrollView`-with-capacity — two enclosing scroll surfaces,
1371        /// not one.
1372        in_nested_scroll: bool,
1373        /// The child requests keyboard focus on its own primary `Down`.
1374        focusable_child: bool,
1375    }
1376
1377    impl Default for Cfg {
1378        fn default() -> Self {
1379            Cfg {
1380                policy: DragPolicy::Auto,
1381                feedback: SourceFeedback::Dim,
1382                custom_ghost: false,
1383                in_scroll: false,
1384                in_nested_scroll: false,
1385                focusable_child: false,
1386            }
1387        }
1388    }
1389
1390    struct App {
1391        cfg: Cfg,
1392        coordinator: DragCoordinator,
1393        log: Log,
1394        taps: u32,
1395        item: u32,
1396    }
1397
1398    /// A button-like leaf: paints its rect, records every pointer event in
1399    /// its own space (and every key it is routed), captures on a primary
1400    /// `Down` — optionally taking focus with it — and counts an in-bounds `Up`
1401    /// as a tap.
1402    struct Probe {
1403        size: Size,
1404        log: Log,
1405        focusable: bool,
1406    }
1407
1408    struct ProbeWidget {
1409        size: Size,
1410        log: Log,
1411        focusable: bool,
1412    }
1413
1414    impl View<App> for Probe {
1415        type Element = ProbeWidget;
1416        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
1417            ProbeWidget {
1418                size: self.size,
1419                log: Rc::clone(&self.log),
1420                focusable: self.focusable,
1421            }
1422        }
1423        fn rebuild(
1424            &self,
1425            _prev: &Self,
1426            element: &mut ProbeWidget,
1427            _ctx: &mut BuildCtx<'_>,
1428        ) -> ChangeFlags {
1429            element.size = self.size;
1430            element.log = Rc::clone(&self.log);
1431            element.focusable = self.focusable;
1432            ChangeFlags::NONE
1433        }
1434    }
1435
1436    impl Widget for ProbeWidget {
1437        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1438            bc.constrain(self.size)
1439        }
1440        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1441            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1442        }
1443        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1444            if let InputEvent::Key(key) = event {
1445                self.log.borrow_mut().push(format!("Key {:?}", key.key));
1446                return EventResult::Handled;
1447            }
1448            let InputEvent::Pointer(p) = event else {
1449                return EventResult::Ignored;
1450            };
1451            self.log
1452                .borrow_mut()
1453                .push(format!("{:?}@{},{}", p.phase, p.position.x, p.position.y));
1454            match p.phase {
1455                PointerPhase::Down => {
1456                    ctx.capture_pointer();
1457                    if self.focusable && presses(p) {
1458                        ctx.request_focus();
1459                    }
1460                }
1461                PointerPhase::Up => {
1462                    let inside =
1463                        Rect::from_origin_size(Point::ZERO, ctx.size()).contains(p.position);
1464                    if inside {
1465                        ctx.state_mut::<App>().taps += 1;
1466                    }
1467                }
1468                PointerPhase::Move | PointerPhase::Cancel => {}
1469            }
1470            EventResult::Handled
1471        }
1472    }
1473
1474    fn logic(state: &mut App) -> StackView<App> {
1475        let cfg = state.cfg;
1476        let mut source = draggable(
1477            Probe {
1478                size: CHILD,
1479                log: Rc::clone(&state.log),
1480                focusable: cfg.focusable_child,
1481            },
1482            state.coordinator.clone(),
1483            |state: &App| state.item,
1484        )
1485        .policy(cfg.policy)
1486        .while_dragging(cfg.feedback);
1487        if cfg.custom_ghost {
1488            source = source.ghost(|_: &App| crate::SizedBox(Some(30.0), Some(20.0)));
1489        }
1490        let placed = any(Padding(
1491            EdgeInsets {
1492                left: AT.x,
1493                top: AT.y,
1494                right: 0.0,
1495                bottom: 0.0,
1496            },
1497            source,
1498        ));
1499        if cfg.in_scroll {
1500            return stack().child(crate::scroll_view(
1501                stack()
1502                    .child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
1503                    .child(placed),
1504            ));
1505        }
1506        if cfg.in_nested_scroll {
1507            // A page scroll surface (the window's own height, 2_000 px of
1508            // content, so it has real scroll capacity) wrapping a column
1509            // scroll surface whose own content fits its own viewport exactly
1510            // — a column with nothing of its own to scroll, same as a short
1511            // kanban column, which still forwards every `Down` and still has
1512            // a touch-slop takeover of its own to fire.
1513            let inner = any(crate::scroll_view(stack().child(placed)));
1514            return stack().child(crate::scroll_view(
1515                stack()
1516                    .child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
1517                    .child(inner),
1518            ));
1519        }
1520        stack().child(placed)
1521    }
1522
1523    struct Harness {
1524        root: RenderRoot<App, StackView<App>>,
1525        state: App,
1526        clock_ms: f64,
1527        changes: Rc<RefCell<Vec<DragStateChange>>>,
1528        _subscription: crate::drag::DragSubscription,
1529    }
1530
1531    impl Harness {
1532        fn new(cfg: Cfg) -> Self {
1533            let coordinator = DragCoordinator::new();
1534            let changes = Rc::new(RefCell::new(Vec::new()));
1535            let sink = Rc::clone(&changes);
1536            let subscription = coordinator.subscribe(move |change| sink.borrow_mut().push(change));
1537            let mut harness = Harness {
1538                root: RenderRoot::new(),
1539                state: App {
1540                    cfg,
1541                    coordinator,
1542                    log: Rc::new(RefCell::new(Vec::new())),
1543                    taps: 0,
1544                    item: 7,
1545                },
1546                clock_ms: 0.0,
1547                changes,
1548                _subscription: subscription,
1549            };
1550            harness.frame();
1551            harness
1552        }
1553
1554        /// One whole frame — rebuild, layout, paint — returning the scene.
1555        fn frame(&mut self) -> RecordingScene {
1556            let mut build: fn(&mut App) -> StackView<App> = logic;
1557            self.root.rebuild(&mut build, &mut self.state);
1558            self.root.layout(WINDOW);
1559            self.clock_ms += 16.0;
1560            let mut scene = RecordingScene::default();
1561            self.root.paint(
1562                &mut scene,
1563                FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
1564            );
1565            scene
1566        }
1567
1568        fn mouse(&mut self, phase: PointerPhase, x: f64, y: f64) {
1569            self.root
1570                .event(&mut self.state, &pointer_event(phase, Point::new(x, y)));
1571        }
1572
1573        fn touch(&mut self, phase: PointerPhase, x: f64, y: f64) {
1574            self.root.event(
1575                &mut self.state,
1576                &InputEvent::PointerContact {
1577                    pointer_id: PointerId::touch(0),
1578                    event: PointerEvent {
1579                        phase,
1580                        position: Point::new(x, y),
1581                        button: PointerButton::Primary,
1582                    },
1583                },
1584            );
1585        }
1586
1587        fn phase(&self) -> DragPhase {
1588            self.state.coordinator.phase()
1589        }
1590
1591        fn key(&mut self, key: Key) {
1592            self.root.event(
1593                &mut self.state,
1594                &InputEvent::Key(frust_core::KeyEvent {
1595                    key,
1596                    modifiers: frust_core::Modifiers::default(),
1597                    repeat: false,
1598                }),
1599            );
1600        }
1601
1602        fn escape(&mut self) {
1603            self.key(Key::Named(NamedKey::Escape));
1604        }
1605
1606        /// Register a drop target with window-space `bounds`, as a painted
1607        /// target would.
1608        fn target(&mut self, bounds: Rect) -> DragTargetId {
1609            let drag = &self.state.coordinator;
1610            let id = drag.new_target_id();
1611            drag.register_target(id);
1612            drag.set_target_bounds(id, bounds);
1613            id
1614        }
1615
1616        fn log(&self) -> Vec<String> {
1617            self.state.log.borrow().clone()
1618        }
1619
1620        /// Press at window `(30, 120)` — `(20, 20)` inside the child — and
1621        /// drag past the threshold to `(60, 150)`.
1622        fn mouse_drag(&mut self) {
1623            self.mouse(PointerPhase::Down, 30.0, 120.0);
1624            self.mouse(PointerPhase::Move, 60.0, 150.0);
1625        }
1626    }
1627
1628    fn rects(scene: &RecordingScene) -> Vec<Rect> {
1629        scene
1630            .rects
1631            .iter()
1632            .map(|(origin, size)| Rect::from_origin_size(*origin, *size))
1633            .collect()
1634    }
1635
1636    fn rest() -> Rect {
1637        Rect::from_origin_size(Point::ZERO + AT, CHILD)
1638    }
1639
1640    #[test]
1641    fn a_mouse_drag_begins_past_the_threshold_and_not_before() {
1642        let mut h = Harness::new(Cfg::default());
1643        h.mouse(PointerPhase::Down, 30.0, 120.0);
1644        assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
1645        h.mouse(PointerPhase::Move, 34.0, 122.0);
1646        assert_eq!(
1647            h.phase(),
1648            DragPhase::Armed,
1649            "4.5 px is inside the 6 px threshold"
1650        );
1651        h.mouse(PointerPhase::Move, 37.0, 120.0);
1652        assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
1653        assert_eq!(
1654            h.state.coordinator.with_payload(|item: &u32| *item),
1655            Some(7),
1656            "the payload builder ran against the state at begin"
1657        );
1658        let session = h.state.coordinator.state();
1659        let session = session.session().expect("a session is in flight");
1660        assert_eq!(session.pointer, Point::new(37.0, 120.0));
1661        assert_eq!(session.ghost_offset, Vec2::new(-20.0, -20.0));
1662    }
1663
1664    #[test]
1665    fn a_short_click_passes_through_to_the_child() {
1666        let mut h = Harness::new(Cfg::default());
1667        h.mouse(PointerPhase::Down, 30.0, 120.0);
1668        h.mouse(PointerPhase::Move, 32.0, 121.0);
1669        h.mouse(PointerPhase::Up, 32.0, 121.0);
1670        assert_eq!(h.state.taps, 1, "the child saw the whole click");
1671        assert_eq!(h.log(), vec!["Down@20,20", "Move@22,21", "Up@22,21"]);
1672        assert_eq!(h.phase(), DragPhase::Idle, "the arm was released");
1673        assert!(!h.root.is_pointer_captured());
1674    }
1675
1676    #[test]
1677    fn a_short_tap_on_touch_passes_through_to_the_child() {
1678        let mut h = Harness::new(Cfg::default());
1679        h.touch(PointerPhase::Down, 30.0, 120.0);
1680        h.frame();
1681        h.touch(PointerPhase::Up, 30.0, 120.0);
1682        assert_eq!(h.state.taps, 1);
1683        assert_eq!(h.phase(), DragPhase::Idle);
1684    }
1685
1686    #[test]
1687    fn beginning_a_drag_cancels_the_childs_press() {
1688        let mut h = Harness::new(Cfg::default());
1689        h.mouse_drag();
1690        h.mouse(PointerPhase::Up, 60.0, 150.0);
1691        assert_eq!(h.state.taps, 0, "a drag is never also a tap");
1692        assert_eq!(h.log(), vec!["Down@20,20", "Cancel@50,50"]);
1693    }
1694
1695    #[test]
1696    fn a_touch_long_press_begins_the_drag() {
1697        let mut h = Harness::new(Cfg::default());
1698        h.touch(PointerPhase::Down, 30.0, 120.0);
1699        // Drift inside the touch slop doesn't count as travel: a 10 px move
1700        // would have begun a mouse drag.
1701        h.touch(PointerPhase::Move, 40.0, 120.0);
1702        assert_eq!(h.phase(), DragPhase::Armed);
1703        let mut frames = 0;
1704        while h.phase() != DragPhase::Dragging {
1705            h.frame();
1706            frames += 1;
1707            assert!(frames < 60, "the hold never began a drag");
1708        }
1709        // 16 ms frames: the threshold is crossed on the paint ~500 ms in, and
1710        // the next frame's Housekeeping flush starts the drag.
1711        assert!(
1712            frames >= 31,
1713            "began after {frames} frames — before the hold elapsed"
1714        );
1715        assert_eq!(
1716            h.state.coordinator.state().session().map(|s| s.pointer),
1717            Some(Point::new(40.0, 120.0)),
1718            "the drag starts where the finger is held"
1719        );
1720        assert_eq!(h.log().last().map(String::as_str), Some("Cancel@30,20"));
1721    }
1722
1723    #[test]
1724    fn an_enclosing_scroll_view_does_not_steal_a_drag_in_flight() {
1725        let mut h = Harness::new(Cfg {
1726            in_scroll: true,
1727            ..Cfg::default()
1728        });
1729        h.touch(PointerPhase::Down, 30.0, 120.0);
1730        let mut frames = 0;
1731        while h.phase() != DragPhase::Dragging {
1732            h.frame();
1733            frames += 1;
1734            assert!(frames < 60, "the hold never began a drag");
1735        }
1736        // Well past the scroll view's own touch slop.
1737        h.touch(PointerPhase::Move, 30.0, 220.0);
1738        h.touch(PointerPhase::Move, 30.0, 320.0);
1739        assert_eq!(
1740            h.phase(),
1741            DragPhase::Dragging,
1742            "the scroll view took nothing over"
1743        );
1744        assert_eq!(
1745            h.state.coordinator.state().session().map(|s| s.pointer),
1746            Some(Point::new(30.0, 320.0))
1747        );
1748        let scene = h.frame();
1749        assert_eq!(
1750            rects(&scene).first(),
1751            Some(&rest()),
1752            "the content did not scroll under the drag"
1753        );
1754        h.touch(PointerPhase::Up, 30.0, 320.0);
1755        assert_eq!(h.phase(), DragPhase::Idle);
1756    }
1757
1758    #[test]
1759    fn a_drag_vetoes_every_enclosing_scroll_surface_not_only_the_nearest() {
1760        // Two scroll surfaces, the way a scrollable page hosts a scrollable
1761        // column: the outer has real capacity (2_000 px of content against
1762        // the window's own height) while the inner's content fits its own
1763        // viewport exactly — a column with nothing of its own to scroll,
1764        // which still forwards the `Down` and still has a slop to fire at.
1765        // Dispatch is parent-first, so the *outer* surface's own takeover
1766        // check runs before the inner's ever does — raising only the nearest
1767        // cell (the pre-fix behaviour) leaves the outer free to cancel the
1768        // child right there, before the inner even gets a say.
1769        let mut h = Harness::new(Cfg {
1770            in_nested_scroll: true,
1771            ..Cfg::default()
1772        });
1773        let target = h.target(Rect::new(0.0, 300.0, WINDOW.width, WINDOW.height));
1774        h.mouse(PointerPhase::Down, 30.0, 120.0);
1775        assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
1776        // Past the 6 px mouse drag threshold: the session begins and every
1777        // enclosing surface's veto is raised.
1778        h.mouse(PointerPhase::Move, 37.0, 120.0);
1779        assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
1780        // Past both surfaces' 18 px touch slop (they share the same
1781        // `down_start`) and into the target registered below the inner
1782        // surface's own bounds.
1783        h.mouse(PointerPhase::Move, 30.0, 320.0);
1784        assert_eq!(
1785            h.phase(),
1786            DragPhase::Dragging,
1787            "neither enclosing scroll surface took the gesture over at its own slop"
1788        );
1789        assert_eq!(
1790            h.state
1791                .coordinator
1792                .state()
1793                .session()
1794                .and_then(|s| s.hovered),
1795            Some(target),
1796            "the drag reached a target the inner surface doesn't even cover"
1797        );
1798        let scene = h.frame();
1799        assert_eq!(
1800            rects(&scene).first(),
1801            Some(&rest()),
1802            "neither surface scrolled its content under the drag"
1803        );
1804        h.mouse(PointerPhase::Up, 30.0, 320.0);
1805        assert_eq!(
1806            h.phase(),
1807            DragPhase::Dropping,
1808            "the coordinator reached Dropping on the target outside the inner surface"
1809        );
1810        h.state.coordinator.complete_drop();
1811        assert_eq!(h.phase(), DragPhase::Idle, "the drop completed");
1812    }
1813
1814    #[test]
1815    fn a_touch_press_that_moves_before_the_hold_is_the_childs() {
1816        let mut h = Harness::new(Cfg::default());
1817        h.touch(PointerPhase::Down, 30.0, 120.0);
1818        h.frame();
1819        h.touch(PointerPhase::Move, 30.0, 160.0);
1820        assert_eq!(
1821            h.phase(),
1822            DragPhase::Idle,
1823            "travel past the slop gives it up"
1824        );
1825        for _ in 0..40 {
1826            h.frame();
1827        }
1828        assert_eq!(h.phase(), DragPhase::Idle, "no later hold revives it");
1829        h.touch(PointerPhase::Up, 30.0, 160.0);
1830        assert_eq!(h.log(), vec!["Down@20,20", "Move@20,60", "Up@20,60"]);
1831    }
1832
1833    #[test]
1834    fn an_explicit_policy_overrides_the_device() {
1835        let mut h = Harness::new(Cfg {
1836            policy: DragPolicy::LongPress,
1837            ..Cfg::default()
1838        });
1839        h.mouse(PointerPhase::Down, 30.0, 120.0);
1840        h.mouse(PointerPhase::Move, 40.0, 120.0);
1841        assert_eq!(h.phase(), DragPhase::Armed, "a long-press mouse drag waits");
1842
1843        let mut h = Harness::new(Cfg {
1844            policy: DragPolicy::Immediate,
1845            ..Cfg::default()
1846        });
1847        h.touch(PointerPhase::Down, 30.0, 120.0);
1848        h.touch(PointerPhase::Move, 40.0, 120.0);
1849        assert_eq!(
1850            h.phase(),
1851            DragPhase::Dragging,
1852            "an immediate touch drag doesn't"
1853        );
1854    }
1855
1856    #[test]
1857    fn the_ghost_opens_and_closes_with_the_session() {
1858        let mut h = Harness::new(Cfg::default());
1859        let idle = h.frame();
1860        assert_eq!(rects(&idle), vec![rest()], "no ghost before a drag");
1861
1862        h.mouse_drag();
1863        let dragging = h.frame();
1864        // Pointer (60, 150) minus the (20, 20) grab point: the snapshot of the
1865        // child, at the child's size, under the ghost's opacity layer.
1866        let ghost = Rect::from_origin_size(Point::new(40.0, 130.0), CHILD);
1867        assert_eq!(rects(&dragging), vec![rest(), ghost]);
1868        assert!(
1869            dragging
1870                .layers
1871                .contains(&(ghost.origin(), CHILD, GHOST_OPACITY)),
1872            "{:?}",
1873            dragging.layers
1874        );
1875
1876        h.mouse(PointerPhase::Move, 200.0, 400.0);
1877        let moved = h.frame();
1878        assert_eq!(
1879            rects(&moved).last().copied(),
1880            Some(Rect::from_origin_size(Point::new(180.0, 380.0), CHILD)),
1881            "the ghost follows the pointer"
1882        );
1883
1884        h.mouse(PointerPhase::Up, 200.0, 400.0);
1885        let ended = h.frame();
1886        assert_eq!(
1887            rects(&ended),
1888            vec![rest()],
1889            "the ghost closed with the session"
1890        );
1891        assert!(ended.layers.is_empty());
1892    }
1893
1894    #[test]
1895    fn the_pointer_stays_captured_for_the_whole_drag() {
1896        let mut h = Harness::new(Cfg::default());
1897        h.mouse_drag();
1898        h.frame();
1899        assert!(h.root.is_pointer_captured());
1900        // Far outside the source: the capture still delivers it here.
1901        h.mouse(PointerPhase::Move, 390.0, 590.0);
1902        assert_eq!(
1903            h.state.coordinator.state().session().map(|s| s.pointer),
1904            Some(Point::new(390.0, 590.0))
1905        );
1906        assert!(h.root.is_pointer_captured());
1907        h.mouse(PointerPhase::Up, 390.0, 590.0);
1908        assert!(!h.root.is_pointer_captured(), "released on the physical Up");
1909    }
1910
1911    #[test]
1912    fn releasing_over_a_target_drops_on_it() {
1913        let mut h = Harness::new(Cfg::default());
1914        let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
1915        h.mouse_drag();
1916        assert_eq!(
1917            h.state
1918                .coordinator
1919                .state()
1920                .session()
1921                .and_then(|s| s.hovered),
1922            None,
1923            "(60, 150) is outside the target"
1924        );
1925        h.mouse(PointerPhase::Move, 75.0, 155.0);
1926        assert_eq!(
1927            h.state
1928                .coordinator
1929                .state()
1930                .session()
1931                .and_then(|s| s.hovered),
1932            Some(target),
1933            "the move resolved the target under the pointer"
1934        );
1935        h.mouse(PointerPhase::Up, 80.0, 160.0);
1936        match h.state.coordinator.state() {
1937            DragState::Dropping { session, target: t } => {
1938                assert_eq!(t, target);
1939                assert_eq!(session.pointer, Point::new(80.0, 160.0));
1940            }
1941            other => panic!("expected a drop, got {other:?}"),
1942        }
1943    }
1944
1945    #[test]
1946    fn releasing_outside_every_target_cancels() {
1947        let mut h = Harness::new(Cfg::default());
1948        let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
1949        h.mouse_drag();
1950        h.mouse(PointerPhase::Move, 75.0, 155.0);
1951        // Released just outside the target, off the hover it had.
1952        h.mouse(PointerPhase::Up, 200.0, 160.0);
1953        assert_eq!(h.phase(), DragPhase::Idle);
1954        let changes = h.changes.borrow();
1955        let tail: Vec<_> = changes.iter().rev().take(4).rev().copied().collect();
1956        assert_eq!(
1957            tail,
1958            vec![
1959                DragStateChange::Leave { target },
1960                DragStateChange::Move {
1961                    pointer: Point::new(200.0, 160.0)
1962                },
1963                DragStateChange::Phase {
1964                    previous: DragPhase::Dragging,
1965                    next: DragPhase::Cancelled,
1966                },
1967                DragStateChange::Phase {
1968                    previous: DragPhase::Cancelled,
1969                    next: DragPhase::Idle,
1970                },
1971            ]
1972        );
1973    }
1974
1975    #[test]
1976    fn escape_cancels_the_drag_and_restores_focus() {
1977        let mut h = Harness::new(Cfg::default());
1978        assert!(
1979            !h.root.is_focus_active(),
1980            "nothing focused before the press"
1981        );
1982        let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
1983        h.mouse_drag();
1984        h.frame();
1985        assert!(
1986            h.root.is_focus_active(),
1987            "the press holds focus for the source"
1988        );
1989        assert_eq!(
1990            h.state
1991                .coordinator
1992                .state()
1993                .session()
1994                .and_then(|s| s.hovered),
1995            Some(target)
1996        );
1997        h.escape();
1998        assert_eq!(h.phase(), DragPhase::Idle, "Escape cancelled the session");
1999        assert!(h.changes.borrow().contains(&DragStateChange::Phase {
2000            previous: DragPhase::Dragging,
2001            next: DragPhase::Cancelled,
2002        }));
2003        assert!(
2004            h.changes
2005                .borrow()
2006                .contains(&DragStateChange::Leave { target }),
2007            "the hovered target heard Leave, not a drop"
2008        );
2009        assert!(
2010            !h.root.is_focus_active(),
2011            "focus is back to what it was before the press"
2012        );
2013        let after = h.frame();
2014        assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2015
2016        // The rest of the still-pressed gesture is swallowed.
2017        h.state.log.borrow_mut().clear();
2018        h.mouse(PointerPhase::Move, 32.0, 122.0);
2019        h.mouse(PointerPhase::Up, 32.0, 122.0);
2020        assert!(h.log().is_empty(), "{:?}", h.log());
2021        assert_eq!(h.state.taps, 0);
2022        assert!(!h.root.is_pointer_captured());
2023
2024        // And the next press works normally.
2025        h.mouse(PointerPhase::Down, 30.0, 120.0);
2026        h.mouse(PointerPhase::Up, 30.0, 120.0);
2027        assert_eq!(h.state.taps, 1);
2028    }
2029
2030    #[test]
2031    fn escape_cancels_through_a_child_that_holds_focus_and_leaves_it_focused() {
2032        let mut h = Harness::new(Cfg {
2033            focusable_child: true,
2034            ..Cfg::default()
2035        });
2036        h.mouse_drag();
2037        assert!(h.root.is_focus_active());
2038        h.escape();
2039        assert_eq!(h.phase(), DragPhase::Idle);
2040        assert!(
2041            !h.log().iter().any(|line| line.starts_with("Key")),
2042            "the source consumed Escape: {:?}",
2043            h.log()
2044        );
2045        h.mouse(PointerPhase::Up, 60.0, 150.0);
2046        // The child's own focus was never the source's to release.
2047        assert!(h.root.is_focus_active(), "the child is still focused");
2048        h.key(Key::Character("a".into()));
2049        assert_eq!(
2050            h.log().last().map(String::as_str),
2051            Some("Key Character(\"a\")"),
2052            "keys still reach the focused child"
2053        );
2054    }
2055
2056    #[test]
2057    fn escape_outside_a_drag_reaches_the_child() {
2058        let mut h = Harness::new(Cfg {
2059            focusable_child: true,
2060            ..Cfg::default()
2061        });
2062        h.mouse(PointerPhase::Down, 30.0, 120.0);
2063        h.mouse(PointerPhase::Up, 30.0, 120.0);
2064        h.escape();
2065        assert_eq!(
2066            h.log().last().map(String::as_str),
2067            Some("Key Named(Escape)")
2068        );
2069    }
2070
2071    #[test]
2072    fn a_drop_releases_the_focus_path_the_press_claimed() {
2073        let mut h = Harness::new(Cfg::default());
2074        h.mouse_drag();
2075        h.mouse(PointerPhase::Up, 60.0, 150.0);
2076        assert_eq!(h.phase(), DragPhase::Idle);
2077        // The focus path is gone: a key routes to nothing, and a later Escape
2078        // (no session) is inert.
2079        h.escape();
2080        assert!(!h.log().iter().any(|line| line.starts_with("Key")));
2081        // The next press elsewhere settles the root's own flag.
2082        h.mouse(PointerPhase::Down, 300.0, 500.0);
2083        h.mouse(PointerPhase::Up, 300.0, 500.0);
2084        assert!(!h.root.is_focus_active());
2085    }
2086
2087    #[test]
2088    fn a_pointer_cancel_abandons_the_session() {
2089        let mut h = Harness::new(Cfg::default());
2090        h.mouse_drag();
2091        h.frame();
2092        h.mouse(PointerPhase::Cancel, 60.0, 150.0);
2093        assert_eq!(h.phase(), DragPhase::Idle);
2094        assert!(h.changes.borrow().contains(&DragStateChange::Phase {
2095            previous: DragPhase::Dragging,
2096            next: DragPhase::Cancelled,
2097        }));
2098        assert!(!h.root.is_pointer_captured());
2099        let after = h.frame();
2100        assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2101        assert_eq!(h.state.taps, 0);
2102    }
2103
2104    #[test]
2105    fn a_session_cancelled_elsewhere_closes_the_ghost_and_swallows_the_rest() {
2106        let mut h = Harness::new(Cfg::default());
2107        h.mouse_drag();
2108        h.frame();
2109        h.state.coordinator.cancel();
2110        let after = h.frame();
2111        assert_eq!(rects(&after), vec![rest()], "closed on the next frame");
2112        h.state.log.borrow_mut().clear();
2113        h.mouse(PointerPhase::Move, 90.0, 160.0);
2114        h.mouse(PointerPhase::Up, 90.0, 160.0);
2115        assert!(
2116            h.log().is_empty(),
2117            "the child hears nothing more: {:?}",
2118            h.log()
2119        );
2120        assert_eq!(h.phase(), DragPhase::Idle);
2121
2122        // And the next press works normally.
2123        h.mouse(PointerPhase::Down, 30.0, 120.0);
2124        h.mouse(PointerPhase::Up, 30.0, 120.0);
2125        assert_eq!(h.state.taps, 1);
2126    }
2127
2128    #[test]
2129    fn dim_feedback_paints_the_source_under_a_dim_layer() {
2130        let mut h = Harness::new(Cfg::default());
2131        h.mouse_drag();
2132        let scene = h.frame();
2133        assert_eq!(
2134            scene.layers.first(),
2135            Some(&(rest().origin(), CHILD, DIM_OPACITY))
2136        );
2137        assert_eq!(rects(&scene).first(), Some(&rest()));
2138    }
2139
2140    #[test]
2141    fn placeholder_feedback_leaves_the_rest_position_empty() {
2142        let mut h = Harness::new(Cfg {
2143            feedback: SourceFeedback::Placeholder,
2144            ..Cfg::default()
2145        });
2146        h.mouse_drag();
2147        let scene = h.frame();
2148        assert_eq!(
2149            rects(&scene),
2150            vec![Rect::from_origin_size(Point::new(40.0, 130.0), CHILD)],
2151            "only the ghost paints the child"
2152        );
2153    }
2154
2155    #[test]
2156    fn no_feedback_leaves_the_source_untouched() {
2157        let mut h = Harness::new(Cfg {
2158            feedback: SourceFeedback::None,
2159            ..Cfg::default()
2160        });
2161        h.mouse_drag();
2162        let scene = h.frame();
2163        assert_eq!(
2164            scene.layers.len(),
2165            1,
2166            "only the ghost's layer: {:?}",
2167            scene.layers
2168        );
2169        assert_eq!(rects(&scene).first(), Some(&rest()));
2170    }
2171
2172    #[test]
2173    fn a_ghost_builder_replaces_the_snapshot() {
2174        let mut h = Harness::new(Cfg {
2175            custom_ghost: true,
2176            ..Cfg::default()
2177        });
2178        h.mouse_drag();
2179        let scene = h.frame();
2180        assert_eq!(rects(&scene), vec![rest()], "the child is not snapshotted");
2181        assert!(
2182            scene.layers.contains(&(
2183                Point::new(40.0, 130.0),
2184                Size::new(30.0, 20.0),
2185                GHOST_OPACITY
2186            )),
2187            "the built ghost floats at the pointer, at its own size: {:?}",
2188            scene.layers
2189        );
2190    }
2191
2192    #[test]
2193    fn a_secondary_press_never_arms() {
2194        let mut h = Harness::new(Cfg::default());
2195        h.root.event(
2196            &mut h.state,
2197            &InputEvent::Pointer(PointerEvent {
2198                phase: PointerPhase::Down,
2199                position: Point::new(30.0, 120.0),
2200                button: PointerButton::Secondary,
2201            }),
2202        );
2203        assert_eq!(h.phase(), DragPhase::Idle);
2204        assert_eq!(h.log(), vec!["Down@20,20"], "it still reaches the child");
2205    }
2206
2207    #[test]
2208    fn a_plain_tap_leaves_the_source_focused_for_a_later_keyboard_lift() {
2209        let mut h = Harness::new(Cfg::default());
2210        assert!(!h.root.is_focus_active(), "nothing focused yet");
2211        h.mouse(PointerPhase::Down, 30.0, 120.0);
2212        h.mouse(PointerPhase::Up, 30.0, 120.0);
2213        assert_eq!(h.state.taps, 1, "the tap still reaches the child");
2214        assert!(
2215            h.root.is_focus_active(),
2216            "a plain tap keeps the focus claim, unlike a drag's end"
2217        );
2218    }
2219
2220    #[test]
2221    fn keyboard_lift_cycle_and_drop_deliver_the_payload_to_the_hovered_target() {
2222        let mut h = Harness::new(Cfg::default());
2223        let first = h.target(Rect::new(0.0, 0.0, 100.0, 100.0));
2224        let second = h.target(Rect::new(100.0, 0.0, 200.0, 100.0));
2225
2226        // Focus the source with a plain tap — no drag, no pointer capture.
2227        h.mouse(PointerPhase::Down, 30.0, 120.0);
2228        h.mouse(PointerPhase::Up, 30.0, 120.0);
2229        assert!(h.root.is_focus_active());
2230
2231        h.key(Key::Named(NamedKey::Enter));
2232        assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
2233        assert_eq!(
2234            h.state.coordinator.with_payload(|item: &u32| *item),
2235            Some(7),
2236            "the payload builder ran against the state"
2237        );
2238        assert_eq!(
2239            h.state
2240                .coordinator
2241                .state()
2242                .session()
2243                .and_then(|s| s.hovered),
2244            None,
2245            "nothing hovered until a cycle step"
2246        );
2247
2248        h.key(Key::Named(NamedKey::ArrowRight));
2249        assert_eq!(
2250            h.state
2251                .coordinator
2252                .state()
2253                .session()
2254                .and_then(|s| s.hovered),
2255            Some(first),
2256            "the first registered target is hovered after one step"
2257        );
2258
2259        h.key(Key::Named(NamedKey::ArrowRight));
2260        assert_eq!(
2261            h.state
2262                .coordinator
2263                .state()
2264                .session()
2265                .and_then(|s| s.hovered),
2266            Some(second)
2267        );
2268
2269        h.key(Key::Named(NamedKey::Enter));
2270        assert_eq!(
2271            h.phase(),
2272            DragPhase::Dropping,
2273            "Enter drops on the hovered target"
2274        );
2275        match h.state.coordinator.state() {
2276            DragState::Dropping { target, .. } => assert_eq!(target, second),
2277            other => panic!("expected Dropping, got {other:?}"),
2278        }
2279        assert_eq!(
2280            h.state.coordinator.take_payload::<u32>().map(|p| *p),
2281            Some(7),
2282            "the payload the lift carried reaches the dropped-on target"
2283        );
2284        h.state.coordinator.complete_drop();
2285        assert_eq!(h.phase(), DragPhase::Idle);
2286        assert!(
2287            !h.root.is_focus_active(),
2288            "a completed drop releases focus, like a pointer drop"
2289        );
2290    }
2291
2292    #[test]
2293    fn keyboard_drop_with_nothing_hovered_cancels() {
2294        let mut h = Harness::new(Cfg::default());
2295        h.mouse(PointerPhase::Down, 30.0, 120.0);
2296        h.mouse(PointerPhase::Up, 30.0, 120.0);
2297        h.key(Key::Named(NamedKey::Enter));
2298        assert_eq!(h.phase(), DragPhase::Dragging);
2299        h.key(Key::Named(NamedKey::Enter));
2300        assert_eq!(h.phase(), DragPhase::Idle, "dropping on nothing cancels");
2301    }
2302
2303    #[test]
2304    fn escape_cancels_a_keyboard_lift_and_a_later_lift_still_works() {
2305        let mut h = Harness::new(Cfg::default());
2306        h.mouse(PointerPhase::Down, 30.0, 120.0);
2307        h.mouse(PointerPhase::Up, 30.0, 120.0);
2308        h.key(Key::Named(NamedKey::Enter));
2309        assert_eq!(h.phase(), DragPhase::Dragging);
2310        h.escape();
2311        assert_eq!(h.phase(), DragPhase::Idle);
2312        assert!(
2313            !h.root.is_focus_active(),
2314            "Escape ends the focus session outright, even for a keyboard lift"
2315        );
2316
2317        // Not stuck in a swallowing state: another tap, then another lift.
2318        h.mouse(PointerPhase::Down, 30.0, 120.0);
2319        h.mouse(PointerPhase::Up, 30.0, 120.0);
2320        h.key(Key::Named(NamedKey::Enter));
2321        assert_eq!(h.phase(), DragPhase::Dragging, "a later lift still works");
2322    }
2323
2324    #[test]
2325    fn space_also_lifts_and_drops() {
2326        let mut h = Harness::new(Cfg::default());
2327        h.mouse(PointerPhase::Down, 30.0, 120.0);
2328        h.mouse(PointerPhase::Up, 30.0, 120.0);
2329        h.key(Key::Character(" ".into()));
2330        assert_eq!(h.phase(), DragPhase::Dragging, "Space lifts");
2331        h.key(Key::Character(" ".into()));
2332        assert_eq!(
2333            h.phase(),
2334            DragPhase::Idle,
2335            "Space drops (on nothing: a cancel)"
2336        );
2337    }
2338
2339    #[test]
2340    fn a_focused_child_keeps_its_own_keys_the_lift_chord_included() {
2341        let mut h = Harness::new(Cfg {
2342            focusable_child: true,
2343            ..Cfg::default()
2344        });
2345        h.mouse(PointerPhase::Down, 30.0, 120.0);
2346        h.mouse(PointerPhase::Up, 30.0, 120.0);
2347        assert!(h.root.is_focus_active(), "the child holds its own focus");
2348        h.key(Key::Named(NamedKey::Enter));
2349        assert_eq!(
2350            h.phase(),
2351            DragPhase::Idle,
2352            "the child's own focus claim takes the key first — no lift"
2353        );
2354        assert_eq!(h.log().last().map(String::as_str), Some("Key Named(Enter)"));
2355    }
2356
2357    #[test]
2358    fn semantics_reports_a_button_role_with_click_and_a_label_for_each_state() {
2359        let mut h = Harness::new(Cfg::default());
2360        let update = h.root.semantics();
2361        let (_, node) = update
2362            .nodes
2363            .iter()
2364            .find(|(_, n)| n.role() == Role::Button)
2365            .expect("a Role::Button node was pushed");
2366        assert_eq!(
2367            node.label(),
2368            Some("Draggable"),
2369            "idle names no verb Click does not perform"
2370        );
2371        assert_eq!(node.description(), Some("Press Enter to lift"));
2372        assert!(node.supports_action(Action::Click));
2373
2374        h.mouse_drag();
2375        h.frame();
2376        let update = h.root.semantics();
2377        let (_, node) = update
2378            .nodes
2379            .iter()
2380            .find(|(_, n)| n.role() == Role::Button)
2381            .expect("a Role::Button node was pushed");
2382        assert_eq!(
2383            node.label(),
2384            Some("Dragging"),
2385            "a pointer session flips the label, but Click still only cancels it"
2386        );
2387
2388        h.mouse(PointerPhase::Up, 60.0, 150.0);
2389        h.mouse(PointerPhase::Down, 30.0, 120.0);
2390        h.mouse(PointerPhase::Up, 30.0, 120.0);
2391        h.key(Key::Named(NamedKey::Enter));
2392        let update = h.root.semantics();
2393        let (_, node) = update
2394            .nodes
2395            .iter()
2396            .find(|(_, n)| n.role() == Role::Button)
2397            .expect("a Role::Button node was pushed");
2398        assert_eq!(
2399            node.label(),
2400            Some("Drop"),
2401            "a live keyboard session labels Click honestly: it drops"
2402        );
2403    }
2404
2405    /// Where `perform_accessibility_action`'s `Action::Click` synthesizes its
2406    /// `Down`+`Up` pair: this node's own bounds center, in window space.
2407    fn node_center() -> Point {
2408        Point::new(AT.x + CHILD.width / 2.0, AT.y + CHILD.height / 2.0)
2409    }
2410
2411    #[test]
2412    fn an_accessibility_click_on_an_idle_focused_source_is_a_plain_tap() {
2413        let mut h = Harness::new(Cfg::default());
2414        // Focus the source with a plain tap first, same as a real click would.
2415        h.mouse(PointerPhase::Down, 30.0, 120.0);
2416        h.mouse(PointerPhase::Up, 30.0, 120.0);
2417        assert!(h.root.is_focus_active());
2418        h.state.taps = 0;
2419        h.state.log.borrow_mut().clear();
2420
2421        // `Action::Click` reaches the widget as a synthesized `Down` then
2422        // `Up` at the node's own center — reproduced here through the
2423        // widget's own event path, not the accessibility seam itself.
2424        let center = node_center();
2425        h.mouse(PointerPhase::Down, center.x, center.y);
2426        h.mouse(PointerPhase::Up, center.x, center.y);
2427
2428        assert_eq!(h.state.taps, 1, "Click reaches the child as a plain tap");
2429        assert_eq!(h.phase(), DragPhase::Idle, "Click never lifts a drag");
2430    }
2431
2432    #[test]
2433    fn an_accessibility_click_during_a_live_keyboard_session_with_nothing_hovered_cancels_it() {
2434        let mut h = Harness::new(Cfg::default());
2435        h.mouse(PointerPhase::Down, 30.0, 120.0);
2436        h.mouse(PointerPhase::Up, 30.0, 120.0);
2437        h.key(Key::Named(NamedKey::Enter));
2438        assert_eq!(
2439            h.phase(),
2440            DragPhase::Dragging,
2441            "Enter lifts a keyboard session"
2442        );
2443        assert_eq!(
2444            h.state.taps, 1,
2445            "the earlier focusing tap reached the child"
2446        );
2447        h.state.log.borrow_mut().clear();
2448
2449        // `Action::Click` arriving mid-session: the same synthesized `Down`
2450        // then `Up` at the node's own center. With nothing hovered this
2451        // performs exactly what Enter/Space would — a cancel, not a drop —
2452        // reusing that chord rather than treating the `Down` as a stray
2453        // press; the matching `Up` is swallowed, never reaching the child
2454        // as a second tap.
2455        let center = node_center();
2456        h.mouse(PointerPhase::Down, center.x, center.y);
2457        h.mouse(PointerPhase::Up, center.x, center.y);
2458
2459        assert_eq!(h.phase(), DragPhase::Idle, "the session ended, not dropped");
2460        assert_eq!(h.state.taps, 1, "the matching Up never reached the child");
2461        assert!(h.log().is_empty(), "the Down/Up never reached the child");
2462        assert!(
2463            h.changes.borrow().iter().any(|c| matches!(
2464                c,
2465                DragStateChange::Phase {
2466                    next: DragPhase::Cancelled,
2467                    ..
2468                }
2469            )),
2470            "cancelled, never a drop: {:?}",
2471            h.changes.borrow()
2472        );
2473        assert!(
2474            !h.changes.borrow().iter().any(|c| matches!(
2475                c,
2476                DragStateChange::Phase {
2477                    next: DragPhase::Dropping,
2478                    ..
2479                }
2480            )),
2481            "never a drop: {:?}",
2482            h.changes.borrow()
2483        );
2484    }
2485
2486    #[test]
2487    fn an_accessibility_click_during_a_live_keyboard_session_drops_onto_the_hovered_target() {
2488        let mut h = Harness::new(Cfg::default());
2489        let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
2490        h.mouse(PointerPhase::Down, 30.0, 120.0);
2491        h.mouse(PointerPhase::Up, 30.0, 120.0);
2492        h.key(Key::Named(NamedKey::Enter));
2493        h.key(Key::Named(NamedKey::ArrowRight));
2494        assert_eq!(
2495            h.state
2496                .coordinator
2497                .state()
2498                .session()
2499                .and_then(|s| s.hovered),
2500            Some(target),
2501            "cycling landed on the only registered target"
2502        );
2503        h.state.log.borrow_mut().clear();
2504
2505        // The same synthesized `Down`+`Up` as above, but with a target
2506        // hovered: it drops onto it, exactly like Enter/Space would, and
2507        // the matching `Up` still never reaches the child.
2508        let center = node_center();
2509        h.mouse(PointerPhase::Down, center.x, center.y);
2510        h.mouse(PointerPhase::Up, center.x, center.y);
2511
2512        assert_eq!(
2513            h.phase(),
2514            DragPhase::Dropping,
2515            "the coordinator reached Dropping on the hovered target"
2516        );
2517        match h.state.coordinator.state() {
2518            DragState::Dropping { target: on, .. } => assert_eq!(on, target),
2519            other => panic!("expected Dropping, got {other:?}"),
2520        }
2521        assert!(h.log().is_empty(), "the Down/Up never reached the child");
2522
2523        h.state.coordinator.complete_drop();
2524        assert_eq!(h.phase(), DragPhase::Idle);
2525    }
2526
2527    #[test]
2528    fn a_keyboard_session_ended_elsewhere_returns_to_idle_ready_for_another_lift() {
2529        let mut h = Harness::new(Cfg::default());
2530        h.mouse(PointerPhase::Down, 30.0, 120.0);
2531        h.mouse(PointerPhase::Up, 30.0, 120.0);
2532        h.key(Key::Named(NamedKey::Enter));
2533        assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
2534
2535        // Ended from outside this source entirely — a target's own
2536        // click-to-drop handling, or a cancel raised elsewhere — never
2537        // through this widget's own key or pointer paths.
2538        h.state.coordinator.cancel();
2539        let after = h.frame();
2540        assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2541        assert!(
2542            h.root.is_focus_active(),
2543            "the focus claim is left exactly as it was, not released"
2544        );
2545
2546        // Not stuck swallowing a pointer gesture that will never arrive: a
2547        // later lift still works.
2548        h.key(Key::Named(NamedKey::Enter));
2549        assert_eq!(
2550            h.phase(),
2551            DragPhase::Dragging,
2552            "the source landed back on Idle, ready for another lift"
2553        );
2554    }
2555}