From d18db208ffd6dfa314048b4b3f252ef5911c4975 Mon Sep 17 00:00:00 2001 From: Francesca Sunshine <2096580+francesca64@users.noreply.github.com> Date: Wed, 13 Dec 2017 06:22:03 -0500 Subject: [PATCH] x11: Implement file drag and drop (#360) * x11: Implement file drag and drop * Fixed typo --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/lib.rs | 2 + src/platform/linux/x11/dnd.rs | 234 +++++++++++++++++++++++++++++++ src/platform/linux/x11/mod.rs | 153 +++++++++++++++++--- src/platform/linux/x11/util.rs | 107 ++++++++++++++ src/platform/linux/x11/window.rs | 18 +++ 7 files changed, 498 insertions(+), 18 deletions(-) create mode 100644 src/platform/linux/x11/dnd.rs create mode 100644 src/platform/linux/x11/util.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a32dba..aecd7151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - Add support for `Touch` for emscripten backend. +- Added support for `DroppedFile`, `HoveredFile`, and `HoveredFileCancelled` to X11 backend. # Version 0.9.0 (2017-12-01) diff --git a/Cargo.toml b/Cargo.toml index 27f9c794..86b52d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,4 @@ wayland-protocols = { version = "0.12.0", features = ["unstable_protocols"] } wayland-kbd = "0.13.0" wayland-window = "0.13.0" x11-dl = "2.8" +percent-encoding = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 34a7ed21..caa2d180 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,8 @@ extern crate core_foundation; extern crate core_graphics; #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] extern crate x11_dl; +#[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] +extern crate percent_encoding; #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd"))] #[macro_use] extern crate wayland_client; diff --git a/src/platform/linux/x11/dnd.rs b/src/platform/linux/x11/dnd.rs new file mode 100644 index 00000000..59278873 --- /dev/null +++ b/src/platform/linux/x11/dnd.rs @@ -0,0 +1,234 @@ +use std::io; +use std::sync::Arc; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; + +use libc::{c_char, c_int, c_long, c_uchar, c_ulong}; +use percent_encoding::percent_decode; + +use super::{ffi, util, XConnection, XError}; + +const DND_ATOMS_LEN: usize = 12; + +#[derive(Debug)] +pub struct DndAtoms { + pub aware: ffi::Atom, + pub enter: ffi::Atom, + pub leave: ffi::Atom, + pub drop: ffi::Atom, + pub position: ffi::Atom, + pub status: ffi::Atom, + pub action_private: ffi::Atom, + pub selection: ffi::Atom, + pub finished: ffi::Atom, + pub type_list: ffi::Atom, + pub uri_list: ffi::Atom, + pub none: ffi::Atom, +} + +impl DndAtoms { + pub fn new(xconn: &Arc) -> Result { + let mut atoms = Vec::with_capacity(DND_ATOMS_LEN); + + let mut names = [ + b"XdndAware\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndEnter\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndLeave\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndDrop\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndPosition\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndStatus\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndActionPrivate\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndSelection\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndFinished\0".to_owned().as_mut_ptr() as *mut c_char, + b"XdndTypeList\0".to_owned().as_mut_ptr() as *mut c_char, + b"text/uri-list\0".to_owned().as_mut_ptr() as *mut c_char, + b"None\0".to_owned().as_mut_ptr() as *mut c_char, + ]; + + unsafe { + (xconn.xlib.XInternAtoms)( + xconn.display, + names.as_mut_ptr(), + DND_ATOMS_LEN as c_int, + ffi::False, + atoms.as_mut_ptr(), + ); + } + xconn.check_errors()?; + unsafe { atoms.set_len(DND_ATOMS_LEN); } + Ok(DndAtoms { + aware: atoms[0], + enter: atoms[1], + leave: atoms[2], + drop: atoms[3], + position: atoms[4], + status: atoms[5], + action_private: atoms[6], + selection: atoms[7], + finished: atoms[8], + type_list: atoms[9], + uri_list: atoms[10], + none: atoms[11], + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum DndState { + Accepted, + Rejected, +} + +#[derive(Debug)] +pub enum DndDataParseError { + EmptyData, + InvalidUtf8(Utf8Error), + HostnameSpecified(String), + UnexpectedProtocol(String), + UnresolvablePath(io::Error), +} + +impl From for DndDataParseError { + fn from(e: Utf8Error) -> Self { + DndDataParseError::InvalidUtf8(e) + } +} + +impl From for DndDataParseError { + fn from(e: io::Error) -> Self { + DndDataParseError::UnresolvablePath(e) + } +} + +pub struct Dnd { + xconn: Arc, + pub atoms: DndAtoms, + // Populated by XdndEnter event handler + pub version: Option, + pub type_list: Option>, + // Populated by XdndPosition event handler + pub source_window: Option, + // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) + pub result: Option, DndDataParseError>>, +} + +impl Dnd { + pub fn new(xconn: Arc) -> Result { + let atoms = DndAtoms::new(&xconn)?; + Ok(Dnd { + xconn, + atoms, + version: None, + type_list: None, + source_window: None, + result: None, + }) + } + + pub fn reset(&mut self) { + self.version = None; + self.type_list = None; + self.source_window = None; + self.result = None; + } + + pub unsafe fn send_status( + &self, + this_window: c_ulong, + target_window: c_ulong, + state: DndState, + ) { + let (accepted, action) = match state { + DndState::Accepted => (1, self.atoms.action_private as c_long), + DndState::Rejected => (0, self.atoms.none as c_long), + }; + util::send_client_msg( + &self.xconn, + target_window, + self.atoms.status, + (this_window as c_long, accepted, 0, 0, action), + ); + } + + pub unsafe fn send_finished( + &self, + this_window: c_ulong, + target_window: c_ulong, + state: DndState, + ) { + let (accepted, action) = match state { + DndState::Accepted => (1, self.atoms.action_private as c_long), + DndState::Rejected => (0, self.atoms.none as c_long), + }; + util::send_client_msg( + &self.xconn, + target_window, + self.atoms.finished, + (this_window as c_long, accepted, action, 0, 0), + ); + } + + pub unsafe fn get_type_list( + &self, + source_window: c_ulong, + ) -> Result, util::GetPropertyError> { + util::get_property( + &self.xconn, + source_window, + self.atoms.type_list, + ffi::XA_ATOM, + ) + } + + pub unsafe fn convert_selection(&self, window: c_ulong, time: c_ulong) { + (self.xconn.xlib.XConvertSelection)( + self.xconn.display, + self.atoms.selection, + self.atoms.uri_list, + self.atoms.selection, + window, + time, + ); + } + + pub unsafe fn read_data( + &self, + window: c_ulong, + ) -> Result, util::GetPropertyError> { + util::get_property( + &self.xconn, + window, + self.atoms.selection, + self.atoms.uri_list, + ) + } + + pub fn parse_data(&self, data: &mut Vec) -> Result, DndDataParseError> { + if !data.is_empty() { + let mut path_list = Vec::new(); + let decoded = percent_decode(data).decode_utf8()?.into_owned(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // The format is specified as protocol://host/path + // However, it's typically simply protocol:///path + let path_str = if uri.starts_with("file://") { + let path_str = uri.replace("file://", ""); + if !path_str.starts_with('/') { + // A hostname is specified + // Supporting this case is beyond the scope of my mental health + return Err(DndDataParseError::HostnameSpecified(path_str)); + } + path_str + } else { + // Only the file protocol is supported + return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); + }; + + let path = Path::new(&path_str).canonicalize()?; + path_list.push(path); + } + Ok(path_list) + } else { + Err(DndDataParseError::EmptyData) + } + } +} diff --git a/src/platform/linux/x11/mod.rs b/src/platform/linux/x11/mod.rs index 1e13c6d6..2d73add2 100644 --- a/src/platform/linux/x11/mod.rs +++ b/src/platform/linux/x11/mod.rs @@ -16,12 +16,16 @@ use std::sync::atomic::{self, AtomicBool}; use std::collections::HashMap; use std::ffi::CStr; -use libc::{self, c_uchar, c_char, c_int}; +use libc::{self, c_uchar, c_char, c_int, c_ulong, c_long}; mod events; mod monitor; mod window; mod xdisplay; +mod dnd; +mod util; + +use self::dnd::{Dnd, DndState}; // API TRANSITION // @@ -33,6 +37,7 @@ mod xdisplay; pub struct EventsLoop { display: Arc, wm_delete_window: ffi::Atom, + dnd: Dnd, windows: Arc>>, devices: Mutex>, xi2ext: XExtension, @@ -55,6 +60,9 @@ impl EventsLoop { let wm_delete_window = unsafe { (display.xlib.XInternAtom)(display.display, b"WM_DELETE_WINDOW\0".as_ptr() as *const c_char, 0) }; display.check_errors().expect("Failed to call XInternAtom"); + let dnd = Dnd::new(Arc::clone(&display)) + .expect("Failed to call XInternAtoms when initializing drag and drop"); + let xi2ext = unsafe { let mut result = XExtension { opcode: mem::uninitialized(), @@ -93,13 +101,14 @@ impl EventsLoop { let result = EventsLoop { pending_wakeup: Arc::new(AtomicBool::new(false)), - display: display, - wm_delete_window: wm_delete_window, + display, + wm_delete_window, + dnd, windows: Arc::new(Mutex::new(HashMap::new())), devices: Mutex::new(HashMap::new()), - xi2ext: xi2ext, - root: root, - wakeup_dummy_window: wakeup_dummy_window, + xi2ext, + root, + wakeup_dummy_window, }; { @@ -138,19 +147,17 @@ impl EventsLoop { pub fn poll_events(&mut self, mut callback: F) where F: FnMut(Event) { - let xlib = &self.display.xlib; - let mut xev = unsafe { mem::uninitialized() }; loop { // Get next event unsafe { // Ensure XNextEvent won't block - let count = (xlib.XPending)(self.display.display); + let count = (self.display.xlib.XPending)(self.display.display); if count == 0 { break; } - (xlib.XNextEvent)(self.display.display, &mut xev); + (self.display.xlib.XNextEvent)(self.display.display, &mut xev); } self.process_event(&mut xev, &mut callback); } @@ -161,12 +168,10 @@ impl EventsLoop { { self.pending_wakeup.store(false, atomic::Ordering::Relaxed); - let xlib = &self.display.xlib; - let mut xev = unsafe { mem::uninitialized() }; loop { - unsafe { (xlib.XNextEvent)(self.display.display, &mut xev) }; // Blocks as necessary + unsafe { (self.display.xlib.XNextEvent)(self.display.display, &mut xev) }; // Blocks as necessary let mut control_flow = ControlFlow::Continue; @@ -187,7 +192,7 @@ impl EventsLoop { } } - fn process_event(&self, xev: &mut ffi::XEvent, mut callback: F) + fn process_event(&mut self, xev: &mut ffi::XEvent, mut callback: F) where F: FnMut(Event) { let xlib = &self.display.xlib; @@ -210,11 +215,123 @@ impl EventsLoop { if client_msg.data.get_long(0) as ffi::Atom == self.wm_delete_window { callback(Event::WindowEvent { window_id: wid, event: WindowEvent::Closed }) - } else { - if self.pending_wakeup.load(atomic::Ordering::Relaxed) { - self.pending_wakeup.store(false, atomic::Ordering::Relaxed); - callback(Event::Awakened); + } else if client_msg.message_type == self.dnd.atoms.enter { + let source_window = client_msg.data.get_long(0) as c_ulong; + let flags = client_msg.data.get_long(1); + let version = flags >> 24; + self.dnd.version = Some(version); + let has_more_types = flags - (flags & (c_long::max_value() - 1)) == 1; + if !has_more_types { + let type_list = vec![ + client_msg.data.get_long(2) as c_ulong, + client_msg.data.get_long(3) as c_ulong, + client_msg.data.get_long(4) as c_ulong + ]; + self.dnd.type_list = Some(type_list); + } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { + self.dnd.type_list = Some(more_types); } + } else if client_msg.message_type == self.dnd.atoms.position { + // This event occurs every time the mouse moves while a file's being dragged + // over our window. We emit HoveredFile in response; while the Mac OS X backend + // does that upon a drag entering, XDnD doesn't have access to the actual drop + // data until this event. For parity with other platforms, we only emit + // HoveredFile the first time, though if winit's API is later extended to + // supply position updates with HoveredFile or another event, implementing + // that here would be trivial. + + let source_window = client_msg.data.get_long(0) as c_ulong; + + // Equivalent to (x << shift) | y + // where shift = mem::size_of::() * 8 + // Note that coordinates are in "desktop space", not "window space" + // (in x11 parlance, they're root window coordinates) + //let packed_coordinates = client_msg.data.get_long(2); + //let shift = mem::size_of::() * 8; + //let x = packed_coordinates >> shift; + //let y = packed_coordinates & !(x << shift); + + // By our own state flow, version should never be None at this point. + let version = self.dnd.version.unwrap_or(5); + + // Action is specified in versions 2 and up, though we don't need it anyway. + //let action = client_msg.data.get_long(4); + + let accepted = if let Some(ref type_list) = self.dnd.type_list { + type_list.contains(&self.dnd.atoms.uri_list) + } else { + false + }; + + if accepted { + self.dnd.source_window = Some(source_window); + unsafe { + if self.dnd.result.is_none() { + let time = if version >= 1 { + client_msg.data.get_long(3) as c_ulong + } else { + // In version 0, time isn't specified + ffi::CurrentTime + }; + // This results in the SelectionNotify event below + self.dnd.convert_selection(xwindow, time); + } + self.dnd.send_status(xwindow, source_window, DndState::Accepted); + } + } else { + unsafe { + self.dnd.send_status(xwindow, source_window, DndState::Rejected); + self.dnd.send_finished(xwindow, source_window, DndState::Rejected); + } + self.dnd.reset(); + } + } else if client_msg.message_type == self.dnd.atoms.drop { + if let Some(source_window) = self.dnd.source_window { + if let Some(Ok(ref path_list)) = self.dnd.result { + for path in path_list { + callback(Event::WindowEvent { + window_id: wid, + event: WindowEvent::DroppedFile(path.clone()), + }); + } + } + unsafe { + self.dnd.send_finished(xwindow, source_window, DndState::Accepted); + } + } + self.dnd.reset(); + } else if client_msg.message_type == self.dnd.atoms.leave { + self.dnd.reset(); + callback(Event::WindowEvent { + window_id: wid, + event: WindowEvent::HoveredFileCancelled, + }); + } else if self.pending_wakeup.load(atomic::Ordering::Relaxed) { + self.pending_wakeup.store(false, atomic::Ordering::Relaxed); + callback(Event::Awakened); + } + } + + ffi::SelectionNotify => { + let xsel: &ffi::XSelectionEvent = xev.as_ref(); + if xsel.property == self.dnd.atoms.selection { + let mut result = None; + + // This is where we receive data from drag and drop + if let Ok(mut data) = unsafe { self.dnd.read_data(xwindow) } { + let parse_result = self.dnd.parse_data(&mut data); + if let Ok(ref path_list) = parse_result { + for path in path_list { + callback(Event::WindowEvent { + window_id: wid, + event: WindowEvent::HoveredFile(path.clone()), + }); + } + } + result = Some(parse_result); + } + + self.dnd.result = result; } } diff --git a/src/platform/linux/x11/util.rs b/src/platform/linux/x11/util.rs new file mode 100644 index 00000000..0969b3e5 --- /dev/null +++ b/src/platform/linux/x11/util.rs @@ -0,0 +1,107 @@ +use std::mem; +use std::ptr; +use std::sync::Arc; + +use libc::{c_char, c_int, c_long, c_short, c_uchar, c_ulong}; + +use super::{ffi, XConnection, XError}; + +pub unsafe fn send_client_msg( + xconn: &Arc, + target_window: c_ulong, + message_type: ffi::Atom, + data: (c_long, c_long, c_long, c_long, c_long), +) { + let mut event: ffi::XClientMessageEvent = mem::uninitialized(); + event.type_ = ffi::ClientMessage; + event.display = xconn.display; + event.window = target_window; + event.message_type = message_type; + event.format = 32; + event.data = ffi::ClientMessageData::new(); + event.data.set_long(0, data.0); + event.data.set_long(1, data.1); + event.data.set_long(2, data.2); + event.data.set_long(3, data.3); + event.data.set_long(4, data.4); + + (xconn.xlib.XSendEvent)( + xconn.display, + target_window, + ffi::False, + ffi::NoEventMask, + &mut event.into(), + ); +} + +#[derive(Debug)] +pub enum GetPropertyError { + XError(XError), + TypeMismatch(ffi::Atom), + FormatMismatch(c_int), + NothingAllocated, +} + +pub unsafe fn get_property( + xconn: &Arc, + window: c_ulong, + property: ffi::Atom, + property_type: ffi::Atom, +) -> Result, GetPropertyError> { + let mut data = Vec::new(); + + let mut done = false; + while !done { + let mut actual_type: ffi::Atom = mem::uninitialized(); + let mut actual_format: c_int = mem::uninitialized(); + let mut byte_count: c_ulong = mem::uninitialized(); + let mut bytes_after: c_ulong = mem::uninitialized(); + let mut buf: *mut c_uchar = ptr::null_mut(); + (xconn.xlib.XGetWindowProperty)( + xconn.display, + window, + property, + (data.len() / 4) as c_long, + 1024, + ffi::False, + property_type, + &mut actual_type, + &mut actual_format, + &mut byte_count, + &mut bytes_after, + &mut buf, + ); + + if let Err(e) = xconn.check_errors() { + return Err(GetPropertyError::XError(e)); + } + + if actual_type != property_type { + return Err(GetPropertyError::TypeMismatch(actual_type)); + } + + // Fun fact: actual_format ISN'T the size of the type; it's more like a really bad enum + let format_mismatch = match actual_format as usize { + 8 => mem::size_of::() != mem::size_of::(), + 16 => mem::size_of::() != mem::size_of::(), + 32 => mem::size_of::() != mem::size_of::(), + _ => true, // this won't actually be reached; the XError condition above is triggered + }; + + if format_mismatch { + return Err(GetPropertyError::FormatMismatch(actual_format)); + } + + if !buf.is_null() { + let mut buf = + Vec::from_raw_parts(buf as *mut T, byte_count as usize, byte_count as usize); + data.append(&mut buf); + } else { + return Err(GetPropertyError::NothingAllocated); + } + + done = bytes_after == 0; + } + + Ok(data) +} diff --git a/src/platform/linux/x11/window.rs b/src/platform/linux/x11/window.rs index b74e7e6d..04f6802a 100644 --- a/src/platform/linux/x11/window.rs +++ b/src/platform/linux/x11/window.rs @@ -125,6 +125,24 @@ impl Window2 { win }; + // Enable drag and drop + unsafe { + let atom_name: *const libc::c_char = b"XdndAware\0".as_ptr() as _; + let atom = (display.xlib.XInternAtom)(display.display, atom_name, ffi::False); + let version = &5; // Latest version; hasn't changed since 2002 + (display.xlib.XChangeProperty)( + display.display, + window, + atom, + ffi::XA_ATOM, + 32, + ffi::PropModeReplace, + version, + 1 + ); + display.check_errors().expect("Failed to set drag and drop properties"); + } + // Set ICCCM WM_CLASS property based on initial window title // Must be done *before* mapping the window by ICCCM 4.1.2.5 unsafe {