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>);