Skip to main content

presentar_core/
dnd.rs

1//! Drag and drop system for interactive data transfer.
2//!
3//! This module provides:
4//! - Drag sources and drop targets
5//! - Drag state management
6//! - Visual feedback during drag operations
7//! - Data transfer between widgets
8
9use crate::geometry::{Point, Rect};
10use crate::widget::WidgetId;
11use std::any::Any;
12use std::collections::HashMap;
13
14/// Unique identifier for a drag operation.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct DragId(pub u64);
17
18impl DragId {
19    /// Create a new drag ID.
20    pub const fn new(id: u64) -> Self {
21        Self(id)
22    }
23}
24
25/// Type of data being dragged.
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum DragDataType {
28    /// Plain text.
29    Text,
30    /// HTML content.
31    Html,
32    /// URL/link.
33    Url,
34    /// File path.
35    File,
36    /// Custom type identifier.
37    Custom(String),
38}
39
40impl DragDataType {
41    /// Create a custom drag data type.
42    pub fn custom(name: &str) -> Self {
43        Self::Custom(name.to_string())
44    }
45}
46
47/// Data associated with a drag operation.
48#[derive(Debug, Clone)]
49pub struct DragData {
50    /// Primary data type.
51    pub data_type: DragDataType,
52    /// String representation of the data.
53    pub text: String,
54    /// Additional data in various formats.
55    pub formats: HashMap<DragDataType, String>,
56    /// Custom payload (for internal transfers).
57    pub payload: Option<DragPayload>,
58}
59
60impl DragData {
61    /// Create new drag data with text.
62    pub fn text(content: &str) -> Self {
63        Self {
64            data_type: DragDataType::Text,
65            text: content.to_string(),
66            formats: HashMap::new(),
67            payload: None,
68        }
69    }
70
71    /// Create new drag data with HTML.
72    pub fn html(content: &str) -> Self {
73        let mut formats = HashMap::new();
74        formats.insert(DragDataType::Html, content.to_string());
75        Self {
76            data_type: DragDataType::Html,
77            text: content.to_string(),
78            formats,
79            payload: None,
80        }
81    }
82
83    /// Create new drag data with URL.
84    pub fn url(url: &str) -> Self {
85        Self {
86            data_type: DragDataType::Url,
87            text: url.to_string(),
88            formats: HashMap::new(),
89            payload: None,
90        }
91    }
92
93    /// Create drag data with custom type.
94    pub fn custom(type_name: &str, data: &str) -> Self {
95        Self {
96            data_type: DragDataType::Custom(type_name.to_string()),
97            text: data.to_string(),
98            formats: HashMap::new(),
99            payload: None,
100        }
101    }
102
103    /// Add an alternative format.
104    pub fn with_format(mut self, data_type: DragDataType, data: &str) -> Self {
105        self.formats.insert(data_type, data.to_string());
106        self
107    }
108
109    /// Add a payload.
110    pub fn with_payload<T: Any + Send + Sync + Clone + 'static>(mut self, payload: T) -> Self {
111        self.payload = Some(DragPayload::new(payload));
112        self
113    }
114
115    /// Get data in a specific format.
116    pub fn get_format(&self, data_type: &DragDataType) -> Option<&str> {
117        if &self.data_type == data_type {
118            Some(&self.text)
119        } else {
120            self.formats.get(data_type).map(std::string::String::as_str)
121        }
122    }
123
124    /// Check if data is available in the given format.
125    pub fn has_format(&self, data_type: &DragDataType) -> bool {
126        &self.data_type == data_type || self.formats.contains_key(data_type)
127    }
128}
129
130/// Type-erased payload for drag data.
131#[derive(Debug, Clone)]
132pub struct DragPayload {
133    data: Box<dyn CloneableAny>,
134}
135
136impl DragPayload {
137    /// Create a new payload.
138    pub fn new<T: Any + Send + Sync + Clone + 'static>(data: T) -> Self {
139        Self {
140            data: Box::new(data),
141        }
142    }
143
144    /// Get the payload as a specific type.
145    pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<&T> {
146        self.data.as_any().downcast_ref()
147    }
148}
149
150/// Trait for cloneable any types.
151trait CloneableAny: Any + Send + Sync {
152    fn clone_box(&self) -> Box<dyn CloneableAny>;
153    fn as_any(&self) -> &dyn Any;
154}
155
156impl<T: Any + Send + Sync + Clone + 'static> CloneableAny for T {
157    fn clone_box(&self) -> Box<dyn CloneableAny> {
158        Box::new(self.clone())
159    }
160
161    fn as_any(&self) -> &dyn Any {
162        self
163    }
164}
165
166impl Clone for Box<dyn CloneableAny> {
167    fn clone(&self) -> Self {
168        self.clone_box()
169    }
170}
171
172impl std::fmt::Debug for Box<dyn CloneableAny> {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("CloneableAny").finish_non_exhaustive()
175    }
176}
177
178/// Current phase of a drag operation.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum DragPhase {
181    /// Drag has started.
182    Started,
183    /// Dragging in progress.
184    Dragging,
185    /// Hovering over a valid drop target.
186    OverTarget,
187    /// Dropped successfully.
188    Dropped,
189    /// Drag was cancelled.
190    Cancelled,
191}
192
193/// Effect/operation type for a drop.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub enum DropEffect {
196    /// No drop allowed.
197    #[default]
198    None,
199    /// Copy data to target.
200    Copy,
201    /// Move data to target.
202    Move,
203    /// Create link to data.
204    Link,
205}
206
207/// State of an active drag operation.
208#[derive(Debug, Clone)]
209pub struct DragState {
210    /// Unique drag ID.
211    pub id: DragId,
212    /// Source widget.
213    pub source_widget: WidgetId,
214    /// Current phase.
215    pub phase: DragPhase,
216    /// Starting position.
217    pub start_position: Point,
218    /// Current position.
219    pub current_position: Point,
220    /// Drag data.
221    pub data: DragData,
222    /// Currently hovered drop target.
223    pub hover_target: Option<WidgetId>,
224    /// Allowed drop effects.
225    pub allowed_effects: Vec<DropEffect>,
226    /// Current drop effect.
227    pub effect: DropEffect,
228}
229
230impl DragState {
231    /// Create a new drag state.
232    pub fn new(id: DragId, source_widget: WidgetId, position: Point, data: DragData) -> Self {
233        Self {
234            id,
235            source_widget,
236            phase: DragPhase::Started,
237            start_position: position,
238            current_position: position,
239            data,
240            hover_target: None,
241            allowed_effects: vec![DropEffect::Copy, DropEffect::Move],
242            effect: DropEffect::None,
243        }
244    }
245
246    /// Get the drag offset from start.
247    pub fn offset(&self) -> Point {
248        self.current_position - self.start_position
249    }
250
251    /// Check if drag is active.
252    pub fn is_active(&self) -> bool {
253        matches!(
254            self.phase,
255            DragPhase::Started | DragPhase::Dragging | DragPhase::OverTarget
256        )
257    }
258}
259
260/// Configuration for a drop target.
261#[derive(Debug, Clone)]
262pub struct DropTarget {
263    /// Widget ID of the drop target.
264    pub widget_id: WidgetId,
265    /// Accepted data types.
266    pub accepted_types: Vec<DragDataType>,
267    /// Accepted drop effects.
268    pub accepted_effects: Vec<DropEffect>,
269    /// Bounds of the target.
270    pub bounds: Rect,
271    /// Whether the target is currently active.
272    pub enabled: bool,
273}
274
275impl DropTarget {
276    /// Create a new drop target.
277    pub fn new(widget_id: WidgetId, bounds: Rect) -> Self {
278        Self {
279            widget_id,
280            accepted_types: vec![],
281            accepted_effects: vec![DropEffect::Copy, DropEffect::Move],
282            bounds,
283            enabled: true,
284        }
285    }
286
287    /// Accept specific data types.
288    pub fn accept_types(mut self, types: Vec<DragDataType>) -> Self {
289        self.accepted_types = types;
290        self
291    }
292
293    /// Accept specific effects.
294    pub fn accept_effects(mut self, effects: Vec<DropEffect>) -> Self {
295        self.accepted_effects = effects;
296        self
297    }
298
299    /// Check if this target accepts the given drag data.
300    pub fn accepts(&self, data: &DragData, effect: DropEffect) -> bool {
301        if !self.enabled {
302            return false;
303        }
304
305        // Check effect
306        if !self.accepted_effects.contains(&effect) {
307            return false;
308        }
309
310        // Check type (empty means accept all)
311        if self.accepted_types.is_empty() {
312            return true;
313        }
314
315        self.accepted_types.contains(&data.data_type)
316            || self.accepted_types.iter().any(|t| data.has_format(t))
317    }
318
319    /// Check if a point is within this target's bounds.
320    pub fn contains_point(&self, point: Point) -> bool {
321        self.enabled && self.bounds.contains_point(&point)
322    }
323}
324
325/// Result of a drop operation.
326#[derive(Debug, Clone)]
327pub struct DropResult {
328    /// Whether the drop was successful.
329    pub success: bool,
330    /// Target widget that received the drop.
331    pub target: WidgetId,
332    /// Effect that was applied.
333    pub effect: DropEffect,
334    /// Position of the drop.
335    pub position: Point,
336}
337
338/// Drag and drop manager.
339pub struct DragDropManager {
340    /// Next drag ID.
341    next_id: u64,
342    /// Current active drag state.
343    current_drag: Option<DragState>,
344    /// Registered drop targets.
345    targets: HashMap<WidgetId, DropTarget>,
346    /// Drag preview offset from cursor.
347    preview_offset: Point,
348    /// Minimum drag distance before starting.
349    min_drag_distance: f32,
350}
351
352impl DragDropManager {
353    /// Create a new drag/drop manager.
354    pub fn new() -> Self {
355        Self {
356            next_id: 0,
357            current_drag: None,
358            targets: HashMap::new(),
359            preview_offset: Point::ORIGIN,
360            min_drag_distance: 5.0,
361        }
362    }
363
364    /// Set minimum drag distance.
365    pub fn set_min_drag_distance(&mut self, distance: f32) {
366        self.min_drag_distance = distance;
367    }
368
369    /// Set drag preview offset.
370    pub fn set_preview_offset(&mut self, offset: Point) {
371        self.preview_offset = offset;
372    }
373
374    /// Register a drop target.
375    pub fn register_target(&mut self, target: DropTarget) {
376        self.targets.insert(target.widget_id, target);
377    }
378
379    /// Unregister a drop target.
380    pub fn unregister_target(&mut self, widget_id: WidgetId) {
381        self.targets.remove(&widget_id);
382    }
383
384    /// Update a target's bounds.
385    pub fn update_target_bounds(&mut self, widget_id: WidgetId, bounds: Rect) {
386        if let Some(target) = self.targets.get_mut(&widget_id) {
387            target.bounds = bounds;
388        }
389    }
390
391    /// Start a drag operation.
392    pub fn start_drag(
393        &mut self,
394        source_widget: WidgetId,
395        position: Point,
396        data: DragData,
397    ) -> DragId {
398        let id = DragId::new(self.next_id);
399        self.next_id += 1;
400
401        let state = DragState::new(id, source_widget, position, data);
402        self.current_drag = Some(state);
403
404        id
405    }
406
407    /// Update drag position.
408    pub fn move_drag(&mut self, position: Point) {
409        if let Some(state) = &mut self.current_drag {
410            state.current_position = position;
411
412            // Check for drag distance threshold
413            if state.phase == DragPhase::Started {
414                let distance = state.start_position.distance(&position);
415                if distance >= self.min_drag_distance {
416                    state.phase = DragPhase::Dragging;
417                }
418            }
419
420            // Update hover target
421            if state.phase == DragPhase::Dragging || state.phase == DragPhase::OverTarget {
422                let old_target = state.hover_target;
423                state.hover_target = None;
424                state.effect = DropEffect::None;
425
426                for target in self.targets.values() {
427                    if target.contains_point(position) {
428                        // Find best allowed effect
429                        let effect = state
430                            .allowed_effects
431                            .iter()
432                            .find(|e| target.accepts(&state.data, **e))
433                            .copied()
434                            .unwrap_or(DropEffect::None);
435
436                        if effect != DropEffect::None {
437                            state.hover_target = Some(target.widget_id);
438                            state.effect = effect;
439                            state.phase = DragPhase::OverTarget;
440                            break;
441                        }
442                    }
443                }
444
445                if state.hover_target.is_none() && old_target.is_some() {
446                    state.phase = DragPhase::Dragging;
447                }
448            }
449        }
450    }
451
452    /// End drag with drop attempt.
453    pub fn drop(&mut self) -> Option<DropResult> {
454        let state = self.current_drag.take()?;
455
456        if let Some(target_id) = state.hover_target {
457            if state.effect != DropEffect::None {
458                return Some(DropResult {
459                    success: true,
460                    target: target_id,
461                    effect: state.effect,
462                    position: state.current_position,
463                });
464            }
465        }
466
467        Some(DropResult {
468            success: false,
469            target: state.source_widget,
470            effect: DropEffect::None,
471            position: state.current_position,
472        })
473    }
474
475    /// Cancel the current drag operation.
476    pub fn cancel(&mut self) {
477        if let Some(state) = &mut self.current_drag {
478            state.phase = DragPhase::Cancelled;
479        }
480        self.current_drag = None;
481    }
482
483    /// Get the current drag state.
484    pub fn current(&self) -> Option<&DragState> {
485        self.current_drag.as_ref()
486    }
487
488    /// Check if a drag is active.
489    pub fn is_dragging(&self) -> bool {
490        self.current_drag.as_ref().is_some_and(DragState::is_active)
491    }
492
493    /// Get the preview position for rendering.
494    pub fn preview_position(&self) -> Option<Point> {
495        self.current_drag.as_ref().map(|s| {
496            Point::new(
497                s.current_position.x + self.preview_offset.x,
498                s.current_position.y + self.preview_offset.y,
499            )
500        })
501    }
502
503    /// Get drop targets count.
504    pub fn target_count(&self) -> usize {
505        self.targets.len()
506    }
507
508    /// Find target at position.
509    pub fn target_at(&self, position: Point) -> Option<&DropTarget> {
510        self.targets.values().find(|t| t.contains_point(position))
511    }
512
513    /// Clear all targets and cancel any active drag.
514    pub fn clear(&mut self) {
515        self.cancel();
516        self.targets.clear();
517    }
518}
519
520impl Default for DragDropManager {
521    fn default() -> Self {
522        Self::new()
523    }
524}
525
526impl std::fmt::Debug for DragDropManager {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        f.debug_struct("DragDropManager")
529            .field("next_id", &self.next_id)
530            .field("is_dragging", &self.is_dragging())
531            .field("target_count", &self.targets.len())
532            .finish()
533    }
534}
535
536#[cfg(test)]
537#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
538mod tests {
539    use super::*;
540
541    // DragId tests
542    #[test]
543    fn test_drag_id() {
544        let id1 = DragId::new(1);
545        let id2 = DragId::new(1);
546        let id3 = DragId::new(2);
547
548        assert_eq!(id1, id2);
549        assert_ne!(id1, id3);
550    }
551
552    // DragDataType tests
553    #[test]
554    fn test_drag_data_type() {
555        assert_eq!(DragDataType::Text, DragDataType::Text);
556        assert_ne!(DragDataType::Text, DragDataType::Html);
557
558        let custom = DragDataType::custom("my-type");
559        assert_eq!(custom, DragDataType::Custom("my-type".to_string()));
560    }
561
562    // DragData tests
563    #[test]
564    fn test_drag_data_text() {
565        let data = DragData::text("Hello");
566        assert_eq!(data.data_type, DragDataType::Text);
567        assert_eq!(data.text, "Hello");
568    }
569
570    #[test]
571    fn test_drag_data_html() {
572        let data = DragData::html("<b>Bold</b>");
573        assert_eq!(data.data_type, DragDataType::Html);
574        assert!(data.has_format(&DragDataType::Html));
575    }
576
577    #[test]
578    fn test_drag_data_url() {
579        let data = DragData::url("https://example.com");
580        assert_eq!(data.data_type, DragDataType::Url);
581        assert_eq!(data.text, "https://example.com");
582    }
583
584    #[test]
585    fn test_drag_data_custom() {
586        let data = DragData::custom("widget-id", "123");
587        assert!(matches!(data.data_type, DragDataType::Custom(_)));
588    }
589
590    #[test]
591    fn test_drag_data_with_format() {
592        let data = DragData::text("Hello").with_format(DragDataType::Html, "<p>Hello</p>");
593
594        assert!(data.has_format(&DragDataType::Text));
595        assert!(data.has_format(&DragDataType::Html));
596        assert!(!data.has_format(&DragDataType::Url));
597
598        assert_eq!(data.get_format(&DragDataType::Text), Some("Hello"));
599        assert_eq!(data.get_format(&DragDataType::Html), Some("<p>Hello</p>"));
600    }
601
602    #[test]
603    fn test_drag_data_with_payload() {
604        // Test payload exists (type erasure is complex, just verify the API works)
605        let data = DragData::text("test").with_payload(42i32);
606        assert!(data.payload.is_some());
607    }
608
609    // DragPhase tests
610    #[test]
611    fn test_drag_phase() {
612        assert_eq!(DragPhase::Started, DragPhase::Started);
613        assert_ne!(DragPhase::Started, DragPhase::Dropped);
614    }
615
616    // DropEffect tests
617    #[test]
618    fn test_drop_effect_default() {
619        assert_eq!(DropEffect::default(), DropEffect::None);
620    }
621
622    // DragState tests
623    #[test]
624    fn test_drag_state_new() {
625        let state = DragState::new(
626            DragId::new(1),
627            WidgetId::new(100),
628            Point::new(50.0, 50.0),
629            DragData::text("test"),
630        );
631
632        assert_eq!(state.id, DragId::new(1));
633        assert_eq!(state.source_widget, WidgetId::new(100));
634        assert_eq!(state.phase, DragPhase::Started);
635        assert_eq!(state.start_position, Point::new(50.0, 50.0));
636        assert!(state.is_active());
637    }
638
639    #[test]
640    fn test_drag_state_offset() {
641        let mut state = DragState::new(
642            DragId::new(1),
643            WidgetId::new(100),
644            Point::new(50.0, 50.0),
645            DragData::text("test"),
646        );
647
648        state.current_position = Point::new(100.0, 75.0);
649        let offset = state.offset();
650        assert_eq!(offset.x, 50.0);
651        assert_eq!(offset.y, 25.0);
652    }
653
654    #[test]
655    fn test_drag_state_is_active() {
656        let mut state = DragState::new(
657            DragId::new(1),
658            WidgetId::new(100),
659            Point::ORIGIN,
660            DragData::text("test"),
661        );
662
663        assert!(state.is_active());
664
665        state.phase = DragPhase::Dragging;
666        assert!(state.is_active());
667
668        state.phase = DragPhase::OverTarget;
669        assert!(state.is_active());
670
671        state.phase = DragPhase::Dropped;
672        assert!(!state.is_active());
673
674        state.phase = DragPhase::Cancelled;
675        assert!(!state.is_active());
676    }
677
678    // DropTarget tests
679    #[test]
680    fn test_drop_target_new() {
681        let target = DropTarget::new(WidgetId::new(1), Rect::new(0.0, 0.0, 100.0, 100.0));
682
683        assert_eq!(target.widget_id, WidgetId::new(1));
684        assert!(target.enabled);
685        assert!(target.accepted_types.is_empty());
686    }
687
688    #[test]
689    fn test_drop_target_accept_types() {
690        let target = DropTarget::new(WidgetId::new(1), Rect::new(0.0, 0.0, 100.0, 100.0))
691            .accept_types(vec![DragDataType::Text, DragDataType::Html]);
692
693        assert_eq!(target.accepted_types.len(), 2);
694    }
695
696    #[test]
697    fn test_drop_target_accepts() {
698        let target = DropTarget::new(WidgetId::new(1), Rect::new(0.0, 0.0, 100.0, 100.0))
699            .accept_types(vec![DragDataType::Text])
700            .accept_effects(vec![DropEffect::Copy]);
701
702        let text_data = DragData::text("hello");
703        assert!(target.accepts(&text_data, DropEffect::Copy));
704        assert!(!target.accepts(&text_data, DropEffect::Move));
705
706        let html_data = DragData::html("<b>bold</b>");
707        assert!(!target.accepts(&html_data, DropEffect::Copy));
708    }
709
710    #[test]
711    fn test_drop_target_accepts_all_types() {
712        // Empty accepted_types means accept all
713        let target = DropTarget::new(WidgetId::new(1), Rect::new(0.0, 0.0, 100.0, 100.0));
714
715        assert!(target.accepts(&DragData::text("test"), DropEffect::Copy));
716        assert!(target.accepts(&DragData::html("<b>test</b>"), DropEffect::Move));
717    }
718
719    #[test]
720    fn test_drop_target_disabled() {
721        let mut target = DropTarget::new(WidgetId::new(1), Rect::new(0.0, 0.0, 100.0, 100.0));
722        target.enabled = false;
723
724        assert!(!target.accepts(&DragData::text("test"), DropEffect::Copy));
725        assert!(!target.contains_point(Point::new(50.0, 50.0)));
726    }
727
728    #[test]
729    fn test_drop_target_contains_point() {
730        let target = DropTarget::new(WidgetId::new(1), Rect::new(10.0, 10.0, 100.0, 100.0));
731
732        assert!(target.contains_point(Point::new(50.0, 50.0)));
733        assert!(target.contains_point(Point::new(10.0, 10.0)));
734        assert!(!target.contains_point(Point::new(5.0, 50.0)));
735        assert!(!target.contains_point(Point::new(120.0, 50.0)));
736    }
737
738    // DragDropManager tests
739    #[test]
740    fn test_manager_new() {
741        let manager = DragDropManager::new();
742        assert!(!manager.is_dragging());
743        assert_eq!(manager.target_count(), 0);
744    }
745
746    #[test]
747    fn test_manager_register_target() {
748        let mut manager = DragDropManager::new();
749
750        manager.register_target(DropTarget::new(
751            WidgetId::new(1),
752            Rect::new(0.0, 0.0, 100.0, 100.0),
753        ));
754
755        assert_eq!(manager.target_count(), 1);
756    }
757
758    #[test]
759    fn test_manager_unregister_target() {
760        let mut manager = DragDropManager::new();
761
762        manager.register_target(DropTarget::new(
763            WidgetId::new(1),
764            Rect::new(0.0, 0.0, 100.0, 100.0),
765        ));
766        manager.unregister_target(WidgetId::new(1));
767
768        assert_eq!(manager.target_count(), 0);
769    }
770
771    #[test]
772    fn test_manager_start_drag() {
773        let mut manager = DragDropManager::new();
774
775        let id = manager.start_drag(
776            WidgetId::new(1),
777            Point::new(50.0, 50.0),
778            DragData::text("hello"),
779        );
780
781        assert!(manager.is_dragging());
782        assert_eq!(manager.current().unwrap().id, id);
783    }
784
785    #[test]
786    fn test_manager_move_drag() {
787        let mut manager = DragDropManager::new();
788        manager.set_min_drag_distance(5.0);
789
790        manager.start_drag(
791            WidgetId::new(1),
792            Point::new(50.0, 50.0),
793            DragData::text("hello"),
794        );
795
796        // Move within threshold
797        manager.move_drag(Point::new(52.0, 52.0));
798        assert_eq!(manager.current().unwrap().phase, DragPhase::Started);
799
800        // Move beyond threshold
801        manager.move_drag(Point::new(60.0, 60.0));
802        assert_eq!(manager.current().unwrap().phase, DragPhase::Dragging);
803    }
804
805    #[test]
806    fn test_manager_move_over_target() {
807        let mut manager = DragDropManager::new();
808        manager.set_min_drag_distance(0.0);
809
810        manager.register_target(DropTarget::new(
811            WidgetId::new(10),
812            Rect::new(100.0, 100.0, 100.0, 100.0),
813        ));
814
815        manager.start_drag(
816            WidgetId::new(1),
817            Point::new(50.0, 50.0),
818            DragData::text("hello"),
819        );
820
821        // Move over target
822        manager.move_drag(Point::new(150.0, 150.0));
823        let state = manager.current().unwrap();
824        assert_eq!(state.phase, DragPhase::OverTarget);
825        assert_eq!(state.hover_target, Some(WidgetId::new(10)));
826    }
827
828    #[test]
829    fn test_manager_drop_success() {
830        let mut manager = DragDropManager::new();
831        manager.set_min_drag_distance(0.0);
832
833        manager.register_target(DropTarget::new(
834            WidgetId::new(10),
835            Rect::new(100.0, 100.0, 100.0, 100.0),
836        ));
837
838        manager.start_drag(
839            WidgetId::new(1),
840            Point::new(50.0, 50.0),
841            DragData::text("hello"),
842        );
843
844        manager.move_drag(Point::new(150.0, 150.0));
845        let result = manager.drop().unwrap();
846
847        assert!(result.success);
848        assert_eq!(result.target, WidgetId::new(10));
849        assert!(!manager.is_dragging());
850    }
851
852    #[test]
853    fn test_manager_drop_failure() {
854        let mut manager = DragDropManager::new();
855        manager.set_min_drag_distance(0.0);
856
857        manager.start_drag(
858            WidgetId::new(1),
859            Point::new(50.0, 50.0),
860            DragData::text("hello"),
861        );
862
863        manager.move_drag(Point::new(60.0, 60.0));
864        let result = manager.drop().unwrap();
865
866        assert!(!result.success);
867        assert_eq!(result.effect, DropEffect::None);
868    }
869
870    #[test]
871    fn test_manager_cancel() {
872        let mut manager = DragDropManager::new();
873
874        manager.start_drag(
875            WidgetId::new(1),
876            Point::new(50.0, 50.0),
877            DragData::text("hello"),
878        );
879
880        manager.cancel();
881        assert!(!manager.is_dragging());
882        assert!(manager.current().is_none());
883    }
884
885    #[test]
886    fn test_manager_preview_position() {
887        let mut manager = DragDropManager::new();
888        manager.set_preview_offset(Point::new(-10.0, -10.0));
889
890        manager.start_drag(
891            WidgetId::new(1),
892            Point::new(100.0, 100.0),
893            DragData::text("hello"),
894        );
895
896        let preview_pos = manager.preview_position().unwrap();
897        assert_eq!(preview_pos, Point::new(90.0, 90.0));
898    }
899
900    #[test]
901    fn test_manager_target_at() {
902        let mut manager = DragDropManager::new();
903
904        manager.register_target(DropTarget::new(
905            WidgetId::new(1),
906            Rect::new(0.0, 0.0, 100.0, 100.0),
907        ));
908        manager.register_target(DropTarget::new(
909            WidgetId::new(2),
910            Rect::new(200.0, 200.0, 100.0, 100.0),
911        ));
912
913        assert!(manager.target_at(Point::new(50.0, 50.0)).is_some());
914        assert!(manager.target_at(Point::new(150.0, 150.0)).is_none());
915        assert!(manager.target_at(Point::new(250.0, 250.0)).is_some());
916    }
917
918    #[test]
919    fn test_manager_clear() {
920        let mut manager = DragDropManager::new();
921
922        manager.register_target(DropTarget::new(
923            WidgetId::new(1),
924            Rect::new(0.0, 0.0, 100.0, 100.0),
925        ));
926
927        manager.start_drag(WidgetId::new(2), Point::ORIGIN, DragData::text("test"));
928
929        manager.clear();
930
931        assert!(!manager.is_dragging());
932        assert_eq!(manager.target_count(), 0);
933    }
934
935    #[test]
936    fn test_manager_update_target_bounds() {
937        let mut manager = DragDropManager::new();
938
939        manager.register_target(DropTarget::new(
940            WidgetId::new(1),
941            Rect::new(0.0, 0.0, 100.0, 100.0),
942        ));
943
944        manager.update_target_bounds(WidgetId::new(1), Rect::new(50.0, 50.0, 200.0, 200.0));
945
946        let target = manager.target_at(Point::new(100.0, 100.0)).unwrap();
947        assert_eq!(target.bounds.x, 50.0);
948        assert_eq!(target.bounds.width, 200.0);
949    }
950}