smithay_clipboard/dnd/
mod.rs

1use std::ffi::c_void;
2use std::fmt::Debug;
3use std::sync::mpsc::SendError;
4
5use sctk::reexports::calloop;
6use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
7use sctk::reexports::client::protocol::wl_surface::WlSurface;
8use sctk::reexports::client::{Connection, Proxy};
9use wayland_backend::client::{InvalidId, ObjectId};
10
11use crate::mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
12use crate::Clipboard;
13
14pub mod state;
15
16#[derive(Clone)]
17pub struct DndSurface<T> {
18    pub(crate) surface: WlSurface,
19    pub s: T,
20}
21
22impl<T> Debug for DndSurface<T> {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("Surface").field("surface", &self.surface).finish()
25    }
26}
27
28impl<T: RawSurface> DndSurface<T> {
29    fn new(mut s: T, conn: &Connection) -> Result<Self, InvalidId> {
30        let ptr = unsafe { s.get_ptr() };
31        let id = unsafe { ObjectId::from_ptr(WlSurface::interface(), ptr.cast())? };
32        let surface = WlSurface::from_id(conn, id)?;
33        Ok(Self { s, surface })
34    }
35}
36
37#[cfg(feature = "rwh-6")]
38impl<'a> RawSurface for raw_window_handle::WindowHandle<'a> {
39    unsafe fn get_ptr(&mut self) -> *mut c_void {
40        match self.as_raw() {
41            raw_window_handle::RawWindowHandle::Wayland(handle) => handle.surface.as_ptr().cast(),
42            _ => panic!("Unsupported window handle type."),
43        }
44    }
45}
46
47impl RawSurface for WlSurface {
48    unsafe fn get_ptr(&mut self) -> *mut c_void {
49        self.id().as_ptr().cast()
50    }
51}
52
53pub trait RawSurface {
54    /// # Safety
55    ///
56    /// returned pointer must be a valid `*mut wl_surface` pointer, and it must
57    /// remain valid for as long as `RawSurface` object is alive.
58    unsafe fn get_ptr(&mut self) -> *mut c_void;
59}
60
61pub trait Sender<T> {
62    /// Send an event in the channel
63    fn send(&self, t: DndEvent<T>) -> Result<(), SendError<DndEvent<T>>>;
64}
65
66#[derive(Debug)]
67pub enum SourceEvent {
68    /// DnD operation ended.
69    Finished,
70    /// DnD Cancelled.
71    Cancelled,
72    /// DnD action chosen by the compositor.
73    Action(DndAction),
74    /// Mime accepted by destination.
75    /// If [`None`], no mime types are accepted.
76    Mime(Option<MimeType>),
77    /// DnD Dropped. The operation is still ongoing until receiving a
78    /// [`SourceEvent::Finished`] event.
79    Dropped,
80}
81
82#[derive(Debug)]
83pub enum OfferEvent<T> {
84    Enter {
85        x: f64,
86        y: f64,
87        mime_types: Vec<MimeType>,
88        surface: T,
89    },
90    Motion {
91        x: f64,
92        y: f64,
93    },
94    /// The offer is no longer on a DnD destination.
95    LeaveDestination,
96    /// The offer has left the surface.
97    Leave,
98    /// An offer was dropped
99    Drop,
100    /// If the selected action is ASK, the user must be presented with a choice.
101    /// [`Clipboard::set_action`] should then be called before data can be
102    /// requested and th DnD operation can be finished.
103    SelectedAction(DndAction),
104    Data {
105        data: Vec<u8>,
106        mime_type: MimeType,
107    },
108}
109
110/// A rectangle with a logical location and size relative to a [`DndSurface`]
111#[derive(Debug, Default, Clone)]
112pub struct Rectangle {
113    pub x: f64,
114    pub y: f64,
115    pub width: f64,
116    pub height: f64,
117}
118
119impl Rectangle {
120    fn contains(&self, x: f64, y: f64) -> bool {
121        self.x <= x && self.x + self.width >= x && self.y <= y && self.y + self.height >= y
122    }
123}
124
125#[derive(Debug, Clone)]
126pub struct DndDestinationRectangle {
127    /// A unique ID
128    pub id: u128,
129    /// The rectangle representing this destination.
130    pub rectangle: Rectangle,
131    /// Accepted mime types in this rectangle
132    pub mime_types: Vec<MimeType>,
133    /// Accepted actions in this rectangle
134    pub actions: DndAction,
135    /// Prefered action in this rectangle
136    pub preferred: DndAction,
137}
138
139pub enum DndRequest<T> {
140    /// Init DnD
141    InitDnd(Box<dyn crate::dnd::Sender<T> + Send>),
142    /// Register a surface for receiving Dnd events.
143    Surface(DndSurface<T>, Vec<DndDestinationRectangle>),
144    /// Start a Dnd operation with the given source surface and data.
145    StartDnd {
146        internal: bool,
147        source: DndSurface<T>,
148        icon: Option<Icon<DndSurface<T>>>,
149        content: Box<dyn AsMimeTypes + Send>,
150        actions: DndAction,
151    },
152    /// Peek the data of an active DnD offer
153    Peek(MimeType),
154    /// Set the DnD action chosen by the user.
155    SetAction(DndAction),
156    /// End an active DnD Source
157    DndEnd,
158}
159
160#[derive(Debug)]
161pub enum DndEvent<T> {
162    /// Dnd Offer event with the corresponding destination rectangle ID.
163    Offer(Option<u128>, OfferEvent<T>),
164    /// Dnd Source event.
165    Source(SourceEvent),
166}
167
168impl<T> Sender<T> for calloop::channel::Sender<DndEvent<T>> {
169    fn send(&self, t: DndEvent<T>) -> Result<(), SendError<DndEvent<T>>> {
170        self.send(t)
171    }
172}
173
174impl<T> Sender<T> for calloop::channel::SyncSender<DndEvent<T>> {
175    fn send(&self, t: DndEvent<T>) -> Result<(), SendError<DndEvent<T>>> {
176        self.send(t)
177    }
178}
179
180pub enum Icon<S> {
181    Surface(S),
182    /// Argb8888 or Xrgb8888 encoded image data pre-multiplied by alpha.
183    Buf {
184        width: u32,
185        height: u32,
186        data: Vec<u8>,
187        transparent: bool,
188    },
189}
190
191impl<T: RawSurface> Clipboard<T> {
192    /// Set up DnD operations for the Clipboard
193    pub fn init_dnd(
194        &self,
195        tx: Box<dyn Sender<T> + Send>,
196    ) -> Result<(), SendError<crate::worker::Command<T>>> {
197        self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::InitDnd(tx)))
198    }
199
200    /// Start a DnD operation on the given surface with some data
201    pub fn start_dnd<D: AsMimeTypes + Send + 'static>(
202        &self,
203        internal: bool,
204        source_surface: T,
205        icon_surface: Option<Icon<T>>,
206        content: D,
207        actions: DndAction,
208    ) {
209        let source = DndSurface::new(source_surface, &self.connection).unwrap();
210        let icon = icon_surface.map(|i| match i {
211            Icon::Surface(s) => Icon::Surface(DndSurface::new(s, &self.connection).unwrap()),
212            Icon::Buf { width, height, data, transparent } => {
213                Icon::Buf { width, height, data, transparent }
214            },
215        });
216        _ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::StartDnd {
217            internal,
218            source,
219            icon,
220            content: Box::new(content),
221            actions,
222        }));
223    }
224
225    /// End the current DnD operation, if there is one
226    pub fn end_dnd(&self) {
227        _ = self.request_sender.send(crate::worker::Command::DndRequest(DndRequest::DndEnd));
228    }
229
230    /// Register a surface for receiving DnD offers
231    /// Rectangles should be provided in order of decreasing priority.
232    /// This method c~an be called multiple time for a single surface if the
233    /// rectangles change.
234    pub fn register_dnd_destination(&self, surface: T, rectangles: Vec<DndDestinationRectangle>) {
235        let s = DndSurface::new(surface, &self.connection).unwrap();
236
237        _ = self
238            .request_sender
239            .send(crate::worker::Command::DndRequest(DndRequest::Surface(s, rectangles)));
240    }
241
242    /// Set the final action after presenting the user with a choice
243    pub fn set_action(&self, action: DndAction) {
244        _ = self
245            .request_sender
246            .send(crate::worker::Command::DndRequest(DndRequest::SetAction(action)));
247    }
248
249    /// Peek at the contents of a DnD offer
250    pub fn peek_offer<D: AllowedMimeTypes + 'static>(
251        &self,
252        mime_type: Option<MimeType>,
253    ) -> std::io::Result<D> {
254        let Some(mime_type) = mime_type.or_else(|| D::allowed().first().cloned()) else {
255            return Err(std::io::Error::new(std::io::ErrorKind::Other, "No mime type provided."));
256        };
257
258        self.request_sender
259            .send(crate::worker::Command::DndRequest(DndRequest::Peek(mime_type)))
260            .map_err(|_| {
261            std::io::Error::new(std::io::ErrorKind::Other, "Failed to send Peek request.")
262        })?;
263
264        self.request_receiver
265            .recv()
266            .map_err(|_| {
267                std::io::Error::new(std::io::ErrorKind::Other, "Failed to receive data request.")
268            })
269            .and_then(|ret| {
270                D::try_from(ret?).map_err(|_| {
271                    std::io::Error::new(
272                        std::io::ErrorKind::Other,
273                        "Failed to convert data to requested type.",
274                    )
275                })
276            })
277    }
278}