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::collections::btree_map::BTreeMap;
14use alloc::vec::Vec;
15
16use crate::dom::{DomId, DomNodeId, NodeId, OptionDomNodeId};
17use crate::geom::LogicalPosition;
18use crate::selection::TextCursor;
19use crate::window::WindowPosition;
20
21use azul_css::{AzString, OptionString, StringVec};
22
23/// Type of the active drag operation.
24///
25/// This enum unifies all drag types into a single discriminated union,
26/// making it easy to handle different drag behaviors in one place.
27#[derive(Debug, Clone, PartialEq)]
28#[repr(C, u8)]
29pub enum ActiveDragType {
30    /// Text selection drag - user is selecting text by dragging
31    TextSelection(TextSelectionDrag),
32    /// Scrollbar thumb drag - user is dragging a scrollbar thumb
33    ScrollbarThumb(ScrollbarThumbDrag),
34    /// Node drag-and-drop - user is dragging a DOM node
35    Node(NodeDrag),
36    /// Window drag - user is moving the window (titlebar drag)
37    WindowMove(WindowMoveDrag),
38    /// Window resize - user is resizing the window (edge/corner drag)
39    WindowResize(WindowResizeDrag),
40    /// File drop from OS - user is dragging file(s) from the OS
41    FileDrop(FileDropDrag),
42}
43
44/// Text selection drag state.
45///
46/// Tracks the anchor point (where selection started) and current position.
47#[derive(Debug, Clone, PartialEq)]
48#[repr(C)]
49pub struct TextSelectionDrag {
50    /// DOM ID where the selection started
51    pub dom_id: DomId,
52    /// The IFC root node where selection started (e.g., <p> element)
53    pub anchor_ifc_node: NodeId,
54    /// The anchor cursor position (fixed during drag)
55    pub anchor_cursor: Option<TextCursor>,
56    /// Mouse position where drag started
57    pub start_mouse_position: LogicalPosition,
58    /// Current mouse position
59    pub current_mouse_position: LogicalPosition,
60    /// Whether we should auto-scroll (mouse near edge of scroll container)
61    pub auto_scroll_direction: AutoScrollDirection,
62    /// The scroll container that should be auto-scrolled (if any)
63    pub auto_scroll_container: Option<NodeId>,
64}
65
66/// Scrollbar thumb drag state.
67///
68/// Tracks which scrollbar is being dragged and the current offset.
69#[derive(Debug, Clone, Copy, PartialEq)]
70#[repr(C)]
71pub struct ScrollbarThumbDrag {
72    /// The scroll container node being scrolled
73    pub scroll_container_node: NodeId,
74    /// Whether this is the vertical or horizontal scrollbar
75    pub axis: ScrollbarAxis,
76    /// Mouse Y position where drag started (for calculating delta)
77    pub start_mouse_position: LogicalPosition,
78    /// Scroll offset when drag started
79    pub start_scroll_offset: f32,
80    /// Current mouse position
81    pub current_mouse_position: LogicalPosition,
82    /// Track length in pixels (for calculating scroll ratio)
83    pub track_length_px: f32,
84    /// Content length in pixels (for calculating scroll ratio)
85    pub content_length_px: f32,
86    /// Viewport length in pixels (for calculating scroll ratio)
87    pub viewport_length_px: f32,
88}
89
90/// Which scrollbar axis is being dragged
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[repr(C)]
93pub enum ScrollbarAxis {
94    Vertical,
95    Horizontal,
96}
97
98/// Node drag-and-drop state.
99///
100/// Tracks a DOM node being dragged for reordering or moving.
101#[derive(Debug, Clone, PartialEq)]
102#[repr(C)]
103pub struct NodeDrag {
104    /// DOM ID of the node being dragged
105    pub dom_id: DomId,
106    /// Node ID being dragged
107    pub node_id: NodeId,
108    /// Position where drag started
109    pub start_position: LogicalPosition,
110    /// Current drag position
111    pub current_position: LogicalPosition,
112    /// Offset from node origin to click point (for correct visual positioning)
113    pub drag_offset: LogicalPosition,
114    /// Optional: DOM node currently under cursor (drop target)
115    pub current_drop_target: OptionDomNodeId,
116    /// Previous drop target (for generating DragEnter/DragLeave events)
117    pub previous_drop_target: OptionDomNodeId,
118    /// Drag data (MIME types and content)
119    pub drag_data: DragData,
120    /// Whether the current drop target has accepted the drop via accept_drop()
121    pub drop_accepted: bool,
122    /// Drop effect set by the drop target
123    pub drop_effect: DropEffect,
124}
125
126/// Window move drag state.
127///
128/// Tracks the window being moved via titlebar drag.
129#[derive(Debug, Clone, PartialEq)]
130#[repr(C)]
131pub struct WindowMoveDrag {
132    /// Position where window drag started (in screen coordinates)
133    pub start_position: LogicalPosition,
134    /// Current drag position
135    pub current_position: LogicalPosition,
136    /// Initial window position before drag
137    pub initial_window_position: WindowPosition,
138}
139
140/// Window resize drag state.
141///
142/// Tracks the window being resized via edge/corner drag.
143#[derive(Debug, Clone, Copy, PartialEq)]
144#[repr(C)]
145pub struct WindowResizeDrag {
146    /// Which edge/corner is being dragged
147    pub edge: WindowResizeEdge,
148    /// Position where resize started
149    pub start_position: LogicalPosition,
150    /// Current drag position
151    pub current_position: LogicalPosition,
152    /// Initial window size before resize
153    pub initial_width: u32,
154    /// Initial window height before resize
155    pub initial_height: u32,
156}
157
158/// Which edge or corner of the window is being resized
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[repr(C)]
161pub enum WindowResizeEdge {
162    Top,
163    Bottom,
164    Left,
165    Right,
166    TopLeft,
167    TopRight,
168    BottomLeft,
169    BottomRight,
170}
171
172/// File drop from OS drag state.
173///
174/// Tracks files being dragged from the operating system.
175#[derive(Debug, Clone, PartialEq)]
176#[repr(C)]
177pub struct FileDropDrag {
178    /// Files being dragged (as string paths)
179    pub files: StringVec,
180    /// Current position of drag cursor
181    pub position: LogicalPosition,
182    /// DOM node under cursor (potential drop target)
183    pub drop_target: OptionDomNodeId,
184    /// Allowed drop effect
185    pub drop_effect: DropEffect,
186}
187
188/// Direction for auto-scrolling during drag operations
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
190#[repr(C)]
191pub enum AutoScrollDirection {
192    /// No auto-scroll needed
193    #[default]
194    None,
195    /// Scroll up (mouse near top edge)
196    Up,
197    /// Scroll down (mouse near bottom edge)
198    Down,
199    /// Scroll left (mouse near left edge)
200    Left,
201    /// Scroll right (mouse near right edge)
202    Right,
203    /// Scroll up-left (mouse near top-left corner)
204    UpLeft,
205    /// Scroll up-right (mouse near top-right corner)
206    UpRight,
207    /// Scroll down-left (mouse near bottom-left corner)
208    DownLeft,
209    /// Scroll down-right (mouse near bottom-right corner)
210    DownRight,
211}
212
213/// Drop effect (what happens when dropped)
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
215#[repr(C)]
216pub enum DropEffect {
217    /// No effect
218    #[default]
219    None,
220    /// Copy the data
221    Copy,
222    /// Move the data
223    Move,
224    /// Create link
225    Link,
226}
227
228/// Drag data (like HTML5 DataTransfer)
229#[derive(Debug, Clone, PartialEq, Default)]
230pub struct DragData {
231    /// MIME type -> data mapping
232    ///
233    /// e.g., "text/plain" -> "Hello World"
234    pub data: BTreeMap<AzString, Vec<u8>>,
235    /// Allowed drag operations
236    pub effect_allowed: DragEffect,
237}
238
239/// Drag/drop effect (like HTML5 dropEffect)
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
241#[repr(C)]
242pub enum DragEffect {
243    /// No drop allowed
244    #[default]
245    None,
246    /// Copy operation
247    Copy,
248    /// Move operation
249    Move,
250    /// Link/shortcut operation
251    Link,
252}
253
254impl DragData {
255    /// Create new empty drag data
256    pub fn new() -> Self {
257        Self {
258            data: BTreeMap::new(),
259            effect_allowed: DragEffect::Copy,
260        }
261    }
262
263    /// Set data for a MIME type
264    pub fn set_data(&mut self, mime_type: impl Into<AzString>, data: Vec<u8>) {
265        self.data.insert(mime_type.into(), data);
266    }
267
268    /// Get data for a MIME type
269    pub fn get_data(&self, mime_type: &str) -> Option<&[u8]> {
270        self.data.get(&AzString::from(mime_type)).map(|v| v.as_slice())
271    }
272
273    /// Set plain text data
274    pub fn set_text(&mut self, text: impl Into<AzString>) {
275        let text_str = text.into();
276        self.set_data("text/plain", text_str.as_str().as_bytes().to_vec());
277    }
278
279    /// Get plain text data
280    pub fn get_text(&self) -> Option<AzString> {
281        self.get_data("text/plain")
282            .map(|bytes| AzString::from(core::str::from_utf8(bytes).unwrap_or("")))
283    }
284}
285
286/// The unified drag context.
287///
288/// This struct wraps `ActiveDragType` and provides common metadata
289/// that applies to all drag operations.
290#[derive(Debug, Clone, PartialEq)]
291pub struct DragContext {
292    /// The specific type of drag operation
293    pub drag_type: ActiveDragType,
294    /// Session ID from gesture detection (links back to GestureManager)
295    pub session_id: u64,
296    /// Whether the drag has been cancelled (e.g., Escape pressed)
297    pub cancelled: bool,
298}
299
300impl DragContext {
301    /// Create a new drag context
302    pub fn new(drag_type: ActiveDragType, session_id: u64) -> Self {
303        Self {
304            drag_type,
305            session_id,
306            cancelled: false,
307        }
308    }
309
310    /// Create a text selection drag
311    pub fn text_selection(
312        dom_id: DomId,
313        anchor_ifc_node: NodeId,
314        start_mouse_position: LogicalPosition,
315        session_id: u64,
316    ) -> Self {
317        Self::new(
318            ActiveDragType::TextSelection(TextSelectionDrag {
319                dom_id,
320                anchor_ifc_node,
321                anchor_cursor: None,
322                start_mouse_position,
323                current_mouse_position: start_mouse_position,
324                auto_scroll_direction: AutoScrollDirection::None,
325                auto_scroll_container: None,
326            }),
327            session_id,
328        )
329    }
330
331    /// Create a scrollbar thumb drag
332    pub fn scrollbar_thumb(
333        scroll_container_node: NodeId,
334        axis: ScrollbarAxis,
335        start_mouse_position: LogicalPosition,
336        start_scroll_offset: f32,
337        track_length_px: f32,
338        content_length_px: f32,
339        viewport_length_px: f32,
340        session_id: u64,
341    ) -> Self {
342        Self::new(
343            ActiveDragType::ScrollbarThumb(ScrollbarThumbDrag {
344                scroll_container_node,
345                axis,
346                start_mouse_position,
347                start_scroll_offset,
348                current_mouse_position: start_mouse_position,
349                track_length_px,
350                content_length_px,
351                viewport_length_px,
352            }),
353            session_id,
354        )
355    }
356
357    /// Create a node drag
358    pub fn node_drag(
359        dom_id: DomId,
360        node_id: NodeId,
361        start_position: LogicalPosition,
362        drag_data: DragData,
363        session_id: u64,
364    ) -> Self {
365        Self::new(
366            ActiveDragType::Node(NodeDrag {
367                dom_id,
368                node_id,
369                start_position,
370                current_position: start_position,
371                drag_offset: LogicalPosition::zero(),
372                current_drop_target: OptionDomNodeId::None,
373                previous_drop_target: OptionDomNodeId::None,
374                drag_data,
375                drop_accepted: false,
376                drop_effect: DropEffect::None,
377            }),
378            session_id,
379        )
380    }
381
382    /// Create a window move drag
383    pub fn window_move(
384        start_position: LogicalPosition,
385        initial_window_position: WindowPosition,
386        session_id: u64,
387    ) -> Self {
388        Self::new(
389            ActiveDragType::WindowMove(WindowMoveDrag {
390                start_position,
391                current_position: start_position,
392                initial_window_position,
393            }),
394            session_id,
395        )
396    }
397
398    /// Create a file drop drag
399    pub fn file_drop(files: Vec<AzString>, position: LogicalPosition, session_id: u64) -> Self {
400        Self::new(
401            ActiveDragType::FileDrop(FileDropDrag {
402                files: files.into(),
403                position,
404                drop_target: OptionDomNodeId::None,
405                drop_effect: DropEffect::Copy,
406            }),
407            session_id,
408        )
409    }
410
411    /// Update the current mouse position for all drag types
412    pub fn update_position(&mut self, position: LogicalPosition) {
413        match &mut self.drag_type {
414            ActiveDragType::TextSelection(ref mut drag) => {
415                drag.current_mouse_position = position;
416            }
417            ActiveDragType::ScrollbarThumb(ref mut drag) => {
418                drag.current_mouse_position = position;
419            }
420            ActiveDragType::Node(ref mut drag) => {
421                drag.current_position = position;
422            }
423            ActiveDragType::WindowMove(ref mut drag) => {
424                drag.current_position = position;
425            }
426            ActiveDragType::WindowResize(ref mut drag) => {
427                drag.current_position = position;
428            }
429            ActiveDragType::FileDrop(ref mut drag) => {
430                drag.position = position;
431            }
432        }
433    }
434
435    /// Get the current mouse position
436    pub fn current_position(&self) -> LogicalPosition {
437        match &self.drag_type {
438            ActiveDragType::TextSelection(drag) => drag.current_mouse_position,
439            ActiveDragType::ScrollbarThumb(drag) => drag.current_mouse_position,
440            ActiveDragType::Node(drag) => drag.current_position,
441            ActiveDragType::WindowMove(drag) => drag.current_position,
442            ActiveDragType::WindowResize(drag) => drag.current_position,
443            ActiveDragType::FileDrop(drag) => drag.position,
444        }
445    }
446
447    /// Get the start position
448    pub fn start_position(&self) -> LogicalPosition {
449        match &self.drag_type {
450            ActiveDragType::TextSelection(drag) => drag.start_mouse_position,
451            ActiveDragType::ScrollbarThumb(drag) => drag.start_mouse_position,
452            ActiveDragType::Node(drag) => drag.start_position,
453            ActiveDragType::WindowMove(drag) => drag.start_position,
454            ActiveDragType::WindowResize(drag) => drag.start_position,
455            ActiveDragType::FileDrop(drag) => drag.position, // No start for file drops
456        }
457    }
458
459    /// Check if this is a text selection drag
460    pub fn is_text_selection(&self) -> bool {
461        matches!(self.drag_type, ActiveDragType::TextSelection(_))
462    }
463
464    /// Check if this is a scrollbar thumb drag
465    pub fn is_scrollbar_thumb(&self) -> bool {
466        matches!(self.drag_type, ActiveDragType::ScrollbarThumb(_))
467    }
468
469    /// Check if this is a node drag
470    pub fn is_node_drag(&self) -> bool {
471        matches!(self.drag_type, ActiveDragType::Node(_))
472    }
473
474    /// Check if this is a window move drag
475    pub fn is_window_move(&self) -> bool {
476        matches!(self.drag_type, ActiveDragType::WindowMove(_))
477    }
478
479    /// Check if this is a file drop
480    pub fn is_file_drop(&self) -> bool {
481        matches!(self.drag_type, ActiveDragType::FileDrop(_))
482    }
483
484    /// Get as text selection drag (if applicable)
485    pub fn as_text_selection(&self) -> Option<&TextSelectionDrag> {
486        match &self.drag_type {
487            ActiveDragType::TextSelection(drag) => Some(drag),
488            _ => None,
489        }
490    }
491
492    /// Get as mutable text selection drag (if applicable)
493    pub fn as_text_selection_mut(&mut self) -> Option<&mut TextSelectionDrag> {
494        match &mut self.drag_type {
495            ActiveDragType::TextSelection(drag) => Some(drag),
496            _ => None,
497        }
498    }
499
500    /// Get as scrollbar thumb drag (if applicable)
501    pub fn as_scrollbar_thumb(&self) -> Option<&ScrollbarThumbDrag> {
502        match &self.drag_type {
503            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
504            _ => None,
505        }
506    }
507
508    /// Get as mutable scrollbar thumb drag (if applicable)
509    pub fn as_scrollbar_thumb_mut(&mut self) -> Option<&mut ScrollbarThumbDrag> {
510        match &mut self.drag_type {
511            ActiveDragType::ScrollbarThumb(drag) => Some(drag),
512            _ => None,
513        }
514    }
515
516    /// Get as node drag (if applicable)
517    pub fn as_node_drag(&self) -> Option<&NodeDrag> {
518        match &self.drag_type {
519            ActiveDragType::Node(drag) => Some(drag),
520            _ => None,
521        }
522    }
523
524    /// Get as mutable node drag (if applicable)
525    pub fn as_node_drag_mut(&mut self) -> Option<&mut NodeDrag> {
526        match &mut self.drag_type {
527            ActiveDragType::Node(drag) => Some(drag),
528            _ => None,
529        }
530    }
531
532    /// Get as window move drag (if applicable)
533    pub fn as_window_move(&self) -> Option<&WindowMoveDrag> {
534        match &self.drag_type {
535            ActiveDragType::WindowMove(drag) => Some(drag),
536            _ => None,
537        }
538    }
539
540    /// Get as file drop (if applicable)
541    pub fn as_file_drop(&self) -> Option<&FileDropDrag> {
542        match &self.drag_type {
543            ActiveDragType::FileDrop(drag) => Some(drag),
544            _ => None,
545        }
546    }
547
548    /// Get as mutable file drop (if applicable)
549    pub fn as_file_drop_mut(&mut self) -> Option<&mut FileDropDrag> {
550        match &mut self.drag_type {
551            ActiveDragType::FileDrop(drag) => Some(drag),
552            _ => None,
553        }
554    }
555
556    /// Calculate scroll delta for scrollbar thumb drag
557    ///
558    /// Returns the new scroll offset based on current mouse position.
559    pub fn calculate_scrollbar_scroll_offset(&self) -> Option<f32> {
560        let drag = self.as_scrollbar_thumb()?;
561        
562        // Calculate mouse delta along the drag axis
563        let mouse_delta = match drag.axis {
564            ScrollbarAxis::Vertical => {
565                drag.current_mouse_position.y - drag.start_mouse_position.y
566            }
567            ScrollbarAxis::Horizontal => {
568                drag.current_mouse_position.x - drag.start_mouse_position.x
569            }
570        };
571
572        // Calculate the scrollable range
573        let scrollable_range = drag.content_length_px - drag.viewport_length_px;
574        if scrollable_range <= 0.0 || drag.track_length_px <= 0.0 {
575            return Some(drag.start_scroll_offset);
576        }
577
578        // Calculate thumb length (proportional to viewport/content ratio)
579        let thumb_length = (drag.viewport_length_px / drag.content_length_px) * drag.track_length_px;
580        let scrollable_track = drag.track_length_px - thumb_length;
581
582        if scrollable_track <= 0.0 {
583            return Some(drag.start_scroll_offset);
584        }
585
586        // Convert mouse delta to scroll delta
587        let scroll_ratio = mouse_delta / scrollable_track;
588        let scroll_delta = scroll_ratio * scrollable_range;
589
590        // Calculate new scroll offset
591        let new_offset = drag.start_scroll_offset + scroll_delta;
592
593        // Clamp to valid range
594        Some(new_offset.clamp(0.0, scrollable_range))
595    }
596
597    /// Remap NodeIds stored in this drag context after DOM reconciliation.
598    ///
599    /// When the DOM is regenerated during an active drag, NodeIds can change.
600    /// This updates all stored NodeIds using the old→new mapping.
601    /// Returns `false` if a critical NodeId was removed (drag should be cancelled).
602    pub fn remap_node_ids(
603        &mut self,
604        dom_id: DomId,
605        node_id_map: &alloc::collections::BTreeMap<NodeId, NodeId>,
606    ) -> bool {
607        match &mut self.drag_type {
608            ActiveDragType::TextSelection(ref mut drag) => {
609                if drag.dom_id != dom_id {
610                    return true;
611                }
612                if let Some(&new_id) = node_id_map.get(&drag.anchor_ifc_node) {
613                    drag.anchor_ifc_node = new_id;
614                } else {
615                    return false; // anchor node removed
616                }
617                if let Some(ref mut container) = drag.auto_scroll_container {
618                    if let Some(&new_id) = node_id_map.get(container) {
619                        *container = new_id;
620                    } else {
621                        drag.auto_scroll_container = None;
622                    }
623                }
624                true
625            }
626            ActiveDragType::ScrollbarThumb(ref mut drag) => {
627                if let Some(&new_id) = node_id_map.get(&drag.scroll_container_node) {
628                    drag.scroll_container_node = new_id;
629                    true
630                } else {
631                    false // scroll container removed
632                }
633            }
634            ActiveDragType::Node(ref mut drag) => {
635                if drag.dom_id != dom_id {
636                    return true;
637                }
638                if let Some(&new_id) = node_id_map.get(&drag.node_id) {
639                    drag.node_id = new_id;
640                } else {
641                    return false; // dragged node removed
642                }
643                // Drop target remap
644                if let Some(dt) = drag.current_drop_target.into_option() {
645                    if dt.dom == dom_id {
646                        if let Some(old_nid) = dt.node.into_crate_internal() {
647                            if let Some(&new_nid) = node_id_map.get(&old_nid) {
648                                drag.current_drop_target = Some(DomNodeId {
649                                    dom: dom_id,
650                                    node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
651                                }).into();
652                            } else {
653                                drag.current_drop_target = OptionDomNodeId::None;
654                            }
655                        }
656                    }
657                }
658                true
659            }
660            // WindowMove, WindowResize, and FileDrop don't reference DOM NodeIds
661            ActiveDragType::WindowMove(_) | ActiveDragType::WindowResize(_) => true,
662            ActiveDragType::FileDrop(ref mut drag) => {
663                if let Some(dt) = drag.drop_target.into_option() {
664                    if dt.dom == dom_id {
665                        if let Some(old_nid) = dt.node.into_crate_internal() {
666                            if let Some(&new_nid) = node_id_map.get(&old_nid) {
667                                drag.drop_target = Some(DomNodeId {
668                                    dom: dom_id,
669                                    node: crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_nid)),
670                                }).into();
671                            } else {
672                                drag.drop_target = OptionDomNodeId::None;
673                            }
674                        }
675                    }
676                }
677                true
678            }
679        }
680    }
681}
682
683azul_css::impl_option!(
684    DragContext,
685    OptionDragContext,
686    copy = false,
687    [Debug, Clone, PartialEq]
688);