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, PartialEq)]
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    /// Whether we should auto-scroll (mouse near edge of scroll container)
60    pub auto_scroll_direction: AutoScrollDirection,
61    /// The scroll container that should be auto-scrolled (if any)
62    pub auto_scroll_container: Option<NodeId>,
63}
64
65/// Scrollbar thumb drag state.
66///
67/// Tracks which scrollbar is being dragged and the current offset.
68#[derive(Debug, Clone, Copy, PartialEq)]
69#[repr(C)]
70pub struct ScrollbarThumbDrag {
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)]
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, PartialEq)]
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)]
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)]
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/// Direction for auto-scrolling during drag operations
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
189#[repr(C)]
190pub enum AutoScrollDirection {
191    #[default]
192    None,
193    Up,
194    Down,
195    Left,
196    Right,
197    UpLeft,
198    UpRight,
199    DownLeft,
200    DownRight,
201}
202
203/// Drop effect — the operation that will happen if the data is dropped
204/// on the current target (HTML5 `DataTransfer.dropEffect`).
205///
206/// This is a strict subset of `DragEffect`: a drop target selects one of
207/// these four outcomes, which must also be allowed by the source's
208/// `effect_allowed`.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210#[repr(C)]
211pub enum DropEffect {
212    /// No drop allowed / the drop is rejected. Default.
213    #[default]
214    None,
215    /// Drop will copy the data (source retains its copy).
216    Copy,
217    /// Drop will create a link/shortcut to the data.
218    Link,
219    /// Drop will move the data (source should remove its copy).
220    Move,
221}
222
223/// Allowed drag effects — the set of operations the drag source permits
224/// (HTML5 `DataTransfer.effectAllowed`).
225///
226/// The drop target's `DropEffect` must be a member of this set for the
227/// drop to succeed. Semantic superset of `DropEffect` that adds the
228/// HTML5 combined-permission values (`CopyLink`, `CopyMove`, `LinkMove`,
229/// `All`) and the pre-drag `Uninitialized` sentinel.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231#[repr(C)]
232pub enum DragEffect {
233    /// Allowed set has not been initialized yet (equivalent to `All` in
234    /// most user agents). Default for fresh drags.
235    #[default]
236    Uninitialized,
237    /// No drop is permitted.
238    None,
239    /// Only Copy is permitted.
240    Copy,
241    /// Copy or Link is permitted.
242    CopyLink,
243    /// Copy or Move is permitted.
244    CopyMove,
245    /// Only Link is permitted.
246    Link,
247    /// Link or Move is permitted.
248    LinkMove,
249    /// Only Move is permitted.
250    Move,
251    /// Any of Copy, Link, or Move is permitted.
252    All,
253}
254
255/// FFI-safe (`mime_type`, `data`) pair used by [`DragData`] in place of
256/// a `BTreeMap<AzString, Vec<u8>>` entry.
257#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
258#[repr(C)]
259pub struct MimeTypeData {
260    pub mime_type: AzString,
261    pub data: U8Vec,
262}
263
264impl_option!(
265    MimeTypeData,
266    OptionMimeTypeData,
267    copy = false,
268    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
269);
270
271impl_vec!(
272    MimeTypeData,
273    MimeTypeDataVec,
274    MimeTypeDataVecDestructor,
275    MimeTypeDataVecDestructorType,
276    MimeTypeDataVecSlice,
277    OptionMimeTypeData
278);
279impl_vec_mut!(MimeTypeData, MimeTypeDataVec);
280impl_vec_debug!(MimeTypeData, MimeTypeDataVec);
281impl_vec_partialord!(MimeTypeData, MimeTypeDataVec);
282impl_vec_ord!(MimeTypeData, MimeTypeDataVec);
283impl_vec_clone!(MimeTypeData, MimeTypeDataVec, MimeTypeDataVecDestructor);
284impl_vec_partialeq!(MimeTypeData, MimeTypeDataVec);
285impl_vec_eq!(MimeTypeData, MimeTypeDataVec);
286impl_vec_hash!(MimeTypeData, MimeTypeDataVec);
287
288/// Drag data (HTML5 `DataTransfer`).
289///
290/// Holds the payload(s) being transferred during a drag operation, keyed
291/// by MIME type, plus the set of operations the source allows.
292#[derive(Debug, Default, Clone, PartialEq)]
293#[repr(C)]
294pub struct DragData {
295    /// MIME type -> data mapping (vec-of-pairs for FFI compatibility).
296    ///
297    /// e.g., `"text/plain" -> "Hello World"`.
298    pub data: MimeTypeDataVec,
299    /// Set of drag operations the source permits for this drag.
300    pub effect_allowed: DragEffect,
301}
302
303impl DragData {
304    /// Create new empty drag data
305    pub fn new() -> Self {
306        Self {
307            data: MimeTypeDataVec::new(),
308            effect_allowed: DragEffect::Uninitialized,
309        }
310    }
311
312    /// Set data for a MIME type. Replaces any existing entry for the
313    /// same MIME type.
314    pub fn set_data(&mut self, mime_type: impl Into<AzString>, data: Vec<u8>) {
315        let mime_type = mime_type.into();
316        let value: U8Vec = data.into();
317        if let Some(entry) = self
318            .data
319            .as_mut()
320            .iter_mut()
321            .find(|e| e.mime_type == mime_type)
322        {
323            entry.data = value;
324        } else {
325            self.data.push(MimeTypeData {
326                mime_type,
327                data: value,
328            });
329        }
330    }
331
332    /// Get data for a MIME type
333    pub fn get_data(&self, mime_type: &str) -> Option<&[u8]> {
334        self.data
335            .as_ref()
336            .iter()
337            .find(|e| e.mime_type.as_str() == mime_type)
338            .map(|e| e.data.as_ref())
339    }
340
341    /// Set plain text data
342    pub fn set_text(&mut self, text: impl Into<AzString>) {
343        let text_str = text.into();
344        self.set_data("text/plain", text_str.as_str().as_bytes().to_vec());
345    }
346
347    /// Get plain text data
348    pub fn get_text(&self) -> Option<AzString> {
349        self.get_data("text/plain")
350            .map(|bytes| AzString::from(core::str::from_utf8(bytes).unwrap_or("")))
351    }
352}
353
354/// The unified drag context.
355///
356/// This struct wraps `ActiveDragType` and provides common metadata
357/// that applies to all drag operations.
358///
359/// Note: this type is Rust-only and not exposed through the C API.
360#[derive(Debug, Clone, PartialEq)]
361pub struct DragContext {
362    /// The specific type of drag operation
363    pub drag_type: ActiveDragType,
364    /// Session ID from gesture detection (links back to GestureManager)
365    pub session_id: u64,
366    /// Whether the drag has been cancelled (e.g., Escape pressed)
367    pub cancelled: bool,
368}
369
370impl DragContext {
371    /// Create a new drag context
372    pub fn new(drag_type: ActiveDragType, session_id: u64) -> Self {
373        Self {
374            drag_type,
375            session_id,
376            cancelled: false,
377        }
378    }
379
380    /// Create a text selection drag
381    pub fn text_selection(
382        dom_id: DomId,
383        anchor_ifc_node: NodeId,
384        start_mouse_position: LogicalPosition,
385        session_id: u64,
386    ) -> Self {
387        Self::new(
388            ActiveDragType::TextSelection(TextSelectionDrag {
389                dom_id,
390                anchor_ifc_node,
391                anchor_cursor: None,
392                start_mouse_position,
393                current_mouse_position: start_mouse_position,
394                auto_scroll_direction: AutoScrollDirection::None,
395                auto_scroll_container: None,
396            }),
397            session_id,
398        )
399    }
400
401    /// Create a scrollbar thumb drag
402    pub fn scrollbar_thumb(
403        scroll_container_node: NodeId,
404        axis: ScrollbarAxis,
405        start_mouse_position: LogicalPosition,
406        start_scroll_offset: f32,
407        track_length_px: f32,
408        content_length_px: f32,
409        viewport_length_px: f32,
410        session_id: u64,
411    ) -> Self {
412        Self::new(
413            ActiveDragType::ScrollbarThumb(ScrollbarThumbDrag {
414                scroll_container_node,
415                axis,
416                start_mouse_position,
417                start_scroll_offset,
418                current_mouse_position: start_mouse_position,
419                track_length_px,
420                content_length_px,
421                viewport_length_px,
422            }),
423            session_id,
424        )
425    }
426
427    /// Create a node drag
428    pub fn node_drag(
429        dom_id: DomId,
430        node_id: NodeId,
431        start_position: LogicalPosition,
432        drag_data: DragData,
433        session_id: u64,
434    ) -> Self {
435        Self::new(
436            ActiveDragType::Node(NodeDrag {
437                dom_id,
438                node_id,
439                start_position,
440                current_position: start_position,
441                drag_offset: LogicalPosition::zero(),
442                current_drop_target: OptionDomNodeId::None,
443                previous_drop_target: OptionDomNodeId::None,
444                drag_data,
445                drop_accepted: false,
446                drop_effect: DropEffect::None,
447            }),
448            session_id,
449        )
450    }
451
452    /// Create a window move drag
453    pub fn window_move(
454        start_position: LogicalPosition,
455        initial_window_position: WindowPosition,
456        session_id: u64,
457    ) -> Self {
458        Self::new(
459            ActiveDragType::WindowMove(WindowMoveDrag {
460                start_position,
461                current_position: start_position,
462                initial_window_position,
463            }),
464            session_id,
465        )
466    }
467
468    /// Create a file drop drag
469    pub fn file_drop(files: Vec<AzString>, position: LogicalPosition, session_id: u64) -> Self {
470        Self::new(
471            ActiveDragType::FileDrop(FileDropDrag {
472                files: files.into(),
473                position,
474                drop_target: OptionDomNodeId::None,
475                drop_effect: DropEffect::Copy,
476            }),
477            session_id,
478        )
479    }
480
481    /// Update the current mouse position for all drag types
482    pub fn update_position(&mut self, position: LogicalPosition) {
483        match &mut self.drag_type {
484            ActiveDragType::TextSelection(ref mut drag) => {
485                drag.current_mouse_position = position;
486            }
487            ActiveDragType::ScrollbarThumb(ref mut drag) => {
488                drag.current_mouse_position = position;
489            }
490            ActiveDragType::Node(ref mut drag) => {
491                drag.current_position = position;
492            }
493            ActiveDragType::WindowMove(ref mut drag) => {
494                drag.current_position = position;
495            }
496            ActiveDragType::WindowResize(ref mut drag) => {
497                drag.current_position = position;
498            }
499            ActiveDragType::FileDrop(ref mut drag) => {
500                drag.position = position;
501            }
502        }
503    }
504
505    /// Get the current mouse position
506    pub fn current_position(&self) -> LogicalPosition {
507        match &self.drag_type {
508            ActiveDragType::TextSelection(drag) => drag.current_mouse_position,
509            ActiveDragType::ScrollbarThumb(drag) => drag.current_mouse_position,
510            ActiveDragType::Node(drag) => drag.current_position,
511            ActiveDragType::WindowMove(drag) => drag.current_position,
512            ActiveDragType::WindowResize(drag) => drag.current_position,
513            ActiveDragType::FileDrop(drag) => drag.position,
514        }
515    }
516
517    /// Get the start position
518    pub fn start_position(&self) -> LogicalPosition {
519        match &self.drag_type {
520            ActiveDragType::TextSelection(drag) => drag.start_mouse_position,
521            ActiveDragType::ScrollbarThumb(drag) => drag.start_mouse_position,
522            ActiveDragType::Node(drag) => drag.start_position,
523            ActiveDragType::WindowMove(drag) => drag.start_position,
524            ActiveDragType::WindowResize(drag) => drag.start_position,
525            ActiveDragType::FileDrop(drag) => drag.position, // No start for file drops
526        }
527    }
528
529    /// Check if this is a text selection drag
530    pub fn is_text_selection(&self) -> bool {
531        matches!(self.drag_type, ActiveDragType::TextSelection(_))
532    }
533
534    /// Check if this is a scrollbar thumb drag
535    pub fn is_scrollbar_thumb(&self) -> bool {
536        matches!(self.drag_type, ActiveDragType::ScrollbarThumb(_))
537    }
538
539    /// Check if this is a node drag
540    pub fn is_node_drag(&self) -> bool {
541        matches!(self.drag_type, ActiveDragType::Node(_))
542    }
543
544    /// Check if this is a window move drag
545    pub fn is_window_move(&self) -> bool {
546        matches!(self.drag_type, ActiveDragType::WindowMove(_))
547    }
548
549    /// Check if this is a file drop
550    pub fn is_file_drop(&self) -> bool {
551        matches!(self.drag_type, ActiveDragType::FileDrop(_))
552    }
553
554    /// Get as text selection drag (if applicable)
555    pub fn as_text_selection(&self) -> Option<&TextSelectionDrag> {
556        match &self.drag_type {
557            ActiveDragType::TextSelection(drag) => Some(drag),
558            _ => None,
559        }
560    }
561
562    /// Get as mutable text selection drag (if applicable)
563    pub fn as_text_selection_mut(&mut self) -> Option<&mut TextSelectionDrag> {
564        match &mut self.drag_type {
565            ActiveDragType::TextSelection(drag) => Some(drag),
566            _ => None,
567        }
568    }
569
570    /// Get as scrollbar thumb drag (if applicable)
571    pub fn as_scrollbar_thumb(&self) -> Option<&ScrollbarThumbDrag> {
572        match &self.drag_type {
573            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
574            _ => None,
575        }
576    }
577
578    /// Get as mutable scrollbar thumb drag (if applicable)
579    pub fn as_scrollbar_thumb_mut(&mut self) -> Option<&mut ScrollbarThumbDrag> {
580        match &mut self.drag_type {
581            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
582            _ => None,
583        }
584    }
585
586    /// Get as node drag (if applicable)
587    pub fn as_node_drag(&self) -> Option<&NodeDrag> {
588        match &self.drag_type {
589            ActiveDragType::Node(drag) => Some(drag),
590            _ => None,
591        }
592    }
593
594    /// Get as mutable node drag (if applicable)
595    pub fn as_node_drag_mut(&mut self) -> Option<&mut NodeDrag> {
596        match &mut self.drag_type {
597            ActiveDragType::Node(drag) => Some(drag),
598            _ => None,
599        }
600    }
601
602    /// Get as window move drag (if applicable)
603    pub fn as_window_move(&self) -> Option<&WindowMoveDrag> {
604        match &self.drag_type {
605            ActiveDragType::WindowMove(drag) => Some(drag),
606            _ => None,
607        }
608    }
609
610    /// Get as file drop (if applicable)
611    pub fn as_file_drop(&self) -> Option<&FileDropDrag> {
612        match &self.drag_type {
613            ActiveDragType::FileDrop(drag) => Some(drag),
614            _ => None,
615        }
616    }
617
618    /// Get as mutable file drop (if applicable)
619    pub fn as_file_drop_mut(&mut self) -> Option<&mut FileDropDrag> {
620        match &mut self.drag_type {
621            ActiveDragType::FileDrop(drag) => Some(drag),
622            _ => None,
623        }
624    }
625
626    /// Calculate scroll delta for scrollbar thumb drag
627    ///
628    /// Returns the new scroll offset based on current mouse position.
629    pub fn calculate_scrollbar_scroll_offset(&self) -> Option<f32> {
630        let drag = self.as_scrollbar_thumb()?;
631        
632        // Calculate mouse delta along the drag axis
633        let mouse_delta = match drag.axis {
634            ScrollbarAxis::Vertical => {
635                drag.current_mouse_position.y - drag.start_mouse_position.y
636            }
637            ScrollbarAxis::Horizontal => {
638                drag.current_mouse_position.x - drag.start_mouse_position.x
639            }
640        };
641
642        // Calculate the scrollable range
643        let scrollable_range = drag.content_length_px - drag.viewport_length_px;
644        if scrollable_range <= 0.0 || drag.track_length_px <= 0.0 {
645            return Some(drag.start_scroll_offset);
646        }
647
648        // Calculate thumb length (proportional to viewport/content ratio)
649        let thumb_length = (drag.viewport_length_px / drag.content_length_px) * drag.track_length_px;
650        let scrollable_track = drag.track_length_px - thumb_length;
651
652        if scrollable_track <= 0.0 {
653            return Some(drag.start_scroll_offset);
654        }
655
656        // Convert mouse delta to scroll delta
657        let scroll_ratio = mouse_delta / scrollable_track;
658        let scroll_delta = scroll_ratio * scrollable_range;
659
660        // Calculate new scroll offset
661        let new_offset = drag.start_scroll_offset + scroll_delta;
662
663        // Clamp to valid range
664        Some(new_offset.clamp(0.0, scrollable_range))
665    }
666
667    /// Remap a drop target's NodeId using the old→new mapping.
668    /// Clears the target if the old NodeId was removed.
669    fn remap_drop_target(
670        target: &mut OptionDomNodeId,
671        dom_id: DomId,
672        node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
673    ) {
674        let dt = match target.into_option() {
675            Some(dt) if dt.dom == dom_id => dt,
676            _ => return,
677        };
678        let old_nid = match dt.node.into_crate_internal() {
679            Some(nid) => nid,
680            None => return,
681        };
682        if let Some(&new_nid) = node_id_map.get(&old_nid) {
683            *target = Some(DomNodeId {
684                dom: dom_id,
685                node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
686            }).into();
687        } else {
688            *target = OptionDomNodeId::None;
689        }
690    }
691
692    /// Remap NodeIds stored in this drag context after DOM reconciliation.
693    ///
694    /// When the DOM is regenerated during an active drag, NodeIds can change.
695    /// This updates all stored NodeIds using the old→new mapping.
696    /// Returns `false` if a critical NodeId was removed (drag should be cancelled).
697    pub fn remap_node_ids(
698        &mut self,
699        dom_id: DomId,
700        node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
701    ) -> bool {
702        match &mut self.drag_type {
703            ActiveDragType::TextSelection(ref mut drag) => {
704                if drag.dom_id != dom_id {
705                    return true;
706                }
707                if let Some(&new_id) = node_id_map.get(&drag.anchor_ifc_node) {
708                    drag.anchor_ifc_node = new_id;
709                } else {
710                    return false; // anchor node removed
711                }
712                if let Some(ref mut container) = drag.auto_scroll_container {
713                    if let Some(&new_id) = node_id_map.get(container) {
714                        *container = new_id;
715                    } else {
716                        drag.auto_scroll_container = None;
717                    }
718                }
719                true
720            }
721            ActiveDragType::ScrollbarThumb(ref mut drag) => {
722                if let Some(&new_id) = node_id_map.get(&drag.scroll_container_node) {
723                    drag.scroll_container_node = new_id;
724                    true
725                } else {
726                    false // scroll container removed
727                }
728            }
729            ActiveDragType::Node(ref mut drag) => {
730                if drag.dom_id != dom_id {
731                    return true;
732                }
733                if let Some(&new_id) = node_id_map.get(&drag.node_id) {
734                    drag.node_id = new_id;
735                } else {
736                    return false; // dragged node removed
737                }
738                // Drop target remap
739                Self::remap_drop_target(&mut drag.current_drop_target, dom_id, node_id_map);
740                true
741            }
742            // WindowMove, WindowResize, and FileDrop don't reference DOM NodeIds
743            ActiveDragType::WindowMove(_) | ActiveDragType::WindowResize(_) => true,
744            ActiveDragType::FileDrop(ref mut drag) => {
745                Self::remap_drop_target(&mut drag.drop_target, dom_id, node_id_map);
746                true
747            }
748        }
749    }
750}
751
752azul_css::impl_option!(
753    DragContext,
754    OptionDragContext,
755    copy = false,
756    [Debug, Clone, PartialEq]
757);
758
759
760/// Drag offset from the cursor position at drag start (logical pixels).
761/// `dx`/`dy` are the delta from drag start to current position.
762#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
763#[repr(C)]
764pub struct DragDelta {
765    pub dx: f32,
766    pub dy: f32,
767}
768
769impl DragDelta {
770    #[inline(always)]
771    pub const fn new(dx: f32, dy: f32) -> Self {
772        Self { dx, dy }
773    }
774    #[inline(always)]
775    pub const fn zero() -> Self {
776        Self::new(0.0, 0.0)
777    }
778}
779
780impl_option!(
781    DragDelta,
782    OptionDragDelta,
783    [Debug, Copy, Clone, PartialEq, PartialOrd]
784);