Skip to main content

frust_widgets/drag/
coordinator.rs

1//! [`DragCoordinator`]: the shared drag-session state machine. See the
2//! [module docs](super) for the session model, transition rules and
3//! notification contract.
4//!
5//! # Keyboard sessions
6//!
7//! [`DragCoordinator::lift`] begins a session the same way
8//! [`DragCoordinator::arm`] + [`DragCoordinator::begin`] do, but with no
9//! pointer driving it: [`DragSession::keyboard`] marks the result, and
10//! [`DragCoordinator::update_pointer`]/[`DragCoordinator::resolve_hover`]
11//! both no-op for one rather than resolving a registry point that was never
12//! reported. Hover instead advances explicitly, through
13//! [`DragCoordinator::move_to_next_target`]/
14//! [`DragCoordinator::move_to_previous_target`] — cycling the registered
15//! targets in registration ("tree") order, wrapping at either end, and
16//! starting at the first (next) or last (previous) target when nothing is
17//! hovered yet. Cycling skips exactly what pointer resolution skips: a
18//! target that accepts a different payload type, and one that did not
19//! report visible bounds in the latest paint pass (see the [module
20//! docs](super) on *Target resolution*). Both raise the same `Leave`/`Enter` notification pair
21//! pointer resolution does, so a target subscriber (or poller — see
22//! [`mod@super::target`]) sees identical callbacks regardless of which drove
23//! the session. [`DragCoordinator::drop`]/[`DragCoordinator::cancel`] are
24//! unchanged: a keyboard session drops on whatever
25//! [`move_to_next_target`](DragCoordinator::move_to_next_target)/
26//! [`move_to_previous_target`](DragCoordinator::move_to_previous_target)
27//! left hovered (or cancels, with nothing hovered) exactly like a pointer
28//! release does. The ghost's placement for a keyboard session — anchored at
29//! the source's own bounds on [`lift`](DragCoordinator::lift), then at the
30//! hovered target's bounds on every `Enter` — is
31//! [`mod@super::draggable`]'s concern, not the coordinator's; see its
32//! module docs.
33
34use std::any::{Any, TypeId};
35use std::cell::{Ref, RefCell};
36use std::collections::{HashMap, VecDeque};
37use std::fmt;
38use std::path::PathBuf;
39use std::rc::{Rc, Weak};
40
41use frust_core::FrameTime;
42use kurbo::{Point, Rect, Vec2};
43
44/// Identity of a draggable source, allocated by
45/// [`DragCoordinator::new_source_id`] and unique within that coordinator.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct DragSourceId(u64);
48
49impl DragSourceId {
50    /// The raw id.
51    pub fn get(self) -> u64 {
52        self.0
53    }
54}
55
56/// Identity of a drop target, allocated by
57/// [`DragCoordinator::new_target_id`] and unique within that coordinator.
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct DragTargetId(u64);
60
61impl DragTargetId {
62    /// The raw id.
63    pub fn get(self) -> u64 {
64        self.0
65    }
66}
67
68/// Where a drag session came from.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum DragKind {
71    /// Started in-app by a source through [`DragCoordinator::arm`] +
72    /// [`DragCoordinator::begin`]; carries whatever payload the source handed
73    /// over.
74    Internal,
75    /// An OS file drag entering the window, started by
76    /// [`DragCoordinator::begin_external`]. Has no source; its payload is the
77    /// same `Vec<PathBuf>` this variant lists.
78    ExternalFiles(Vec<PathBuf>),
79}
80
81/// The phase tag of a drag session — what a [`DragStateChange::Phase`]
82/// reports and [`DragCoordinator::phase`] reads.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub enum DragPhase {
85    /// No session.
86    Idle,
87    /// A source was pressed and may start a drag.
88    Armed,
89    /// A drag is in flight.
90    Dragging,
91    /// Released over a target; waiting for it to claim the payload and call
92    /// [`DragCoordinator::complete_drop`].
93    Dropping,
94    /// Transient: the session was abandoned. Only ever reported between the
95    /// phase it left and `Idle`; [`DragCoordinator::phase`] never reads it.
96    Cancelled,
97}
98
99/// A snapshot of an in-flight (`Dragging`/`Dropping`) session, minus the
100/// payload itself.
101#[derive(Clone, Debug, PartialEq)]
102pub struct DragSession {
103    /// Where the session came from.
104    pub kind: DragKind,
105    /// The source that started it; `None` for [`DragKind::ExternalFiles`].
106    pub source: Option<DragSourceId>,
107    /// The point the source was pressed at (for an external session, the
108    /// point the OS drag first entered at).
109    pub press: Point,
110    /// The latest pointer position ([`DragCoordinator::update_pointer`]).
111    pub pointer: Point,
112    /// Offset from the pointer to the ghost's origin
113    /// ([`DragCoordinator::set_ghost_offset`]); zero until a source sets it.
114    pub ghost_offset: Vec2,
115    /// The target currently under the pointer, if any. While `Dropping` it
116    /// is the drop target.
117    pub hovered: Option<DragTargetId>,
118    /// The payload's type, while the payload is still held — `None` once a
119    /// target has moved it out with [`DragCoordinator::take_payload`].
120    pub payload_type: Option<TypeId>,
121    /// Whether this session was begun by [`DragCoordinator::lift`] rather
122    /// than [`DragCoordinator::begin`]/[`DragCoordinator::begin_external`] —
123    /// a keyboard session, with no pointer driving it.
124    /// [`DragCoordinator::update_pointer`]/[`DragCoordinator::resolve_hover`]
125    /// no-op for one; hover instead advances only through
126    /// [`DragCoordinator::move_to_next_target`]/
127    /// [`DragCoordinator::move_to_previous_target`] (see the [module
128    /// docs](self#keyboard-sessions)).
129    pub keyboard: bool,
130}
131
132impl DragSession {
133    /// Where the ghost's origin sits: the pointer plus the ghost offset.
134    pub fn ghost_origin(&self) -> Point {
135        self.pointer + self.ghost_offset
136    }
137}
138
139/// A snapshot of a coordinator's state ([`DragCoordinator::state`]).
140#[derive(Clone, Debug, PartialEq)]
141pub enum DragState {
142    /// No session.
143    Idle,
144    /// `source` was pressed at `press` and may start a drag.
145    Armed {
146        /// The pressed source.
147        source: DragSourceId,
148        /// Where it was pressed.
149        press: Point,
150        /// Whether this arm came from [`DragCoordinator::lift`] rather than
151        /// [`DragCoordinator::arm`] — see [`DragSession::keyboard`].
152        keyboard: bool,
153    },
154    /// A drag is in flight.
155    Dragging(DragSession),
156    /// Released over `target`, awaiting [`DragCoordinator::complete_drop`].
157    Dropping {
158        /// The session being dropped.
159        session: DragSession,
160        /// The target it was released over.
161        target: DragTargetId,
162    },
163}
164
165impl DragState {
166    /// This state's phase tag.
167    pub fn phase(&self) -> DragPhase {
168        match self {
169            DragState::Idle => DragPhase::Idle,
170            DragState::Armed { .. } => DragPhase::Armed,
171            DragState::Dragging(_) => DragPhase::Dragging,
172            DragState::Dropping { .. } => DragPhase::Dropping,
173        }
174    }
175
176    /// The in-flight session, while `Dragging` or `Dropping`.
177    pub fn session(&self) -> Option<&DragSession> {
178        match self {
179            DragState::Dragging(session) | DragState::Dropping { session, .. } => Some(session),
180            DragState::Idle | DragState::Armed { .. } => None,
181        }
182    }
183}
184
185/// One change a [`DragCoordinator::subscribe`] listener hears.
186#[derive(Clone, Copy, Debug, PartialEq)]
187pub enum DragStateChange {
188    /// The session moved from `previous` to `next`.
189    Phase {
190        /// The phase left.
191        previous: DragPhase,
192        /// The phase entered.
193        next: DragPhase,
194    },
195    /// The pointer moved onto `target` while dragging.
196    Enter {
197        /// The newly hovered target.
198        target: DragTargetId,
199    },
200    /// The pointer left `target`, or the session hovering it (or about to
201    /// drop on it) was cancelled.
202    Leave {
203        /// The target no longer hovered.
204        target: DragTargetId,
205    },
206    /// The dragged pointer moved to `pointer`.
207    Move {
208        /// The new pointer position.
209        pointer: Point,
210    },
211}
212
213/// A subscribed [`DragCoordinator::subscribe`] listener.
214type Listener = Rc<dyn Fn(DragStateChange)>;
215
216/// An in-flight session's full state, payload included.
217struct Session {
218    kind: DragKind,
219    source: Option<DragSourceId>,
220    press: Point,
221    pointer: Point,
222    ghost_offset: Vec2,
223    hovered: Option<DragTargetId>,
224    payload: Option<Box<dyn Any>>,
225    /// Recorded at `begin` rather than read back off the box, where
226    /// `type_id()` would resolve on `Box<dyn Any>` itself.
227    payload_type: TypeId,
228    /// See [`DragSession::keyboard`].
229    keyboard: bool,
230}
231
232impl Session {
233    fn snapshot(&self) -> DragSession {
234        DragSession {
235            kind: self.kind.clone(),
236            source: self.source,
237            press: self.press,
238            pointer: self.pointer,
239            ghost_offset: self.ghost_offset,
240            hovered: self.hovered,
241            payload_type: self.payload.as_ref().map(|_| self.payload_type),
242            keyboard: self.keyboard,
243        }
244    }
245}
246
247/// The coordinator's internal state machine.
248enum Machine {
249    Idle,
250    Armed {
251        source: DragSourceId,
252        press: Point,
253        keyboard: bool,
254    },
255    Dragging(Session),
256    Dropping {
257        session: Session,
258        target: DragTargetId,
259    },
260}
261
262impl Machine {
263    fn phase(&self) -> DragPhase {
264        match self {
265            Machine::Idle => DragPhase::Idle,
266            Machine::Armed { .. } => DragPhase::Armed,
267            Machine::Dragging(_) => DragPhase::Dragging,
268            Machine::Dropping { .. } => DragPhase::Dropping,
269        }
270    }
271
272    fn session(&self) -> Option<&Session> {
273        match self {
274            Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
275            Machine::Idle | Machine::Armed { .. } => None,
276        }
277    }
278
279    fn session_mut(&mut self) -> Option<&mut Session> {
280        match self {
281            Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
282            Machine::Idle | Machine::Armed { .. } => None,
283        }
284    }
285}
286
287/// The state every clone of a [`DragCoordinator`] shares.
288struct Shared {
289    machine: Machine,
290    /// The next source/target id handed out (one counter for both kinds).
291    next_id: u64,
292    /// Subscribed listeners in subscription order, keyed by the id their
293    /// guard removes them by.
294    listeners: Vec<(u64, Listener)>,
295    /// The id the next [`DragCoordinator::subscribe`] hands out.
296    next_listener: u64,
297    /// Changes produced and not yet delivered to every subscriber.
298    queue: VecDeque<DragStateChange>,
299    /// Whether a [`DragCoordinator::dispatch`] loop is running further up
300    /// the stack — a re-entrant mutation queues behind it instead of
301    /// delivering nested.
302    dispatching: bool,
303    /// Registered drop targets, keyed by id. An entry exists while the
304    /// target is registered.
305    targets: HashMap<DragTargetId, TargetEntry>,
306    /// The registration stamp the next newly registered target takes.
307    next_registration: u64,
308    /// The paint pass the latest bounds report belongs to — only targets
309    /// stamped with it are resolvable (see the [module docs](super)).
310    pass: u64,
311    /// The frame time that opened `pass`, when a clocked report
312    /// ([`DragCoordinator::report_target_bounds`]) opened it.
313    pass_frame: Option<FrameTime>,
314}
315
316/// One registered drop target's resolution record.
317#[derive(Clone, Copy, Debug)]
318struct TargetEntry {
319    /// When it registered, relative to every other target: a later stamp wins
320    /// an overlap in [`DragCoordinator::target_at`].
321    registration: u64,
322    /// The payload type it accepts, or `None` for a target registered untyped
323    /// ([`DragCoordinator::register_target`]), which accepts any.
324    accepts: Option<TypeId>,
325    /// Its visible window-space bounds as of its latest report; `None` until
326    /// the target first reports them, or when that report was clipped to
327    /// nothing.
328    bounds: Option<Rect>,
329    /// The paint pass of its latest report; `None` until it first reports.
330    reported: Option<u64>,
331}
332
333impl Default for Shared {
334    fn default() -> Self {
335        Self {
336            machine: Machine::Idle,
337            next_id: 1,
338            listeners: Vec::new(),
339            next_listener: 0,
340            queue: VecDeque::new(),
341            dispatching: false,
342            targets: HashMap::new(),
343            next_registration: 0,
344            pass: 0,
345            pass_frame: None,
346        }
347    }
348}
349
350impl Shared {
351    /// Abandon whatever is in flight, landing on `Idle`: a hovered target (or
352    /// an unclaimed drop's target) hears `Leave`, then the
353    /// `→ Cancelled → Idle` pair. Returns the abandoned session, if one was
354    /// in flight (`Dragging`/`Dropping`). A no-op from `Idle`.
355    fn cancel(&mut self, changes: &mut Vec<DragStateChange>) -> Option<Session> {
356        let previous = self.machine.phase();
357        let (leave, session) = match std::mem::replace(&mut self.machine, Machine::Idle) {
358            Machine::Idle => return None,
359            Machine::Armed { .. } => (None, None),
360            Machine::Dragging(session) => (session.hovered, Some(session)),
361            Machine::Dropping { session, target } => (Some(target), Some(session)),
362        };
363        if let Some(target) = leave {
364            changes.push(DragStateChange::Leave { target });
365        }
366        changes.push(DragStateChange::Phase {
367            previous,
368            next: DragPhase::Cancelled,
369        });
370        changes.push(DragStateChange::Phase {
371            previous: DragPhase::Cancelled,
372            next: DragPhase::Idle,
373        });
374        session
375    }
376
377    /// The payload type of the session in flight, if any — what a target's
378    /// accepted type is gated against.
379    fn live_payload_type(&self) -> Option<TypeId> {
380        self.machine.session().map(|session| session.payload_type)
381    }
382
383    /// `entry`'s bounds if it may take part in resolution or cycling right
384    /// now: reported in the latest paint pass, visible (not clipped to
385    /// nothing), and — while a session is in flight — accepting its payload
386    /// type (an untyped entry accepts any).
387    fn resolvable(&self, entry: &TargetEntry, payload: Option<TypeId>) -> Option<Rect> {
388        self.resolvable_as_of(entry, payload, self.pass)
389    }
390
391    /// [`Shared::resolvable`], against pass number `pass` rather than
392    /// always the current one — the building block
393    /// [`Shared::target_at_mid_pass`] uses to also admit the pass
394    /// immediately before the current one.
395    fn resolvable_as_of(
396        &self,
397        entry: &TargetEntry,
398        payload: Option<TypeId>,
399        pass: u64,
400    ) -> Option<Rect> {
401        if entry.reported != Some(pass) {
402            return None;
403        }
404        if let (Some(accepts), Some(payload)) = (entry.accepts, payload)
405            && accepts != payload
406        {
407            return None;
408        }
409        entry.bounds
410    }
411
412    /// The resolvable target whose bounds contain `point` — the latest
413    /// registered among overlapping ones (see the [module docs](super)).
414    fn target_at(&self, point: Point) -> Option<DragTargetId> {
415        let payload = self.live_payload_type();
416        self.targets
417            .iter()
418            .filter(|(_, entry)| {
419                self.resolvable(entry, payload)
420                    .is_some_and(|rect| rect.contains(point))
421            })
422            .max_by_key(|(_, entry)| entry.registration)
423            .map(|(&id, _)| id)
424    }
425
426    /// [`Shared::target_at`], but also admitting a target stamped with the
427    /// pass immediately before the current one — for
428    /// [`Shared::resolve_hover_mid_pass`], called mid-paint before every
429    /// target sharing the coordinator has necessarily repainted the frame
430    /// in progress, so a later-painting sibling that simply has not
431    /// reported *yet* this pass is not mistaken for one that stopped
432    /// reporting altogether. One pass of tolerance only: a target silent
433    /// for a whole pass on top of that is excluded exactly as
434    /// [`Shared::target_at`] excludes it. The overlap tie-break is
435    /// unchanged — later registration still wins regardless of which of
436    /// the two passes a candidate's bounds last came from — so a
437    /// later-registered target that simply has not repainted yet this pass
438    /// still outranks an earlier-registered one that already has, exactly
439    /// as it would once both catch up.
440    fn target_at_mid_pass(&self, point: Point) -> Option<DragTargetId> {
441        let payload = self.live_payload_type();
442        let previous = self.pass.checked_sub(1);
443        self.targets
444            .iter()
445            .filter(|(_, entry)| {
446                self.resolvable_as_of(entry, payload, self.pass)
447                    .or_else(|| {
448                        previous.and_then(|pass| self.resolvable_as_of(entry, payload, pass))
449                    })
450                    .is_some_and(|rect| rect.contains(point))
451            })
452            .max_by_key(|(_, entry)| entry.registration)
453            .map(|(&id, _)| id)
454    }
455
456    /// Record a bounds report for `id`, stamped with the paint pass it
457    /// belongs to. A clocked report (`frame` is `Some`) opens a new pass when
458    /// its frame time differs from the one that opened the current pass; an
459    /// unclocked one opens a new pass when this target already reported in
460    /// the current one (it is painting again, so a new pass has begun).
461    /// Returns whether `id` is registered.
462    fn record_bounds(
463        &mut self,
464        id: DragTargetId,
465        bounds: Option<Rect>,
466        frame: Option<FrameTime>,
467    ) -> bool {
468        let Some(reported) = self.targets.get(&id).map(|entry| entry.reported) else {
469            return false;
470        };
471        let opens = match frame {
472            Some(frame) => self.pass_frame != Some(frame),
473            None => reported == Some(self.pass),
474        };
475        if opens {
476            self.pass += 1;
477            self.pass_frame = frame;
478        }
479        let pass = self.pass;
480        if let Some(entry) = self.targets.get_mut(&id) {
481            entry.bounds = bounds;
482            entry.reported = Some(pass);
483        }
484        true
485    }
486
487    /// While `Dragging`, make `target` the hovered one: the old one (if any)
488    /// hears `Leave`, then the new one (if any) `Enter`. Nothing when
489    /// unchanged or not `Dragging`.
490    fn set_hovered(&mut self, target: Option<DragTargetId>, changes: &mut Vec<DragStateChange>) {
491        if let Machine::Dragging(session) = &mut self.machine
492            && session.hovered != target
493        {
494            if let Some(old) = session.hovered {
495                changes.push(DragStateChange::Leave { target: old });
496            }
497            session.hovered = target;
498            if let Some(target) = target {
499                changes.push(DragStateChange::Enter { target });
500            }
501        }
502    }
503
504    /// While `Dragging`, re-resolve the hovered target under the session's
505    /// current pointer against the registry's current bounds. A no-op for a
506    /// keyboard session ([`DragSession::keyboard`]) — there is no pointer to
507    /// resolve against, so this never overrides what
508    /// [`Shared::move_target`] last set.
509    fn resolve_hover(&mut self, changes: &mut Vec<DragStateChange>) {
510        let Machine::Dragging(session) = &self.machine else {
511            return;
512        };
513        if session.keyboard {
514            return;
515        }
516        let target = self.target_at(session.pointer);
517        self.set_hovered(target, changes);
518    }
519
520    /// [`Shared::resolve_hover`]'s twin for a caller mid-paint, before
521    /// every target sharing the coordinator has necessarily repainted the
522    /// frame in progress — see [`DragCoordinator::resolve_hover`]'s doc for
523    /// why that matters and [`Shared::target_at_mid_pass`] for the
524    /// tolerance itself. Otherwise identical: a no-op for a keyboard
525    /// session or outside `Dragging`.
526    fn resolve_hover_mid_pass(&mut self, changes: &mut Vec<DragStateChange>) {
527        let Machine::Dragging(session) = &self.machine else {
528            return;
529        };
530        if session.keyboard {
531            return;
532        }
533        let target = self.target_at_mid_pass(session.pointer);
534        self.set_hovered(target, changes);
535    }
536
537    /// `Idle → Armed`. The caller has already left `Idle` reachable.
538    fn arm_from_idle(
539        &mut self,
540        source: DragSourceId,
541        press: Point,
542        keyboard: bool,
543        changes: &mut Vec<DragStateChange>,
544    ) {
545        debug_assert!(matches!(self.machine, Machine::Idle));
546        self.machine = Machine::Armed {
547            source,
548            press,
549            keyboard,
550        };
551        changes.push(DragStateChange::Phase {
552            previous: DragPhase::Idle,
553            next: DragPhase::Armed,
554        });
555    }
556
557    /// Cycle the hovered target by `step` (`1`/`-1`) through the resolvable
558    /// targets ([`Shared::resolvable`]: visible in the latest paint pass and
559    /// accepting the session's payload type) in registration ("tree") order —
560    /// the [`DragCoordinator::move_to_next_target`]/
561    /// [`DragCoordinator::move_to_previous_target`] drive. Starts at the
562    /// first (`step > 0`) or last (`step < 0`) target when nothing (or a
563    /// target no longer resolvable) is hovered, and wraps around at either
564    /// end. Raises the same `Leave`/`Enter` pair pointer resolution does
565    /// ([`Shared::set_hovered`]). A no-op outside `Dragging` or with no
566    /// resolvable targets.
567    ///
568    /// Nothing here scrolls an off-screen target into view: only a target
569    /// visible in the latest paint pass is resolvable at all, so a list
570    /// taller than its viewport is only ever cycled across its currently
571    /// visible rows (`drag-keyboard-cycle-visible-targets-only`,
572    /// `docs/LIMITATIONS.md`).
573    fn move_target(&mut self, step: i64, changes: &mut Vec<DragStateChange>) {
574        let Machine::Dragging(session) = &self.machine else {
575            return;
576        };
577        let payload = Some(session.payload_type);
578        let mut ordered: Vec<(u64, DragTargetId)> = self
579            .targets
580            .iter()
581            .filter(|(_, entry)| self.resolvable(entry, payload).is_some())
582            .map(|(&id, entry)| (entry.registration, id))
583            .collect();
584        if ordered.is_empty() {
585            return;
586        }
587        ordered.sort_unstable();
588        let ids: Vec<DragTargetId> = ordered.into_iter().map(|(_, id)| id).collect();
589        let len = ids.len() as i64;
590        let next = match session
591            .hovered
592            .and_then(|current| ids.iter().position(|&id| id == current))
593        {
594            Some(index) => ids[(index as i64 + step).rem_euclid(len) as usize],
595            None if step >= 0 => ids[0],
596            None => ids[ids.len() - 1],
597        };
598        self.set_hovered(Some(next), changes);
599    }
600}
601
602/// A cloneable handle onto one drag scope's session state: sources
603/// [`arm`](Self::arm)/[`begin`](Self::begin) a drag and report its pointer
604/// with [`update_pointer`](Self::update_pointer), which resolves the hovered
605/// target against the registry ([`set_hovered`](Self::set_hovered) overrides
606/// it), and it ends in [`drop`](Self::drop) +
607/// [`complete_drop`](Self::complete_drop) or [`cancel`](Self::cancel). Every
608/// clone shares one state. See the [module docs](super) for the transition
609/// rules.
610///
611/// ```
612/// use frust_widgets::drag::{DragCoordinator, DragPhase};
613/// use kurbo::{Point, Rect};
614///
615/// let drag = DragCoordinator::new();
616/// let source = drag.new_source_id();
617/// let target = drag.new_target_id();
618/// drag.register_target(target);
619/// drag.set_target_bounds(target, Rect::new(50.0, 0.0, 150.0, 100.0));
620///
621/// drag.arm(source, Point::new(10.0, 10.0));
622/// drag.begin(42_u32);
623/// // Resolution: the pointer is inside the target's reported bounds.
624/// drag.update_pointer(Point::new(80.0, 40.0));
625/// assert_eq!(drag.state().session().and_then(|s| s.hovered), Some(target));
626/// assert_eq!(drag.drop(), Some(target));
627///
628/// // The target claims the payload, then completes the drop.
629/// assert_eq!(drag.take_payload::<u32>().as_deref(), Some(&42));
630/// drag.complete_drop();
631/// assert_eq!(drag.phase(), DragPhase::Idle);
632/// ```
633#[derive(Clone, Default)]
634pub struct DragCoordinator {
635    shared: Rc<RefCell<Shared>>,
636}
637
638impl fmt::Debug for DragCoordinator {
639    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640        let shared = self.shared.borrow();
641        f.debug_struct("DragCoordinator")
642            .field("phase", &shared.machine.phase())
643            .field("listeners", &shared.listeners.len())
644            .finish()
645    }
646}
647
648impl DragCoordinator {
649    /// A coordinator with no session in flight.
650    pub fn new() -> Self {
651        Self::default()
652    }
653
654    /// Allocate a source id, unique within this coordinator.
655    pub fn new_source_id(&self) -> DragSourceId {
656        DragSourceId(self.next_id())
657    }
658
659    /// Allocate a target id, unique within this coordinator.
660    pub fn new_target_id(&self) -> DragTargetId {
661        DragTargetId(self.next_id())
662    }
663
664    fn next_id(&self) -> u64 {
665        let mut shared = self.shared.borrow_mut();
666        let id = shared.next_id;
667        shared.next_id += 1;
668        id
669    }
670
671    /// A snapshot of the current state (payload excluded).
672    pub fn state(&self) -> DragState {
673        match &self.shared.borrow().machine {
674            Machine::Idle => DragState::Idle,
675            Machine::Armed {
676                source,
677                press,
678                keyboard,
679            } => DragState::Armed {
680                source: *source,
681                press: *press,
682                keyboard: *keyboard,
683            },
684            Machine::Dragging(session) => DragState::Dragging(session.snapshot()),
685            Machine::Dropping { session, target } => DragState::Dropping {
686                session: session.snapshot(),
687                target: *target,
688            },
689        }
690    }
691
692    /// The current phase — never [`DragPhase::Cancelled`], which is transient.
693    pub fn phase(&self) -> DragPhase {
694        self.shared.borrow().machine.phase()
695    }
696
697    /// `source` was pressed at `press`: `Idle → Armed`.
698    ///
699    /// Re-arming while `Armed`, or while an unclaimed drop is still
700    /// `Dropping`, cancels that first (`→ Cancelled → Idle`, the drop target
701    /// hearing `Leave`), then arms. Arming while `Dragging` is ignored with a
702    /// debug-build log — the session in flight owns the pointer. Returns
703    /// whether the coordinator is now armed by this call.
704    pub fn arm(&self, source: DragSourceId, press: Point) -> bool {
705        self.mutate(|shared, changes| {
706            if matches!(shared.machine, Machine::Dragging(_)) {
707                log_ignored("arm", "a drag is already in flight");
708                return false;
709            }
710            shared.cancel(changes);
711            shared.arm_from_idle(source, press, false, changes);
712            true
713        })
714    }
715
716    /// `source` is lifted by keyboard, with no pointer: `Idle → Armed`,
717    /// marked as a keyboard session so the [`begin`](Self::begin) that
718    /// follows carries [`DragSession::keyboard`] into `Dragging` — see the
719    /// [module docs](self#keyboard-sessions). Same re-arm/ignore rules as
720    /// [`arm`](Self::arm): re-arming while `Armed` (or while an unclaimed
721    /// drop is still `Dropping`) cancels that first; lifting while
722    /// `Dragging` is ignored (debug-build log). Returns whether the
723    /// coordinator is now armed by this call.
724    pub fn lift(&self, source: DragSourceId) -> bool {
725        self.mutate(|shared, changes| {
726            if matches!(shared.machine, Machine::Dragging(_)) {
727                log_ignored("lift", "a drag is already in flight");
728                return false;
729            }
730            shared.cancel(changes);
731            shared.arm_from_idle(source, Point::ZERO, true, changes);
732            true
733        })
734    }
735
736    /// Move the hovered target to the next one registered after it
737    /// (registration/tree order, wrapping to the first) — the keyboard
738    /// `ArrowRight`/`ArrowDown` drive for a [`lift`](Self::lift)ed session.
739    /// Only targets pointer resolution would consider take part: those
740    /// accepting the session's payload type and visible in the latest paint
741    /// pass. Starts at the first of them when nothing is hovered yet.
742    /// Raises the same `Leave`/`Enter` pair pointer resolution does. A
743    /// no-op outside `Dragging` or with no such targets — see the
744    /// [module docs](self#keyboard-sessions).
745    ///
746    /// Reaches only a target visible in the latest paint pass, and nothing
747    /// scrolls an off-screen one into view on the keyboard's behalf — a
748    /// list taller than its viewport cycles only across its currently
749    /// visible rows (`drag-keyboard-cycle-visible-targets-only`,
750    /// `docs/LIMITATIONS.md`).
751    pub fn move_to_next_target(&self) {
752        self.mutate(|shared, changes| shared.move_target(1, changes));
753    }
754
755    /// The mirror of
756    /// [`move_to_next_target`](Self::move_to_next_target) — the keyboard
757    /// `ArrowLeft`/`ArrowUp` drive, wrapping to the last eligible target.
758    /// Carries the same visible-targets-only reach.
759    pub fn move_to_previous_target(&self) {
760        self.mutate(|shared, changes| shared.move_target(-1, changes));
761    }
762
763    /// The armed source starts dragging `payload`: `Armed → Dragging`, with
764    /// the pointer at the press point and nothing hovered.
765    ///
766    /// `begin` without an arm is an error and is ignored (debug-build log),
767    /// as is `begin` while `Dropping`. A second `begin` while `Dragging`
768    /// cancels the first session (a hovered target hearing `Leave`) and
769    /// restarts from the same source with this payload — re-armed at the
770    /// first session's press point, then dragging from its current pointer
771    /// and ghost offset, so subscribers hear the full
772    /// `Dragging → Cancelled → Idle → Armed → Dragging` walk. An external
773    /// session has no source to restart from: it is cancelled and this
774    /// `begin` ignored. Returns whether a session is now dragging this
775    /// payload.
776    pub fn begin<T: Any>(&self, payload: T) -> bool {
777        self.mutate(|shared, changes| {
778            let (source, press, pointer, ghost_offset, keyboard) = match &shared.machine {
779                Machine::Idle => {
780                    log_ignored("begin", "nothing is armed");
781                    return false;
782                }
783                Machine::Dropping { .. } => {
784                    log_ignored("begin", "a drop is still being completed");
785                    return false;
786                }
787                Machine::Armed {
788                    source,
789                    press,
790                    keyboard,
791                } => (*source, *press, *press, Vec2::ZERO, *keyboard),
792                Machine::Dragging(_) => {
793                    let first = shared
794                        .cancel(changes)
795                        .expect("a Dragging machine cancels to its session");
796                    let Some(source) = first.source else {
797                        log_ignored("begin", "the cancelled session had no source to restart");
798                        return false;
799                    };
800                    shared.arm_from_idle(source, first.press, first.keyboard, changes);
801                    (
802                        source,
803                        first.press,
804                        first.pointer,
805                        first.ghost_offset,
806                        first.keyboard,
807                    )
808                }
809            };
810            shared.machine = Machine::Dragging(Session {
811                kind: DragKind::Internal,
812                source: Some(source),
813                press,
814                pointer,
815                ghost_offset,
816                hovered: None,
817                payload: Some(Box::new(payload)),
818                payload_type: TypeId::of::<T>(),
819                keyboard,
820            });
821            changes.push(DragStateChange::Phase {
822                previous: DragPhase::Armed,
823                next: DragPhase::Dragging,
824            });
825            true
826        })
827    }
828
829    /// An OS file drag entered at `pointer`: `Idle → Dragging` with
830    /// [`DragKind::ExternalFiles`], no source, and `paths` as the payload (a
831    /// `Vec<PathBuf>`, so `payload_is::<Vec<PathBuf>>()` holds). Any session
832    /// already in flight — armed, dragging or dropping — is cancelled first.
833    pub fn begin_external(&self, paths: Vec<PathBuf>, pointer: Point) {
834        self.mutate(|shared, changes| {
835            shared.cancel(changes);
836            shared.machine = Machine::Dragging(Session {
837                kind: DragKind::ExternalFiles(paths.clone()),
838                source: None,
839                press: pointer,
840                pointer,
841                ghost_offset: Vec2::ZERO,
842                hovered: None,
843                payload: Some(Box::new(paths)),
844                payload_type: TypeId::of::<Vec<PathBuf>>(),
845                keyboard: false,
846            });
847            changes.push(DragStateChange::Phase {
848                previous: DragPhase::Idle,
849                next: DragPhase::Dragging,
850            });
851        });
852    }
853
854    /// Replace the held `Vec<PathBuf>` payload of an in-flight
855    /// [`DragKind::ExternalFiles`] session with `paths`.
856    ///
857    /// A hover-phase file-drag notification carries no paths of its own (the
858    /// platform source delivers one file per hover with no "this is
859    /// everything" signal), so a caller that opens the session on hover via
860    /// [`begin_external`](Self::begin_external) starts it with an empty list
861    /// and calls this once the real list is known — typically right before
862    /// [`drop`](Self::drop), when the platform's drop notification finally
863    /// carries every path. Notifies nothing: this replaces payload data, not
864    /// phase or hover state, so no subscriber needs telling, and the target
865    /// that eventually [`take_payload`](Self::take_payload)s it sees exactly
866    /// `paths`. A silent no-op unless `Dragging` with `DragKind::ExternalFiles`
867    /// — in particular, never while `Dropping` (too late: the target may have
868    /// already taken the old payload).
869    pub fn set_external_payload(&self, paths: Vec<PathBuf>) {
870        if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine
871            && matches!(session.kind, DragKind::ExternalFiles(_))
872        {
873            session.kind = DragKind::ExternalFiles(paths.clone());
874            session.payload = Some(Box::new(paths));
875        }
876    }
877
878    /// The dragged pointer is at `pointer` (window space): record it, then
879    /// resolve the hovered target under it — the latest-registered target
880    /// whose reported bounds contain it ([`target_at`](Self::target_at)), or
881    /// none. A hover change is reported as `Leave` (the old target) then
882    /// `Enter` (the new one), and a changed pointer then as a
883    /// [`DragStateChange::Move`], so a newly entered target's first `Move` is
884    /// already inside it. Called with an unchanged pointer it still
885    /// re-resolves, which is how bounds that moved under a stationary pointer
886    /// are picked up (see also [`resolve_hover`](Self::resolve_hover)). A
887    /// silent no-op unless `Dragging`.
888    ///
889    /// Resolution reads the registry only: it never hit-tests the widget tree
890    /// (the ghost floats in a transparent overlay pod above it), and a hover
891    /// set explicitly with [`set_hovered`](Self::set_hovered) stands only
892    /// until the next call.
893    ///
894    /// A silent no-op for a keyboard session ([`DragSession::keyboard`]) —
895    /// nothing drives a real pointer during one, and a stray call must not
896    /// fight the hover [`move_to_next_target`](Self::move_to_next_target)/
897    /// [`move_to_previous_target`](Self::move_to_previous_target) last set
898    /// (see the [module docs](self#keyboard-sessions)).
899    pub fn update_pointer(&self, pointer: Point) {
900        self.mutate(|shared, changes| {
901            let Machine::Dragging(session) = &mut shared.machine else {
902                return;
903            };
904            if session.keyboard {
905                return;
906            }
907            let moved = session.pointer != pointer;
908            session.pointer = pointer;
909            shared.resolve_hover(changes);
910            if moved {
911                changes.push(DragStateChange::Move { pointer });
912            }
913        });
914    }
915
916    /// Re-resolve the hovered target under the current pointer against the
917    /// registry's current bounds, without moving the pointer — for a pass
918    /// that moved targets under a stationary pointer (an auto-scrolled list
919    /// re-reporting its rows' bounds). Raises the same `Leave`/`Enter` pair
920    /// [`update_pointer`](Self::update_pointer) does, and nothing when the
921    /// hover is unchanged. A silent no-op unless `Dragging`.
922    ///
923    /// Unlike [`update_pointer`](Self::update_pointer), this one tolerates
924    /// a target stamped with the pass immediately before the current one:
925    /// [`mod@super::auto_scroll`]'s zone calls it right after its own child
926    /// paints, which can be *before* a later sibling sharing this
927    /// coordinator gets its own turn to repaint in the same frame (a later
928    /// column, a `Stack` layer above, an overlay). Without that one-pass
929    /// tolerance, the sibling's still-current bounds would read as culled
930    /// for the moment between the two, flipping the hover away and back
931    /// for no visible reason. A target silent for a whole pass on top of
932    /// that one is excluded exactly as `update_pointer` excludes it — the
933    /// tolerance never masks a target that has genuinely stopped painting.
934    /// `update_pointer`/[`target_at`](Self::target_at) and keyboard cycling
935    /// stay strict to the latest pass; a pointer or keyboard event is never
936    /// dispatched mid-paint, so they never race a sibling's own turn.
937    pub fn resolve_hover(&self) {
938        self.mutate(|shared, changes| shared.resolve_hover_mid_pass(changes));
939    }
940
941    /// Set where the ghost's origin sits relative to the pointer (see
942    /// [`DragSession::ghost_origin`]). Notifies nothing — a source sets it
943    /// right after [`begin`](Self::begin), before the ghost first paints. A
944    /// silent no-op unless `Dragging`.
945    pub fn set_ghost_offset(&self, offset: Vec2) {
946        if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine {
947            session.ghost_offset = offset;
948        }
949    }
950
951    /// Override the hovered target with `target`, bypassing resolution: the
952    /// old one (if any) hears `Leave`, then the new one (if any) hears
953    /// `Enter`. Nothing when unchanged; a silent no-op unless `Dragging`. The
954    /// override stands until the next [`update_pointer`](Self::update_pointer)
955    /// or [`resolve_hover`](Self::resolve_hover) re-resolves against the
956    /// registry.
957    pub fn set_hovered(&self, target: Option<DragTargetId>) {
958        self.mutate(|shared, changes| shared.set_hovered(target, changes));
959    }
960
961    /// Register an untyped drop target for bounds tracking and resolution:
962    /// it accepts a session of any payload type. A target that accepts one
963    /// payload type registers with
964    /// [`register_target_for`](Self::register_target_for) instead, so
965    /// resolution and keyboard cycling skip it for every other type.
966    ///
967    /// The target is stamped after every target already registered — a later
968    /// registration wins an overlap in [`target_at`](Self::target_at).
969    /// Idempotent: re-registering an already-registered target is a no-op and
970    /// keeps its original stamp and accepted type.
971    pub fn register_target(&self, id: DragTargetId) {
972        self.register(id, None);
973    }
974
975    /// Register a drop target that accepts payloads of type `T` only:
976    /// [`target_at`](Self::target_at), pointer resolution and
977    /// [`move_to_next_target`](Self::move_to_next_target)/
978    /// [`move_to_previous_target`](Self::move_to_previous_target) consider it
979    /// only while the session in flight carries a `T` (an
980    /// [`DragKind::ExternalFiles`] session carries a `Vec<PathBuf>`).
981    /// Otherwise exactly [`register_target`](Self::register_target), same
982    /// stamping and idempotence.
983    pub fn register_target_for<T: Any>(&self, id: DragTargetId) {
984        self.register(id, Some(TypeId::of::<T>()));
985    }
986
987    fn register(&self, id: DragTargetId, accepts: Option<TypeId>) {
988        let mut shared = self.shared.borrow_mut();
989        if shared.targets.contains_key(&id) {
990            return;
991        }
992        let registration = shared.next_registration;
993        shared.next_registration += 1;
994        shared.targets.insert(
995            id,
996            TargetEntry {
997                registration,
998                accepts,
999                bounds: None,
1000                reported: None,
1001            },
1002        );
1003    }
1004
1005    /// Unregister a drop target. A hovered target is un-hovered and hears
1006    /// `Leave`; the pending target of an unclaimed drop ends the session
1007    /// instead — exactly a [`cancel`](Self::cancel): one `Leave`, then
1008    /// `Dropping → Cancelled → Idle`, the payload discarded — since no one is
1009    /// left to claim it. A silent no-op for an unregistered target.
1010    pub fn unregister_target(&self, id: DragTargetId) {
1011        self.mutate(|shared, changes| {
1012            shared.targets.remove(&id);
1013            match &mut shared.machine {
1014                Machine::Dragging(session) if session.hovered == Some(id) => {
1015                    session.hovered = None;
1016                    changes.push(DragStateChange::Leave { target: id });
1017                }
1018                Machine::Dropping { target, .. } if *target == id => {
1019                    shared.cancel(changes);
1020                }
1021                _ => {}
1022            }
1023        });
1024    }
1025
1026    /// Record the window-space bounds of a registered target, unclocked —
1027    /// see [`report_target_bounds`](Self::report_target_bounds), which a
1028    /// painted target uses instead. The report is stamped into the current
1029    /// paint pass, opening a new one when this target already reported in
1030    /// the current pass. Updates persist across drag sessions. Ignored (with
1031    /// a debug-build log) if the target is not registered.
1032    pub fn set_target_bounds(&self, id: DragTargetId, bounds: Rect) {
1033        if !self
1034            .shared
1035            .borrow_mut()
1036            .record_bounds(id, Some(bounds), None)
1037        {
1038            log_ignored("set_target_bounds", "the target is not registered");
1039        }
1040    }
1041
1042    /// Record what a registered target painted at frame `frame`: its visible
1043    /// window-space `bounds`, already clipped to its ancestors' visible
1044    /// region — `None` when clipped to nothing, which leaves it resolvable
1045    /// nowhere. Every report is stamped with a paint pass: a report whose
1046    /// `frame` differs from the one that opened the current pass opens a new
1047    /// pass, and only targets that reported in the latest pass resolve or
1048    /// cycle, so a target that stopped painting (culled, or scrolled out of a
1049    /// culling container) cannot resolve against stale bounds. A
1050    /// [`FrameTime::ZERO`] `frame` (no shell clock) falls back to
1051    /// [`set_target_bounds`](Self::set_target_bounds)'s unclocked stamping.
1052    /// See the [module docs](super) for the pass contract and its limit.
1053    /// Ignored (with a debug-build log) if the target is not registered.
1054    pub fn report_target_bounds(&self, id: DragTargetId, bounds: Option<Rect>, frame: FrameTime) {
1055        let frame = (frame != FrameTime::ZERO).then_some(frame);
1056        if !self.shared.borrow_mut().record_bounds(id, bounds, frame) {
1057            log_ignored("report_target_bounds", "the target is not registered");
1058        }
1059    }
1060
1061    /// Query the latest reported window-space bounds of a registered target,
1062    /// or `None` if the target is not registered, has not reported yet, or
1063    /// last reported itself clipped to nothing. Reads the record as-is: it
1064    /// does not check that the report belongs to the latest paint pass.
1065    pub fn target_bounds(&self, id: DragTargetId) -> Option<Rect> {
1066        self.shared
1067            .borrow()
1068            .targets
1069            .get(&id)
1070            .and_then(|entry| entry.bounds)
1071    }
1072
1073    /// The registered target whose reported bounds contain `point` (window
1074    /// space); where several overlap, the one registered **last** wins — see
1075    /// the [module docs](super) for why registration order stands in for
1076    /// paint order. Only targets that reported visible bounds in the latest
1077    /// paint pass are considered, and — while a session is in flight — only
1078    /// those accepting its payload type ([`register_target_for`](Self::register_target_for)).
1079    /// Returns `None` if no such target contains the point.
1080    pub fn target_at(&self, point: Point) -> Option<DragTargetId> {
1081        self.shared.borrow().target_at(point)
1082    }
1083
1084    /// Whether the session in flight (`Dragging`/`Dropping`) is an OS file
1085    /// drag ([`DragKind::ExternalFiles`]). `false` with no session in flight.
1086    pub fn is_external(&self) -> bool {
1087        self.shared
1088            .borrow()
1089            .machine
1090            .session()
1091            .is_some_and(|session| matches!(session.kind, DragKind::ExternalFiles(_)))
1092    }
1093
1094    /// All registered drop targets, in registration order.
1095    pub fn registered_targets(&self) -> Vec<DragTargetId> {
1096        let shared = self.shared.borrow();
1097        let mut targets: Vec<(u64, DragTargetId)> = shared
1098            .targets
1099            .iter()
1100            .map(|(&id, entry)| (entry.registration, id))
1101            .collect();
1102        targets.sort_unstable();
1103        targets.into_iter().map(|(_, id)| id).collect()
1104    }
1105
1106    /// Release the drag: over a hovered target, `Dragging → Dropping` (the
1107    /// target hears no `Leave` — it is the drop target) and the target is
1108    /// returned; with nothing hovered, or from `Armed`, exactly
1109    /// [`cancel`](Self::cancel) and `None`. A no-op (`None`) from `Idle` or
1110    /// an already-`Dropping` session.
1111    pub fn drop(&self) -> Option<DragTargetId> {
1112        self.mutate(|shared, changes| match &shared.machine {
1113            Machine::Idle | Machine::Dropping { .. } => None,
1114            Machine::Armed { .. } => {
1115                shared.cancel(changes);
1116                None
1117            }
1118            Machine::Dragging(session) => {
1119                let Some(target) = session.hovered else {
1120                    shared.cancel(changes);
1121                    return None;
1122                };
1123                let Machine::Dragging(session) =
1124                    std::mem::replace(&mut shared.machine, Machine::Idle)
1125                else {
1126                    unreachable!("matched Dragging above");
1127                };
1128                shared.machine = Machine::Dropping { session, target };
1129                changes.push(DragStateChange::Phase {
1130                    previous: DragPhase::Dragging,
1131                    next: DragPhase::Dropping,
1132                });
1133                Some(target)
1134            }
1135        })
1136    }
1137
1138    /// The drop target finished: `Dropping → Idle`, discarding the payload
1139    /// if it was never taken. A silent no-op unless `Dropping`.
1140    pub fn complete_drop(&self) {
1141        self.mutate(|shared, changes| {
1142            if matches!(shared.machine, Machine::Dropping { .. }) {
1143                shared.machine = Machine::Idle;
1144                changes.push(DragStateChange::Phase {
1145                    previous: DragPhase::Dropping,
1146                    next: DragPhase::Idle,
1147                });
1148            }
1149        });
1150    }
1151
1152    /// Abandon whatever is in flight: from any non-`Idle` phase, a hovered
1153    /// target (or an unclaimed drop's target) hears `Leave`, then
1154    /// `→ Cancelled → Idle`; the payload is discarded. A no-op from `Idle`.
1155    pub fn cancel(&self) {
1156        self.mutate(|shared, changes| {
1157            shared.cancel(changes);
1158        });
1159    }
1160
1161    /// The OS file drag is over: end a [`DragKind::ExternalFiles`] session
1162    /// still `Dragging`, exactly as [`cancel`](Self::cancel) would (a hovered
1163    /// target hears `Leave`, then `→ Cancelled → Idle`), and report whether
1164    /// one was ended.
1165    ///
1166    /// Idempotent and narrow, so every drop target can call it on the same
1167    /// end-of-drag broadcast and exactly one call does anything: a no-op
1168    /// (`false`) from `Idle`, from `Armed`, for any internal session (pointer
1169    /// or keyboard — an OS drag ending says nothing about it), and for an
1170    /// external session already `Dropping` — that one belongs to the target
1171    /// it was released over, which claims or cancels it on its own poll.
1172    pub fn end_external(&self) -> bool {
1173        self.mutate(|shared, changes| {
1174            let ends = matches!(
1175                &shared.machine,
1176                Machine::Dragging(session) if matches!(session.kind, DragKind::ExternalFiles(_))
1177            );
1178            if ends {
1179                shared.cancel(changes);
1180            }
1181            ends
1182        })
1183    }
1184
1185    /// Whether a payload of type `T` is held (`Dragging`/`Dropping`, not yet
1186    /// taken).
1187    pub fn payload_is<T: Any>(&self) -> bool {
1188        self.shared
1189            .borrow()
1190            .machine
1191            .session()
1192            .is_some_and(|session| {
1193                session.payload.is_some() && session.payload_type == TypeId::of::<T>()
1194            })
1195    }
1196
1197    /// Borrow the held payload as a `T`, or `None` (no session, payload
1198    /// taken, or a different type).
1199    ///
1200    /// The returned guard borrows the coordinator's shared state: calling any
1201    /// method that mutates the coordinator (on this or any clone) while it is
1202    /// alive panics on the borrow. Prefer [`with_payload`](Self::with_payload)
1203    /// when the read can be scoped to a closure.
1204    pub fn payload_ref<T: Any>(&self) -> Option<Ref<'_, T>> {
1205        Ref::filter_map(self.shared.borrow(), |shared| {
1206            shared
1207                .machine
1208                .session()?
1209                .payload
1210                .as_ref()?
1211                .downcast_ref::<T>()
1212        })
1213        .ok()
1214    }
1215
1216    /// Run `f` over the held payload as a `T`, returning its result, or
1217    /// `None` (no session, payload taken, or a different type). The shared
1218    /// state stays borrowed only for the duration of `f`, which must not
1219    /// mutate the coordinator.
1220    pub fn with_payload<T: Any, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R> {
1221        self.payload_ref::<T>().map(|payload| f(&payload))
1222    }
1223
1224    /// Move the held payload out as a `T` — only while `Dragging` or
1225    /// `Dropping`, and only when it is a `T`; otherwise `None` and the
1226    /// payload stays put. A second take returns `None`.
1227    pub fn take_payload<T: Any>(&self) -> Option<Box<T>> {
1228        let mut shared = self.shared.borrow_mut();
1229        let session = shared.machine.session_mut()?;
1230        if session.payload_type != TypeId::of::<T>() {
1231            return None;
1232        }
1233        session.payload.take()?.downcast::<T>().ok()
1234    }
1235
1236    /// Hear every [`DragStateChange`], in order, until the returned guard is
1237    /// dropped. Listeners run in subscription order with the coordinator's
1238    /// state released, so a listener may read or drive the coordinator; see
1239    /// the [module docs](super) for the ordering guarantee under re-entrancy.
1240    pub fn subscribe<F: Fn(DragStateChange) + 'static>(&self, listener: F) -> DragSubscription {
1241        let mut shared = self.shared.borrow_mut();
1242        let id = shared.next_listener;
1243        shared.next_listener += 1;
1244        shared.listeners.push((id, Rc::new(listener)));
1245        DragSubscription {
1246            shared: Rc::downgrade(&self.shared),
1247            id,
1248        }
1249    }
1250
1251    /// Run one mutation against the shared state, then deliver the changes
1252    /// it produced. The borrow ends before any listener runs.
1253    fn mutate<R>(&self, f: impl FnOnce(&mut Shared, &mut Vec<DragStateChange>) -> R) -> R {
1254        let mut changes = Vec::new();
1255        let result = {
1256            let mut shared = self.shared.borrow_mut();
1257            let result = f(&mut shared, &mut changes);
1258            shared.queue.extend(changes.iter().copied());
1259            result
1260        };
1261        if !changes.is_empty() {
1262            frust_core::mark_pending_result_flush();
1263            self.dispatch();
1264        }
1265        result
1266    }
1267
1268    /// Deliver every queued change to every live listener, in order. A call
1269    /// made while an outer dispatch is running returns at once: the outer
1270    /// loop picks up whatever the re-entrant mutation queued, so every
1271    /// listener hears every change in the order it happened.
1272    fn dispatch(&self) {
1273        {
1274            let mut shared = self.shared.borrow_mut();
1275            if shared.dispatching {
1276                return;
1277            }
1278            shared.dispatching = true;
1279        }
1280        let _guard = DispatchGuard(&self.shared);
1281        loop {
1282            let (change, listeners) = {
1283                let mut shared = self.shared.borrow_mut();
1284                let Some(change) = shared.queue.pop_front() else {
1285                    break;
1286                };
1287                (change, shared.listeners.clone())
1288            };
1289            for (id, listener) in listeners {
1290                // A listener unsubscribed by an earlier one hears nothing more.
1291                let live = self
1292                    .shared
1293                    .borrow()
1294                    .listeners
1295                    .iter()
1296                    .any(|(other, _)| *other == id);
1297                if live {
1298                    listener(change);
1299                }
1300            }
1301        }
1302    }
1303}
1304
1305/// Clears the dispatching flag when a [`DragCoordinator::dispatch`] loop
1306/// ends — including by a listener's panic, which also drops the undelivered
1307/// changes rather than replaying them late on the next mutation.
1308struct DispatchGuard<'a>(&'a RefCell<Shared>);
1309
1310impl Drop for DispatchGuard<'_> {
1311    fn drop(&mut self) {
1312        if let Ok(mut shared) = self.0.try_borrow_mut() {
1313            shared.dispatching = false;
1314            if std::thread::panicking() {
1315                shared.queue.clear();
1316            }
1317        }
1318    }
1319}
1320
1321/// The guard [`DragCoordinator::subscribe`] returns: dropping it unsubscribes
1322/// the listener. Holds the coordinator weakly, so it never keeps it alive.
1323#[must_use = "dropping a DragSubscription unsubscribes its listener at once"]
1324pub struct DragSubscription {
1325    shared: Weak<RefCell<Shared>>,
1326    id: u64,
1327}
1328
1329impl fmt::Debug for DragSubscription {
1330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1331        f.debug_struct("DragSubscription")
1332            .field("id", &self.id)
1333            .finish()
1334    }
1335}
1336
1337impl Drop for DragSubscription {
1338    fn drop(&mut self) {
1339        if let Some(shared) = self.shared.upgrade() {
1340            shared
1341                .borrow_mut()
1342                .listeners
1343                .retain(|(id, _)| *id != self.id);
1344        }
1345    }
1346}
1347
1348/// Report an ignored call in debug builds; a no-op in release.
1349fn log_ignored(_call: &str, _why: &str) {
1350    #[cfg(debug_assertions)]
1351    eprintln!("frust-widgets: DragCoordinator::{_call} ignored: {_why}");
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356    use std::cell::RefCell;
1357
1358    use super::*;
1359
1360    const PRESS: Point = Point::new(10.0, 10.0);
1361    const MOVED: Point = Point::new(20.0, 30.0);
1362    const OTHER_PRESS: Point = Point::new(5.0, 5.0);
1363    const FILE_ENTRY: Point = Point::new(1.0, 2.0);
1364
1365    /// A coordinator with two sources, two targets and a change log.
1366    struct Fixture {
1367        drag: DragCoordinator,
1368        source: DragSourceId,
1369        other_source: DragSourceId,
1370        first: DragTargetId,
1371        second: DragTargetId,
1372        log: Rc<RefCell<Vec<DragStateChange>>>,
1373        _subscription: DragSubscription,
1374    }
1375
1376    impl Fixture {
1377        fn new() -> Self {
1378            let drag = DragCoordinator::new();
1379            let log = Rc::new(RefCell::new(Vec::new()));
1380            let sink = Rc::clone(&log);
1381            let subscription = drag.subscribe(move |change| sink.borrow_mut().push(change));
1382            Self {
1383                source: drag.new_source_id(),
1384                other_source: drag.new_source_id(),
1385                first: drag.new_target_id(),
1386                second: drag.new_target_id(),
1387                drag,
1388                log,
1389                _subscription: subscription,
1390            }
1391        }
1392
1393        fn take_log(&self) -> Vec<DragStateChange> {
1394            std::mem::take(&mut *self.log.borrow_mut())
1395        }
1396    }
1397
1398    /// The phase a table row starts from.
1399    #[derive(Clone, Copy, Debug)]
1400    enum Start {
1401        Idle,
1402        Armed,
1403        /// Dragging an internal payload, nothing hovered.
1404        Dragging,
1405        /// Dragging an internal payload over the first target.
1406        Hovering,
1407        /// Dropped on the first target, not yet completed.
1408        Dropping,
1409        /// Dragging an OS file drop, nothing hovered.
1410        External,
1411    }
1412
1413    impl Start {
1414        fn reach(self, f: &Fixture) {
1415            match self {
1416                Start::Idle => {}
1417                Start::Armed => {
1418                    f.drag.arm(f.source, PRESS);
1419                }
1420                Start::Dragging => {
1421                    f.drag.arm(f.source, PRESS);
1422                    f.drag.begin(7_i32);
1423                }
1424                Start::Hovering => {
1425                    Start::Dragging.reach(f);
1426                    f.drag.set_hovered(Some(f.first));
1427                }
1428                Start::Dropping => {
1429                    Start::Hovering.reach(f);
1430                    f.drag.drop();
1431                }
1432                Start::External => {
1433                    f.drag
1434                        .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
1435                }
1436            }
1437        }
1438    }
1439
1440    /// The operation a table row applies.
1441    #[derive(Clone, Copy, Debug)]
1442    enum Op {
1443        Arm,
1444        Begin,
1445        BeginExternal,
1446        Move,
1447        HoverSecond,
1448        HoverNone,
1449        Drop,
1450        Complete,
1451        Cancel,
1452    }
1453
1454    impl Op {
1455        fn apply(self, f: &Fixture) {
1456            match self {
1457                Op::Arm => {
1458                    f.drag.arm(f.other_source, OTHER_PRESS);
1459                }
1460                Op::Begin => {
1461                    f.drag.begin("second");
1462                }
1463                Op::BeginExternal => {
1464                    f.drag
1465                        .begin_external(vec![PathBuf::from("/tmp/b.txt")], FILE_ENTRY);
1466                }
1467                Op::Move => f.drag.update_pointer(MOVED),
1468                Op::HoverSecond => f.drag.set_hovered(Some(f.second)),
1469                Op::HoverNone => f.drag.set_hovered(None),
1470                Op::Drop => {
1471                    f.drag.drop();
1472                }
1473                Op::Complete => f.drag.complete_drop(),
1474                Op::Cancel => f.drag.cancel(),
1475            }
1476        }
1477    }
1478
1479    /// Symbolic expected changes, resolved against a fixture's ids.
1480    #[derive(Clone, Copy, Debug)]
1481    enum Expect {
1482        Phase(DragPhase, DragPhase),
1483        EnterSecond,
1484        LeaveFirst,
1485        Moved,
1486    }
1487
1488    impl Expect {
1489        fn resolve(self, f: &Fixture) -> DragStateChange {
1490            match self {
1491                Expect::Phase(previous, next) => DragStateChange::Phase { previous, next },
1492                Expect::EnterSecond => DragStateChange::Enter { target: f.second },
1493                Expect::LeaveFirst => DragStateChange::Leave { target: f.first },
1494                Expect::Moved => DragStateChange::Move { pointer: MOVED },
1495            }
1496        }
1497    }
1498
1499    use DragPhase::{Armed, Cancelled, Dragging, Dropping, Idle};
1500
1501    const CANCEL_FROM_ARMED: &[Expect] = &[
1502        Expect::Phase(Armed, Cancelled),
1503        Expect::Phase(Cancelled, Idle),
1504    ];
1505    const CANCEL_FROM_DRAGGING: &[Expect] = &[
1506        Expect::Phase(Dragging, Cancelled),
1507        Expect::Phase(Cancelled, Idle),
1508    ];
1509
1510    /// Every (start phase, operation) pair: the phase it lands on and the
1511    /// exact change sequence subscribers hear.
1512    fn table() -> Vec<(Start, Op, DragPhase, Vec<Expect>)> {
1513        use Expect::{EnterSecond, LeaveFirst, Moved, Phase};
1514        let cat = |parts: &[&[Expect]]| parts.concat();
1515        vec![
1516            // From Idle: only arm and begin_external leave it.
1517            (Start::Idle, Op::Arm, Armed, vec![Phase(Idle, Armed)]),
1518            (Start::Idle, Op::Begin, Idle, vec![]),
1519            (
1520                Start::Idle,
1521                Op::BeginExternal,
1522                Dragging,
1523                vec![Phase(Idle, Dragging)],
1524            ),
1525            (Start::Idle, Op::Move, Idle, vec![]),
1526            (Start::Idle, Op::HoverSecond, Idle, vec![]),
1527            (Start::Idle, Op::HoverNone, Idle, vec![]),
1528            (Start::Idle, Op::Drop, Idle, vec![]),
1529            (Start::Idle, Op::Complete, Idle, vec![]),
1530            (Start::Idle, Op::Cancel, Idle, vec![]),
1531            // From Armed.
1532            (
1533                Start::Armed,
1534                Op::Arm,
1535                Armed,
1536                cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Armed)]]),
1537            ),
1538            (
1539                Start::Armed,
1540                Op::Begin,
1541                Dragging,
1542                vec![Phase(Armed, Dragging)],
1543            ),
1544            (
1545                Start::Armed,
1546                Op::BeginExternal,
1547                Dragging,
1548                cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Dragging)]]),
1549            ),
1550            (Start::Armed, Op::Move, Armed, vec![]),
1551            (Start::Armed, Op::HoverSecond, Armed, vec![]),
1552            (Start::Armed, Op::HoverNone, Armed, vec![]),
1553            (Start::Armed, Op::Drop, Idle, CANCEL_FROM_ARMED.to_vec()),
1554            (Start::Armed, Op::Complete, Armed, vec![]),
1555            (Start::Armed, Op::Cancel, Idle, CANCEL_FROM_ARMED.to_vec()),
1556            // From Dragging with nothing hovered.
1557            (Start::Dragging, Op::Arm, Dragging, vec![]),
1558            (
1559                Start::Dragging,
1560                Op::Begin,
1561                Dragging,
1562                cat(&[
1563                    CANCEL_FROM_DRAGGING,
1564                    &[Phase(Idle, Armed), Phase(Armed, Dragging)],
1565                ]),
1566            ),
1567            (
1568                Start::Dragging,
1569                Op::BeginExternal,
1570                Dragging,
1571                cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
1572            ),
1573            (Start::Dragging, Op::Move, Dragging, vec![Moved]),
1574            (
1575                Start::Dragging,
1576                Op::HoverSecond,
1577                Dragging,
1578                vec![EnterSecond],
1579            ),
1580            (Start::Dragging, Op::HoverNone, Dragging, vec![]),
1581            (
1582                Start::Dragging,
1583                Op::Drop,
1584                Idle,
1585                CANCEL_FROM_DRAGGING.to_vec(),
1586            ),
1587            (Start::Dragging, Op::Complete, Dragging, vec![]),
1588            (
1589                Start::Dragging,
1590                Op::Cancel,
1591                Idle,
1592                CANCEL_FROM_DRAGGING.to_vec(),
1593            ),
1594            // From Dragging over the first target.
1595            (Start::Hovering, Op::Arm, Dragging, vec![]),
1596            (
1597                Start::Hovering,
1598                Op::Begin,
1599                Dragging,
1600                cat(&[
1601                    &[LeaveFirst],
1602                    CANCEL_FROM_DRAGGING,
1603                    &[Phase(Idle, Armed), Phase(Armed, Dragging)],
1604                ]),
1605            ),
1606            (
1607                Start::Hovering,
1608                Op::BeginExternal,
1609                Dragging,
1610                cat(&[
1611                    &[LeaveFirst],
1612                    CANCEL_FROM_DRAGGING,
1613                    &[Phase(Idle, Dragging)],
1614                ]),
1615            ),
1616            // The first target was hovered by override and never registered,
1617            // so resolving under the moved pointer finds nothing.
1618            (Start::Hovering, Op::Move, Dragging, vec![LeaveFirst, Moved]),
1619            (
1620                Start::Hovering,
1621                Op::HoverSecond,
1622                Dragging,
1623                vec![LeaveFirst, EnterSecond],
1624            ),
1625            (Start::Hovering, Op::HoverNone, Dragging, vec![LeaveFirst]),
1626            (
1627                Start::Hovering,
1628                Op::Drop,
1629                Dropping,
1630                vec![Phase(Dragging, Dropping)],
1631            ),
1632            (Start::Hovering, Op::Complete, Dragging, vec![]),
1633            (
1634                Start::Hovering,
1635                Op::Cancel,
1636                Idle,
1637                cat(&[&[LeaveFirst], CANCEL_FROM_DRAGGING]),
1638            ),
1639            // From Dropping on the first target.
1640            (
1641                Start::Dropping,
1642                Op::Arm,
1643                Armed,
1644                vec![
1645                    LeaveFirst,
1646                    Phase(Dropping, Cancelled),
1647                    Phase(Cancelled, Idle),
1648                    Phase(Idle, Armed),
1649                ],
1650            ),
1651            (Start::Dropping, Op::Begin, Dropping, vec![]),
1652            (
1653                Start::Dropping,
1654                Op::BeginExternal,
1655                Dragging,
1656                vec![
1657                    LeaveFirst,
1658                    Phase(Dropping, Cancelled),
1659                    Phase(Cancelled, Idle),
1660                    Phase(Idle, Dragging),
1661                ],
1662            ),
1663            (Start::Dropping, Op::Move, Dropping, vec![]),
1664            (Start::Dropping, Op::HoverSecond, Dropping, vec![]),
1665            (Start::Dropping, Op::HoverNone, Dropping, vec![]),
1666            (Start::Dropping, Op::Drop, Dropping, vec![]),
1667            (
1668                Start::Dropping,
1669                Op::Complete,
1670                Idle,
1671                vec![Phase(Dropping, Idle)],
1672            ),
1673            (
1674                Start::Dropping,
1675                Op::Cancel,
1676                Idle,
1677                vec![
1678                    LeaveFirst,
1679                    Phase(Dropping, Cancelled),
1680                    Phase(Cancelled, Idle),
1681                ],
1682            ),
1683            // From an external (OS file) drag with nothing hovered.
1684            (Start::External, Op::Arm, Dragging, vec![]),
1685            // The first is cancelled; with no source to restart from, the
1686            // second begin is ignored.
1687            (
1688                Start::External,
1689                Op::Begin,
1690                Idle,
1691                CANCEL_FROM_DRAGGING.to_vec(),
1692            ),
1693            (
1694                Start::External,
1695                Op::BeginExternal,
1696                Dragging,
1697                cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
1698            ),
1699            (Start::External, Op::Move, Dragging, vec![Moved]),
1700            (
1701                Start::External,
1702                Op::HoverSecond,
1703                Dragging,
1704                vec![EnterSecond],
1705            ),
1706            (Start::External, Op::HoverNone, Dragging, vec![]),
1707            (
1708                Start::External,
1709                Op::Drop,
1710                Idle,
1711                CANCEL_FROM_DRAGGING.to_vec(),
1712            ),
1713            (Start::External, Op::Complete, Dragging, vec![]),
1714            (
1715                Start::External,
1716                Op::Cancel,
1717                Idle,
1718                CANCEL_FROM_DRAGGING.to_vec(),
1719            ),
1720        ]
1721    }
1722
1723    #[test]
1724    fn every_transition_lands_on_the_tabled_phase_with_the_tabled_changes() {
1725        let rows = table();
1726        assert_eq!(rows.len(), 6 * 9, "every start phase x every operation");
1727        for (start, op, phase, expect) in rows {
1728            let f = Fixture::new();
1729            start.reach(&f);
1730            f.take_log();
1731            op.apply(&f);
1732            let expected: Vec<DragStateChange> = expect.iter().map(|e| e.resolve(&f)).collect();
1733            assert_eq!(f.take_log(), expected, "{start:?} + {op:?}: changes");
1734            assert_eq!(f.drag.phase(), phase, "{start:?} + {op:?}: phase");
1735            assert_eq!(
1736                f.drag.state().phase(),
1737                phase,
1738                "{start:?} + {op:?}: state snapshot agrees with phase"
1739            );
1740            assert!(
1741                f.drag.phase() != Cancelled,
1742                "{start:?} + {op:?}: Cancelled is never a resting phase"
1743            );
1744        }
1745    }
1746
1747    #[test]
1748    fn setup_paths_report_their_own_changes() {
1749        let f = Fixture::new();
1750        Start::Dropping.reach(&f);
1751        assert_eq!(
1752            f.take_log(),
1753            vec![
1754                DragStateChange::Phase {
1755                    previous: Idle,
1756                    next: Armed
1757                },
1758                DragStateChange::Phase {
1759                    previous: Armed,
1760                    next: Dragging
1761                },
1762                DragStateChange::Enter { target: f.first },
1763                DragStateChange::Phase {
1764                    previous: Dragging,
1765                    next: Dropping
1766                },
1767            ]
1768        );
1769    }
1770
1771    #[test]
1772    fn set_external_payload_replaces_the_held_paths_and_notifies_nothing() {
1773        let f = Fixture::new();
1774        f.drag.begin_external(Vec::new(), FILE_ENTRY);
1775        f.take_log();
1776
1777        let real_paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")];
1778        f.drag.set_external_payload(real_paths.clone());
1779        assert_eq!(
1780            f.take_log(),
1781            Vec::<DragStateChange>::new(),
1782            "replacing the payload notifies no subscriber"
1783        );
1784        assert_eq!(
1785            f.drag.with_payload::<Vec<PathBuf>, _>(Clone::clone),
1786            Some(real_paths.clone())
1787        );
1788
1789        f.drag.set_hovered(Some(f.first));
1790        f.take_log();
1791        assert_eq!(f.drag.drop(), Some(f.first));
1792        assert_eq!(
1793            f.drag.take_payload::<Vec<PathBuf>>().map(|b| *b),
1794            Some(real_paths),
1795            "the drop target receives the replaced paths, not the empty hover list"
1796        );
1797    }
1798
1799    #[test]
1800    fn set_external_payload_is_a_no_op_outside_an_external_dragging_session() {
1801        let f = Fixture::new();
1802        // Idle: no session at all.
1803        f.drag
1804            .set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
1805        assert_eq!(f.drag.phase(), DragPhase::Idle);
1806
1807        // An internal session's payload must never be mistaken for a file list.
1808        f.drag.arm(f.source, PRESS);
1809        f.drag.begin(7_i32);
1810        f.drag
1811            .set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
1812        assert_eq!(f.drag.with_payload::<i32, _>(|v| *v), Some(7));
1813    }
1814
1815    #[test]
1816    fn a_session_snapshot_tracks_source_pointer_hover_and_drop_target() {
1817        let f = Fixture::new();
1818        f.drag.arm(f.source, PRESS);
1819        assert_eq!(
1820            f.drag.state(),
1821            DragState::Armed {
1822                source: f.source,
1823                press: PRESS,
1824                keyboard: false,
1825            }
1826        );
1827        f.drag.begin(7_i32);
1828        f.drag.set_ghost_offset(Vec2::new(-4.0, -6.0));
1829        f.drag.update_pointer(MOVED);
1830        f.drag.set_hovered(Some(f.first));
1831        let DragState::Dragging(session) = f.drag.state() else {
1832            panic!("expected Dragging");
1833        };
1834        assert_eq!(session.kind, DragKind::Internal);
1835        assert_eq!(session.source, Some(f.source));
1836        assert_eq!(session.press, PRESS);
1837        assert_eq!(session.pointer, MOVED);
1838        assert_eq!(session.ghost_origin(), Point::new(16.0, 24.0));
1839        assert_eq!(session.hovered, Some(f.first));
1840        assert_eq!(session.payload_type, Some(TypeId::of::<i32>()));
1841
1842        assert_eq!(f.drag.drop(), Some(f.first));
1843        let DragState::Dropping { session, target } = f.drag.state() else {
1844            panic!("expected Dropping");
1845        };
1846        assert_eq!(target, f.first);
1847        assert_eq!(session.pointer, MOVED);
1848    }
1849
1850    #[test]
1851    fn a_second_begin_restarts_from_the_same_source_at_the_current_pointer() {
1852        let f = Fixture::new();
1853        f.drag.arm(f.source, PRESS);
1854        assert!(f.drag.begin(1_u8));
1855        f.drag.set_ghost_offset(Vec2::new(-2.0, -2.0));
1856        f.drag.update_pointer(MOVED);
1857        assert!(f.drag.begin("replacement"));
1858        let DragState::Dragging(session) = f.drag.state() else {
1859            panic!("expected Dragging");
1860        };
1861        assert_eq!(session.source, Some(f.source));
1862        assert_eq!(session.press, PRESS);
1863        assert_eq!(session.pointer, MOVED);
1864        assert_eq!(session.ghost_offset, Vec2::new(-2.0, -2.0));
1865        assert_eq!(session.hovered, None);
1866        assert!(f.drag.payload_is::<&str>());
1867        assert!(
1868            !f.drag.payload_is::<u8>(),
1869            "the first payload was discarded"
1870        );
1871    }
1872
1873    #[test]
1874    fn begin_without_arm_and_arm_while_dragging_report_failure() {
1875        let f = Fixture::new();
1876        assert!(!f.drag.begin(1_u8), "begin from Idle is ignored");
1877        assert!(f.drag.arm(f.source, PRESS));
1878        assert!(f.drag.begin(1_u8));
1879        assert!(
1880            !f.drag.arm(f.other_source, OTHER_PRESS),
1881            "arm while dragging is ignored"
1882        );
1883        assert_eq!(
1884            f.drag.state().session().and_then(|s| s.source),
1885            Some(f.source)
1886        );
1887    }
1888
1889    #[test]
1890    fn a_typed_payload_round_trips_and_moves_out_once() {
1891        #[derive(Debug, PartialEq)]
1892        struct Card {
1893            id: u32,
1894            title: String,
1895        }
1896        let f = Fixture::new();
1897        assert!(!f.drag.payload_is::<Card>(), "no payload while Idle");
1898        assert!(f.drag.take_payload::<Card>().is_none());
1899        f.drag.arm(f.source, PRESS);
1900        assert!(
1901            f.drag.payload_ref::<Card>().is_none(),
1902            "no payload while Armed"
1903        );
1904        f.drag.begin(Card {
1905            id: 3,
1906            title: "three".into(),
1907        });
1908
1909        assert!(f.drag.payload_is::<Card>());
1910        assert!(!f.drag.payload_is::<u32>());
1911        assert_eq!(f.drag.payload_ref::<Card>().map(|card| card.id), Some(3));
1912        assert!(
1913            f.drag.payload_ref::<String>().is_none(),
1914            "wrong type reads None"
1915        );
1916        assert_eq!(
1917            f.drag
1918                .with_payload(|card: &Card| card.title.clone())
1919                .as_deref(),
1920            Some("three")
1921        );
1922        assert_eq!(f.drag.with_payload(|n: &u32| *n), None);
1923        assert!(
1924            f.drag.take_payload::<u32>().is_none(),
1925            "a wrong-type take leaves the payload in place"
1926        );
1927        assert!(f.drag.payload_is::<Card>());
1928
1929        // Still held into Dropping, and taken there.
1930        f.drag.set_hovered(Some(f.first));
1931        f.drag.drop();
1932        let taken = f
1933            .drag
1934            .take_payload::<Card>()
1935            .expect("the drop target claims it");
1936        assert_eq!(
1937            *taken,
1938            Card {
1939                id: 3,
1940                title: "three".into()
1941            }
1942        );
1943        assert!(
1944            f.drag.take_payload::<Card>().is_none(),
1945            "a second take is None"
1946        );
1947        assert!(!f.drag.payload_is::<Card>());
1948        assert_eq!(
1949            f.drag.state().session().and_then(|s| s.payload_type),
1950            None,
1951            "the snapshot reports the payload gone"
1952        );
1953        f.drag.complete_drop();
1954        assert_eq!(f.drag.phase(), Idle);
1955    }
1956
1957    #[test]
1958    fn a_payload_is_discarded_by_cancel_and_by_an_unclaimed_completion() {
1959        let f = Fixture::new();
1960        let probe = Rc::new(());
1961        f.drag.arm(f.source, PRESS);
1962        f.drag.begin(Rc::clone(&probe));
1963        assert_eq!(Rc::strong_count(&probe), 2);
1964        f.drag.cancel();
1965        assert_eq!(Rc::strong_count(&probe), 1, "cancel drops the payload");
1966
1967        f.drag.arm(f.source, PRESS);
1968        f.drag.begin(Rc::clone(&probe));
1969        f.drag.set_hovered(Some(f.first));
1970        f.drag.drop();
1971        assert_eq!(Rc::strong_count(&probe), 2, "still held while Dropping");
1972        f.drag.complete_drop();
1973        assert_eq!(
1974            Rc::strong_count(&probe),
1975            1,
1976            "an unclaimed payload is dropped on completion"
1977        );
1978    }
1979
1980    #[test]
1981    fn subscribers_hear_every_change_in_subscription_order() {
1982        let drag = DragCoordinator::new();
1983        let source = drag.new_source_id();
1984        let log = Rc::new(RefCell::new(Vec::new()));
1985        let subscriptions: Vec<DragSubscription> = (0..3)
1986            .map(|index| {
1987                let sink = Rc::clone(&log);
1988                drag.subscribe(move |change| sink.borrow_mut().push((index, change)))
1989            })
1990            .collect();
1991        drag.arm(source, PRESS);
1992        let armed = DragStateChange::Phase {
1993            previous: Idle,
1994            next: Armed,
1995        };
1996        assert_eq!(*log.borrow(), vec![(0, armed), (1, armed), (2, armed)]);
1997
1998        // Dropping a guard unsubscribes that listener only.
1999        log.borrow_mut().clear();
2000        let mut subscriptions = subscriptions;
2001        drop(subscriptions.remove(1));
2002        drag.cancel();
2003        let to_cancelled = DragStateChange::Phase {
2004            previous: Armed,
2005            next: Cancelled,
2006        };
2007        let to_idle = DragStateChange::Phase {
2008            previous: Cancelled,
2009            next: Idle,
2010        };
2011        assert_eq!(
2012            *log.borrow(),
2013            vec![
2014                (0, to_cancelled),
2015                (2, to_cancelled),
2016                (0, to_idle),
2017                (2, to_idle),
2018            ]
2019        );
2020    }
2021
2022    #[test]
2023    fn a_reentrant_listener_keeps_the_global_order_for_every_subscriber() {
2024        let drag = DragCoordinator::new();
2025        let source = drag.new_source_id();
2026        let target = drag.new_target_id();
2027        let log = Rc::new(RefCell::new(Vec::new()));
2028
2029        // The first listener cancels as soon as it hears the drop, reading
2030        // the state while it is at it.
2031        let handle = drag.clone();
2032        let sink = Rc::clone(&log);
2033        let _first = drag.subscribe(move |change| {
2034            sink.borrow_mut().push(("first", change));
2035            if change
2036                == (DragStateChange::Phase {
2037                    previous: Dragging,
2038                    next: Dropping,
2039                })
2040            {
2041                assert_eq!(handle.phase(), Dropping);
2042                handle.cancel();
2043                assert_eq!(
2044                    handle.phase(),
2045                    Idle,
2046                    "the re-entrant cancel applied at once"
2047                );
2048            }
2049        });
2050        let sink = Rc::clone(&log);
2051        let _second = drag.subscribe(move |change| sink.borrow_mut().push(("second", change)));
2052
2053        drag.arm(source, PRESS);
2054        drag.begin(());
2055        drag.set_hovered(Some(target));
2056        log.borrow_mut().clear();
2057        drag.drop();
2058
2059        let dropping = DragStateChange::Phase {
2060            previous: Dragging,
2061            next: Dropping,
2062        };
2063        let leave = DragStateChange::Leave { target };
2064        let to_cancelled = DragStateChange::Phase {
2065            previous: Dropping,
2066            next: Cancelled,
2067        };
2068        let to_idle = DragStateChange::Phase {
2069            previous: Cancelled,
2070            next: Idle,
2071        };
2072        assert_eq!(
2073            *log.borrow(),
2074            vec![
2075                ("first", dropping),
2076                ("second", dropping),
2077                ("first", leave),
2078                ("second", leave),
2079                ("first", to_cancelled),
2080                ("second", to_cancelled),
2081                ("first", to_idle),
2082                ("second", to_idle),
2083            ],
2084            "the second subscriber hears the drop before the cancel it triggered"
2085        );
2086    }
2087
2088    #[test]
2089    fn a_listener_unsubscribed_mid_dispatch_hears_nothing_more() {
2090        let drag = DragCoordinator::new();
2091        let source = drag.new_source_id();
2092        let heard = Rc::new(RefCell::new(0_u32));
2093        let victim: Rc<RefCell<Option<DragSubscription>>> = Rc::new(RefCell::new(None));
2094
2095        let slot = Rc::clone(&victim);
2096        let _killer = drag.subscribe(move |_| {
2097            slot.borrow_mut().take();
2098        });
2099        let count = Rc::clone(&heard);
2100        *victim.borrow_mut() = Some(drag.subscribe(move |_| *count.borrow_mut() += 1));
2101
2102        drag.arm(source, PRESS);
2103        assert_eq!(*heard.borrow(), 0);
2104    }
2105
2106    #[test]
2107    fn a_subscription_outliving_its_coordinator_drops_cleanly() {
2108        let drag = DragCoordinator::new();
2109        let subscription = drag.subscribe(|_| {});
2110        drop(drag);
2111        drop(subscription);
2112    }
2113
2114    #[test]
2115    fn a_panicking_listener_does_not_wedge_dispatch() {
2116        let drag = DragCoordinator::new();
2117        let source = drag.new_source_id();
2118        let heard = Rc::new(RefCell::new(Vec::new()));
2119        let sink = Rc::clone(&heard);
2120        let armed = Rc::new(std::cell::Cell::new(true));
2121        let trip = Rc::clone(&armed);
2122        let _panicky = drag.subscribe(move |_| {
2123            if trip.replace(false) {
2124                panic!("listener failure");
2125            }
2126        });
2127        let _recorder = drag.subscribe(move |change| sink.borrow_mut().push(change));
2128
2129        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2130            drag.arm(source, PRESS);
2131        }));
2132        assert!(outcome.is_err());
2133        assert_eq!(drag.phase(), Armed, "the transition itself applied");
2134        heard.borrow_mut().clear();
2135        drag.cancel();
2136        assert_eq!(
2137            *heard.borrow(),
2138            vec![
2139                DragStateChange::Phase {
2140                    previous: Armed,
2141                    next: Cancelled
2142                },
2143                DragStateChange::Phase {
2144                    previous: Cancelled,
2145                    next: Idle
2146                },
2147            ],
2148            "later changes are delivered, the undelivered one is not replayed"
2149        );
2150    }
2151
2152    #[test]
2153    fn an_external_file_session_runs_its_whole_lifecycle() {
2154        let f = Fixture::new();
2155        let paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.png")];
2156        f.drag.begin_external(paths.clone(), FILE_ENTRY);
2157        let DragState::Dragging(session) = f.drag.state() else {
2158            panic!("expected Dragging");
2159        };
2160        assert_eq!(session.kind, DragKind::ExternalFiles(paths.clone()));
2161        assert_eq!(session.source, None, "an OS drag has no source");
2162        assert_eq!(session.press, FILE_ENTRY);
2163        assert_eq!(session.pointer, FILE_ENTRY);
2164        assert!(f.drag.payload_is::<Vec<PathBuf>>());
2165
2166        f.drag.update_pointer(MOVED);
2167        f.drag.set_hovered(Some(f.first));
2168        f.drag.set_hovered(Some(f.second));
2169        assert_eq!(f.drag.drop(), Some(f.second));
2170        assert_eq!(
2171            f.drag.take_payload::<Vec<PathBuf>>().map(|paths| *paths),
2172            Some(paths)
2173        );
2174        f.drag.complete_drop();
2175        assert_eq!(
2176            f.take_log(),
2177            vec![
2178                DragStateChange::Phase {
2179                    previous: Idle,
2180                    next: Dragging
2181                },
2182                DragStateChange::Move { pointer: MOVED },
2183                DragStateChange::Enter { target: f.first },
2184                DragStateChange::Leave { target: f.first },
2185                DragStateChange::Enter { target: f.second },
2186                DragStateChange::Phase {
2187                    previous: Dragging,
2188                    next: Dropping
2189                },
2190                DragStateChange::Phase {
2191                    previous: Dropping,
2192                    next: Idle
2193                },
2194            ]
2195        );
2196        assert_eq!(f.drag.state(), DragState::Idle);
2197    }
2198
2199    #[test]
2200    fn an_external_session_cancelled_by_the_os_leaves_its_hovered_target() {
2201        let f = Fixture::new();
2202        f.drag
2203            .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
2204        f.drag.set_hovered(Some(f.first));
2205        f.take_log();
2206        f.drag.cancel();
2207        assert_eq!(
2208            f.take_log(),
2209            vec![
2210                DragStateChange::Leave { target: f.first },
2211                DragStateChange::Phase {
2212                    previous: Dragging,
2213                    next: Cancelled
2214                },
2215                DragStateChange::Phase {
2216                    previous: Cancelled,
2217                    next: Idle
2218                },
2219            ]
2220        );
2221        assert!(!f.drag.payload_is::<Vec<PathBuf>>());
2222    }
2223
2224    #[test]
2225    fn unchanged_pointer_and_hover_notify_nothing() {
2226        let f = Fixture::new();
2227        f.drag.register_target(f.first);
2228        f.drag
2229            .set_target_bounds(f.first, Rect::from_center_size(PRESS, (20.0, 20.0)));
2230        Start::Hovering.reach(&f);
2231        f.take_log();
2232        f.drag.update_pointer(PRESS);
2233        f.drag.set_hovered(Some(f.first));
2234        assert!(f.take_log().is_empty());
2235    }
2236
2237    #[test]
2238    fn ids_are_unique_and_every_change_raises_a_flush() {
2239        let f = Fixture::new();
2240        let mut ids = vec![
2241            f.source.get(),
2242            f.other_source.get(),
2243            f.first.get(),
2244            f.second.get(),
2245        ];
2246        ids.sort_unstable();
2247        ids.dedup();
2248        assert_eq!(ids.len(), 4);
2249
2250        frust_core::take_pending_result_flush();
2251        f.drag.update_pointer(MOVED); // Idle: no change, no flush
2252        assert!(!frust_core::take_pending_result_flush());
2253        f.drag.arm(f.source, PRESS);
2254        assert!(frust_core::take_pending_result_flush());
2255    }
2256
2257    #[test]
2258    fn clones_share_one_session() {
2259        let f = Fixture::new();
2260        let other = f.drag.clone();
2261        other.arm(f.source, PRESS);
2262        assert_eq!(f.drag.phase(), Armed);
2263        assert_eq!(
2264            f.take_log().len(),
2265            1,
2266            "a clone's change reaches the original's subscriber"
2267        );
2268    }
2269
2270    #[test]
2271    fn register_and_unregister_targets() {
2272        let f = Fixture::new();
2273        assert_eq!(f.drag.registered_targets().len(), 0);
2274        f.drag.register_target(f.first);
2275        assert_eq!(f.drag.registered_targets().len(), 1);
2276        assert!(f.drag.registered_targets().contains(&f.first));
2277        // Re-registering is idempotent.
2278        f.drag.register_target(f.first);
2279        assert_eq!(f.drag.registered_targets().len(), 1);
2280        f.drag.register_target(f.second);
2281        assert_eq!(f.drag.registered_targets().len(), 2);
2282        f.drag.unregister_target(f.first);
2283        assert_eq!(f.drag.registered_targets().len(), 1);
2284        assert!(!f.drag.registered_targets().contains(&f.first));
2285        assert!(f.drag.registered_targets().contains(&f.second));
2286    }
2287
2288    #[test]
2289    fn unregister_while_hovered_emits_leave_and_clears_hover() {
2290        let f = Fixture::new();
2291        f.drag.register_target(f.first);
2292        f.drag.register_target(f.second);
2293        f.drag.arm(f.source, PRESS);
2294        f.drag.begin(7_i32);
2295        f.drag.set_hovered(Some(f.first));
2296        f.take_log();
2297        // Unregistering the hovered target emits Leave.
2298        f.drag.unregister_target(f.first);
2299        let changes = f.take_log();
2300        assert_eq!(
2301            changes,
2302            vec![DragStateChange::Leave { target: f.first }],
2303            "unregister while hovered emits Leave"
2304        );
2305        assert_eq!(
2306            f.drag.state().session().and_then(|s| s.hovered),
2307            None,
2308            "hover is cleared after unregister"
2309        );
2310    }
2311
2312    #[test]
2313    fn unregistering_the_pending_drop_target_ends_the_session() {
2314        let f = Fixture::new();
2315        f.drag.register_target(f.first);
2316        f.drag.arm(f.source, PRESS);
2317        f.drag.begin(7_i32);
2318        f.drag.set_hovered(Some(f.first));
2319        f.drag.drop();
2320        f.take_log();
2321        f.drag.unregister_target(f.first);
2322        assert_eq!(
2323            f.take_log(),
2324            vec![
2325                DragStateChange::Leave { target: f.first },
2326                DragStateChange::Phase {
2327                    previous: DragPhase::Dropping,
2328                    next: DragPhase::Cancelled,
2329                },
2330                DragStateChange::Phase {
2331                    previous: DragPhase::Cancelled,
2332                    next: DragPhase::Idle,
2333                },
2334            ],
2335            "one Leave, then the session is cancelled"
2336        );
2337        assert_eq!(f.drag.state(), DragState::Idle);
2338        assert!(
2339            !f.drag.payload_is::<i32>(),
2340            "the unclaimed payload is discarded"
2341        );
2342        // Nothing is left to leave a second time.
2343        f.drag.complete_drop();
2344        f.drag.cancel();
2345        assert!(f.take_log().is_empty());
2346    }
2347
2348    #[test]
2349    fn unregistering_another_target_while_dropping_changes_nothing() {
2350        let f = Fixture::new();
2351        f.drag.register_target(f.first);
2352        f.drag.register_target(f.second);
2353        f.drag.arm(f.source, PRESS);
2354        f.drag.begin(7_i32);
2355        f.drag.set_hovered(Some(f.first));
2356        f.drag.drop();
2357        f.take_log();
2358        f.drag.unregister_target(f.second);
2359        assert!(f.take_log().is_empty());
2360        assert_eq!(f.drag.phase(), DragPhase::Dropping);
2361    }
2362
2363    #[test]
2364    fn set_and_query_target_bounds() {
2365        let f = Fixture::new();
2366        f.drag.register_target(f.first);
2367        assert_eq!(f.drag.target_bounds(f.first), None, "unset bounds are None");
2368        let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
2369        f.drag.set_target_bounds(f.first, rect);
2370        assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2371        let new_rect = Rect::from_origin_size(Point::new(5.0, 10.0), (200.0, 100.0));
2372        f.drag.set_target_bounds(f.first, new_rect);
2373        assert_eq!(f.drag.target_bounds(f.first), Some(new_rect));
2374    }
2375
2376    #[test]
2377    fn set_target_bounds_on_unknown_id_is_ignored() {
2378        let f = Fixture::new();
2379        let unregistered = f.drag.new_target_id();
2380        let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
2381        // Should not panic, and should be logged in debug builds.
2382        f.drag.set_target_bounds(unregistered, rect);
2383        assert_eq!(f.drag.target_bounds(unregistered), None);
2384    }
2385
2386    #[test]
2387    fn target_at_finds_single_containing_target() {
2388        let f = Fixture::new();
2389        f.drag.register_target(f.first);
2390        let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
2391        f.drag.set_target_bounds(f.first, rect);
2392        let point = Point::new(50.0, 50.0);
2393        assert_eq!(
2394            f.drag.target_at(point),
2395            Some(f.first),
2396            "point inside bounds returns the target"
2397        );
2398        let outside = Point::new(200.0, 200.0);
2399        assert_eq!(
2400            f.drag.target_at(outside),
2401            None,
2402            "point outside all bounds returns None"
2403        );
2404    }
2405
2406    #[test]
2407    fn a_nested_target_registered_after_its_container_wins() {
2408        let f = Fixture::new();
2409        f.drag.register_target(f.first);
2410        f.drag.register_target(f.second);
2411        // The container, registered first.
2412        let large = Rect::from_origin_size(Point::new(0.0, 0.0), (200.0, 200.0));
2413        f.drag.set_target_bounds(f.first, large);
2414        // A target nested inside it, registered after it.
2415        let small = Rect::from_origin_size(Point::new(50.0, 50.0), (100.0, 100.0));
2416        f.drag.set_target_bounds(f.second, small);
2417        assert_eq!(f.drag.target_at(Point::new(100.0, 100.0)), Some(f.second));
2418        assert_eq!(
2419            f.drag.target_at(Point::new(10.0, 10.0)),
2420            Some(f.first),
2421            "outside the nested target the container still resolves"
2422        );
2423    }
2424
2425    #[test]
2426    fn two_overlapping_targets_resolve_to_the_later_registration() {
2427        let f = Fixture::new();
2428        // Registered in the opposite order to their ids, and the later one is
2429        // the larger: registration order alone decides, not id or area.
2430        f.drag.register_target(f.second);
2431        f.drag.register_target(f.first);
2432        f.drag
2433            .set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
2434        f.drag
2435            .set_target_bounds(f.first, Rect::new(50.0, 50.0, 300.0, 300.0));
2436        let overlap = Point::new(75.0, 75.0);
2437        assert_eq!(f.drag.target_at(overlap), Some(f.first));
2438        assert_eq!(f.drag.registered_targets(), vec![f.second, f.first]);
2439
2440        // A drag resolves the same way.
2441        f.drag.arm(f.source, PRESS);
2442        f.drag.begin(1_u8);
2443        f.take_log();
2444        f.drag.update_pointer(overlap);
2445        assert_eq!(
2446            f.take_log(),
2447            vec![
2448                DragStateChange::Enter { target: f.first },
2449                DragStateChange::Move { pointer: overlap },
2450            ]
2451        );
2452
2453        // Re-registering keeps the original stamp; registering anew after an
2454        // unregister moves the target to the end.
2455        f.drag.register_target(f.second);
2456        assert_eq!(f.drag.target_at(overlap), Some(f.first));
2457        f.drag.unregister_target(f.second);
2458        f.drag.register_target(f.second);
2459        f.drag
2460            .set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
2461        assert_eq!(f.drag.target_at(overlap), Some(f.second));
2462    }
2463
2464    #[test]
2465    fn moving_across_three_targets_enters_and_leaves_each_in_turn() {
2466        let f = Fixture::new();
2467        let third = f.drag.new_target_id();
2468        // Three side-by-side columns with a gap between the second and third.
2469        let columns = [
2470            (f.first, Rect::new(0.0, 0.0, 100.0, 300.0)),
2471            (f.second, Rect::new(100.0, 0.0, 200.0, 300.0)),
2472            (third, Rect::new(220.0, 0.0, 320.0, 300.0)),
2473        ];
2474        for (id, rect) in columns {
2475            f.drag.register_target(id);
2476            f.drag.set_target_bounds(id, rect);
2477        }
2478        f.drag.arm(f.source, Point::new(50.0, 10.0));
2479        f.drag.begin(1_u8);
2480        f.take_log();
2481
2482        let path = [
2483            Point::new(50.0, 50.0),   // first
2484            Point::new(60.0, 60.0),   // still first
2485            Point::new(150.0, 60.0),  // second
2486            Point::new(210.0, 60.0),  // the gap
2487            Point::new(250.0, 60.0),  // third
2488            Point::new(250.0, 400.0), // below everything
2489        ];
2490        for point in path {
2491            f.drag.update_pointer(point);
2492        }
2493        let mv = |pointer: Point| DragStateChange::Move { pointer };
2494        assert_eq!(
2495            f.take_log(),
2496            vec![
2497                DragStateChange::Enter { target: f.first },
2498                mv(path[0]),
2499                mv(path[1]),
2500                DragStateChange::Leave { target: f.first },
2501                DragStateChange::Enter { target: f.second },
2502                mv(path[2]),
2503                DragStateChange::Leave { target: f.second },
2504                mv(path[3]),
2505                DragStateChange::Enter { target: third },
2506                mv(path[4]),
2507                DragStateChange::Leave { target: third },
2508                mv(path[5]),
2509            ]
2510        );
2511        assert_eq!(
2512            f.drag.drop(),
2513            None,
2514            "released outside every target: a cancel"
2515        );
2516        assert_eq!(f.drag.phase(), Idle);
2517    }
2518
2519    #[test]
2520    fn an_explicit_hover_stands_until_the_next_resolution() {
2521        let f = Fixture::new();
2522        f.drag.register_target(f.first);
2523        f.drag
2524            .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2525        Start::Dragging.reach(&f);
2526        f.drag.set_hovered(Some(f.second));
2527        assert_eq!(
2528            f.drag.state().session().and_then(|s| s.hovered),
2529            Some(f.second),
2530            "the override applies at once"
2531        );
2532        f.take_log();
2533        f.drag.update_pointer(Point::new(40.0, 40.0));
2534        assert_eq!(
2535            f.take_log(),
2536            vec![
2537                DragStateChange::Leave { target: f.second },
2538                DragStateChange::Enter { target: f.first },
2539                DragStateChange::Move {
2540                    pointer: Point::new(40.0, 40.0)
2541                },
2542            ]
2543        );
2544    }
2545
2546    #[test]
2547    fn resolve_hover_follows_bounds_that_move_under_a_still_pointer() {
2548        let f = Fixture::new();
2549        f.drag.register_target(f.first);
2550        f.drag.register_target(f.second);
2551        f.drag
2552            .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 50.0));
2553        f.drag
2554            .set_target_bounds(f.second, Rect::new(0.0, 50.0, 100.0, 100.0));
2555        f.drag.arm(f.source, PRESS);
2556        f.drag.begin(1_u8);
2557        f.drag.update_pointer(Point::new(10.0, 40.0));
2558        f.take_log();
2559
2560        // The list scrolls up by 20 px: the second row now sits under the
2561        // pointer, which has not moved.
2562        f.drag
2563            .set_target_bounds(f.first, Rect::new(0.0, -20.0, 100.0, 30.0));
2564        f.drag
2565            .set_target_bounds(f.second, Rect::new(0.0, 30.0, 100.0, 80.0));
2566        assert!(f.take_log().is_empty(), "reporting bounds notifies nothing");
2567        f.drag.resolve_hover();
2568        assert_eq!(
2569            f.take_log(),
2570            vec![
2571                DragStateChange::Leave { target: f.first },
2572                DragStateChange::Enter { target: f.second },
2573            ]
2574        );
2575        f.drag.resolve_hover();
2576        assert!(f.take_log().is_empty(), "an unchanged hover is silent");
2577
2578        // Outside `Dragging` it is a no-op.
2579        f.drag.drop();
2580        f.take_log();
2581        f.drag.resolve_hover();
2582        assert!(f.take_log().is_empty());
2583    }
2584
2585    #[test]
2586    fn target_at_ignores_targets_without_bounds() {
2587        let f = Fixture::new();
2588        f.drag.register_target(f.first);
2589        f.drag.register_target(f.second);
2590        // Only set bounds for the second target.
2591        let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (100.0, 100.0));
2592        f.drag.set_target_bounds(f.second, rect);
2593        let point = Point::new(50.0, 50.0);
2594        assert_eq!(
2595            f.drag.target_at(point),
2596            Some(f.second),
2597            "ignores registered targets without bounds set"
2598        );
2599    }
2600
2601    #[test]
2602    fn target_bounds_persist_across_drag_sessions() {
2603        let f = Fixture::new();
2604        f.drag.register_target(f.first);
2605        let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
2606        f.drag.set_target_bounds(f.first, rect);
2607        // Start and complete a drag session.
2608        f.drag.arm(f.source, PRESS);
2609        f.drag.begin(7_i32);
2610        f.drag.update_pointer(MOVED);
2611        f.drag.drop();
2612        f.drag.complete_drop();
2613        // Bounds should still be there after the session.
2614        assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2615    }
2616
2617    #[test]
2618    fn lift_begins_a_keyboard_session_with_no_pointer_resolution() {
2619        let f = Fixture::new();
2620        f.drag.register_target(f.first);
2621        f.drag
2622            .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2623        assert!(f.drag.lift(f.source));
2624        assert_eq!(
2625            f.drag.state(),
2626            DragState::Armed {
2627                source: f.source,
2628                press: Point::ZERO,
2629                keyboard: true,
2630            }
2631        );
2632        assert!(f.drag.begin(9_u8));
2633        let DragState::Dragging(session) = f.drag.state() else {
2634            panic!("expected Dragging");
2635        };
2636        assert!(session.keyboard, "begin carries the keyboard flag over");
2637        assert_eq!(session.hovered, None);
2638
2639        // A stray pointer report never fights the keyboard session: it
2640        // resolves nothing even though the pointer lands inside a
2641        // registered target's bounds.
2642        f.take_log();
2643        f.drag.update_pointer(Point::new(50.0, 50.0));
2644        assert!(f.take_log().is_empty());
2645        assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2646        f.drag.resolve_hover();
2647        assert!(f.take_log().is_empty());
2648    }
2649
2650    #[test]
2651    fn move_to_next_and_previous_target_cycle_in_registration_order_and_wrap() {
2652        let f = Fixture::new();
2653        let third = f.drag.new_target_id();
2654        f.drag.register_target(f.first);
2655        f.drag.register_target(f.second);
2656        f.drag.register_target(third);
2657        for id in [f.first, f.second, third] {
2658            f.drag
2659                .set_target_bounds(id, Rect::new(0.0, 0.0, 10.0, 10.0));
2660        }
2661        f.drag.lift(f.source);
2662        f.drag.begin(1_u8);
2663        f.take_log();
2664
2665        f.drag.move_to_next_target();
2666        assert_eq!(
2667            f.drag.state().session().and_then(|s| s.hovered),
2668            Some(f.first),
2669            "starts at the first registered target"
2670        );
2671        f.drag.move_to_next_target();
2672        assert_eq!(
2673            f.drag.state().session().and_then(|s| s.hovered),
2674            Some(f.second)
2675        );
2676        f.drag.move_to_next_target();
2677        assert_eq!(
2678            f.drag.state().session().and_then(|s| s.hovered),
2679            Some(third)
2680        );
2681        f.drag.move_to_next_target();
2682        assert_eq!(
2683            f.drag.state().session().and_then(|s| s.hovered),
2684            Some(f.first),
2685            "wraps back to the first"
2686        );
2687        assert_eq!(
2688            f.take_log(),
2689            vec![
2690                DragStateChange::Enter { target: f.first },
2691                DragStateChange::Leave { target: f.first },
2692                DragStateChange::Enter { target: f.second },
2693                DragStateChange::Leave { target: f.second },
2694                DragStateChange::Enter { target: third },
2695                DragStateChange::Leave { target: third },
2696                DragStateChange::Enter { target: f.first },
2697            ]
2698        );
2699
2700        f.drag.move_to_previous_target();
2701        assert_eq!(
2702            f.drag.state().session().and_then(|s| s.hovered),
2703            Some(third),
2704            "wraps back to the last going the other way"
2705        );
2706    }
2707
2708    #[test]
2709    fn move_to_previous_target_starts_at_the_last_when_nothing_is_hovered() {
2710        let f = Fixture::new();
2711        f.drag.register_target(f.first);
2712        f.drag.register_target(f.second);
2713        f.drag
2714            .set_target_bounds(f.first, Rect::new(0.0, 0.0, 10.0, 10.0));
2715        f.drag
2716            .set_target_bounds(f.second, Rect::new(0.0, 0.0, 10.0, 10.0));
2717        f.drag.lift(f.source);
2718        f.drag.begin(1_u8);
2719        f.drag.move_to_previous_target();
2720        assert_eq!(
2721            f.drag.state().session().and_then(|s| s.hovered),
2722            Some(f.second)
2723        );
2724    }
2725
2726    #[test]
2727    fn move_target_is_a_no_op_outside_dragging_or_with_no_targets() {
2728        let f = Fixture::new();
2729        f.drag.move_to_next_target();
2730        assert_eq!(f.drag.phase(), DragPhase::Idle);
2731        f.drag.lift(f.source);
2732        f.take_log();
2733        f.drag.move_to_next_target();
2734        assert!(
2735            f.take_log().is_empty(),
2736            "Armed is not Dragging yet — still a no-op"
2737        );
2738        f.drag.begin(1_u8);
2739        f.take_log();
2740        f.drag.move_to_next_target();
2741        assert!(f.take_log().is_empty(), "no registered targets to cycle");
2742    }
2743
2744    #[test]
2745    fn lift_while_dragging_is_ignored() {
2746        let f = Fixture::new();
2747        f.drag.arm(f.source, PRESS);
2748        f.drag.begin(1_u8);
2749        assert!(!f.drag.lift(f.other_source));
2750        assert_eq!(
2751            f.drag.state().session().and_then(|s| s.source),
2752            Some(f.source)
2753        );
2754    }
2755
2756    /// A frame time for a clocked bounds report in pass `n`.
2757    fn frame(n: u64) -> FrameTime {
2758        FrameTime::from_nanos(n * 16_000_000)
2759    }
2760
2761    #[test]
2762    fn resolution_skips_a_later_registered_target_of_another_payload_type() {
2763        let f = Fixture::new();
2764        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2765        f.drag.register_target_for::<u8>(f.first);
2766        f.drag.register_target_for::<u32>(f.second);
2767        f.drag.set_target_bounds(f.first, rect);
2768        f.drag.set_target_bounds(f.second, rect);
2769        let inside = Point::new(50.0, 50.0);
2770        assert_eq!(
2771            f.drag.target_at(inside),
2772            Some(f.second),
2773            "with no session, the later registration wins as before"
2774        );
2775
2776        f.drag.arm(f.source, PRESS);
2777        f.drag.begin(3_u8);
2778        assert_eq!(f.drag.target_at(inside), Some(f.first));
2779        f.take_log();
2780        f.drag.update_pointer(inside);
2781        assert_eq!(
2782            f.take_log(),
2783            vec![
2784                DragStateChange::Enter { target: f.first },
2785                DragStateChange::Move { pointer: inside },
2786            ],
2787            "the u8 session resolves onto the u8 target under the u32 one"
2788        );
2789        f.drag.resolve_hover();
2790        assert_eq!(
2791            f.drag.state().session().and_then(|s| s.hovered),
2792            Some(f.first)
2793        );
2794    }
2795
2796    #[test]
2797    fn resolution_finds_nothing_when_only_other_types_are_under_the_pointer() {
2798        let f = Fixture::new();
2799        f.drag.register_target_for::<u32>(f.first);
2800        f.drag
2801            .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2802        f.drag.arm(f.source, PRESS);
2803        f.drag.begin("text");
2804        f.drag.update_pointer(Point::new(50.0, 50.0));
2805        assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2806        assert_eq!(f.drag.drop(), None, "released over nothing: a cancel");
2807        assert_eq!(f.drag.phase(), DragPhase::Idle);
2808    }
2809
2810    #[test]
2811    fn an_external_session_resolves_only_path_list_targets() {
2812        let f = Fixture::new();
2813        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2814        f.drag.register_target_for::<Vec<PathBuf>>(f.first);
2815        f.drag.register_target_for::<u32>(f.second);
2816        f.drag.set_target_bounds(f.first, rect);
2817        f.drag.set_target_bounds(f.second, rect);
2818        f.drag
2819            .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
2820        f.drag.update_pointer(Point::new(50.0, 50.0));
2821        assert_eq!(
2822            f.drag.state().session().and_then(|s| s.hovered),
2823            Some(f.first)
2824        );
2825    }
2826
2827    #[test]
2828    fn keyboard_cycling_skips_targets_of_another_payload_type() {
2829        let f = Fixture::new();
2830        let third = f.drag.new_target_id();
2831        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2832        f.drag.register_target_for::<u32>(f.first);
2833        f.drag.register_target_for::<u8>(f.second);
2834        f.drag.register_target(third);
2835        for id in [f.first, f.second, third] {
2836            f.drag.set_target_bounds(id, rect);
2837        }
2838        f.drag.lift(f.source);
2839        f.drag.begin(1_u8);
2840        let hovered = || f.drag.state().session().and_then(|s| s.hovered);
2841        f.drag.move_to_next_target();
2842        assert_eq!(hovered(), Some(f.second), "the u32 target is skipped");
2843        f.drag.move_to_next_target();
2844        assert_eq!(hovered(), Some(third), "an untyped target accepts any");
2845        f.drag.move_to_next_target();
2846        assert_eq!(hovered(), Some(f.second), "wraps past the u32 target again");
2847        f.drag.move_to_previous_target();
2848        assert_eq!(hovered(), Some(third));
2849    }
2850
2851    #[test]
2852    fn a_target_clipped_to_nothing_is_not_resolved_or_cycled() {
2853        let f = Fixture::new();
2854        f.drag.register_target(f.first);
2855        f.drag.register_target(f.second);
2856        f.drag
2857            .report_target_bounds(f.first, Some(Rect::new(0.0, 0.0, 100.0, 100.0)), frame(1));
2858        f.drag.report_target_bounds(f.second, None, frame(1));
2859        assert_eq!(f.drag.target_bounds(f.second), None);
2860        assert_eq!(f.drag.target_at(Point::new(50.0, 50.0)), Some(f.first));
2861
2862        f.drag.lift(f.source);
2863        f.drag.begin(1_u8);
2864        f.drag.move_to_next_target();
2865        f.drag.move_to_next_target();
2866        assert_eq!(
2867            f.drag.state().session().and_then(|s| s.hovered),
2868            Some(f.first),
2869            "the clipped target is never cycled to"
2870        );
2871    }
2872
2873    #[test]
2874    fn a_target_that_stopped_reporting_is_not_resolved() {
2875        let f = Fixture::new();
2876        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2877        let inside = Point::new(50.0, 50.0);
2878        f.drag.register_target(f.first);
2879        f.drag.register_target(f.second);
2880        f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2881        f.drag.report_target_bounds(
2882            f.second,
2883            Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
2884            frame(1),
2885        );
2886        assert_eq!(f.drag.target_at(inside), Some(f.first));
2887
2888        // The next frame paints only the second target: the first keeps its
2889        // old bounds on record but no longer resolves.
2890        f.drag.report_target_bounds(
2891            f.second,
2892            Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
2893            frame(2),
2894        );
2895        assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2896        assert_eq!(f.drag.target_at(inside), None);
2897
2898        f.drag.arm(f.source, PRESS);
2899        f.drag.begin(1_u8);
2900        f.drag.update_pointer(inside);
2901        assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2902
2903        // Painting again restores it.
2904        f.drag.report_target_bounds(f.first, Some(rect), frame(3));
2905        f.drag.resolve_hover();
2906        assert_eq!(
2907            f.drag.state().session().and_then(|s| s.hovered),
2908            Some(f.first)
2909        );
2910    }
2911
2912    #[test]
2913    fn resolve_hover_tolerates_a_target_one_pass_behind_but_not_two() {
2914        let f = Fixture::new();
2915        let hovered_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2916        let elsewhere = Rect::new(200.0, 0.0, 300.0, 100.0);
2917        let point = Point::new(50.0, 50.0);
2918        f.drag.register_target(f.first);
2919        f.drag.register_target(f.second);
2920        f.drag
2921            .report_target_bounds(f.first, Some(elsewhere), frame(1));
2922        f.drag
2923            .report_target_bounds(f.second, Some(hovered_rect), frame(1));
2924        f.drag.arm(f.source, PRESS);
2925        f.drag.begin(1_u8);
2926        f.drag.update_pointer(point);
2927        assert_eq!(
2928            f.drag.state().session().and_then(|s| s.hovered),
2929            Some(f.second)
2930        );
2931        f.take_log();
2932
2933        // The next pass: only the first has repainted so far, as if an
2934        // auto-scroll zone enclosing it called `resolve_hover` right after
2935        // its own child painted, before a later sibling got its own turn.
2936        // The second's bounds are one pass behind, not stale, so the hover
2937        // does not flicker.
2938        f.drag
2939            .report_target_bounds(f.first, Some(elsewhere), frame(2));
2940        f.drag.resolve_hover();
2941        assert!(
2942            f.take_log().is_empty(),
2943            "a target one pass behind is tolerated, not treated as culled"
2944        );
2945        assert_eq!(
2946            f.drag.state().session().and_then(|s| s.hovered),
2947            Some(f.second)
2948        );
2949
2950        // A further pass with the second still silent: it is now two full
2951        // passes behind — genuinely stale, the tolerance window has
2952        // expired, and the culled-target guarantee holds exactly as
2953        // before.
2954        f.drag
2955            .report_target_bounds(f.first, Some(elsewhere), frame(3));
2956        f.drag.resolve_hover();
2957        assert_eq!(
2958            f.take_log(),
2959            vec![DragStateChange::Leave { target: f.second }],
2960            "two full passes without repainting is culled, not tolerated"
2961        );
2962    }
2963
2964    #[test]
2965    fn a_stale_target_is_not_cycled_to() {
2966        let f = Fixture::new();
2967        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2968        f.drag.register_target(f.first);
2969        f.drag.register_target(f.second);
2970        f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2971        f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2972        f.drag.report_target_bounds(f.second, Some(rect), frame(2));
2973        f.drag.lift(f.source);
2974        f.drag.begin(1_u8);
2975        f.drag.move_to_next_target();
2976        assert_eq!(
2977            f.drag.state().session().and_then(|s| s.hovered),
2978            Some(f.second)
2979        );
2980        f.drag.move_to_next_target();
2981        assert_eq!(
2982            f.drag.state().session().and_then(|s| s.hovered),
2983            Some(f.second),
2984            "only the target painted in the latest pass is cycled"
2985        );
2986    }
2987
2988    #[test]
2989    fn painting_twice_in_one_frame_keeps_the_pass_open() {
2990        let f = Fixture::new();
2991        let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2992        f.drag.register_target(f.first);
2993        f.drag.register_target(f.second);
2994        f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2995        f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2996        f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2997        assert_eq!(
2998            f.drag.target_at(Point::new(5.0, 5.0)),
2999            Some(f.second),
3000            "same frame time, same pass: both still resolve"
3001        );
3002        f.drag.unregister_target(f.second);
3003        assert_eq!(f.drag.target_at(Point::new(5.0, 5.0)), Some(f.first));
3004    }
3005
3006    #[test]
3007    fn an_unclocked_repeat_report_opens_the_next_pass() {
3008        let f = Fixture::new();
3009        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
3010        let inside = Point::new(50.0, 50.0);
3011        f.drag.register_target(f.first);
3012        f.drag.register_target(f.second);
3013        f.drag.set_target_bounds(f.first, rect);
3014        f.drag
3015            .set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
3016        assert_eq!(f.drag.target_at(inside), Some(f.first));
3017        // The second reports again: a new pass, which the first missed.
3018        f.drag
3019            .set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
3020        assert_eq!(f.drag.target_at(inside), None);
3021        // A zero frame time counts as unclocked.
3022        f.drag
3023            .report_target_bounds(f.first, Some(rect), FrameTime::ZERO);
3024        assert_eq!(f.drag.target_at(inside), Some(f.first));
3025    }
3026
3027    #[test]
3028    fn later_registration_still_wins_among_visible_type_matching_overlaps() {
3029        let f = Fixture::new();
3030        let third = f.drag.new_target_id();
3031        let fourth = f.drag.new_target_id();
3032        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
3033        let inside = Point::new(50.0, 50.0);
3034        f.drag.register_target_for::<u8>(f.first);
3035        f.drag.register_target_for::<u8>(f.second);
3036        // Registered later still, but stale and of another type respectively.
3037        f.drag.register_target_for::<u8>(third);
3038        f.drag.register_target_for::<u32>(fourth);
3039        f.drag.report_target_bounds(third, Some(rect), frame(1));
3040        for id in [f.first, f.second, fourth] {
3041            f.drag.report_target_bounds(id, Some(rect), frame(2));
3042        }
3043        f.drag.arm(f.source, PRESS);
3044        f.drag.begin(1_u8);
3045        f.drag.update_pointer(inside);
3046        assert_eq!(
3047            f.drag.state().session().and_then(|s| s.hovered),
3048            Some(f.second)
3049        );
3050    }
3051
3052    #[test]
3053    fn is_external_reports_the_kind_of_the_session_in_flight() {
3054        let f = Fixture::new();
3055        assert!(!f.drag.is_external());
3056        f.drag.arm(f.source, PRESS);
3057        assert!(!f.drag.is_external());
3058        f.drag.begin(1_u8);
3059        assert!(!f.drag.is_external());
3060        f.drag.cancel();
3061        f.drag.register_target(f.first);
3062        f.drag
3063            .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3064        assert!(f.drag.is_external());
3065        f.drag.set_hovered(Some(f.first));
3066        f.drag.drop();
3067        assert!(f.drag.is_external(), "still external while Dropping");
3068        f.drag.complete_drop();
3069        assert!(!f.drag.is_external());
3070    }
3071
3072    #[test]
3073    fn end_external_ends_a_hovering_external_session_once() {
3074        let f = Fixture::new();
3075        f.drag
3076            .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3077        f.drag.set_hovered(Some(f.first));
3078        f.take_log();
3079
3080        assert!(f.drag.end_external(), "the first call ends it");
3081        assert_eq!(
3082            f.take_log(),
3083            vec![
3084                DragStateChange::Leave { target: f.first },
3085                DragStateChange::Phase {
3086                    previous: Dragging,
3087                    next: Cancelled
3088                },
3089                DragStateChange::Phase {
3090                    previous: Cancelled,
3091                    next: Idle
3092                },
3093            ]
3094        );
3095        assert_eq!(f.drag.state(), DragState::Idle);
3096
3097        assert!(!f.drag.end_external(), "every later call is a no-op");
3098        assert!(f.take_log().is_empty());
3099    }
3100
3101    #[test]
3102    fn end_external_leaves_internal_armed_and_dropping_sessions_alone() {
3103        let f = Fixture::new();
3104        assert!(!f.drag.end_external(), "nothing in flight");
3105
3106        f.drag.arm(f.source, PRESS);
3107        assert!(!f.drag.end_external());
3108        assert_eq!(f.drag.phase(), DragPhase::Armed);
3109
3110        f.drag.begin(1_u8);
3111        assert!(!f.drag.end_external());
3112        assert_eq!(f.drag.phase(), DragPhase::Dragging);
3113        f.drag.cancel();
3114
3115        assert!(f.drag.lift(f.source), "a keyboard session");
3116        f.drag.begin(2_u8);
3117        assert!(!f.drag.end_external());
3118        assert!(
3119            f.drag.payload_is::<u8>(),
3120            "the keyboard session is untouched"
3121        );
3122        f.drag.cancel();
3123
3124        f.drag
3125            .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3126        f.drag.set_hovered(Some(f.first));
3127        assert_eq!(f.drag.drop(), Some(f.first));
3128        f.take_log();
3129        assert!(
3130            !f.drag.end_external(),
3131            "a released drop is its target's to claim"
3132        );
3133        assert!(f.take_log().is_empty());
3134        assert_eq!(f.drag.phase(), DragPhase::Dropping);
3135    }
3136}