x11: Implement file drag and drop (#360)

* x11: Implement file drag and drop

* Fixed typo
This commit is contained in:
Francesca Sunshine 2017-12-13 06:22:03 -05:00 committed by Pierre Krieger
parent d2dd82c146
commit d18db208ff
7 changed files with 498 additions and 18 deletions

View file

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

View file

@ -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"

View file

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

View file

@ -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<XConnection>) -> Result<Self, XError> {
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<Utf8Error> for DndDataParseError {
fn from(e: Utf8Error) -> Self {
DndDataParseError::InvalidUtf8(e)
}
}
impl From<io::Error> for DndDataParseError {
fn from(e: io::Error) -> Self {
DndDataParseError::UnresolvablePath(e)
}
}
pub struct Dnd {
xconn: Arc<XConnection>,
pub atoms: DndAtoms,
// Populated by XdndEnter event handler
pub version: Option<c_long>,
pub type_list: Option<Vec<c_ulong>>,
// Populated by XdndPosition event handler
pub source_window: Option<c_ulong>,
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
}
impl Dnd {
pub fn new(xconn: Arc<XConnection>) -> Result<Self, XError> {
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<Vec<ffi::Atom>, 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<Vec<c_uchar>, util::GetPropertyError> {
util::get_property(
&self.xconn,
window,
self.atoms.selection,
self.atoms.uri_list,
)
}
pub fn parse_data(&self, data: &mut Vec<c_uchar>) -> Result<Vec<PathBuf>, 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)
}
}
}

View file

@ -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<XConnection>,
wm_delete_window: ffi::Atom,
dnd: Dnd,
windows: Arc<Mutex<HashMap<WindowId, WindowData>>>,
devices: Mutex<HashMap<DeviceId, Device>>,
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<F>(&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<F>(&self, xev: &mut ffi::XEvent, mut callback: F)
fn process_event<F>(&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::<c_short>() * 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::<libc::c_short>() * 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;
}
}

View file

@ -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<XConnection>,
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<T>(
xconn: &Arc<XConnection>,
window: c_ulong,
property: ffi::Atom,
property_type: ffi::Atom,
) -> Result<Vec<T>, 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::<T>() != mem::size_of::<c_char>(),
16 => mem::size_of::<T>() != mem::size_of::<c_short>(),
32 => mem::size_of::<T>() != mem::size_of::<c_long>(),
_ => 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)
}

View file

@ -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 {