⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/desktop_ext/
dnd.rs

1//! Drag-and-Drop
2//!
3//! wl_data_offer protocol with enter/leave/drop/motion events.
4
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8use super::clipboard::ClipboardMime;
9
10/// Errors that can occur during drag-and-drop operations.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum DndError {
13    /// No drag operation is in progress.
14    NotDragging,
15    /// A drag is already in progress.
16    AlreadyDragging,
17    /// The drop target rejected the drop.
18    DropRejected,
19    /// No MIME type matches between source and target.
20    NoMimeMatch,
21    /// Invalid surface ID.
22    InvalidSurface,
23    /// The drag operation was cancelled.
24    Cancelled,
25}
26
27impl core::fmt::Display for DndError {
28    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29        match self {
30            Self::NotDragging => write!(f, "not dragging"),
31            Self::AlreadyDragging => write!(f, "already dragging"),
32            Self::DropRejected => write!(f, "drop rejected"),
33            Self::NoMimeMatch => write!(f, "no MIME match"),
34            Self::InvalidSurface => write!(f, "invalid surface"),
35            Self::Cancelled => write!(f, "drag cancelled"),
36        }
37    }
38}
39
40/// State machine for drag-and-drop operations.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum DndState {
43    /// No drag operation active.
44    #[default]
45    Idle,
46    /// A drag is in progress (user holding mouse button).
47    Dragging,
48    /// Over a valid drop target, waiting for drop confirmation.
49    DropPending,
50    /// Drop was accepted and data transfer is happening.
51    Transferring,
52}
53
54/// Events emitted by the DnD subsystem.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum DndEvent {
57    /// Drag entered a surface.
58    Enter { surface_id: u32, x: i32, y: i32 },
59    /// Drag left a surface.
60    Leave { surface_id: u32 },
61    /// Drag moved within a surface.
62    Motion { surface_id: u32, x: i32, y: i32 },
63    /// Drop occurred on a surface.
64    Drop { surface_id: u32, x: i32, y: i32 },
65    /// Drag was cancelled.
66    Cancelled,
67}
68
69/// Information about a drag source.
70#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg(feature = "alloc")]
72pub struct DragSource {
73    /// Surface ID that initiated the drag.
74    pub source_surface: u32,
75    /// MIME types offered by the source.
76    pub offered_mimes: Vec<ClipboardMime>,
77    /// Position where drag started.
78    pub origin_x: i32,
79    pub origin_y: i32,
80    /// Visual feedback: ghost image dimensions (width, height).
81    pub ghost_width: u32,
82    pub ghost_height: u32,
83}
84
85/// Information about a drop target.
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[cfg(feature = "alloc")]
88pub struct DropTarget {
89    /// Surface ID that can receive drops.
90    pub surface_id: u32,
91    /// MIME types accepted by this target.
92    pub accepted_mimes: Vec<ClipboardMime>,
93    /// Bounding box (x, y, width, height).
94    pub x: i32,
95    pub y: i32,
96    pub width: u32,
97    pub height: u32,
98}
99
100#[cfg(feature = "alloc")]
101impl DropTarget {
102    /// Check if a point is within this drop target's bounds.
103    pub fn contains(&self, px: i32, py: i32) -> bool {
104        px >= self.x
105            && px < self.x.saturating_add(self.width as i32)
106            && py >= self.y
107            && py < self.y.saturating_add(self.height as i32)
108    }
109}
110
111/// Drag-and-drop manager.
112#[derive(Debug)]
113#[cfg(feature = "alloc")]
114pub struct DndManager {
115    /// Current DnD state.
116    state: DndState,
117    /// Active drag source (if dragging).
118    source: Option<DragSource>,
119    /// Currently hovered surface.
120    hover_surface: Option<u32>,
121    /// Current cursor position during drag.
122    cursor_x: i32,
123    cursor_y: i32,
124    /// Registered drop targets.
125    targets: Vec<DropTarget>,
126    /// Pending events.
127    events: Vec<DndEvent>,
128}
129
130#[cfg(feature = "alloc")]
131impl Default for DndManager {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[cfg(feature = "alloc")]
138impl DndManager {
139    /// Create a new DnD manager.
140    pub fn new() -> Self {
141        Self {
142            state: DndState::Idle,
143            source: None,
144            hover_surface: None,
145            cursor_x: 0,
146            cursor_y: 0,
147            targets: Vec::new(),
148            events: Vec::new(),
149        }
150    }
151
152    /// Begin a drag operation.
153    pub fn start_drag(
154        &mut self,
155        source_surface: u32,
156        offered_mimes: Vec<ClipboardMime>,
157        origin_x: i32,
158        origin_y: i32,
159        ghost_width: u32,
160        ghost_height: u32,
161    ) -> Result<(), DndError> {
162        if self.state != DndState::Idle {
163            return Err(DndError::AlreadyDragging);
164        }
165
166        self.source = Some(DragSource {
167            source_surface,
168            offered_mimes,
169            origin_x,
170            origin_y,
171            ghost_width,
172            ghost_height,
173        });
174        self.state = DndState::Dragging;
175        self.cursor_x = origin_x;
176        self.cursor_y = origin_y;
177
178        Ok(())
179    }
180
181    /// Update cursor position during drag. Performs hit testing and emits
182    /// events.
183    pub fn motion(&mut self, x: i32, y: i32) -> Result<(), DndError> {
184        if self.state != DndState::Dragging && self.state != DndState::DropPending {
185            return Err(DndError::NotDragging);
186        }
187
188        self.cursor_x = x;
189        self.cursor_y = y;
190
191        // Hit-test against registered drop targets.
192        let hit = self.targets.iter().find(|t| t.contains(x, y));
193
194        let new_surface = hit.map(|t| t.surface_id);
195        let old_surface = self.hover_surface;
196
197        // Emit leave/enter events on surface change.
198        if new_surface != old_surface {
199            if let Some(old_id) = old_surface {
200                self.events.push(DndEvent::Leave { surface_id: old_id });
201            }
202            if let Some(new_id) = new_surface {
203                self.events.push(DndEvent::Enter {
204                    surface_id: new_id,
205                    x,
206                    y,
207                });
208                self.state = DndState::DropPending;
209            } else {
210                self.state = DndState::Dragging;
211            }
212            self.hover_surface = new_surface;
213        } else if let Some(sid) = new_surface {
214            self.events.push(DndEvent::Motion {
215                surface_id: sid,
216                x,
217                y,
218            });
219        }
220
221        Ok(())
222    }
223
224    /// Perform a drop at the current position.
225    pub fn drop_action(&mut self) -> Result<DndEvent, DndError> {
226        if self.state != DndState::DropPending {
227            return Err(DndError::NotDragging);
228        }
229
230        let surface_id = self.hover_surface.ok_or(DndError::InvalidSurface)?;
231        let source = self.source.as_ref().ok_or(DndError::NotDragging)?;
232
233        // Check MIME compatibility.
234        let target = self
235            .targets
236            .iter()
237            .find(|t| t.surface_id == surface_id)
238            .ok_or(DndError::InvalidSurface)?;
239
240        let has_match = source
241            .offered_mimes
242            .iter()
243            .any(|m| target.accepted_mimes.contains(m));
244
245        if !has_match {
246            self.cancel();
247            return Err(DndError::NoMimeMatch);
248        }
249
250        let event = DndEvent::Drop {
251            surface_id,
252            x: self.cursor_x,
253            y: self.cursor_y,
254        };
255        self.events.push(event);
256
257        self.state = DndState::Transferring;
258
259        Ok(event)
260    }
261
262    /// Cancel the current drag operation.
263    pub fn cancel(&mut self) {
264        if let Some(sid) = self.hover_surface.take() {
265            self.events.push(DndEvent::Leave { surface_id: sid });
266        }
267        self.events.push(DndEvent::Cancelled);
268        self.source = None;
269        self.state = DndState::Idle;
270    }
271
272    /// Complete the data transfer (called after successful drop).
273    pub fn finish_transfer(&mut self) {
274        self.source = None;
275        self.hover_surface = None;
276        self.state = DndState::Idle;
277    }
278
279    /// Register a drop target.
280    pub fn register_target(&mut self, target: DropTarget) {
281        // Remove existing target with same surface ID.
282        self.targets.retain(|t| t.surface_id != target.surface_id);
283        self.targets.push(target);
284    }
285
286    /// Unregister a drop target.
287    pub fn unregister_target(&mut self, surface_id: u32) {
288        self.targets.retain(|t| t.surface_id != surface_id);
289    }
290
291    /// Get current DnD state.
292    pub fn state(&self) -> DndState {
293        self.state
294    }
295
296    /// Get cursor position during drag.
297    pub fn cursor_position(&self) -> (i32, i32) {
298        (self.cursor_x, self.cursor_y)
299    }
300
301    /// Get the active drag source info.
302    pub fn source(&self) -> Option<&DragSource> {
303        self.source.as_ref()
304    }
305
306    /// Get the ghost image position (centered on cursor).
307    pub fn ghost_position(&self) -> Option<(i32, i32, u32, u32)> {
308        self.source.as_ref().map(|s| {
309            (
310                self.cursor_x - (s.ghost_width as i32 / 2),
311                self.cursor_y - (s.ghost_height as i32 / 2),
312                s.ghost_width,
313                s.ghost_height,
314            )
315        })
316    }
317
318    /// Drain pending events.
319    pub fn drain_events(&mut self) -> Vec<DndEvent> {
320        core::mem::take(&mut self.events)
321    }
322
323    /// Find the best matching MIME between source and a specific target.
324    pub fn negotiate_mime(&self, surface_id: u32) -> Option<ClipboardMime> {
325        let source = self.source.as_ref()?;
326        let target = self.targets.iter().find(|t| t.surface_id == surface_id)?;
327        source
328            .offered_mimes
329            .iter()
330            .find(|m| target.accepted_mimes.contains(m))
331            .copied()
332    }
333}