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

            
13
use alloc::vec::Vec;
14

            
15
use crate::dom::{DomId, DomNodeId, NodeId, OptionDomNodeId};
16
use crate::geom::LogicalPosition;
17
use crate::selection::TextCursor;
18
use crate::window::WindowPosition;
19

            
20
use 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)]
28
pub 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)]
48
pub 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)]
70
pub 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)]
92
pub 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)]
102
pub 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)]
130
pub 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)]
144
pub 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)]
160
pub 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)]
176
pub 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)]
190
pub 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)]
211
pub 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)]
232
pub 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)]
259
pub struct MimeTypeData {
260
    pub mime_type: AzString,
261
    pub data: U8Vec,
262
}
263

            
264
impl_option!(
265
    MimeTypeData,
266
    OptionMimeTypeData,
267
    copy = false,
268
    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
269
);
270

            
271
impl_vec!(
272
    MimeTypeData,
273
    MimeTypeDataVec,
274
    MimeTypeDataVecDestructor,
275
    MimeTypeDataVecDestructorType,
276
    MimeTypeDataVecSlice,
277
    OptionMimeTypeData
278
);
279
impl_vec_mut!(MimeTypeData, MimeTypeDataVec);
280
impl_vec_debug!(MimeTypeData, MimeTypeDataVec);
281
impl_vec_partialord!(MimeTypeData, MimeTypeDataVec);
282
impl_vec_ord!(MimeTypeData, MimeTypeDataVec);
283
impl_vec_clone!(MimeTypeData, MimeTypeDataVec, MimeTypeDataVecDestructor);
284
impl_vec_partialeq!(MimeTypeData, MimeTypeDataVec);
285
impl_vec_eq!(MimeTypeData, MimeTypeDataVec);
286
impl_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)]
294
pub 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

            
303
impl 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)]
361
pub 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

            
370
impl 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

            
752
azul_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)]
764
pub struct DragDelta {
765
    pub dx: f32,
766
    pub dy: f32,
767
}
768

            
769
impl 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

            
780
impl_option!(
781
    DragDelta,
782
    OptionDragDelta,
783
    [Debug, Copy, Clone, PartialEq, PartialOrd]
784
);