use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{
Atom, AtomEnum, ClientMessageEvent, ConnectionExt as _, CreateWindowAux, EventMask, PropMode,
SelectionNotifyEvent, SelectionRequestEvent, Window, WindowClass, SELECTION_NOTIFY_EVENT,
};
use x11rb::protocol::Event;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
const DRAG_PATH_LIMIT: usize = 1_024;
const URI_LIST_BYTES_LIMIT: usize = 128 * 1024;
const EVENT_LIMIT: usize = 64;
const WINDOW_HIERARCHY_LIMIT: usize = 64;
const STATUS_RESPONSE_TIMEOUT: Duration = Duration::from_secs(1);
const DROP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
x11rb::atom_manager! {
Atoms: AtomsCookie {
TARGETS,
GUTH_DRAG_TIMESTAMP,
XdndActionCopy,
XdndAware,
XdndDrop,
XdndEnter,
XdndFinished,
XdndLeave,
XdndPosition,
XdndProxy,
XdndSelection,
XdndStatus,
XdndTypeList,
TextUriList: b"text/uri-list",
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DragOutcome {
None,
Initiated(usize),
Completed(usize),
Rejected,
}
pub struct NativeFileDrag {
x11: Option<X11DragSource>,
}
impl NativeFileDrag {
pub fn new(context: &eframe::CreationContext<'_>) -> Self {
let app_window = context
.window_handle()
.ok()
.and_then(|handle| match handle.as_raw() {
RawWindowHandle::Xlib(handle) => u32::try_from(handle.window).ok(),
RawWindowHandle::Xcb(handle) => Some(handle.window.get()),
_ => None,
});
Self {
x11: app_window.and_then(|window| X11DragSource::new(window).ok()),
}
}
pub fn update(
&mut self,
paths: &[PathBuf],
pointer_down: bool,
pointer_released: bool,
cancelled: bool,
) -> DragOutcome {
self.x11.as_mut().map_or(DragOutcome::None, |source| {
source.update(paths, pointer_down, pointer_released, cancelled)
})
}
pub fn is_active(&self) -> bool {
self.x11.as_ref().is_some_and(X11DragSource::is_active)
}
}
struct X11DragSource {
connection: RustConnection,
root: Window,
app_window: Window,
source_window: Window,
atoms: Atoms,
drag: Option<ActiveDrag>,
pending_outcome: Option<DragOutcome>,
failed_until_release: bool,
}
struct ActiveDrag {
paths: Vec<PathBuf>,
uri_list: Vec<u8>,
target: Option<DragTarget>,
accepted: bool,
position_pending: bool,
release_pending: bool,
release_position_sent: bool,
dropped: bool,
deadline: Option<Instant>,
timestamp: u32,
owns_selection: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct DragTarget {
window: Window,
recipient: Window,
version: u32,
}
impl X11DragSource {
fn new(app_window: Window) -> Result<Self, String> {
let (connection, screen_number) = x11rb::connect(None).map_err(display_error)?;
let root = connection.setup().roots[screen_number].root;
let source_window = connection.generate_id().map_err(display_error)?;
connection
.create_window(
x11rb::COPY_FROM_PARENT as u8,
source_window,
root,
0,
0,
1,
1,
0,
WindowClass::INPUT_ONLY,
0,
&CreateWindowAux::new().event_mask(EventMask::PROPERTY_CHANGE),
)
.map_err(display_error)?
.check()
.map_err(display_error)?;
let atoms = Atoms::new(&connection)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
connection.flush().map_err(display_error)?;
Ok(Self {
connection,
root,
app_window,
source_window,
atoms,
drag: None,
pending_outcome: None,
failed_until_release: false,
})
}
fn is_active(&self) -> bool {
self.drag.is_some()
}
fn update(
&mut self,
paths: &[PathBuf],
pointer_down: bool,
pointer_released: bool,
cancelled: bool,
) -> DragOutcome {
if self.poll_events().is_err() {
self.reset(true);
return DragOutcome::Rejected;
}
if let Some(outcome) = self.pending_outcome.take() {
return outcome;
}
if self
.drag
.as_ref()
.is_some_and(|drag| drag.deadline.is_some_and(|time| time <= Instant::now()))
{
self.reset(true);
return DragOutcome::Rejected;
}
if cancelled {
self.cancel();
return DragOutcome::Rejected;
}
if self.failed_until_release {
if !pointer_down {
self.failed_until_release = false;
}
return DragOutcome::None;
}
if self.drag.as_ref().is_some_and(|drag| drag.release_pending) {
return self.advance_release();
}
if self.drag.as_ref().is_some_and(|drag| drag.dropped) {
return DragOutcome::None;
}
if paths.is_empty() {
if self.drag.is_some() {
self.cancel();
}
return DragOutcome::None;
}
if self.drag.is_none() && pointer_down && self.start(paths).is_err() {
self.reset(true);
self.failed_until_release = true;
return DragOutcome::Rejected;
}
if self.drag.is_none() {
return DragOutcome::None;
}
if self.drag.as_ref().is_some_and(|drag| drag.paths != paths) {
self.cancel();
if self.start(paths).is_err() {
self.reset(true);
self.failed_until_release = true;
return DragOutcome::Rejected;
}
}
let position_sent = match self.update_target() {
Ok(position_sent) => position_sent,
Err(_) => {
self.cancel();
return DragOutcome::Rejected;
}
};
if pointer_released || !pointer_down {
drag_debug("primary pointer released");
if let Some(drag) = self.drag.as_mut() {
drag.release_pending = true;
drag.release_position_sent = position_sent;
drag.deadline = Some(Instant::now() + STATUS_RESPONSE_TIMEOUT);
}
return self.advance_release();
}
DragOutcome::None
}
fn start(&mut self, paths: &[PathBuf]) -> Result<(), String> {
if paths.is_empty() || paths.len() > DRAG_PATH_LIMIT {
return Err(format!(
"drag must contain between 1 and {DRAG_PATH_LIMIT} paths"
));
}
if paths.iter().any(|path| !path.is_absolute()) {
return Err("all drag paths must be absolute".to_string());
}
let paths = paths.to_vec();
let path_count = paths.len();
let uri_list = encode_uri_list(&paths)?;
self.connection
.change_property32(
PropMode::REPLACE,
self.source_window,
self.atoms.XdndTypeList,
AtomEnum::ATOM,
&[self.atoms.TextUriList],
)
.map_err(display_error)?
.check()
.map_err(display_error)?;
self.connection
.set_selection_owner(
self.source_window,
self.atoms.XdndSelection,
x11rb::CURRENT_TIME,
)
.map_err(display_error)?
.check()
.map_err(display_error)?;
let timestamp = self.server_timestamp()?;
let owner = self
.connection
.get_selection_owner(self.atoms.XdndSelection)
.map_err(display_error)?
.reply()
.map_err(display_error)?
.owner;
if owner != self.source_window {
return Err(format!(
"could not own the XDND selection (owner={owner}, timestamp={timestamp})"
));
}
self.connection.flush().map_err(display_error)?;
self.drag = Some(ActiveDrag {
paths,
uri_list,
target: None,
accepted: false,
position_pending: false,
release_pending: false,
release_position_sent: false,
dropped: false,
deadline: None,
timestamp,
owns_selection: true,
});
drag_debug(&format!("started {path_count} paths"));
Ok(())
}
fn server_timestamp(&self) -> Result<u32, String> {
self.connection
.change_property8(
PropMode::REPLACE,
self.source_window,
self.atoms.GUTH_DRAG_TIMESTAMP,
AtomEnum::INTEGER,
&[0],
)
.map_err(display_error)?
.check()
.map_err(display_error)?;
self.connection.flush().map_err(display_error)?;
loop {
match self.connection.wait_for_event().map_err(display_error)? {
Event::PropertyNotify(event)
if event.window == self.source_window
&& event.atom == self.atoms.GUTH_DRAG_TIMESTAMP =>
{
return Ok(event.time);
}
_ => {}
}
}
}
fn update_target(&mut self) -> Result<bool, String> {
let pointer = self
.connection
.query_pointer(self.root)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
let target = self.find_target(pointer.child)?;
let previous = self.drag.as_ref().and_then(|drag| drag.target);
if target != previous {
drag_debug(&format!("target {previous:?} -> {target:?}"));
if let Some(previous) = previous {
self.send_target(
previous,
self.atoms.XdndLeave,
[self.source_window, 0, 0, 0, 0],
)?;
}
if let Some(target) = target {
self.send_target(
target,
self.atoms.XdndEnter,
[
self.source_window,
target.version.min(5) << 24,
self.atoms.TextUriList,
0,
0,
],
)?;
}
if let Some(drag) = self.drag.as_mut() {
drag.target = target;
drag.accepted = false;
drag.position_pending = false;
}
}
let send_position = target.is_some()
&& self
.drag
.as_ref()
.is_some_and(|drag| !drag.position_pending);
if let Some(target) = target.filter(|_| send_position) {
let coordinates =
(u32::from(pointer.root_x as u16) << 16) | u32::from(pointer.root_y as u16);
let timestamp = self
.drag
.as_ref()
.map(|drag| drag.timestamp)
.unwrap_or(x11rb::CURRENT_TIME);
self.send_target(
target,
self.atoms.XdndPosition,
[
self.source_window,
0,
coordinates,
timestamp,
self.atoms.XdndActionCopy,
],
)?;
if let Some(drag) = self.drag.as_mut() {
drag.accepted = false;
drag.position_pending = true;
}
return Ok(true);
}
Ok(false)
}
fn advance_release(&mut self) -> DragOutcome {
let Some(drag) = self.drag.as_ref() else {
return DragOutcome::None;
};
if drag.position_pending {
return DragOutcome::None;
}
if !drag.release_position_sent {
if self.update_target().is_err() {
self.cancel();
return DragOutcome::Rejected;
}
if let Some(drag) = self.drag.as_mut() {
drag.release_position_sent = true;
drag.deadline = Some(Instant::now() + STATUS_RESPONSE_TIMEOUT);
if drag.position_pending {
return DragOutcome::None;
}
}
}
self.initiate_drop()
}
fn initiate_drop(&mut self) -> DragOutcome {
let Some(drag) = self.drag.as_ref() else {
return DragOutcome::None;
};
let count = drag.paths.len();
let target = drag.target;
let accepted = drag.accepted;
let timestamp = drag.timestamp;
if let Some(target) = target.filter(|_| accepted) {
if self
.send_target(
target,
self.atoms.XdndDrop,
[self.source_window, 0, timestamp, 0, 0],
)
.is_ok()
{
if let Some(drag) = self.drag.as_mut() {
drag.release_pending = false;
drag.dropped = true;
drag.deadline = Some(Instant::now() + DROP_RESPONSE_TIMEOUT);
}
return DragOutcome::Initiated(count);
}
}
self.cancel();
DragOutcome::Rejected
}
fn cancel(&mut self) {
if let Some(target) = self
.drag
.as_ref()
.filter(|drag| !drag.dropped)
.and_then(|drag| drag.target)
{
let _ = self.send_target(
target,
self.atoms.XdndLeave,
[self.source_window, 0, 0, 0, 0],
);
}
self.reset(true);
}
fn reset(&mut self, release_selection: bool) {
if release_selection {
if let Some(drag) = self.drag.as_ref().filter(|drag| drag.owns_selection) {
let _ = self.connection.set_selection_owner(
x11rb::NONE,
self.atoms.XdndSelection,
drag.timestamp,
);
let _ = self.connection.flush();
}
}
self.drag = None;
}
fn find_target(&self, child: Window) -> Result<Option<DragTarget>, String> {
let mut current = if child == x11rb::NONE {
self.root
} else {
child
};
for _ in 0..WINDOW_HIERARCHY_LIMIT {
let reply = self
.connection
.query_pointer(current)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
if reply.child == x11rb::NONE || reply.child == current {
break;
}
current = reply.child;
}
let mut ancestors = Vec::new();
for _ in 0..WINDOW_HIERARCHY_LIMIT {
if current == self.app_window || current == self.source_window {
return Ok(None);
}
ancestors.push(current);
if current == self.root {
break;
}
let tree = self
.connection
.query_tree(current)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
if tree.parent == x11rb::NONE || tree.parent == current {
break;
}
current = tree.parent;
}
for window in ancestors {
let recipient = self.xdnd_proxy(window)?.unwrap_or(window);
if let Some(version) = self.xdnd_version(recipient)? {
return Ok(Some(DragTarget {
window,
recipient,
version,
}));
}
}
Ok(None)
}
fn xdnd_version(&self, window: Window) -> Result<Option<u32>, String> {
let property = self
.connection
.get_property(false, window, self.atoms.XdndAware, AtomEnum::ATOM, 0, 1)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
if property.type_ != u32::from(AtomEnum::ATOM) || property.format != 32 {
return Ok(None);
}
Ok(property
.value32()
.and_then(|mut values| values.next())
.filter(|version| *version >= 3))
}
fn xdnd_proxy(&self, window: Window) -> Result<Option<Window>, String> {
let proxy = self
.connection
.get_property(false, window, self.atoms.XdndProxy, AtomEnum::WINDOW, 0, 1)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
if proxy.type_ != u32::from(AtomEnum::WINDOW) || proxy.format != 32 {
return Ok(None);
}
let Some(proxy) = proxy.value32().and_then(|mut values| values.next()) else {
return Ok(None);
};
let confirmation = self
.connection
.get_property(false, proxy, self.atoms.XdndProxy, AtomEnum::WINDOW, 0, 1)
.map_err(display_error)?
.reply()
.map_err(display_error)?;
Ok(
(confirmation.type_ == u32::from(AtomEnum::WINDOW) && confirmation.format == 32)
.then(|| confirmation.value32().and_then(|mut values| values.next()))
.flatten()
.filter(|confirmed| *confirmed == proxy),
)
}
fn poll_events(&mut self) -> Result<(), String> {
for _ in 0..EVENT_LIMIT {
let Some(event) = self.connection.poll_for_event().map_err(display_error)? else {
break;
};
match event {
Event::ClientMessage(event)
if event.type_ == self.atoms.XdndStatus
&& event.format == 32
&& event.window == self.source_window =>
{
let data = event.data.as_data32();
if let Some(drag) = self.drag.as_mut() {
if drag.position_pending
&& drag.target.is_some_and(|target| target.window == data[0])
{
drag.position_pending = false;
drag.accepted =
data[1] & 1 != 0 && data[4] == self.atoms.XdndActionCopy;
drag_debug(&format!("status accepted={}", drag.accepted));
}
}
}
Event::ClientMessage(event)
if event.type_ == self.atoms.XdndFinished
&& event.format == 32
&& event.window == self.source_window =>
{
let data = event.data.as_data32();
if let Some((count, version)) = self.drag.as_ref().and_then(|drag| {
drag.target
.filter(|target| drag.dropped && target.window == data[0])
.map(|target| (drag.paths.len(), target.version))
}) {
let succeeded = version < 5
|| (data[1] & 1 != 0 && data[2] == self.atoms.XdndActionCopy);
self.pending_outcome = Some(if succeeded {
DragOutcome::Completed(count)
} else {
DragOutcome::Rejected
});
self.reset(true);
}
}
Event::SelectionRequest(event) => self.answer_selection_request(event)?,
Event::SelectionClear(event)
if event.selection == self.atoms.XdndSelection && self.drag.is_some() =>
{
self.pending_outcome = Some(DragOutcome::Rejected);
self.reset(false);
}
_ => {}
}
}
Ok(())
}
fn answer_selection_request(&self, request: SelectionRequestEvent) -> Result<(), String> {
if request.owner != self.source_window || request.selection != self.atoms.XdndSelection {
return Ok(());
}
let property = if request.property == x11rb::NONE {
request.target
} else {
request.property
};
let active = self.drag.as_ref().filter(|drag| {
drag.dropped && drag.owns_selection && timestamp_not_older(request.time, drag.timestamp)
});
let result = if request.target == self.atoms.TextUriList {
active.map_or(Err(()), |drag| {
self.connection
.change_property8(
PropMode::REPLACE,
request.requestor,
property,
self.atoms.TextUriList,
&drag.uri_list,
)
.map_err(|_| ())?
.check()
.map_err(|_| ())
})
} else if request.target == self.atoms.TARGETS && active.is_some() {
self.connection
.change_property32(
PropMode::REPLACE,
request.requestor,
property,
AtomEnum::ATOM,
&[self.atoms.TextUriList, self.atoms.TARGETS],
)
.map_err(|_| ())
.and_then(|cookie| cookie.check().map_err(|_| ()))
} else {
Err(())
};
let notify = SelectionNotifyEvent {
response_type: SELECTION_NOTIFY_EVENT,
sequence: 0,
time: request.time,
requestor: request.requestor,
selection: request.selection,
target: request.target,
property: if result.is_ok() {
property
} else {
x11rb::NONE
},
};
self.connection
.send_event(false, request.requestor, EventMask::NO_EVENT, notify)
.map_err(display_error)?
.check()
.map_err(display_error)?;
self.connection.flush().map_err(display_error)
}
fn send_target(
&self,
target: DragTarget,
message_type: Atom,
data: [u32; 5],
) -> Result<(), String> {
let event = ClientMessageEvent::new(32, target.window, message_type, data);
self.connection
.send_event(false, target.recipient, EventMask::NO_EVENT, event)
.map_err(display_error)?
.check()
.map_err(display_error)?;
self.connection.flush().map_err(display_error)
}
}
impl Drop for X11DragSource {
fn drop(&mut self) {
self.cancel();
let _ = self.connection.destroy_window(self.source_window);
let _ = self.connection.flush();
}
}
fn encode_uri_list(paths: &[PathBuf]) -> Result<Vec<u8>, String> {
if paths.is_empty() || paths.len() > DRAG_PATH_LIMIT {
return Err(format!(
"drag must contain between 1 and {DRAG_PATH_LIMIT} paths"
));
}
let mut output = Vec::new();
for path in paths {
let uri = file_uri(path)?;
if output.len().saturating_add(uri.len()).saturating_add(2) > URI_LIST_BYTES_LIMIT {
return Err("drag URI list exceeds the supported size".to_string());
}
output.extend_from_slice(uri.as_bytes());
output.extend_from_slice(b"\r\n");
}
Ok(output)
}
fn timestamp_not_older(candidate: u32, reference: u32) -> bool {
candidate == x11rb::CURRENT_TIME || candidate.wrapping_sub(reference) < (1_u32 << 31)
}
fn file_uri(path: &Path) -> Result<String, String> {
if !path.is_absolute() {
return Err("drag path must be absolute".to_string());
}
#[cfg(unix)]
let bytes = {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes()
};
#[cfg(not(unix))]
let bytes = path
.to_str()
.ok_or_else(|| "drag path is not valid UTF-8".to_string())?
.as_bytes();
let mut uri = String::from("file://");
for byte in bytes {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/') {
uri.push(char::from(*byte));
} else {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
uri.push('%');
uri.push(char::from(HEX[usize::from(byte >> 4)]));
uri.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
}
Ok(uri)
}
fn display_error(error: impl std::fmt::Display) -> String {
error.to_string()
}
fn drag_debug(message: &str) {
if std::env::var_os("GUTH_DRAG_DEBUG").is_some() {
eprintln!("guth drag-out: {message}");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn poll_x11_event(connection: &RustConnection) -> Option<Event> {
for _ in 0..100 {
if let Ok(Some(event)) = connection.poll_for_event() {
return Some(event);
}
std::thread::sleep(Duration::from_millis(2));
}
None
}
#[test]
fn uri_list_encodes_spaces_controls_and_delimiters() {
let paths = vec![
PathBuf::from("/tmp/hello world.txt"),
PathBuf::from("/tmp/a\nb#c.txt"),
];
assert_eq!(
encode_uri_list(&paths).unwrap(),
b"file:///tmp/hello%20world.txt\r\nfile:///tmp/a%0Ab%23c.txt\r\n"
);
}
#[cfg(unix)]
#[test]
fn uri_list_preserves_non_utf8_paths() {
use std::os::unix::ffi::OsStringExt;
let path = PathBuf::from(std::ffi::OsString::from_vec(b"/tmp/a\xffb".to_vec()));
assert_eq!(file_uri(&path).unwrap(), "file:///tmp/a%FFb");
}
#[test]
fn uri_list_rejects_relative_paths() {
assert!(file_uri(Path::new("relative.txt")).is_err());
}
#[test]
fn uri_list_rejects_excessive_path_counts() {
let paths = (0..=DRAG_PATH_LIMIT)
.map(|index| PathBuf::from(format!("/tmp/{index}")))
.collect::<Vec<_>>();
assert!(encode_uri_list(&paths).is_err());
}
#[test]
fn xdnd_source_serves_uri_list_to_accepting_target() {
let Ok((target_connection, screen_number)) = x11rb::connect(None) else {
return;
};
let root = target_connection.setup().roots[screen_number].root;
let target = target_connection.generate_id().unwrap();
let proxy = target_connection.generate_id().unwrap();
target_connection
.create_window(
x11rb::COPY_FROM_PARENT as u8,
target,
root,
0,
0,
1,
1,
0,
WindowClass::INPUT_OUTPUT,
0,
&CreateWindowAux::new(),
)
.unwrap()
.check()
.unwrap();
target_connection
.create_window(
x11rb::COPY_FROM_PARENT as u8,
proxy,
root,
0,
0,
1,
1,
0,
WindowClass::INPUT_OUTPUT,
0,
&CreateWindowAux::new(),
)
.unwrap()
.check()
.unwrap();
let target_atoms = Atoms::new(&target_connection).unwrap().reply().unwrap();
target_connection
.change_property32(
PropMode::REPLACE,
proxy,
target_atoms.XdndAware,
AtomEnum::ATOM,
&[5],
)
.unwrap()
.check()
.unwrap();
for (window, value) in [(target, proxy), (proxy, proxy)] {
target_connection
.change_property32(
PropMode::REPLACE,
window,
target_atoms.XdndProxy,
AtomEnum::WINDOW,
&[value],
)
.unwrap()
.check()
.unwrap();
}
let property = target_connection
.intern_atom(false, b"GUTH_XDND_TEST")
.unwrap()
.reply()
.unwrap()
.atom;
target_connection.flush().unwrap();
let mut source = X11DragSource::new(x11rb::NONE).unwrap();
let drag_target = DragTarget {
window: target,
recipient: proxy,
version: 5,
};
assert_eq!(source.find_target(target).unwrap(), Some(drag_target));
let paths = vec![PathBuf::from("/tmp/drag out.txt")];
source.start(&paths).unwrap();
source.drag.as_mut().unwrap().target = Some(drag_target);
source.drag.as_mut().unwrap().position_pending = true;
source
.send_target(
drag_target,
source.atoms.XdndEnter,
[
source.source_window,
5_u32 << 24,
source.atoms.TextUriList,
0,
0,
],
)
.unwrap();
assert!(matches!(
poll_x11_event(&target_connection),
Some(Event::ClientMessage(_))
));
let rejected_status = ClientMessageEvent::new(
32,
source.source_window,
target_atoms.XdndStatus,
[target, 1, 0, 0, 0],
);
target_connection
.send_event(
false,
source.source_window,
EventMask::NO_EVENT,
rejected_status,
)
.unwrap()
.check()
.unwrap();
target_connection.flush().unwrap();
for _ in 0..100 {
source.poll_events().unwrap();
if source
.drag
.as_ref()
.is_some_and(|drag| !drag.position_pending)
{
break;
}
std::thread::sleep(Duration::from_millis(2));
}
assert!(!source.drag.as_ref().unwrap().accepted);
source.drag.as_mut().unwrap().position_pending = true;
let accepted_status = ClientMessageEvent::new(
32,
source.source_window,
target_atoms.XdndStatus,
[target, 1, 0, 0, target_atoms.XdndActionCopy],
);
target_connection
.send_event(
false,
source.source_window,
EventMask::NO_EVENT,
accepted_status,
)
.unwrap()
.check()
.unwrap();
target_connection.flush().unwrap();
for _ in 0..100 {
source.poll_events().unwrap();
if source.drag.as_ref().is_some_and(|drag| drag.accepted) {
break;
}
std::thread::sleep(Duration::from_millis(2));
}
assert!(source.drag.as_ref().unwrap().accepted);
source.drag.as_mut().unwrap().release_pending = true;
assert_eq!(source.initiate_drop(), DragOutcome::Initiated(1));
assert!(matches!(
poll_x11_event(&target_connection),
Some(Event::ClientMessage(_))
));
target_connection
.convert_selection(
target,
target_atoms.XdndSelection,
target_atoms.TextUriList,
property,
x11rb::CURRENT_TIME,
)
.unwrap()
.check()
.unwrap();
target_connection.flush().unwrap();
let mut notified = false;
for _ in 0..100 {
source.poll_events().unwrap();
if matches!(
target_connection.poll_for_event().unwrap(),
Some(Event::SelectionNotify(_))
) {
notified = true;
break;
}
std::thread::sleep(Duration::from_millis(2));
}
assert!(notified);
let property = target_connection
.get_property(true, target, property, target_atoms.TextUriList, 0, 1024)
.unwrap()
.reply()
.unwrap();
assert_eq!(property.value, b"file:///tmp/drag%20out.txt\r\n");
let finished = ClientMessageEvent::new(
32,
source.source_window,
target_atoms.XdndFinished,
[target, 1, target_atoms.XdndActionCopy, 0, 0],
);
target_connection
.send_event(false, source.source_window, EventMask::NO_EVENT, finished)
.unwrap()
.check()
.unwrap();
target_connection.flush().unwrap();
for _ in 0..100 {
source.poll_events().unwrap();
if source.pending_outcome.is_some() {
break;
}
std::thread::sleep(Duration::from_millis(2));
}
assert_eq!(source.pending_outcome, Some(DragOutcome::Completed(1)));
let mut old_source = X11DragSource::new(x11rb::NONE).unwrap();
old_source.start(&paths).unwrap();
let mut newer_source = X11DragSource::new(x11rb::NONE).unwrap();
newer_source.start(&paths).unwrap();
for _ in 0..100 {
old_source.poll_events().unwrap();
if old_source.drag.is_none() {
break;
}
std::thread::sleep(Duration::from_millis(2));
}
assert!(old_source.drag.is_none());
let newer_window = newer_source.source_window;
drop(old_source);
assert_eq!(
target_connection
.get_selection_owner(target_atoms.XdndSelection)
.unwrap()
.reply()
.unwrap()
.owner,
newer_window
);
newer_source.cancel();
target_connection.destroy_window(target).unwrap();
target_connection.destroy_window(proxy).unwrap();
target_connection.flush().unwrap();
}
}