Skip to main content

azul_core/
drag.rs

1//! Unified drag context for all drag operations.
2//!
3//! This module provides a single, coherent way to handle all drag operations:
4//! - Text selection drag
5//! - Scrollbar thumb drag
6//! - Node drag-and-drop
7//! - Window drag/resize
8//! - File drop from OS
9//!
10//! The `DragContext` struct tracks the current drag state and provides
11//! a unified interface for event processing.
12
13use alloc::vec::Vec;
14
15use crate::dom::{DomId, DomNodeId, NodeId, OptionDomNodeId};
16use crate::geom::LogicalPosition;
17use crate::selection::TextCursor;
18use crate::window::WindowPosition;
19
20use azul_css::{AzString, StringVec, U8Vec};
21
22/// Type of the active drag operation.
23///
24/// This enum unifies all drag types into a single discriminated union,
25/// making it easy to handle different drag behaviors in one place.
26#[derive(Debug, Clone, PartialEq)]
27#[repr(C, u8)]
28pub enum ActiveDragType {
29    /// Text selection drag - user is selecting text by dragging
30    TextSelection(TextSelectionDrag),
31    /// Scrollbar thumb drag - user is dragging a scrollbar thumb
32    ScrollbarThumb(ScrollbarThumbDrag),
33    /// Node drag-and-drop - user is dragging a DOM node
34    Node(NodeDrag),
35    /// Window drag - user is moving the window (titlebar drag)
36    WindowMove(WindowMoveDrag),
37    /// Window resize - user is resizing the window (edge/corner drag)
38    WindowResize(WindowResizeDrag),
39    /// File drop from OS - user is dragging file(s) from the OS
40    FileDrop(FileDropDrag),
41}
42
43/// Text selection drag state.
44///
45/// Tracks the anchor point (where selection started) and current position.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[repr(C)]
48pub struct TextSelectionDrag {
49    /// DOM ID where the selection started
50    pub dom_id: DomId,
51    /// The IFC root node where selection started (e.g., <p> element)
52    pub anchor_ifc_node: NodeId,
53    /// The anchor cursor position (fixed during drag)
54    pub anchor_cursor: Option<TextCursor>,
55    /// Mouse position where drag started
56    pub start_mouse_position: LogicalPosition,
57    /// Current mouse position
58    pub current_mouse_position: LogicalPosition,
59}
60
61/// Scrollbar thumb drag state.
62///
63/// Tracks which scrollbar is being dragged and the current offset.
64#[derive(Debug, Clone, Copy, PartialEq)]
65#[repr(C)]
66pub struct ScrollbarThumbDrag {
67    /// DOM ID that `scroll_container_node` belongs to. Used to scope
68    /// `remap_node_ids` so a reconciliation of a *different* DOM can't remap
69    /// this drag's node id against an unrelated DOM's old→new map.
70    pub dom_id: DomId,
71    /// The scroll container node being scrolled
72    pub scroll_container_node: NodeId,
73    /// Whether this is the vertical or horizontal scrollbar
74    pub axis: ScrollbarAxis,
75    /// Mouse Y position where drag started (for calculating delta)
76    pub start_mouse_position: LogicalPosition,
77    /// Scroll offset when drag started
78    pub start_scroll_offset: f32,
79    /// Current mouse position
80    pub current_mouse_position: LogicalPosition,
81    /// Track length in pixels (for calculating scroll ratio)
82    pub track_length_px: f32,
83    /// Content length in pixels (for calculating scroll ratio)
84    pub content_length_px: f32,
85    /// Viewport length in pixels (for calculating scroll ratio)
86    pub viewport_length_px: f32,
87}
88
89/// Which scrollbar axis is being dragged
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[repr(C)]
92pub enum ScrollbarAxis {
93    Vertical,
94    Horizontal,
95}
96
97/// Node drag-and-drop state.
98///
99/// Tracks a DOM node being dragged for reordering or moving.
100#[derive(Debug, Clone, PartialEq, Eq)]
101#[repr(C)]
102pub struct NodeDrag {
103    /// DOM ID of the node being dragged
104    pub dom_id: DomId,
105    /// Node ID being dragged
106    pub node_id: NodeId,
107    /// Position where drag started
108    pub start_position: LogicalPosition,
109    /// Current drag position
110    pub current_position: LogicalPosition,
111    /// Offset from node origin to click point (for correct visual positioning)
112    pub drag_offset: LogicalPosition,
113    /// Optional: DOM node currently under cursor (drop target)
114    pub current_drop_target: OptionDomNodeId,
115    /// Previous drop target (for generating DragEnter/DragLeave events)
116    pub previous_drop_target: OptionDomNodeId,
117    /// Drag data (MIME types and content)
118    pub drag_data: DragData,
119    /// Whether the current drop target has accepted the drop via `accept_drop()`
120    pub drop_accepted: bool,
121    /// Drop effect set by the drop target
122    pub drop_effect: DropEffect,
123}
124
125/// Window move drag state.
126///
127/// Tracks the window being moved via titlebar drag.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[repr(C)]
130pub struct WindowMoveDrag {
131    /// Position where window drag started (in screen coordinates)
132    pub start_position: LogicalPosition,
133    /// Current drag position
134    pub current_position: LogicalPosition,
135    /// Initial window position before drag
136    pub initial_window_position: WindowPosition,
137}
138
139/// Window resize drag state.
140///
141/// Tracks the window being resized via edge/corner drag.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[repr(C)]
144pub struct WindowResizeDrag {
145    /// Which edge/corner is being dragged
146    pub edge: WindowResizeEdge,
147    /// Position where resize started
148    pub start_position: LogicalPosition,
149    /// Current drag position
150    pub current_position: LogicalPosition,
151    /// Initial window size before resize
152    pub initial_width: u32,
153    /// Initial window height before resize
154    pub initial_height: u32,
155}
156
157/// Which edge or corner of the window is being resized
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159#[repr(C)]
160pub enum WindowResizeEdge {
161    Top,
162    Bottom,
163    Left,
164    Right,
165    TopLeft,
166    TopRight,
167    BottomLeft,
168    BottomRight,
169}
170
171/// File drop from OS drag state.
172///
173/// Tracks files being dragged from the operating system.
174#[derive(Debug, Clone, PartialEq, Eq)]
175#[repr(C)]
176pub struct FileDropDrag {
177    /// Files being dragged (as string paths)
178    pub files: StringVec,
179    /// Current position of drag cursor
180    pub position: LogicalPosition,
181    /// DOM node under cursor (potential drop target)
182    pub drop_target: OptionDomNodeId,
183    /// Allowed drop effect
184    pub drop_effect: DropEffect,
185}
186
187/// Drop effect — the operation that will happen if the data is dropped
188/// on the current target (HTML5 `DataTransfer.dropEffect`).
189///
190/// This is a strict subset of `DragEffect`: a drop target selects one of
191/// these four outcomes, which must also be allowed by the source's
192/// `effect_allowed`.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194#[repr(C)]
195pub enum DropEffect {
196    /// No drop allowed / the drop is rejected. Default.
197    #[default]
198    None,
199    /// Drop will copy the data (source retains its copy).
200    Copy,
201    /// Drop will create a link/shortcut to the data.
202    Link,
203    /// Drop will move the data (source should remove its copy).
204    Move,
205}
206
207/// Allowed drag effects — the set of operations the drag source permits
208/// (HTML5 `DataTransfer.effectAllowed`).
209///
210/// The drop target's `DropEffect` must be a member of this set for the
211/// drop to succeed. Semantic superset of `DropEffect` that adds the
212/// HTML5 combined-permission values (`CopyLink`, `CopyMove`, `LinkMove`,
213/// `All`) and the pre-drag `Uninitialized` sentinel.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
215#[repr(C)]
216pub enum DragEffect {
217    /// Allowed set has not been initialized yet (equivalent to `All` in
218    /// most user agents). Default for fresh drags.
219    #[default]
220    Uninitialized,
221    /// No drop is permitted.
222    None,
223    /// Only Copy is permitted.
224    Copy,
225    /// Copy or Link is permitted.
226    CopyLink,
227    /// Copy or Move is permitted.
228    CopyMove,
229    /// Only Link is permitted.
230    Link,
231    /// Link or Move is permitted.
232    LinkMove,
233    /// Only Move is permitted.
234    Move,
235    /// Any of Copy, Link, or Move is permitted.
236    All,
237}
238
239/// FFI-safe (`mime_type`, `data`) pair used by [`DragData`] in place of
240/// a `BTreeMap<AzString, Vec<u8>>` entry.
241#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
242#[repr(C)]
243pub struct MimeTypeData {
244    pub mime_type: AzString,
245    pub data: U8Vec,
246}
247
248impl_option!(
249    MimeTypeData,
250    OptionMimeTypeData,
251    copy = false,
252    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
253);
254
255impl_vec!(
256    MimeTypeData,
257    MimeTypeDataVec,
258    MimeTypeDataVecDestructor,
259    MimeTypeDataVecDestructorType,
260    MimeTypeDataVecSlice,
261    OptionMimeTypeData
262);
263impl_vec_mut!(MimeTypeData, MimeTypeDataVec);
264impl_vec_debug!(MimeTypeData, MimeTypeDataVec);
265impl_vec_partialord!(MimeTypeData, MimeTypeDataVec);
266impl_vec_ord!(MimeTypeData, MimeTypeDataVec);
267impl_vec_clone!(MimeTypeData, MimeTypeDataVec, MimeTypeDataVecDestructor);
268impl_vec_partialeq!(MimeTypeData, MimeTypeDataVec);
269impl_vec_eq!(MimeTypeData, MimeTypeDataVec);
270impl_vec_hash!(MimeTypeData, MimeTypeDataVec);
271
272/// Drag data (HTML5 `DataTransfer`).
273///
274/// Holds the payload(s) being transferred during a drag operation, keyed
275/// by MIME type, plus the set of operations the source allows.
276#[derive(Debug, Default, Clone, PartialEq, Eq)]
277#[repr(C)]
278pub struct DragData {
279    /// MIME type -> data mapping (vec-of-pairs for FFI compatibility).
280    ///
281    /// e.g., `"text/plain" -> "Hello World"`.
282    pub data: MimeTypeDataVec,
283    /// Set of drag operations the source permits for this drag.
284    pub effect_allowed: DragEffect,
285}
286
287impl DragData {
288    /// Create new empty drag data
289    #[must_use]
290    pub const fn new() -> Self {
291        Self {
292            data: MimeTypeDataVec::new(),
293            effect_allowed: DragEffect::Uninitialized,
294        }
295    }
296
297    /// Set data for a MIME type. Replaces any existing entry for the
298    /// same MIME type.
299    pub fn set_data(&mut self, mime_type: impl Into<AzString>, data: Vec<u8>) {
300        let mime_type = mime_type.into();
301        let value: U8Vec = data.into();
302        if let Some(entry) = self
303            .data
304            .as_mut()
305            .iter_mut()
306            .find(|e| e.mime_type == mime_type)
307        {
308            entry.data = value;
309        } else {
310            self.data.push(MimeTypeData {
311                mime_type,
312                data: value,
313            });
314        }
315    }
316
317    /// Get data for a MIME type
318    #[must_use]
319    pub fn get_data(&self, mime_type: &str) -> Option<&[u8]> {
320        self.data
321            .as_ref()
322            .iter()
323            .find(|e| e.mime_type.as_str() == mime_type)
324            .map(|e| e.data.as_ref())
325    }
326
327    /// Set plain text data
328    pub fn set_text(&mut self, text: impl Into<AzString>) {
329        let text_str = text.into();
330        self.set_data("text/plain", text_str.as_str().as_bytes().to_vec());
331    }
332
333    /// Get plain text data
334    #[must_use]
335    pub fn get_text(&self) -> Option<AzString> {
336        self.get_data("text/plain")
337            .map(|bytes| AzString::from(core::str::from_utf8(bytes).unwrap_or("")))
338    }
339}
340
341/// The unified drag context.
342///
343/// This struct wraps `ActiveDragType` and provides common metadata
344/// that applies to all drag operations.
345///
346/// Note: this type is Rust-only and not exposed through the C API.
347#[derive(Debug, Clone, PartialEq)]
348pub struct DragContext {
349    /// The specific type of drag operation
350    pub drag_type: ActiveDragType,
351    /// Session ID from gesture detection (links back to `GestureManager`)
352    pub session_id: u64,
353    /// Whether the drag has been cancelled (e.g., Escape pressed)
354    pub cancelled: bool,
355}
356
357impl DragContext {
358    /// Create a new drag context
359    #[must_use]
360    pub const fn new(drag_type: ActiveDragType, session_id: u64) -> Self {
361        Self {
362            drag_type,
363            session_id,
364            cancelled: false,
365        }
366    }
367
368    /// Create a text selection drag
369    #[must_use]
370    pub const fn text_selection(
371        dom_id: DomId,
372        anchor_ifc_node: NodeId,
373        start_mouse_position: LogicalPosition,
374        session_id: u64,
375    ) -> Self {
376        Self::new(
377            ActiveDragType::TextSelection(TextSelectionDrag {
378                dom_id,
379                anchor_ifc_node,
380                anchor_cursor: None,
381                start_mouse_position,
382                current_mouse_position: start_mouse_position,
383            }),
384            session_id,
385        )
386    }
387
388    /// Create a scrollbar thumb drag
389    #[must_use]
390    pub const fn scrollbar_thumb(
391        dom_id: DomId,
392        scroll_container_node: NodeId,
393        axis: ScrollbarAxis,
394        start_mouse_position: LogicalPosition,
395        start_scroll_offset: f32,
396        track_length_px: f32,
397        content_length_px: f32,
398        viewport_length_px: f32,
399        session_id: u64,
400    ) -> Self {
401        Self::new(
402            ActiveDragType::ScrollbarThumb(ScrollbarThumbDrag {
403                dom_id,
404                scroll_container_node,
405                axis,
406                start_mouse_position,
407                start_scroll_offset,
408                current_mouse_position: start_mouse_position,
409                track_length_px,
410                content_length_px,
411                viewport_length_px,
412            }),
413            session_id,
414        )
415    }
416
417    /// Create a node drag
418    #[must_use]
419    pub const fn node_drag(
420        dom_id: DomId,
421        node_id: NodeId,
422        start_position: LogicalPosition,
423        drag_data: DragData,
424        session_id: u64,
425    ) -> Self {
426        Self::new(
427            ActiveDragType::Node(NodeDrag {
428                dom_id,
429                node_id,
430                start_position,
431                current_position: start_position,
432                drag_offset: LogicalPosition::zero(),
433                current_drop_target: OptionDomNodeId::None,
434                previous_drop_target: OptionDomNodeId::None,
435                drag_data,
436                drop_accepted: false,
437                drop_effect: DropEffect::None,
438            }),
439            session_id,
440        )
441    }
442
443    /// Create a window move drag
444    #[must_use]
445    pub const fn window_move(
446        start_position: LogicalPosition,
447        initial_window_position: WindowPosition,
448        session_id: u64,
449    ) -> Self {
450        Self::new(
451            ActiveDragType::WindowMove(WindowMoveDrag {
452                start_position,
453                current_position: start_position,
454                initial_window_position,
455            }),
456            session_id,
457        )
458    }
459
460    /// Create a file drop drag
461    #[must_use]
462    pub fn file_drop(files: Vec<AzString>, position: LogicalPosition, session_id: u64) -> Self {
463        Self::new(
464            ActiveDragType::FileDrop(FileDropDrag {
465                files: files.into(),
466                position,
467                drop_target: OptionDomNodeId::None,
468                drop_effect: DropEffect::Copy,
469            }),
470            session_id,
471        )
472    }
473
474    /// Update the current mouse position for all drag types
475    pub const fn update_position(&mut self, position: LogicalPosition) {
476        match &mut self.drag_type {
477            ActiveDragType::TextSelection(ref mut drag) => {
478                drag.current_mouse_position = position;
479            }
480            ActiveDragType::ScrollbarThumb(ref mut drag) => {
481                drag.current_mouse_position = position;
482            }
483            ActiveDragType::Node(ref mut drag) => {
484                drag.current_position = position;
485            }
486            ActiveDragType::WindowMove(ref mut drag) => {
487                drag.current_position = position;
488            }
489            ActiveDragType::WindowResize(ref mut drag) => {
490                drag.current_position = position;
491            }
492            ActiveDragType::FileDrop(ref mut drag) => {
493                drag.position = position;
494            }
495        }
496    }
497
498    /// Get the current mouse position
499    #[must_use]
500    pub const fn current_position(&self) -> LogicalPosition {
501        match &self.drag_type {
502            ActiveDragType::TextSelection(drag) => drag.current_mouse_position,
503            ActiveDragType::ScrollbarThumb(drag) => drag.current_mouse_position,
504            ActiveDragType::Node(drag) => drag.current_position,
505            ActiveDragType::WindowMove(drag) => drag.current_position,
506            ActiveDragType::WindowResize(drag) => drag.current_position,
507            ActiveDragType::FileDrop(drag) => drag.position,
508        }
509    }
510
511    /// Get the start position
512    #[must_use]
513    pub const fn start_position(&self) -> LogicalPosition {
514        match &self.drag_type {
515            ActiveDragType::TextSelection(drag) => drag.start_mouse_position,
516            ActiveDragType::ScrollbarThumb(drag) => drag.start_mouse_position,
517            ActiveDragType::Node(drag) => drag.start_position,
518            ActiveDragType::WindowMove(drag) => drag.start_position,
519            ActiveDragType::WindowResize(drag) => drag.start_position,
520            ActiveDragType::FileDrop(drag) => drag.position, // No start for file drops
521        }
522    }
523
524    /// Check if this is a text selection drag
525    #[must_use]
526    pub const fn is_text_selection(&self) -> bool {
527        matches!(self.drag_type, ActiveDragType::TextSelection(_))
528    }
529
530    /// Check if this is a scrollbar thumb drag
531    #[must_use]
532    pub const fn is_scrollbar_thumb(&self) -> bool {
533        matches!(self.drag_type, ActiveDragType::ScrollbarThumb(_))
534    }
535
536    /// Check if this is a node drag
537    #[must_use]
538    pub const fn is_node_drag(&self) -> bool {
539        matches!(self.drag_type, ActiveDragType::Node(_))
540    }
541
542    /// Check if this is a window move drag
543    #[must_use]
544    pub const fn is_window_move(&self) -> bool {
545        matches!(self.drag_type, ActiveDragType::WindowMove(_))
546    }
547
548    /// Check if this is a file drop
549    #[must_use]
550    pub const fn is_file_drop(&self) -> bool {
551        matches!(self.drag_type, ActiveDragType::FileDrop(_))
552    }
553
554    /// Get as text selection drag (if applicable)
555    #[must_use]
556    pub const fn as_text_selection(&self) -> Option<&TextSelectionDrag> {
557        match &self.drag_type {
558            ActiveDragType::TextSelection(drag) => Some(drag),
559            _ => None,
560        }
561    }
562
563    /// Get as mutable text selection drag (if applicable)
564    pub const fn as_text_selection_mut(&mut self) -> Option<&mut TextSelectionDrag> {
565        match &mut self.drag_type {
566            ActiveDragType::TextSelection(drag) => Some(drag),
567            _ => None,
568        }
569    }
570
571    /// Get as scrollbar thumb drag (if applicable)
572    #[must_use]
573    pub const fn as_scrollbar_thumb(&self) -> Option<&ScrollbarThumbDrag> {
574        match &self.drag_type {
575            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
576            _ => None,
577        }
578    }
579
580    /// Get as mutable scrollbar thumb drag (if applicable)
581    pub const fn as_scrollbar_thumb_mut(&mut self) -> Option<&mut ScrollbarThumbDrag> {
582        match &mut self.drag_type {
583            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
584            _ => None,
585        }
586    }
587
588    /// Get as node drag (if applicable)
589    #[must_use]
590    pub const fn as_node_drag(&self) -> Option<&NodeDrag> {
591        match &self.drag_type {
592            ActiveDragType::Node(drag) => Some(drag),
593            _ => None,
594        }
595    }
596
597    /// Get as mutable node drag (if applicable)
598    pub const fn as_node_drag_mut(&mut self) -> Option<&mut NodeDrag> {
599        match &mut self.drag_type {
600            ActiveDragType::Node(drag) => Some(drag),
601            _ => None,
602        }
603    }
604
605    /// Get as window move drag (if applicable)
606    #[must_use]
607    pub const fn as_window_move(&self) -> Option<&WindowMoveDrag> {
608        match &self.drag_type {
609            ActiveDragType::WindowMove(drag) => Some(drag),
610            _ => None,
611        }
612    }
613
614    /// Get as file drop (if applicable)
615    #[must_use]
616    pub const fn as_file_drop(&self) -> Option<&FileDropDrag> {
617        match &self.drag_type {
618            ActiveDragType::FileDrop(drag) => Some(drag),
619            _ => None,
620        }
621    }
622
623    /// Get as mutable file drop (if applicable)
624    pub const fn as_file_drop_mut(&mut self) -> Option<&mut FileDropDrag> {
625        match &mut self.drag_type {
626            ActiveDragType::FileDrop(drag) => Some(drag),
627            _ => None,
628        }
629    }
630
631    /// Calculate scroll delta for scrollbar thumb drag
632    ///
633    /// Returns the new scroll offset based on current mouse position.
634    #[must_use]
635    pub fn calculate_scrollbar_scroll_offset(&self) -> Option<f32> {
636        let drag = self.as_scrollbar_thumb()?;
637
638        // Calculate mouse delta along the drag axis
639        let mouse_delta = match drag.axis {
640            ScrollbarAxis::Vertical => drag.current_mouse_position.y - drag.start_mouse_position.y,
641            ScrollbarAxis::Horizontal => {
642                drag.current_mouse_position.x - drag.start_mouse_position.x
643            }
644        };
645
646        // Calculate the scrollable range
647        let scrollable_range = drag.content_length_px - drag.viewport_length_px;
648        // The explicit `is_nan()` (equivalent to the old `!(x > 0.0)`) catches a NaN
649        // scrollable_range — from a NaN, or inf-minus-inf, content/viewport length —
650        // so it never reaches the `clamp(0.0, scrollable_range)` below, whose
651        // f32::clamp would panic (it asserts min <= max, and NaN fails every compare).
652        if scrollable_range <= 0.0 || scrollable_range.is_nan() || drag.track_length_px <= 0.0 {
653            return Some(drag.start_scroll_offset);
654        }
655
656        // Calculate thumb length (proportional to viewport/content ratio)
657        let thumb_length =
658            (drag.viewport_length_px / drag.content_length_px) * drag.track_length_px;
659        let scrollable_track = drag.track_length_px - thumb_length;
660
661        if scrollable_track <= 0.0 {
662            return Some(drag.start_scroll_offset);
663        }
664
665        // Convert mouse delta to scroll delta
666        let scroll_ratio = mouse_delta / scrollable_track;
667        let scroll_delta = scroll_ratio * scrollable_range;
668
669        // Calculate new scroll offset
670        let new_offset = drag.start_scroll_offset + scroll_delta;
671
672        // Clamp to valid range
673        Some(new_offset.clamp(0.0, scrollable_range))
674    }
675
676    /// Remap a drop target's `NodeId` using the old→new mapping.
677    /// Clears the target if the old `NodeId` was removed.
678    fn remap_drop_target(
679        target: &mut OptionDomNodeId,
680        dom_id: DomId,
681        node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
682    ) {
683        let dt = match target.into_option() {
684            Some(dt) if dt.dom == dom_id => dt,
685            _ => return,
686        };
687        let Some(old_nid) = dt.node.into_crate_internal() else {
688            return;
689        };
690        if let Some(&new_nid) = node_id_map.get(&old_nid) {
691            *target = Some(DomNodeId {
692                dom: dom_id,
693                node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
694            })
695            .into();
696        } else {
697            *target = OptionDomNodeId::None;
698        }
699    }
700
701    /// Remap `NodeIds` stored in this drag context after DOM reconciliation.
702    ///
703    /// When the DOM is regenerated during an active drag, `NodeIds` can change.
704    /// This updates all stored `NodeIds` using the old→new mapping.
705    /// Returns `false` if a critical `NodeId` was removed (drag should be cancelled).
706    pub fn remap_node_ids(
707        &mut self,
708        dom_id: DomId,
709        node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
710    ) -> bool {
711        match &mut self.drag_type {
712            ActiveDragType::TextSelection(ref mut drag) => {
713                if drag.dom_id != dom_id {
714                    return true;
715                }
716                if let Some(&new_id) = node_id_map.get(&drag.anchor_ifc_node) {
717                    drag.anchor_ifc_node = new_id;
718                } else {
719                    return false; // anchor node removed
720                }
721                true
722            }
723            ActiveDragType::ScrollbarThumb(ref mut drag) => {
724                // Scope the remap to the DOM this drag belongs to: a different
725                // DOM's reconciliation must not touch our scroll container id.
726                if drag.dom_id != dom_id {
727                    return true;
728                }
729                if let Some(&new_id) = node_id_map.get(&drag.scroll_container_node) {
730                    drag.scroll_container_node = new_id;
731                    true
732                } else {
733                    false // scroll container removed
734                }
735            }
736            ActiveDragType::Node(ref mut drag) => {
737                if drag.dom_id != dom_id {
738                    return true;
739                }
740                if let Some(&new_id) = node_id_map.get(&drag.node_id) {
741                    drag.node_id = new_id;
742                } else {
743                    return false; // dragged node removed
744                }
745                // Drop target remap — both current AND previous, otherwise a
746                // stale `previous_drop_target` keeps a pre-reconciliation NodeId
747                // and later generates spurious DragEnter/DragLeave against a
748                // node that no longer exists (or a different node reusing the id).
749                Self::remap_drop_target(&mut drag.current_drop_target, dom_id, node_id_map);
750                Self::remap_drop_target(&mut drag.previous_drop_target, dom_id, node_id_map);
751                true
752            }
753            // WindowMove, WindowResize, and FileDrop don't reference DOM NodeIds
754            ActiveDragType::WindowMove(_) | ActiveDragType::WindowResize(_) => true,
755            ActiveDragType::FileDrop(ref mut drag) => {
756                Self::remap_drop_target(&mut drag.drop_target, dom_id, node_id_map);
757                true
758            }
759        }
760    }
761}
762
763azul_css::impl_option!(
764    DragContext,
765    OptionDragContext,
766    copy = false,
767    [Debug, Clone, PartialEq]
768);
769
770/// Drag offset from the cursor position at drag start (logical pixels).
771/// `dx`/`dy` are the delta from drag start to current position.
772#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
773#[repr(C)]
774pub struct DragDelta {
775    pub dx: f32,
776    pub dy: f32,
777}
778
779impl DragDelta {
780    #[inline]
781    #[must_use]
782    pub const fn new(dx: f32, dy: f32) -> Self {
783        Self { dx, dy }
784    }
785    #[inline]
786    #[must_use]
787    pub const fn zero() -> Self {
788        Self::new(0.0, 0.0)
789    }
790}
791
792impl_option!(
793    DragDelta,
794    OptionDragDelta,
795    [Debug, Copy, Clone, PartialEq, PartialOrd]
796);
797
798#[cfg(test)]
799#[path = "drag_test.rs"]
800mod drag_test;