git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/backend/dnd.rs (9.6K)

  1 // Drag-and-drop DESTINATION support (wl_data_device).
  2 //
  3 // Opt-in per client: `Application::drop_mimes` returns the mime types the app
  4 // will take, in preference order, and `Application::handle_drop` receives the
  5 // bytes once the source has written them. Both have defaults, so a client that
  6 // implements neither behaves exactly as it did before this module existed —
  7 // it never even accepts an offer, so the drag reads as "not droppable here".
  8 //
  9 // The pipe read runs OFF the main loop on purpose. The source writes into a
 10 // pipe and can be slow (a browser serialising a large image), while the
 11 // engine's calloop loop also drives rendering: reading inline stalls frames,
 12 // and against a source that fills the pipe buffer and waits for us to drain
 13 // it, deadlocks outright. The reader thread posts the finished payload back
 14 // through a calloop channel, so `handle_drop` still runs on the main loop like
 15 // every other Application hook.
 16 
 17 use std::io::Read;
 18 
 19 use smithay_client_toolkit::data_device_manager::{
 20     data_device::{DataDeviceData, DataDeviceHandler},
 21     data_offer::{DataOfferHandler, DragOffer},
 22     data_source::DataSourceHandler,
 23     WritePipe,
 24 };
 25 use smithay_client_toolkit::delegate_data_device;
 26 use smithay_client_toolkit::reexports::client::protocol::{
 27     wl_data_device::WlDataDevice, wl_data_device_manager::DndAction,
 28     wl_data_source::WlDataSource, wl_surface::WlSurface,
 29 };
 30 use smithay_client_toolkit::reexports::client::{Connection, Proxy, QueueHandle};
 31 
 32 use super::window_runner::{Application, EngineState, LogicalPosition};
 33 
 34 /// One completed drop, handed from the reader thread back to the main loop.
 35 pub struct DroppedData {
 36     pub mime: String,
 37     pub bytes: Vec<u8>,
 38     pub pos: LogicalPosition,
 39 }
 40 
 41 /// Bytes we refuse to accumulate from a single drop. A drop is a pasted
 42 /// image or a URL list, not a disk image; without a ceiling a hostile or
 43 /// broken source can grow the reader thread's buffer without bound.
 44 const MAX_DROP_BYTES: usize = 64 * 1024 * 1024;
 45 
 46 impl<A: Application> EngineState<A> {
 47     /// Create this seat's data device unless it already has one. Drops arrive
 48     /// on the seat carrying the drag, and the device has to exist *before* the
 49     /// drag starts to be offered anything, so both seat hooks call this.
 50     pub(crate) fn ensure_data_device(
 51         &mut self,
 52         qh: &QueueHandle<Self>,
 53         seat: &smithay_client_toolkit::reexports::client::protocol::wl_seat::WlSeat,
 54     ) {
 55         if self.data_devices.iter().any(|d| d.data().seat() == seat) {
 56             return;
 57         }
 58         if let Some(manager) = self.data_device_manager.as_ref() {
 59             self.data_devices.push(manager.get_data_device(qh, seat));
 60         }
 61     }
 62 
 63     /// Surface-local logical coordinates for a drag position, matching the
 64     /// transform the pointer path applies (see `handle_pointer_event`).
 65     fn drag_logical(&self, x: f64, y: f64) -> LogicalPosition {
 66         let forced = crate::scale::forced_scale().unwrap_or(1.0);
 67         LogicalPosition::new(x as f32 / forced, y as f32 / forced)
 68     }
 69 
 70     /// The first mime type the app asked for that this offer actually
 71     /// carries. Preference order is the APP's, not the source's — a browser
 72     /// lists `text/html` before `text/uri-list`, and which of those is more
 73     /// useful is the app's call.
 74     fn preferred_mime(&self, offer: &DragOffer) -> Option<String> {
 75         let wanted = self.inner.as_ref()?.drop_mimes();
 76         if wanted.is_empty() {
 77             return None;
 78         }
 79         offer.with_mime_types(|offered| {
 80             wanted
 81                 .iter()
 82                 .find(|w| offered.iter().any(|o| o == *w))
 83                 .map(|w| w.to_string())
 84         })
 85     }
 86 
 87     /// Accept (or explicitly decline) the offer and mirror that in the DnD
 88     /// action, which is what drives the source's cursor feedback: declining
 89     /// with `None` + no action is what makes a browser show "can't drop
 90     /// here" over a client that doesn't want the payload.
 91     fn negotiate_drag(&mut self, offer: &DragOffer, serial: u32) {
 92         let mime = self.preferred_mime(offer);
 93         offer.accept_mime_type(serial, mime.clone());
 94         if mime.is_some() {
 95             offer.set_actions(DndAction::Copy, DndAction::Copy);
 96         } else {
 97             offer.set_actions(DndAction::empty(), DndAction::empty());
 98         }
 99         self.drag_mime = mime;
100     }
101 }
102 
103 impl<A: Application> DataDeviceHandler for EngineState<A> {
104     fn enter(
105         &mut self,
106         _conn: &Connection,
107         _qh: &QueueHandle<Self>,
108         data_device: &WlDataDevice,
109         x: f64,
110         y: f64,
111         _wl_surface: &WlSurface,
112     ) {
113         let Some(offer) = data_device.data::<DataDeviceData>().and_then(|d| d.drag_offer())
114         else {
115             return;
116         };
117         self.drag_pos = self.drag_logical(x, y);
118         let serial = offer.serial;
119         offer.with_mime_types(|m| log::debug!("[dnd] enter, offered: {m:?}"));
120         self.negotiate_drag(&offer, serial);
121         log::debug!("[dnd] accepted mime: {:?}", self.drag_mime);
122     }
123 
124     fn leave(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _data_device: &WlDataDevice) {
125         self.drag_mime = None;
126     }
127 
128     fn motion(
129         &mut self,
130         _conn: &Connection,
131         _qh: &QueueHandle<Self>,
132         data_device: &WlDataDevice,
133         x: f64,
134         y: f64,
135     ) {
136         self.drag_pos = self.drag_logical(x, y);
137         // Re-accept on motion: a source may add mime types mid-drag, and the
138         // action has to be restated or the compositor can clear it.
139         if let Some(offer) = data_device.data::<DataDeviceData>().and_then(|d| d.drag_offer()) {
140             let serial = offer.serial;
141             self.negotiate_drag(&offer, serial);
142         }
143     }
144 
145     /// Clipboard offers are not consumed here — the toolkit has no paste
146     /// path yet. Ignoring the event leaves the offer to SCTK's own cleanup.
147     fn selection(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _dd: &WlDataDevice) {}
148 
149     fn drop_performed(
150         &mut self,
151         _conn: &Connection,
152         _qh: &QueueHandle<Self>,
153         data_device: &WlDataDevice,
154     ) {
155         log::debug!("[dnd] drop_performed, mime={:?}", self.drag_mime);
156         let Some(mime) = self.drag_mime.clone() else { return };
157         let Some(tx) = self.drop_tx.clone() else { return };
158         let Some(offer) = data_device.data::<DataDeviceData>().and_then(|d| d.drag_offer())
159         else {
160             return;
161         };
162         let pipe = match offer.receive(mime.clone()) {
163             Ok(pipe) => pipe,
164             Err(e) => {
165                 log::warn!("[dnd] receive({mime}) failed: {e}");
166                 return;
167             }
168         };
169         let pos = self.drag_pos;
170         // `finish` is deliberately NOT sent here. It tells the source the
171         // operation is complete, and a source is entitled to tear down the
172         // moment it arrives — Firefox does, before it has written a byte, so
173         // finishing before the read drains an empty pipe. The offer is parked
174         // instead and finished once the reader thread reports EOF (see the
175         // drop-channel handler in the engine's `run`).
176         self.pending_drop_offer = Some(offer);
177         std::thread::spawn(move || {
178             let mut pipe = pipe;
179             let mut buf = Vec::new();
180             let mut chunk = [0u8; 16 * 1024];
181             loop {
182                 match pipe.read(&mut chunk) {
183                     Ok(0) => break,
184                     Ok(n) => {
185                         if buf.len() + n > MAX_DROP_BYTES {
186                             log::warn!("[dnd] drop exceeded {MAX_DROP_BYTES} bytes, truncating");
187                             break;
188                         }
189                         buf.extend_from_slice(&chunk[..n]);
190                     }
191                     Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
192                     Err(e) => {
193                         log::warn!("[dnd] read failed: {e}");
194                         return;
195                     }
196                 }
197             }
198             log::debug!("[dnd] read {} bytes for {mime}", buf.len());
199             let _ = tx.send(DroppedData { mime, bytes: buf, pos });
200         });
201         self.drag_mime = None;
202     }
203 }
204 
205 impl<A: Application> DataOfferHandler for EngineState<A> {
206     fn source_actions(
207         &mut self,
208         _conn: &Connection,
209         _qh: &QueueHandle<Self>,
210         offer: &mut DragOffer,
211         _actions: DndAction,
212     ) {
213         if self.drag_mime.is_some() {
214             offer.set_actions(DndAction::Copy, DndAction::Copy);
215         }
216     }
217 
218     fn selected_action(
219         &mut self,
220         _conn: &Connection,
221         _qh: &QueueHandle<Self>,
222         _offer: &mut DragOffer,
223         _actions: DndAction,
224     ) {
225     }
226 }
227 
228 /// Source-side events. The toolkit never creates a `WlDataSource`, so none of
229 /// these can fire; the impl exists because `delegate_data_device!` dispatches
230 /// all three protocol objects through one type.
231 impl<A: Application> DataSourceHandler for EngineState<A> {
232     fn accept_mime(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: Option<String>) {}
233     fn send_request(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: String, _: WritePipe) {}
234     fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
235     fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
236     fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
237     fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: DndAction) {}
238 }
239 
240 delegate_data_device!(@<A: Application> EngineState<A>);