| ... | ... | @@ -0,0 +1,567 @@ |
| 1 | //! The clipboard on Linux. On Wayland it lives on winit's own connection, so other apps' |
| 2 | //! paste requests are answered on the event loop as they arrive, with no thread of its own; |
| 3 | //! text, pages, pictures and files are all read from the selection there. On X11, arboard's. |
| 4 | |
| 5 | use std::{ |
| 6 | error::Error, |
| 7 | io::Read, |
| 8 | os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}, |
| 9 | path::PathBuf, |
| 10 | sync::{Arc, Mutex}, |
| 11 | task::{Context, Wake, Waker}, |
| 12 | time::{Duration, Instant}, |
| 13 | }; |
| 14 | use wayland_client::{ |
| 15 | Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, |
| 16 | backend::{Backend, ObjectData, ObjectId, protocol::Message}, |
| 17 | protocol::{ |
| 18 | wl_data_device::{self, WlDataDevice}, |
| 19 | wl_data_device_manager::WlDataDeviceManager, |
| 20 | wl_data_offer::{self, WlDataOffer}, |
| 21 | wl_data_source::{self, WlDataSource}, |
| 22 | wl_keyboard::{self, WlKeyboard}, |
| 23 | wl_pointer::{self, WlPointer}, |
| 24 | wl_registry::{self, WlRegistry}, |
| 25 | wl_seat::{self, WlSeat}, |
| 26 | wl_surface::{self, WlSurface}, |
| 27 | }, |
| 28 | }; |
| 29 | use wayland_sys::{ |
| 30 | client::{wayland_client_handle, wl_event_queue, wl_proxy}, |
| 31 | ffi_dispatch, |
| 32 | }; |
| 33 | use winit::{ |
| 34 | raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}, |
| 35 | window::Window, |
| 36 | }; |
| 37 | |
| 38 | /// The MIME types text is offered and read as, the most precise first. |
| 39 | const TEXT: [&str; 4] = [ |
| 40 | "text/plain;charset=utf-8", |
| 41 | "UTF8_STRING", |
| 42 | "text/plain", |
| 43 | "STRING", |
| 44 | ]; |
| 45 | /// Snowbound's own format, [`crate::paste::Copied::clip`]. |
| 46 | const CLIP: &str = "application/x-snowbound-clip"; |
| 47 | /// How long a paste waits on the app that copied. |
| 48 | const PATIENCE: Duration = Duration::from_secs(2); |
| 49 | |
| 50 | /// The HTML and Snowbound's own format of this process's last copy through X11. |
| 51 | static LAST_COPY: Mutex<Option<(String, String)>> = Mutex::new(None); |
| 52 | |
| 53 | pub enum Clipboard { |
| 54 | Wayland(Box<Wayland>), |
| 55 | X11(arboard::Clipboard), |
| 56 | } |
| 57 | |
| 58 | impl Clipboard { |
| 59 | pub fn new(window: &Window) -> Result<Self, arboard::Error> { |
| 60 | let handles = window |
| 61 | .display_handle() |
| 62 | .ok() |
| 63 | .zip(window.window_handle().ok()); |
| 64 | match handles.map(|(display, window)| (display.as_raw(), window.as_raw())) { |
| 65 | Some((RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(window))) => Ok( |
| 66 | Self::Wayland(Box::new(Wayland::new(display.display, window.surface))), |
| 67 | ), |
| 68 | _ => arboard::Clipboard::new().map(Self::X11), |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | pub fn set_text(&mut self, text: String) -> Result<(), Box<dyn Error>> { |
| 73 | match self { |
| 74 | Self::Wayland(wayland) => { |
| 75 | wayland.copy(TEXT.map(|kind| (kind, text.clone().into_bytes())).to_vec()) |
| 76 | } |
| 77 | Self::X11(clipboard) => Ok(clipboard.set_text(text)?), |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /// Text, HTML and Snowbound's own format, as one copy: see [`crate::paste::Copied`]. |
| 82 | pub fn set(&mut self, copied: &crate::paste::Copied) -> Result<(), Box<dyn Error>> { |
| 83 | match self { |
| 84 | Self::Wayland(wayland) => { |
| 85 | let text = TEXT.map(|kind| (kind, copied.text.clone().into_bytes())); |
| 86 | let rich = [("text/html", &copied.html), (CLIP, &copied.clip)] |
| 87 | .map(|(kind, bytes)| (kind, bytes.clone().into_bytes())); |
| 88 | wayland.copy(text.into_iter().chain(rich).collect()) |
| 89 | } |
| 90 | Self::X11(clipboard) => { |
| 91 | clipboard.set().html(&copied.html, Some(&copied.text))?; |
| 92 | *LAST_COPY.lock().unwrap() = Some((copied.html.clone(), copied.clip.clone())); |
| 93 | Ok(()) |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | /// What Snowbound itself copied, if it did: on X11, which arboard offers no format of |
| 99 | /// Snowbound's own through, the last copy's, while the clipboard holds its HTML. |
| 100 | pub fn get_clip(&mut self) -> Option<String> { |
| 101 | if let Self::Wayland(wayland) = self { |
| 102 | return String::from_utf8(wayland.paste(CLIP)?).ok(); |
| 103 | } |
| 104 | let html = self.get_html()?; |
| 105 | match &*LAST_COPY.lock().unwrap() { |
| 106 | Some((copied, clip)) if *copied == html => Some(clip.clone()), |
| 107 | _ => None, |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | pub fn get_text(&mut self) -> Result<String, Box<dyn Error>> { |
| 112 | match self { |
| 113 | Self::Wayland(wayland) => { |
| 114 | let bytes = TEXT.iter().find_map(|kind| wayland.paste(kind)); |
| 115 | Ok(String::from_utf8_lossy(&bytes.unwrap_or_default()).into_owned()) |
| 116 | } |
| 117 | Self::X11(clipboard) => Ok(clipboard.get_text()?), |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | pub fn get_files(&mut self) -> Vec<PathBuf> { |
| 122 | use std::os::unix::ffi::OsStrExt; |
| 123 | match self { |
| 124 | Self::Wayland(wayland) => files(&wayland.paste("text/uri-list").unwrap_or_default()), |
| 125 | // arboard splits text/uri-list at LF and keeps the CR its CRLF lines end in. |
| 126 | Self::X11(clipboard) => (clipboard.get().file_list().unwrap_or_default().into_iter()) |
| 127 | .map(|path| { |
| 128 | let bytes = path.as_os_str().as_bytes(); |
| 129 | let bytes = bytes.strip_suffix(b"\r").unwrap_or(bytes); |
| 130 | std::ffi::OsStr::from_bytes(bytes).into() |
| 131 | }) |
| 132 | .collect(), |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | pub fn get_html(&mut self) -> Option<String> { |
| 137 | match self { |
| 138 | Self::Wayland(wayland) => wayland |
| 139 | .paste("text/html") |
| 140 | .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()), |
| 141 | Self::X11(clipboard) => clipboard.get().html().ok(), |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { |
| 146 | match self { |
| 147 | Self::Wayland(wayland) => ["image/png", "image/jpeg", "image/gif"] |
| 148 | .iter() |
| 149 | .find_map(|kind| wayland.paste(kind)), |
| 150 | Self::X11(clipboard) => crate::paste::bitmap(clipboard.get_image().ok()?), |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | /// Answers what the compositor asked of the clipboard since it last woke the event loop. |
| 155 | pub fn serve(&mut self) { |
| 156 | if let Self::Wayland(wayland) = self { |
| 157 | wayland.serve(); |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | /// The local files a `text/uri-list` names; its lines end in CRLF, though some apps end |
| 163 | /// them in LF alone. |
| 164 | fn files(list: &[u8]) -> Vec<PathBuf> { |
| 165 | use std::os::unix::ffi::OsStringExt; |
| 166 | String::from_utf8_lossy(list) |
| 167 | .lines() |
| 168 | .map(|line| line.trim_end_matches('\r')) |
| 169 | .filter(|line| !line.starts_with('#')) |
| 170 | .filter_map(|uri| { |
| 171 | let path = uri.strip_prefix("file://")?; |
| 172 | let path = path.strip_prefix("localhost").unwrap_or(path); |
| 173 | path.starts_with('/') |
| 174 | .then(|| std::ffi::OsString::from_vec(crate::paste::percent_decode(path)).into()) |
| 175 | }) |
| 176 | .collect() |
| 177 | } |
| 178 | |
| 179 | /// A copy's bytes in each MIME type it is offered as. |
| 180 | type Formats = Vec<(&'static str, Vec<u8>)>; |
| 181 | |
| 182 | /// The Wayland clipboard, on winit's connection once the window's first frame lends it. |
| 183 | pub struct Wayland { |
| 184 | lent: Arc<Mutex<Option<Backend>>>, |
| 185 | joined: Option<(Connection, EventQueue<Seat>, Seat)>, |
| 186 | } |
| 187 | |
| 188 | /// What the clipboard knows of the seat: the selection and the serial to claim it with. |
| 189 | #[derive(Default)] |
| 190 | struct Seat { |
| 191 | seat: Option<WlSeat>, |
| 192 | manager: Option<WlDataDeviceManager>, |
| 193 | device: Option<WlDataDevice>, |
| 194 | keyboard: Option<WlKeyboard>, |
| 195 | pointer: Option<WlPointer>, |
| 196 | /// The latest input event's, which claiming the selection must name. |
| 197 | serial: u32, |
| 198 | /// The selection another app offers. |
| 199 | offer: Option<WlDataOffer>, |
| 200 | /// What this app copied, in each format it offers, while it holds the selection. |
| 201 | copied: Option<(WlDataSource, Formats)>, |
| 202 | } |
| 203 | |
| 204 | /// Wakes the event loop to serve the clipboard. |
| 205 | struct Serve; |
| 206 | |
| 207 | impl Wake for Serve { |
| 208 | fn wake(self: Arc<Self>) { |
| 209 | if let Some(proxy) = crate::platform::PROXY.get() { |
| 210 | let _ = proxy.send_event(crate::UserEvent::Clipboard); |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | /// Takes winit's connection from the event that reaches it, a frame callback moved onto |
| 216 | /// winit's queue: winit dispatches only that queue, with its own connection. |
| 217 | struct Lend(Arc<Mutex<Option<Backend>>>); |
| 218 | |
| 219 | impl ObjectData for Lend { |
| 220 | fn event( |
| 221 | self: Arc<Self>, |
| 222 | backend: &Backend, |
| 223 | _: Message<ObjectId, OwnedFd>, |
| 224 | ) -> Option<Arc<dyn ObjectData>> { |
| 225 | *self.0.lock().unwrap() = Some(backend.clone()); |
| 226 | Arc::new(Serve).wake(); |
| 227 | None |
| 228 | } |
| 229 | |
| 230 | fn destroyed(&self, _: ObjectId) {} |
| 231 | } |
| 232 | |
| 233 | impl Wayland { |
| 234 | fn new( |
| 235 | display: std::ptr::NonNull<std::ffi::c_void>, |
| 236 | surface: std::ptr::NonNull<std::ffi::c_void>, |
| 237 | ) -> Self { |
| 238 | let lent = Arc::new(Mutex::new(None)); |
| 239 | // Safety: winit's display and surface outlive the window, which outlives this. |
| 240 | let backend = unsafe { Backend::from_foreign_display(display.as_ptr().cast()) }; |
| 241 | let connection = Connection::from_backend(backend); |
| 242 | let surface = |
| 243 | unsafe { ObjectId::from_ptr(WlSurface::interface(), surface.as_ptr().cast()) } |
| 244 | .and_then(|id| WlSurface::from_id(&connection, id)); |
| 245 | if let Ok(surface) = surface { |
| 246 | let lend: Arc<dyn ObjectData> = Arc::new(Lend(Arc::clone(&lent))); |
| 247 | let frame = wl_surface::Request::Frame {}; |
| 248 | if let Ok(callback) = connection.send_request(&surface, frame, Some(lend)) { |
| 249 | // Safety: both proxies are live, and the callback has had no event yet. |
| 250 | unsafe { onto_queue_of(callback.as_ptr(), surface.id().as_ptr()) }; |
| 251 | } |
| 252 | let _ = connection.flush(); |
| 253 | } |
| 254 | // The callback, on winit's queue, ends under winit's connection, which leaves this one |
| 255 | // a record of it that dropping it would free a second time. |
| 256 | std::mem::forget(connection); |
| 257 | Self { lent, joined: None } |
| 258 | } |
| 259 | |
| 260 | /// Dispatches the seat's events and asks to be woken for the next; first joins winit's |
| 261 | /// connection once lent. |
| 262 | fn serve(&mut self) { |
| 263 | if self.joined.is_none() |
| 264 | && let Some(backend) = self.lent.lock().unwrap().take() |
| 265 | { |
| 266 | let connection = Connection::from_backend(backend); |
| 267 | let queue = connection.new_event_queue(); |
| 268 | connection.display().get_registry(&queue.handle(), ()); |
| 269 | self.joined = Some((connection, queue, Seat::default())); |
| 270 | } |
| 271 | if let Some((_, queue, seat)) = &mut self.joined { |
| 272 | let waker = Waker::from(Arc::new(Serve)); |
| 273 | if let std::task::Poll::Ready(Err(error)) = |
| 274 | queue.poll_dispatch_pending(&mut Context::from_waker(&waker), seat) |
| 275 | { |
| 276 | eprintln!("The clipboard stopped: {error}"); |
| 277 | self.joined = None; |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | fn copy(&mut self, formats: Formats) -> Result<(), Box<dyn Error>> { |
| 283 | self.serve(); |
| 284 | let Some((connection, queue, seat)) = &mut self.joined else { |
| 285 | return Err("The clipboard isn't ready yet.".into()); |
| 286 | }; |
| 287 | let (Some(manager), Some(device)) = (&seat.manager, &seat.device) else { |
| 288 | return Err("The compositor offers no clipboard.".into()); |
| 289 | }; |
| 290 | let source = manager.create_data_source(&queue.handle(), ()); |
| 291 | for (kind, _) in &formats { |
| 292 | source.offer(kind.to_string()); |
| 293 | } |
| 294 | device.set_selection(Some(&source), seat.serial); |
| 295 | // The source replaced is cancelled, and destroyed then. |
| 296 | seat.copied = Some((source, formats)); |
| 297 | connection.flush()?; |
| 298 | Ok(()) |
| 299 | } |
| 300 | |
| 301 | /// The selection as `kind`, where it is offered as that: this app's own copy without a |
| 302 | /// round trip through the compositor, or what another app answers. |
| 303 | fn paste(&mut self, kind: &str) -> Option<Vec<u8>> { |
| 304 | self.serve(); |
| 305 | let (connection, _, seat) = self.joined.as_ref()?; |
| 306 | if let Some((_, formats)) = &seat.copied { |
| 307 | return offered(formats, kind).map(<[u8]>::to_vec); |
| 308 | } |
| 309 | let offer = seat.offer.as_ref()?; |
| 310 | let kinds = offer.data::<Mutex<Vec<String>>>()?; |
| 311 | if !kinds.lock().unwrap().iter().any(|offered| offered == kind) { |
| 312 | return None; |
| 313 | } |
| 314 | let (mut reader, writer) = std::io::pipe().ok()?; |
| 315 | offer.receive(kind.to_owned(), writer.as_fd()); |
| 316 | drop(writer); |
| 317 | connection.flush().ok()?; |
| 318 | let deadline = Instant::now() + PATIENCE; |
| 319 | let mut bytes = Vec::new(); |
| 320 | loop { |
| 321 | if !ready(reader.as_fd(), libc::POLLIN, deadline) { |
| 322 | eprintln!("The app that copied didn't answer the paste in time."); |
| 323 | return None; |
| 324 | } |
| 325 | let mut buffer = [0; 1 << 16]; |
| 326 | match reader.read(&mut buffer) { |
| 327 | Ok(0) => return Some(bytes), |
| 328 | Ok(read) => bytes.extend_from_slice(&buffer[..read]), |
| 329 | Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {} |
| 330 | Err(_) => return None, |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | /// Moves `proxy` onto `owner`'s event queue. libwayland names a proxy's queue only from 1.23 |
| 337 | /// (`wl_proxy_get_queue`), so this reads it from `struct wl_proxy`, laid out alike since 1.0. |
| 338 | unsafe fn onto_queue_of(proxy: *mut wl_proxy, owner: *mut wl_proxy) { |
| 339 | #[repr(C)] |
| 340 | struct Head { |
| 341 | interface: *const std::ffi::c_void, |
| 342 | implementation: *const std::ffi::c_void, |
| 343 | id: u32, |
| 344 | display: *mut std::ffi::c_void, |
| 345 | queue: *mut wl_event_queue, |
| 346 | } |
| 347 | let queue = unsafe { (*owner.cast::<Head>()).queue }; |
| 348 | unsafe { ffi_dispatch!(wayland_client_handle(), wl_proxy_set_queue, proxy, queue) }; |
| 349 | } |
| 350 | |
| 351 | /// The bytes of `formats` in the format `kind`. |
| 352 | fn offered<'a>(formats: &'a [(&str, Vec<u8>)], kind: &str) -> Option<&'a [u8]> { |
| 353 | let (_, bytes) = formats.iter().find(|(offered, _)| *offered == kind)?; |
| 354 | Some(bytes) |
| 355 | } |
| 356 | |
| 357 | /// Whether `fd` is ready for `events` before `deadline`. |
| 358 | fn ready(fd: BorrowedFd, events: i16, deadline: Instant) -> bool { |
| 359 | let left = deadline.saturating_duration_since(Instant::now()); |
| 360 | let mut poll = libc::pollfd { |
| 361 | fd: fd.as_raw_fd(), |
| 362 | events, |
| 363 | revents: 0, |
| 364 | }; |
| 365 | !left.is_zero() && unsafe { libc::poll(&mut poll, 1, left.as_millis() as i32) } > 0 |
| 366 | } |
| 367 | |
| 368 | /// Writes `bytes` to the pipe `fd`, waiting on a slow reader no longer than a paste waits. |
| 369 | fn answer(fd: OwnedFd, bytes: &[u8]) { |
| 370 | use std::io::Write; |
| 371 | let mut pipe = std::fs::File::from(fd); |
| 372 | let deadline = Instant::now() + PATIENCE; |
| 373 | // A pipe ready for writing takes PIPE_BUF bytes without blocking. |
| 374 | for chunk in bytes.chunks(libc::PIPE_BUF) { |
| 375 | if !ready(pipe.as_fd(), libc::POLLOUT, deadline) || pipe.write_all(chunk).is_err() { |
| 376 | return; |
| 377 | } |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | impl Dispatch<WlRegistry, ()> for Seat { |
| 382 | fn event( |
| 383 | seat: &mut Self, |
| 384 | registry: &WlRegistry, |
| 385 | event: wl_registry::Event, |
| 386 | _: &(), |
| 387 | _: &Connection, |
| 388 | queue: &QueueHandle<Self>, |
| 389 | ) { |
| 390 | let wl_registry::Event::Global { |
| 391 | name, |
| 392 | interface, |
| 393 | version, |
| 394 | } = event |
| 395 | else { |
| 396 | return; |
| 397 | }; |
| 398 | match interface.as_str() { |
| 399 | "wl_seat" if seat.seat.is_none() => { |
| 400 | seat.seat = Some(registry.bind(name, version.min(5), queue, ())); |
| 401 | } |
| 402 | "wl_data_device_manager" if seat.manager.is_none() => { |
| 403 | seat.manager = Some(registry.bind(name, version.min(3), queue, ())); |
| 404 | } |
| 405 | _ => return, |
| 406 | } |
| 407 | if let (Some(manager), Some(wl_seat), None) = (&seat.manager, &seat.seat, &seat.device) { |
| 408 | seat.device = Some(manager.get_data_device(wl_seat, queue, ())); |
| 409 | } |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | impl Dispatch<WlSeat, ()> for Seat { |
| 414 | fn event( |
| 415 | seat: &mut Self, |
| 416 | wl_seat: &WlSeat, |
| 417 | event: wl_seat::Event, |
| 418 | _: &(), |
| 419 | _: &Connection, |
| 420 | queue: &QueueHandle<Self>, |
| 421 | ) { |
| 422 | let wl_seat::Event::Capabilities { |
| 423 | capabilities: WEnum::Value(capabilities), |
| 424 | } = event |
| 425 | else { |
| 426 | return; |
| 427 | }; |
| 428 | let keyboard = capabilities.contains(wl_seat::Capability::Keyboard); |
| 429 | if keyboard != seat.keyboard.is_some() { |
| 430 | seat.keyboard = keyboard.then(|| wl_seat.get_keyboard(queue, ())); |
| 431 | } |
| 432 | let pointer = capabilities.contains(wl_seat::Capability::Pointer); |
| 433 | if pointer != seat.pointer.is_some() { |
| 434 | seat.pointer = pointer.then(|| wl_seat.get_pointer(queue, ())); |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | impl Dispatch<WlKeyboard, ()> for Seat { |
| 440 | fn event( |
| 441 | seat: &mut Self, |
| 442 | _: &WlKeyboard, |
| 443 | event: wl_keyboard::Event, |
| 444 | _: &(), |
| 445 | _: &Connection, |
| 446 | _: &QueueHandle<Self>, |
| 447 | ) { |
| 448 | if let wl_keyboard::Event::Enter { serial, .. } | wl_keyboard::Event::Key { serial, .. } = |
| 449 | event |
| 450 | { |
| 451 | seat.serial = serial; |
| 452 | } |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | impl Dispatch<WlPointer, ()> for Seat { |
| 457 | fn event( |
| 458 | seat: &mut Self, |
| 459 | _: &WlPointer, |
| 460 | event: wl_pointer::Event, |
| 461 | _: &(), |
| 462 | _: &Connection, |
| 463 | _: &QueueHandle<Self>, |
| 464 | ) { |
| 465 | if let wl_pointer::Event::Enter { serial, .. } | wl_pointer::Event::Button { serial, .. } = |
| 466 | event |
| 467 | { |
| 468 | seat.serial = serial; |
| 469 | } |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | impl Dispatch<WlDataDeviceManager, ()> for Seat { |
| 474 | fn event( |
| 475 | _: &mut Self, |
| 476 | _: &WlDataDeviceManager, |
| 477 | _: <WlDataDeviceManager as Proxy>::Event, |
| 478 | _: &(), |
| 479 | _: &Connection, |
| 480 | _: &QueueHandle<Self>, |
| 481 | ) { |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | impl Dispatch<WlDataDevice, ()> for Seat { |
| 486 | fn event( |
| 487 | seat: &mut Self, |
| 488 | _: &WlDataDevice, |
| 489 | event: wl_data_device::Event, |
| 490 | _: &(), |
| 491 | _: &Connection, |
| 492 | _: &QueueHandle<Self>, |
| 493 | ) { |
| 494 | match event { |
| 495 | wl_data_device::Event::Selection { id } => { |
| 496 | if let Some(old) = std::mem::replace(&mut seat.offer, id) { |
| 497 | old.destroy(); |
| 498 | } |
| 499 | } |
| 500 | // Drags aren't taken. |
| 501 | wl_data_device::Event::Enter { |
| 502 | id: Some(offer), .. |
| 503 | } => offer.destroy(), |
| 504 | _ => {} |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | wayland_client::event_created_child!(Seat, WlDataDevice, [ |
| 509 | wl_data_device::EVT_DATA_OFFER_OPCODE => (WlDataOffer, Mutex::new(Vec::<String>::new())), |
| 510 | ]); |
| 511 | } |
| 512 | |
| 513 | impl Dispatch<WlDataOffer, Mutex<Vec<String>>> for Seat { |
| 514 | fn event( |
| 515 | _: &mut Self, |
| 516 | _: &WlDataOffer, |
| 517 | event: wl_data_offer::Event, |
| 518 | kinds: &Mutex<Vec<String>>, |
| 519 | _: &Connection, |
| 520 | _: &QueueHandle<Self>, |
| 521 | ) { |
| 522 | if let wl_data_offer::Event::Offer { mime_type } = event { |
| 523 | kinds.lock().unwrap().push(mime_type); |
| 524 | } |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | impl Dispatch<WlDataSource, ()> for Seat { |
| 529 | fn event( |
| 530 | seat: &mut Self, |
| 531 | source: &WlDataSource, |
| 532 | event: wl_data_source::Event, |
| 533 | _: &(), |
| 534 | _: &Connection, |
| 535 | _: &QueueHandle<Self>, |
| 536 | ) { |
| 537 | let copied = seat.copied.as_ref().filter(|(copied, _)| copied == source); |
| 538 | match event { |
| 539 | wl_data_source::Event::Send { mime_type, fd } => { |
| 540 | if let Some(bytes) = copied.and_then(|(_, formats)| offered(formats, &mime_type)) { |
| 541 | answer(fd, bytes); |
| 542 | } |
| 543 | } |
| 544 | wl_data_source::Event::Cancelled => { |
| 545 | if copied.is_some() { |
| 546 | seat.copied = None; |
| 547 | } |
| 548 | source.destroy(); |
| 549 | } |
| 550 | _ => {} |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | #[cfg(test)] |
| 556 | mod tests { |
| 557 | use super::*; |
| 558 | |
| 559 | #[test] |
| 560 | fn uri_lists_name_local_files_whatever_their_line_ends() { |
| 561 | let list = b"# copied\r\nfile:///home/ben/Notes%20One.one\r\nfile://localhost/tmp/a.png\nhttps://example.com/b.png\r\n"; |
| 562 | assert_eq!( |
| 563 | files(list), |
| 564 | ["/home/ben/Notes One.one", "/tmp/a.png"].map(PathBuf::from) |
| 565 | ); |
| 566 | } |
| 567 | } |