Skip to main content

dioxus_dnd/core/
state.rs

1//! The shared drag state. One `DndContext<T>` lives in Dioxus context and is
2//! read/written by `Draggable` and `DropZone` components (and by you, if you
3//! wire events manually).
4//!
5//! Payloads travel through this Rust-side store - not through the browser's
6//! `DataTransfer` - so they can be any `Clone` type with zero serialization.
7//! (`DataTransfer` interop for external drags lives in [`crate::external`].)
8//!
9//! State is held in a [`struct@Store`], Dioxus 0.7's fine-grained reactivity
10//! primitive: each field gets its own lazy subscription. A component that
11//! reads `dnd.over()` in its render only reruns when the hovered zone
12//! changes - not on every pointer move.
13
14use dioxus::prelude::*;
15
16use super::monitor::{CancelReason, DndEvent, DndMonitor, DragSnapshot, DropReceipt};
17use super::session::SourceCompletion;
18use super::types::{DragId, DragMode, DragSessionId, DropEffect, Point, PointerKind, Rect, ZoneId};
19
20/// A snapshot of an in-flight drag.
21///
22/// Deriving [`macro@Store`] generates per-field lenses, which
23/// [`DndContext`]'s accessors use for granular subscriptions.
24#[derive(Store, Debug, Clone, PartialEq)]
25pub struct DragState<T: 'static> {
26    /// The payload currently being dragged, if any.
27    pub payload: Option<T>,
28    /// Zone the drag started from.
29    pub source: Option<ZoneId>,
30    /// Zone the pointer is currently over.
31    pub over: Option<ZoneId>,
32    /// Last known pointer position (client coordinates).
33    pub pointer: Point,
34    /// Where inside the dragged element the user grabbed it.
35    pub grab: Point,
36    /// Effect requested by the draggable.
37    pub effect: DropEffect,
38    /// How this drag is being driven (pointer vs keyboard).
39    pub mode: DragMode,
40    /// Which pointer device drives a pointer drag (mouse/touch/pen).
41    /// Meaningful only while `mode` is [`DragMode::Pointer`]; host-side
42    /// glue reads it to bridge exactly the input layers the device
43    /// needs (see [`PointerKind`]). `Draggable` records it at pickup;
44    /// custom sources that never do get the safe `Mouse` default.
45    pub pointer_kind: PointerKind,
46    /// Client rect of the dragged element, measured at pickup. Feeds
47    /// size-matched ghosts (`DragOverlay { match_source: true }`); `None`
48    /// until the async measurement lands or when a custom source never set
49    /// it.
50    pub source_rect: Option<Rect>,
51    /// Payload of a just-completed keyboard drop, awaiting focus
52    /// restoration: the drop re-mounts the moved item at its landing place
53    /// and the browser dumps focus on `<body>` when the source element
54    /// unmounts, so the matching `Draggable` claims this on mount and
55    /// focuses itself - keyboard users keep their place. Cleared by the
56    /// claim or by the next drag starting.
57    pub refocus: Option<T>,
58    /// Destination rect of a just-completed drop whose overlay is still
59    /// gliding home (the drop-settle animation). While set, `dragging()` is
60    /// false but `payload` stays readable so the ghost keeps its content.
61    pub settle: Option<Rect>,
62}
63
64/// Complete input for starting a drag with an explicit stable identity.
65///
66/// Construct this with [`DragStart::new`] and use the builder methods for
67/// optional metadata. The struct is non-exhaustive so future drag metadata
68/// can be added without breaking downstream callers.
69#[derive(Debug, Clone, PartialEq)]
70#[non_exhaustive]
71pub struct DragStart<T> {
72    pub payload: T,
73    pub source: Option<ZoneId>,
74    pub pointer: Point,
75    pub grab: Point,
76    pub effect: DropEffect,
77    pub mode: DragMode,
78    pub pointer_kind: PointerKind,
79    pub source_rect: Option<Rect>,
80}
81
82impl<T> DragStart<T> {
83    /// Create a pointer drag at `pointer` with default grab and move effect.
84    pub fn new(payload: T, pointer: Point) -> Self {
85        Self {
86            payload,
87            source: None,
88            pointer,
89            grab: Point::default(),
90            effect: DropEffect::default(),
91            mode: DragMode::default(),
92            pointer_kind: PointerKind::default(),
93            source_rect: None,
94        }
95    }
96
97    pub fn with_source(mut self, source: Option<ZoneId>) -> Self {
98        self.source = source;
99        self
100    }
101
102    pub fn with_grab(mut self, grab: Point) -> Self {
103        self.grab = grab;
104        self
105    }
106
107    pub fn with_effect(mut self, effect: DropEffect) -> Self {
108        self.effect = effect;
109        self
110    }
111
112    pub fn with_mode(mut self, mode: DragMode) -> Self {
113        self.mode = mode;
114        self
115    }
116
117    pub fn with_pointer_kind(mut self, pointer_kind: PointerKind) -> Self {
118        self.pointer_kind = pointer_kind;
119        self
120    }
121
122    pub fn with_source_rect(mut self, source_rect: Option<Rect>) -> Self {
123        self.source_rect = source_rect;
124        self
125    }
126}
127
128pub(super) enum DragIdentity {
129    Generated,
130    Explicit(DragId),
131}
132
133impl<T> Default for DragState<T> {
134    fn default() -> Self {
135        Self {
136            payload: None,
137            source: None,
138            over: None,
139            pointer: Point::default(),
140            grab: Point::default(),
141            effect: DropEffect::default(),
142            mode: DragMode::default(),
143            pointer_kind: PointerKind::default(),
144            source_rect: None,
145            refocus: None,
146            settle: None,
147        }
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub(crate) enum DragPhase {
153    #[default]
154    Idle,
155    Dragging,
156    Settling,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq)]
160struct DragRuntimeState {
161    id: Option<DragId>,
162    identity_explicit: bool,
163    session: Option<DragSessionId>,
164    proposed_effect: DropEffect,
165    phase: DragPhase,
166}
167
168impl Default for DragRuntimeState {
169    fn default() -> Self {
170        Self {
171            id: None,
172            identity_explicit: false,
173            session: None,
174            proposed_effect: DropEffect::default(),
175            phase: DragPhase::Idle,
176        }
177    }
178}
179
180pub(super) struct DragRuntime<T: 'static> {
181    state: Signal<DragRuntimeState>,
182    pub(super) completion: Signal<Option<SourceCompletion>>,
183    monitor: DndMonitor<T>,
184}
185
186#[derive(Clone, Copy, PartialEq, Eq)]
187enum PhaseAuthority {
188    /// Preserve the published `from_parts` contract: the caller owns the
189    /// store and may update it through another handle.
190    State,
191    /// Provider/world contexts own every transition, so the private phase is
192    /// authoritative and can enforce terminal-event exclusivity.
193    Runtime,
194}
195
196impl<T> Copy for DragRuntime<T> {}
197impl<T> Clone for DragRuntime<T> {
198    fn clone(&self) -> Self {
199        *self
200    }
201}
202impl<T> PartialEq for DragRuntime<T> {
203    fn eq(&self, other: &Self) -> bool {
204        self.state == other.state
205            && self.completion == other.completion
206            && self.monitor == other.monitor
207    }
208}
209
210impl<T: Clone + 'static> DragRuntime<T> {
211    fn new() -> Self {
212        Self {
213            state: Signal::new(DragRuntimeState::default()),
214            completion: Signal::new(None),
215            monitor: DndMonitor::new(),
216        }
217    }
218}
219
220/// Handle to the shared drag state. Cheap to copy: it contains only Dioxus
221/// store and signal keys.
222pub struct DndContext<T: Clone + 'static> {
223    state: Store<DragState<T>>,
224    /// Screen-reader announcement channel, rendered by
225    /// [`crate::a11y::LiveRegion`].
226    announcement: Signal<String>,
227    pub(super) runtime: DragRuntime<T>,
228    phase_authority: PhaseAuthority,
229}
230
231// Manual impls: `derive` would add unnecessary `T: Copy` / `T: PartialEq`
232// bounds, but the handle contains only store and signal keys.
233impl<T: Clone + 'static> Copy for DndContext<T> {}
234impl<T: Clone + 'static> Clone for DndContext<T> {
235    fn clone(&self) -> Self {
236        *self
237    }
238}
239impl<T: Clone + 'static> PartialEq for DndContext<T> {
240    fn eq(&self, other: &Self) -> bool {
241        // Preserve the 3.x handle identity contract. In particular, two
242        // `from_parts` wrappers around the same announcement remain equal
243        // even though new-feature sidecars are private to each construction.
244        self.announcement == other.announcement
245    }
246}
247
248impl<T: Clone + 'static> DndContext<T> {
249    /// Wrap existing state. Prefer [`crate::core::hooks::use_dnd_provider`].
250    pub fn from_parts(state: Store<DragState<T>>, announcement: Signal<String>) -> Self {
251        Self::from_parts_with_authority(state, announcement, PhaseAuthority::State)
252    }
253
254    /// Construct a provider/world-owned context whose private phase is the
255    /// terminal-state authority. Public `from_parts` cannot use this mode:
256    /// its caller retains the store and is allowed to mutate it independently.
257    pub(crate) fn managed(state: Store<DragState<T>>, announcement: Signal<String>) -> Self {
258        Self::from_parts_with_authority(state, announcement, PhaseAuthority::Runtime)
259    }
260
261    fn from_parts_with_authority(
262        state: Store<DragState<T>>,
263        announcement: Signal<String>,
264        phase_authority: PhaseAuthority,
265    ) -> Self {
266        let phase = if state.settle().peek().is_some() {
267            DragPhase::Settling
268        } else if state.payload().peek().is_some() {
269            DragPhase::Dragging
270        } else {
271            DragPhase::Idle
272        };
273        let mut runtime = DragRuntime::new();
274        {
275            let mut runtime_state = runtime.state.write();
276            runtime_state.phase = phase;
277            runtime_state.proposed_effect = *state.effect().peek();
278        }
279        Self {
280            state,
281            announcement,
282            runtime,
283            phase_authority,
284        }
285    }
286
287    /// Begin a drag. Notifies all fields (state transition).
288    pub fn start(
289        &mut self,
290        payload: T,
291        source: Option<ZoneId>,
292        pointer: Point,
293        grab: Point,
294        effect: DropEffect,
295        mode: DragMode,
296    ) {
297        if !self.prepare_start() {
298            return;
299        }
300        self.start_with_metadata(
301            DragIdentity::Generated,
302            None,
303            DragStart::new(payload, pointer)
304                .with_source(source)
305                .with_grab(grab)
306                .with_effect(effect)
307                .with_mode(mode),
308        );
309    }
310
311    /// Begin a drag with a stable source identity.
312    pub fn start_with_id(&mut self, id: DragId, start: DragStart<T>) {
313        if !self.prepare_start() {
314            return;
315        }
316        self.start_with_metadata(DragIdentity::Explicit(id), None, start);
317    }
318
319    pub(super) fn start_with_metadata(
320        &mut self,
321        identity: DragIdentity,
322        session: Option<DragSessionId>,
323        start: DragStart<T>,
324    ) {
325        let (id, identity_explicit) = match identity {
326            DragIdentity::Generated => (DragId::auto(), false),
327            DragIdentity::Explicit(id) => (id, true),
328        };
329        self.runtime.state.set(DragRuntimeState {
330            id: Some(id),
331            identity_explicit,
332            session,
333            proposed_effect: start.effect,
334            phase: DragPhase::Dragging,
335        });
336        self.state.set(DragState {
337            payload: Some(start.payload),
338            source: start.source,
339            over: None,
340            pointer: start.pointer,
341            grab: start.grab,
342            effect: start.effect,
343            mode: start.mode,
344            pointer_kind: start.pointer_kind,
345            source_rect: start.source_rect,
346            // A new drag supersedes any unclaimed focus restoration.
347            refocus: None,
348            // Starting a new drag interrupts any settle still gliding.
349            settle: None,
350        });
351        self.runtime
352            .monitor
353            .emit_lazy(|| self.snapshot().map(DndEvent::Started));
354    }
355
356    /// Record which pointer device drives the current drag (see
357    /// [`DragState::pointer_kind`]). `Draggable` sets this right after
358    /// pickup from the initiating event's `pointerType`; call it from
359    /// custom pointer sources so host-side glue (cursor pollers, raw
360    /// input bridges) can tell captured pointers from blind ones. Built-in
361    /// sources install this in their initial snapshot; custom sources that
362    /// use `start` or `start_with_id` can refine the default afterwards.
363    pub fn set_pointer_kind(&mut self, kind: PointerKind) {
364        self.state.pointer_kind().set(kind);
365    }
366
367    /// Record that `payload` just landed via a keyboard drop and its new
368    /// element should take focus when it mounts (see
369    /// [`DragState::refocus`]). `Draggable` calls this on its own keyboard
370    /// drops; call it from custom keyboard sources to get the same focus
371    /// continuity.
372    pub fn request_refocus(&mut self, payload: T) {
373        self.state.refocus().set(Some(payload));
374    }
375
376    /// Claim a pending focus restoration if it matches `payload`; returns
377    /// whether the caller should focus itself. First matching claimant
378    /// wins - the request is consumed.
379    pub fn claim_refocus(&mut self, payload: &T) -> bool
380    where
381        T: PartialEq,
382    {
383        let mut refocus = self.state.refocus();
384        let hit = refocus.peek().as_ref() == Some(payload);
385        if hit {
386            refocus.set(None);
387        }
388        hit
389    }
390
391    /// Record the dragged element's client rect (see
392    /// [`DragState::source_rect`]). `Draggable` measures and sets this right
393    /// after pickup; call it from custom drag sources so size-matched ghosts
394    /// (`DragOverlay { match_source: true }`) can dress themselves.
395    pub fn set_source_rect(&mut self, rect: Option<Rect>) {
396        self.state.source_rect().set(rect);
397    }
398
399    /// Update the tracked pointer position (drives `DragOverlay`). Granular:
400    /// only `pointer` subscribers rerun.
401    pub fn update_pointer(&mut self, pointer: Point) {
402        // An exact (0,0) is overwhelmingly a bogus platform report (some
403        // webviews emit it for synthetic events), not a real drag at the
404        // viewport corner; ignore it so the overlay doesn't jump there.
405        if pointer.x == 0.0 && pointer.y == 0.0 {
406            return;
407        }
408        self.state.pointer().set(pointer);
409        self.runtime
410            .monitor
411            .emit_lazy(|| self.snapshot().map(DndEvent::Moved));
412    }
413
414    /// Record the modifier-adjusted effect used by the current pointer
415    /// sample. Geometry refresh completion reuses it to keep rich target
416    /// acceptance consistent with the pointer path.
417    pub(crate) fn set_proposed_effect(&mut self, effect: DropEffect) {
418        let mut runtime = self.runtime.state;
419        if runtime.peek().proposed_effect != effect {
420            runtime.write().proposed_effect = effect;
421        }
422    }
423
424    /// Mark `zone` as hovered. Granular: only `over` subscribers rerun.
425    pub fn enter(&mut self, zone: ZoneId) {
426        let previous = self.over();
427        if previous == Some(zone) {
428            return;
429        }
430        self.state.over().set(Some(zone));
431        self.runtime.monitor.emit_lazy(|| {
432            self.snapshot().map(|drag| DndEvent::TargetChanged {
433                drag,
434                previous,
435                current: Some(zone),
436            })
437        });
438    }
439
440    /// Clear hover, but only if `zone` is still the hovered one (avoids
441    /// enter/leave races between adjacent zones).
442    pub fn leave(&mut self, zone: ZoneId) {
443        let previous = self.over();
444        if previous == Some(zone) {
445            self.state.over().set(None);
446            self.runtime.monitor.emit_lazy(|| {
447                self.snapshot().map(|drag| DndEvent::TargetChanged {
448                    drag,
449                    previous,
450                    current: None,
451                })
452            });
453        }
454    }
455
456    /// Consume the payload on a successful drop. Returns `(payload, source)`.
457    /// After this, `dragging()` is false.
458    pub fn take(&mut self) -> Option<(T, Option<ZoneId>)> {
459        let (payload, source) = {
460            let mut s = self.state.write();
461            (s.payload.take(), s.source)
462        };
463        let payload = payload?;
464        self.state.set(DragState::default());
465        self.runtime.state.set(DragRuntimeState::default());
466        Some((payload, source))
467    }
468
469    /// Consume the payload on a successful drop, like [`Self::take`], but
470    /// enter the *settling* phase instead of resetting: the returned clone
471    /// goes to the drop handler while the stored payload stays readable and
472    /// `settle` records the destination rect, so a settle-enabled
473    /// [`crate::core::components::DragOverlay`] can glide the ghost home.
474    /// After this, `dragging()` is false and `over()` is cleared; call
475    /// [`Self::finish_settle`] (the overlay does) to reset fully.
476    ///
477    /// Custom sources in a joined [`crate::core::world::DndWorld`] must call
478    /// [`crate::core::world::DndWorld::claim_settle`] first: world overlays
479    /// only present and finish a settle for the elected window.
480    pub fn take_settling(&mut self, to: Rect) -> Option<(T, Option<ZoneId>)> {
481        let mut s = self.state.write();
482        let payload = s.payload.clone()?;
483        let source = s.source;
484        s.over = None;
485        s.settle = Some(to);
486        drop(s);
487        self.runtime.state.write().phase = DragPhase::Settling;
488        Some((payload, source))
489    }
490
491    /// Re-aim an in-flight settle at a better rect - typically the landed
492    /// element's own, measured after the drop re-rendered the model
493    /// (`SettleSlot` does this for you). The overlay's glide retargets
494    /// smoothly, mid-flight included. A no-op unless currently settling.
495    pub fn retarget_settle(&mut self, to: Rect) {
496        let mut settle = self.state.settle();
497        // The equality guard is load-bearing: a `SettleSlot` retargets from
498        // an effect that (via its render) subscribes to `settle`, and
499        // signal writes notify even when the value is unchanged - writing
500        // the same rect back would loop effect -> write -> effect forever.
501        if settle.peek().is_some() && *settle.peek() != Some(to) {
502            settle.set(Some(to));
503        }
504    }
505
506    /// End the settling phase and reset all state. A no-op unless currently
507    /// settling, so a late `transitionend` can never clobber a new drag.
508    pub fn finish_settle(&mut self) {
509        if self.phase_peek() == DragPhase::Settling {
510            self.state.set(DragState::default());
511            self.runtime.state.set(DragRuntimeState::default());
512        }
513    }
514
515    pub(crate) fn phase_peek(&self) -> DragPhase {
516        match self.phase_authority {
517            PhaseAuthority::State => {
518                if self.state.settle().peek().is_some() {
519                    DragPhase::Settling
520                } else if self.state.payload().peek().is_some() {
521                    DragPhase::Dragging
522                } else {
523                    DragPhase::Idle
524                }
525            }
526            PhaseAuthority::Runtime => self
527                .runtime
528                .state
529                .try_peek()
530                .map(|runtime| runtime.phase)
531                .unwrap_or(DragPhase::Idle),
532        }
533    }
534
535    /// Is the underlying state still alive? Destructors check this before
536    /// touching the context, because store lens access on a dead store
537    /// panics (even `try_` reads - the selector internals do) and a panic
538    /// in a destructor aborts the process. A world context is process-
539    /// lived so this holds by construction there; the gate keeps every
540    /// other wiring (custom `from_parts` contexts, unforeseen drop orders)
541    /// degrading gracefully instead. Probed through the announcement
542    /// signal, a plain `Signal` created alongside the store, whose
543    /// `try_peek` IS dead-safe.
544    pub(crate) fn alive(&self) -> bool {
545        self.announcement.try_peek().is_ok()
546    }
547
548    /// Abort the drag and reset all state.
549    pub fn cancel(&mut self) {
550        self.cancel_with_reason(CancelReason::User);
551    }
552
553    /// Abort the drag with an explicit reason for monitor consumers.
554    pub fn cancel_with_reason(&mut self, reason: CancelReason) {
555        if self.phase_peek() == DragPhase::Settling {
556            self.finish_settle();
557            return;
558        }
559        if let Some(session) = self.drag_session_id() {
560            if self.cancel_session(session, reason) {
561                return;
562            }
563        }
564        self.cancel_state(reason);
565    }
566
567    pub(super) fn cancel_state(&mut self, reason: CancelReason) {
568        let snapshot = (self.phase_peek() == DragPhase::Dragging
569            && self.runtime.monitor.has_listeners())
570        .then(|| self.snapshot())
571        .flatten();
572        self.state.set(DragState::default());
573        self.runtime.state.set(DragRuntimeState::default());
574        if let Some(drag) = snapshot {
575            self.runtime
576                .monitor
577                .emit(DndEvent::Cancelled { drag, reason });
578        }
579    }
580
581    // --- read accessors -----------------------------------------------
582    // Each reads through a field lens, so render-time reads subscribe only
583    // to that field.
584
585    /// Is a drag currently in flight? False while a completed drop is still
586    /// settling, even though [`Self::payload`] remains readable.
587    pub fn dragging(&self) -> bool {
588        match self.phase_authority {
589            PhaseAuthority::State => {
590                self.state.payload().is_some() && self.state.settle().is_none()
591            }
592            PhaseAuthority::Runtime => self.runtime.state.read().phase == DragPhase::Dragging,
593        }
594    }
595
596    /// Destination rect of a drop currently settling (see
597    /// [`Self::take_settling`]), if any.
598    pub fn settling(&self) -> Option<Rect> {
599        match self.phase_authority {
600            PhaseAuthority::State => self.state.settle().cloned(),
601            PhaseAuthority::Runtime => (self.runtime.state.read().phase == DragPhase::Settling)
602                .then(|| self.state.settle().cloned())
603                .flatten(),
604        }
605    }
606
607    /// Non-subscribing version of [`Self::settling`] for imperative world
608    /// bookkeeping (destructors, event handlers) that must not subscribe.
609    pub(crate) fn settling_peek(&self) -> bool {
610        self.phase_peek() == DragPhase::Settling
611    }
612
613    /// Clone of the current payload, if dragging.
614    pub fn payload(&self) -> Option<T> {
615        self.state.payload().cloned()
616    }
617
618    /// Stable identity of the active draggable.
619    pub fn drag_id(&self) -> Option<DragId> {
620        self.runtime.state.read().id
621    }
622
623    /// Whether the active source deliberately supplied its stable id.
624    pub fn has_explicit_drag_id(&self) -> bool {
625        self.runtime.state.read().identity_explicit
626    }
627
628    /// Fresh identity of the active tracked pointer gesture.
629    pub fn drag_session_id(&self) -> Option<DragSessionId> {
630        self.runtime.state.read().session
631    }
632
633    /// Zone currently hovered.
634    pub fn over(&self) -> Option<ZoneId> {
635        self.state.over().cloned()
636    }
637
638    /// Zone the drag started from.
639    pub fn source(&self) -> Option<ZoneId> {
640        self.state.source().cloned()
641    }
642
643    /// Last known pointer position.
644    pub fn pointer(&self) -> Point {
645        self.state.pointer().cloned()
646    }
647
648    /// Grab offset inside the dragged element.
649    pub fn grab(&self) -> Point {
650        self.state.grab().cloned()
651    }
652
653    /// Client rect of the dragged element measured at pickup, if available.
654    pub fn source_rect(&self) -> Option<Rect> {
655        self.state.source_rect().cloned()
656    }
657
658    /// Effect the drag was started with.
659    pub fn effect(&self) -> DropEffect {
660        self.state.effect().cloned()
661    }
662
663    pub(crate) fn proposed_effect(&self) -> DropEffect {
664        self.runtime.state.read().proposed_effect
665    }
666
667    /// How the current drag is being driven.
668    pub fn mode(&self) -> DragMode {
669        self.state.mode().cloned()
670    }
671
672    /// Which pointer device drives the current drag (meaningful for
673    /// [`DragMode::Pointer`] drags; `Mouse` otherwise and by default).
674    pub fn pointer_kind(&self) -> PointerKind {
675        self.state.pointer_kind().cloned()
676    }
677
678    /// Complete monitor snapshot of the active drag.
679    pub fn snapshot(&self) -> Option<DragSnapshot<T>> {
680        Some(DragSnapshot {
681            id: self.drag_id()?,
682            session: self.drag_session_id(),
683            payload: self.payload()?,
684            source: self.source(),
685            over: self.over(),
686            pointer: self.pointer(),
687            grab: self.grab(),
688            effect: self.effect(),
689            mode: self.mode(),
690            pointer_kind: self.pointer_kind(),
691            source_rect: self.source_rect(),
692        })
693    }
694
695    pub(crate) fn monitor_mut(&mut self) -> DndMonitor<T> {
696        self.runtime.monitor
697    }
698
699    pub(crate) fn emit_dropped(&self, receipt: DropReceipt<T>) {
700        self.runtime.monitor.emit(DndEvent::Dropped(receipt));
701    }
702
703    pub(crate) fn monitor_has_listeners(&self) -> bool {
704        self.runtime.monitor.has_listeners()
705    }
706
707    /// Push a screen-reader announcement (rendered by
708    /// [`crate::a11y::LiveRegion`]). Called automatically by the built-in
709    /// keyboard interaction; call it yourself for custom flows.
710    pub fn announce(&mut self, msg: impl Into<String>) {
711        self.announcement.set(msg.into());
712    }
713
714    /// The current announcement text.
715    pub fn announcement(&self) -> String {
716        self.announcement.read().clone()
717    }
718}