1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
//! Drag-and-drop session model: the [`DragCoordinator`] handle every
//! draggable source and drop target in one scope shares.
//!
//! The coordinator is pure logic — no widget, no paint, no event routing. It
//! is the single source of truth for "is a drag in flight, what does it
//! carry, where is the pointer, and which target is under it", and the
//! widgets built on it (a draggable source, a drop target, cross-container
//! resolution, OS file drops) read and drive it rather than each keeping a
//! copy.
//!
//! # Ambient, not global
//!
//! An app creates one [`DragCoordinator`] per scope that should share drags
//! (usually one per window) and hands a clone to every source and target in
//! it — the same cloneable-handle shape as
//! [`ScrollController`](crate::ScrollController). Every clone shares one
//! state; two coordinators never see each other's sessions, so a source can
//! only ever land on a target holding the same handle. Source and target ids
//! ([`DragSourceId`]/[`DragTargetId`]) are allocated by the coordinator
//! ([`DragCoordinator::new_source_id`]/[`DragCoordinator::new_target_id`]) and
//! are unique within it, not across coordinators.
//!
//! # The state machine
//!
//! ```text
//! Idle ──arm──▶ Armed ──begin──▶ Dragging ──drop (hovered)──▶ Dropping ──complete_drop──▶ Idle
//! Idle ──begin_external──────────▶ Dragging
//! Armed | Dragging | Dropping ──cancel──▶ Cancelled ──▶ Idle
//! ```
//!
//! - **Idle → Armed** ([`DragCoordinator::arm`]): a source saw a press that may
//! become a drag. Re-arming while armed (or while an unclaimed drop is
//! still `Dropping`) cancels the earlier one first; arming while a drag is
//! already in flight is ignored (debug-build log) — the session in flight
//! owns the pointer.
//! - **Armed → Dragging** ([`DragCoordinator::begin`]): the source crossed its
//! initiation policy and hands over the typed payload. `begin` without an
//! arm is an error: it is ignored with a debug-build log. A second `begin`
//! while `Dragging` cancels the first session and restarts from the same
//! source with the new payload, at the first session's current pointer.
//! - **Idle → Dragging** ([`DragCoordinator::begin_external`]): an
//! OS-originated drag ([`DragKind::ExternalFiles`]) has no source and skips
//! `Armed`; its payload is the `Vec<PathBuf>` itself. It supersedes any
//! session in flight (cancelling it first).
//! - **Dragging → Dropping** ([`DragCoordinator::drop`]): released over a
//! hovered target. The session stays `Dropping` — payload still held —
//! until the target claims it with [`DragCoordinator::take_payload`] and
//! calls [`DragCoordinator::complete_drop`] (→ `Idle`). A drop with nothing
//! hovered behaves exactly as a cancel, and so does unregistering the
//! pending drop target before it claims the payload.
//! - **→ Cancelled → Idle** ([`DragCoordinator::cancel`]): legal from every
//! non-`Idle` phase and always lands on `Idle`; a target still hovered (or
//! the target of an unclaimed drop) hears a `Leave` first. `Cancelled` is a
//! transient phase: it is reported as the
//! `Dragging → Cancelled`, `Cancelled → Idle` notification pair, but
//! [`DragCoordinator::state`] already reads `Idle` by the time any
//! subscriber hears either.
//!
//! Pointer moves ([`DragCoordinator::update_pointer`]), hover resolution
//! ([`DragCoordinator::resolve_hover`]) and hover overrides
//! ([`DragCoordinator::set_hovered`]) apply only while `Dragging` and are
//! silent no-ops otherwise, so a per-frame pass may call them
//! unconditionally.
//!
//! # Target resolution
//!
//! The coordinator resolves which target is under the pointer itself, from a
//! registry rather than a hit test — the ghost floats in a transparent overlay
//! pod above everything, and targets in different containers (two kanban
//! columns, a list and a trash bin) must resolve against one another:
//!
//! - **Registry.** A target registers its id together with the payload type
//! it accepts ([`DragCoordinator::register_target_for`]; an untyped
//! [`DragCoordinator::register_target`] accepts any) and reports its
//! visible window-space bounds every paint
//! ([`DragCoordinator::report_target_bounds`]).
//! - **Type gating is enforced at resolution.** Pointer resolution,
//! [`DragCoordinator::target_at`] and keyboard cycling consider only
//! targets whose accepted type is the live session's payload type (an
//! [`DragKind::ExternalFiles`] session's is `Vec<PathBuf>`), so targets of
//! different payload types can share one coordinator — and overlap — without
//! a session ever resolving onto, or cycling through, a target that would
//! refuse it.
//! - **Only what is visible now resolves.** A target reports its bounds
//! clipped to the visible region its ancestors threaded down (a scroll
//! surface's viewport); clipped to nothing, it reports no bounds. Each
//! report is stamped with the paint pass it belongs to: a report carrying a
//! different frame time from the one that opened the current pass opens a
//! new pass, and only targets stamped with the latest pass resolve or cycle,
//! so a target that did not paint this frame (culled, scrolled away) cannot
//! resolve against stale bounds. Without a shell clock
//! ([`frust_core::FrameTime::ZERO`], or the unclocked
//! [`DragCoordinator::set_target_bounds`]) a target reporting a second time
//! is what opens the next pass. **Limit:** a pass is only ever opened by a
//! report, so a frame in which *no* target of a coordinator paints leaves the
//! previous pass standing until one does. Keyboard cycling
//! ([`DragCoordinator::move_to_next_target`]/
//! [`DragCoordinator::move_to_previous_target`]) inherits this same
//! visibility gate: it only reaches a target visible in the latest pass,
//! and nothing scrolls an off-screen one into view on the keyboard's
//! behalf, so a keyboard user can move only between a list's currently
//! *visible* targets once that list is taller than its viewport
//! (`drag-keyboard-cycle-visible-targets-only`, `docs/LIMITATIONS.md`).
//! - **Every pointer report resolves.** [`DragCoordinator::update_pointer`]
//! records the pointer and re-picks the hovered target as
//! [`DragCoordinator::target_at`] of it, reporting a change as `Leave` (old)
//! then `Enter` (new), then the `Move`.
//! [`DragCoordinator::resolve_hover`] re-picks without moving the pointer,
//! for bounds that moved under it (auto-scroll calls it after each step).
//! Because that call happens mid-paint — the auto-scroll zone calls it
//! right after its own child paints, possibly before a later sibling
//! sharing the coordinator gets its own turn this same frame —
//! `resolve_hover` tolerates a target stamped with the pass immediately
//! before the current one, so a sibling that simply has not repainted
//! *yet* is not mistaken for one that stopped painting; `update_pointer`/
//! `target_at` and keyboard cycling stay strict to the latest pass, since
//! a pointer or keyboard event is never dispatched mid-paint.
//! - **Later registration wins an overlap.** Of several visible,
//! type-matching targets whose bounds contain the point, the one registered
//! last is picked. A target
//! registers when its pod is built, before its child is, so a target nested
//! inside another registers after its container and wins inside it; siblings
//! register in tree order, which is paint order, so the one painted on top
//! wins. A target rebuilt later (a fresh pod under a new key) re-registers
//! at the end and wins any overlap with targets that kept their pods.
//! - **Overrides.** [`DragCoordinator::set_hovered`] sets the hover directly
//! and stands only until the next resolution.
//! - **Release.** A [`DragCoordinator::drop`] with nothing hovered — released
//! outside every target — is a cancel.
//!
//! # Payload
//!
//! The payload is a `Box<dyn Any>` with its `TypeId` recorded, read through
//! [`DragCoordinator::payload_is`], [`DragCoordinator::payload_ref`] (a
//! [`Ref`](std::cell::Ref) guard into the shared state — do not hold it across
//! a call that mutates the coordinator, or that call panics on the borrow),
//! [`DragCoordinator::with_payload`] (the closure form, borrow scoped to the
//! closure) and moved out with [`DragCoordinator::take_payload`] while
//! `Dragging` or `Dropping`.
//!
//! # Notifications
//!
//! [`DragCoordinator::subscribe`] registers a listener for every
//! [`DragStateChange`] and returns a [`DragSubscription`] guard that
//! unsubscribes on drop. Every change is delivered to every live subscriber,
//! in subscription order, and every subscriber sees every change in the
//! order the changes happened: a listener may call back into the coordinator
//! (the borrow is released before any listener runs), and the changes that
//! re-entrant call produces are queued behind the ones still being delivered
//! rather than delivered nested. Every mutation that produces a change also
//! raises [`frust_core::mark_pending_result_flush`] so a frame-gated shell
//! runs the frame that repaints it.
//!
//! # Sources
//!
//! [`draggable()`] is the in-app source built on the coordinator: it arms on a
//! primary press, begins the session once the press crosses its [`DragPolicy`]
//! (a distance threshold for a mouse, a stationary long-press for a finger),
//! floats a ghost through the overlay portal at the pointer for the session's
//! length, and drops or cancels on the gesture's `Up`/`Cancel` — or cancels on
//! `Escape`, for which it holds the keyboard focus for the length of the press.
//! A press that never becomes a drag reaches the wrapped child untouched. See
//! the [`mod@draggable`] module docs for the initiation, ghost, feedback and
//! cancel contract.
//!
//! # Targets
//!
//! [`drag_target()`] is the in-app drop target built on the coordinator's
//! registry ([`DragCoordinator::register_target_for`]/
//! [`DragCoordinator::report_target_bounds`]/[`DragCoordinator::target_at`]): it
//! registers a fresh [`DragTargetId`] for its payload type when its pod is
//! built and unregisters it when the pod is dropped (ending the session if it
//! was the pending drop target), reports its clipped window-space bounds every
//! paint so a
//! pointer report can resolve it under the ghost point (see *Target
//! resolution* above for how overlaps are decided). It never subscribes; like
//! [`draggable()`] it polls the coordinator's own state (here on the
//! `Housekeeping` broadcast) and turns what it finds into typed
//! enter/leave/hover/drop callbacks plus a themed highlight. See the
//! [`mod@target`] module docs for the full notification and highlight contract.
//!
//! # Edge auto-scroll
//!
//! [`auto_scroll_zone()`] wraps a scroll surface and, while a session's pointer
//! sits within [`AutoScroll::edge_px`] of its top or bottom edge, jumps the
//! surface's [`ScrollController`](crate::ScrollController) toward that edge
//! every frame at a speed proportional to the depth into the band (up to
//! [`AutoScroll::max_px_per_s`]), stopping at the scroll limits, when the
//! pointer leaves the band, or when the session ends. See the
//! [`mod@auto_scroll`] module docs.
//!
//! # Reorderable lists
//!
//! [`reorderable_list()`] composes [`draggable()`] and [`drag_target()`] into
//! a drag-to-reorder column, needing no new layout: each of its rows carries
//! its own index as the payload, an `N + 1`-gap set of drop targets sits
//! between/around them, and [`ReorderableListView::on_reorder`] fires with
//! the source and destination indices when a drop actually moves something
//! (dropping back on the source's own slot reports nothing). Keyboard
//! lift/cycle/drop is inherited from [`draggable()`] unchanged — the gaps are
//! the only targets this list's sessions resolve or cycle to (type gating is
//! enforced at resolution, so other targets sharing the coordinator under a
//! different payload type are skipped), cycled in the same visual order they
//! are built in. See the [`mod@reorderable`] module docs for the full index
//! convention and limits.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;