use crate::{ClipboardContent, PhysicalPoint, ShellEvent, WindowId};
use super::{Delivery, Read, X11Shell, ffi};
pub const VERSION: u8 = 5;
pub const MIN_VERSION: u8 = 3;
const STATUS_ACCEPT: u32 = 1;
const STATUS_WANT_POSITION: u32 = 1 << 1;
const FINISHED_ACCEPTED: u32 = 1;
const FINISHED_FIELDS_FROM: u8 = 5;
#[must_use]
pub const fn offered_version(word: u32) -> u8 {
(word >> 24) as u8
}
#[must_use]
pub const fn has_type_list(word: u32) -> bool {
word & 1 != 0
}
#[must_use]
pub const fn negotiate(offered: u8) -> Option<u8> {
if offered < MIN_VERSION {
return None;
}
Some(if offered < VERSION { offered } else { VERSION })
}
#[must_use]
pub const fn pack_point(x: i16, y: i16) -> u32 {
((x as u16 as u32) << 16) | (y as u16 as u32)
}
#[must_use]
pub const fn unpack_point(word: u32) -> (i16, i16) {
(
((word >> 16) & 0xffff) as u16 as i16,
(word & 0xffff) as u16 as i16,
)
}
#[must_use]
pub fn inline_types(data: &[u32; 5]) -> Vec<u32> {
data[2..5]
.iter()
.copied()
.filter(|atom| *atom != 0)
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct Drag {
pub source: u32,
pub version: u8,
pub window: Option<WindowId>,
pub target: u32,
pub origin: (i32, i32),
pub position: PhysicalPoint,
}
impl Drag {
#[must_use]
pub const fn accepts(&self) -> bool {
self.window.is_some() && self.target != 0
}
}
impl X11Shell {
pub(super) fn set_xdnd_aware(&self, xid: u32) {
let version = [u32::from(VERSION)];
self.conn.set_property(
xid,
self.conn.atoms.xdnd_aware,
ffi::value::ATOM_ATOM,
32,
&super::window::words_to_bytes(&version),
);
}
pub(super) fn handle_xdnd_message(&mut self, event: &ffi::ClientMessageEvent) -> bool {
let atoms = self.conn.atoms;
if event.type_ == atoms.xdnd_enter {
self.handle_xdnd_enter(event);
} else if event.type_ == atoms.xdnd_position {
self.handle_xdnd_position(event);
} else if event.type_ == atoms.xdnd_leave {
self.handle_xdnd_leave(event);
} else if event.type_ == atoms.xdnd_drop {
self.handle_xdnd_drop(event);
} else {
return false;
}
true
}
fn handle_xdnd_enter(&mut self, event: &ffi::ClientMessageEvent) {
let source = event.data[0];
let offered = offered_version(event.data[1]);
let Some(version) = negotiate(offered) else {
crcbl_core::log::warn!(
"an XDND source announced version {offered}, below the {MIN_VERSION} this \
backend speaks; the drop will be refused"
);
self.drag = Some(Drag {
source,
version: MIN_VERSION,
window: None,
target: 0,
origin: (0, 0),
position: PhysicalPoint::ORIGIN,
});
return;
};
let window = self
.window_by_xid(event.window)
.filter(|window| self.window(*window).is_ok_and(|state| state.accept_drops));
let offered_types = if has_type_list(event.data[1]) {
self.conn
.get_property_words(source, self.conn.atoms.xdnd_type_list)
} else {
inline_types(&event.data)
};
let uri_list = self.conn.atoms.mime_uri_list;
let target = if window.is_some() && offered_types.contains(&uri_list) {
uri_list
} else {
0
};
let origin = window
.and_then(|window| self.window(window).ok())
.and_then(|state| self.origin_now(state.id))
.unwrap_or((0, 0));
self.drag = Some(Drag {
source,
version,
window,
target,
origin,
position: PhysicalPoint::ORIGIN,
});
}
fn handle_xdnd_position(&mut self, event: &ffi::ClientMessageEvent) {
let Some(drag) = self.drag.as_mut() else {
return;
};
if drag.source != event.data[0] {
return;
}
let (root_x, root_y) = unpack_point(event.data[2]);
drag.position = PhysicalPoint::new(
f64::from(i32::from(root_x) - drag.origin.0),
f64::from(i32::from(root_y) - drag.origin.1),
);
let (source, accept) = (drag.source, drag.accepts());
self.send_status(source, event.window, accept);
}
fn handle_xdnd_leave(&mut self, event: &ffi::ClientMessageEvent) {
if self.drag.is_some_and(|drag| drag.source == event.data[0]) {
self.drag = None;
}
}
fn handle_xdnd_drop(&mut self, event: &ffi::ClientMessageEvent) {
let Some(drag) = self.drag.take() else {
self.send_finished(event.data[0], event.window, MIN_VERSION, false);
return;
};
if drag.source != event.data[0] {
self.send_finished(event.data[0], event.window, MIN_VERSION, false);
self.drag = Some(drag);
return;
}
let (Some(window), true) = (drag.window, drag.accepts()) else {
self.send_finished(drag.source, event.window, drag.version, false);
return;
};
let Ok(xid) = self.window(window).map(|state| state.id) else {
self.send_finished(drag.source, event.window, drag.version, false);
return;
};
let time = event.data[2];
let stamp = self.time.event_time(time);
let property = self.conn.atoms.crcbl_xdnd;
let selection = self.conn.atoms.xdnd_selection;
self.convert(xid, selection, drag.target, property, time);
self.reads.push(Read::for_drop(
window,
drag.target,
selection,
property,
Delivery::Drop {
source: drag.source,
version: drag.version,
position: drag.position,
time: stamp,
},
ffi::monotonic_nanos(),
));
}
pub(super) fn finish_drop(
&mut self,
window: WindowId,
delivery: &Delivery,
content: &ClipboardContent,
) {
let Delivery::Drop {
source,
version,
position,
time,
} = *delivery
else {
return;
};
let xid = self.window(window).map_or(0, |state| state.id);
if let ClipboardContent::Bytes(bytes) = content {
for path in crate::parse_uri_list(bytes) {
self.queue.push_back(ShellEvent::DroppedFile {
window,
time,
path,
position: Some(position),
});
}
}
self.send_finished(
source,
xid,
version,
matches!(content, ClipboardContent::Bytes(_)),
);
if xid != 0 {
self.conn.delete_property(xid, self.conn.atoms.crcbl_xdnd);
self.conn.flush();
}
}
pub(super) fn abandon_drop(&self, xid: u32, delivery: &Delivery) {
let Delivery::Drop {
source, version, ..
} = *delivery
else {
return;
};
self.send_finished(source, xid, version, false);
}
pub(super) fn forget_drag(&mut self, window: WindowId) {
if self.drag.is_some_and(|drag| drag.window == Some(window)) {
self.drag = None;
}
}
fn send_status(&self, source: u32, xid: u32, accept: bool) {
let mut flags = STATUS_WANT_POSITION;
let mut action = 0;
if accept {
flags |= STATUS_ACCEPT;
action = self.conn.atoms.xdnd_action_copy;
}
let rectangle = pack_point(0, 0);
self.send_xdnd(
source,
self.conn.atoms.xdnd_status,
[xid, flags, rectangle, rectangle, action],
);
}
fn send_finished(&self, source: u32, xid: u32, version: u8, accepted: bool) {
let (flags, action) = if version >= FINISHED_FIELDS_FROM && accepted {
(FINISHED_ACCEPTED, self.conn.atoms.xdnd_action_copy)
} else {
(0, 0)
};
self.send_xdnd(
source,
self.conn.atoms.xdnd_finished,
[xid, flags, action, 0, 0],
);
}
fn send_xdnd(&self, source: u32, type_: u32, data: [u32; 5]) {
if source == 0 || type_ == 0 {
return;
}
let message = ffi::ClientMessageEvent {
response_type: ffi::event::CLIENT_MESSAGE,
format: 32,
sequence: 0,
window: source,
type_,
data,
};
self.conn.send_event(source, 0, &message);
self.conn.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_announced_version_is_the_high_byte_and_the_flag_is_bit_zero() {
assert_eq!(offered_version(5 << 24), 5);
assert_eq!(
offered_version((5 << 24) | 1),
5,
"the flag is not the version"
);
assert!(has_type_list((5 << 24) | 1));
assert!(!has_type_list(5 << 24));
}
#[test]
fn a_version_is_clamped_down_and_never_up() {
assert_eq!(negotiate(VERSION), Some(VERSION));
assert_eq!(negotiate(3), Some(3));
assert_eq!(negotiate(4), Some(4));
assert_eq!(
negotiate(9),
Some(VERSION),
"we do not speak a version we do not know"
);
assert_eq!(negotiate(2), None);
assert_eq!(negotiate(0), None);
}
#[test]
fn a_packed_point_round_trips_and_keeps_its_sign() {
for point in [
(0, 0),
(1920, 1080),
(-1, -1),
(-1920, 5),
(i16::MIN, i16::MAX),
] {
assert_eq!(
unpack_point(pack_point(point.0, point.1)),
point,
"{point:?}"
);
}
assert_eq!(pack_point(7, 9), (7 << 16) | 9);
}
#[test]
fn the_inline_types_are_three_words_and_padding_is_not_an_atom() {
assert_eq!(inline_types(&[1, 2, 30, 31, 32]), vec![30, 31, 32]);
assert_eq!(inline_types(&[1, 2, 30, 0, 0]), vec![30]);
assert!(inline_types(&[1, 2, 0, 0, 0]).is_empty());
}
#[test]
fn a_drag_with_no_uri_list_and_a_drag_on_no_window_both_refuse() {
let window = {
let mut pool: crcbl_core::Pool<u8> = crcbl_core::Pool::new();
pool.insert(0).cast()
};
let base = Drag {
source: 42,
version: VERSION,
window: Some(window),
target: 77,
origin: (0, 0),
position: PhysicalPoint::ORIGIN,
};
assert!(base.accepts());
assert!(
!Drag { target: 0, ..base }.accepts(),
"no text/uri-list on offer"
);
assert!(
!Drag {
window: None,
..base
}
.accepts(),
"not a window of ours"
);
}
}