use super::{
ACTION_COPY, MANAGER_CREATE_DATA_SOURCE, SOURCE_CANCELLED, SOURCE_DESTROY, SOURCE_OFFER,
SOURCE_SEND, TEXT_MIMES, URI_LIST, WlConn, arg_str, arg_u32, close, read_str, serve,
};
use crate::platform::DropData;
use std::io::Write;
const SOURCE_SET_ACTIONS: u16 = 2;
const SOURCE_DND_DROP_PERFORMED: u16 = 3;
const SOURCE_DND_FINISHED: u16 = 4;
const DEVICE_START_DRAG: u16 = 0;
#[derive(Debug)]
pub(super) struct DragOut {
source: u32,
pub(super) data: DropData,
}
fn mimes(data: &DropData) -> &'static [&'static str] {
match data {
DropData::Files(f) if !f.is_empty() => &[URI_LIST],
DropData::Text(_) => &TEXT_MIMES,
_ => &[],
}
}
fn bytes(data: &DropData, mime: &str) -> Option<Vec<u8>> {
if !mimes(data).iter().any(|m| m.eq_ignore_ascii_case(mime)) {
return None;
}
match data {
DropData::Files(paths) => Some(crate::platform::uri_list(paths).into_bytes()),
DropData::Text(text) if mime == "STRING" => Some(
text.chars()
.map(|c| u8::try_from(u32::from(c)).unwrap_or(b'?'))
.collect(),
),
DropData::Text(text) => Some(text.as_bytes().to_vec()),
DropData::Internal(_) => None,
}
}
pub(in super::super) fn start(conn: &mut WlConn, surface: u32, data: &DropData) -> bool {
let (manager, device) = (conn.clip.manager, conn.clip.device);
let offered = mimes(data);
let Some(serial) = conn.clip.press_serial else {
return false;
};
if device == 0 || offered.is_empty() {
return false;
}
end(conn);
let source = conn.new_id();
let mut a = Vec::new();
arg_u32(&mut a, source);
if conn.send(manager, MANAGER_CREATE_DATA_SOURCE, &a).is_err() {
return false;
}
for mime in offered {
let mut a = Vec::new();
arg_str(&mut a, mime);
let _ = conn.send(source, SOURCE_OFFER, &a);
}
if conn.clip.version >= 3 {
let mut a = Vec::new();
arg_u32(&mut a, ACTION_COPY);
let _ = conn.send(source, SOURCE_SET_ACTIONS, &a);
}
let mut a = Vec::new();
arg_u32(&mut a, source);
arg_u32(&mut a, surface); arg_u32(&mut a, 0); arg_u32(&mut a, serial);
let started = conn.send(device, DEVICE_START_DRAG, &a).is_ok();
let _ = conn.stream.flush();
conn.clip.drag_out = Some(DragOut {
source,
data: data.clone(),
});
started
}
pub(in super::super) fn end(conn: &mut WlConn) {
if let Some(old) = conn.clip.drag_out.take() {
let _ = conn.send(old.source, SOURCE_DESTROY, &[]);
let _ = conn.stream.flush();
conn.clip.dead_sources.push(old.source);
}
}
pub(super) fn handle(conn: &mut WlConn, object: u32, opcode: u16, args: &[u8]) -> bool {
let Some(drag) = conn.clip.drag_out.as_ref().filter(|d| d.source == object) else {
return false;
};
match opcode {
SOURCE_SEND => {
let mime = read_str(args, 0).map(|(m, _)| m).unwrap_or_default();
let data = bytes(&drag.data, &mime);
if let Some(fd) = conn.fds.pop_front() {
match data {
Some(data) => serve(fd, data),
None => unsafe {
close(fd);
},
}
}
}
SOURCE_CANCELLED | SOURCE_DND_FINISHED => {
super::super::dbg(format_args!(
"dnd source: {}",
if opcode == SOURCE_CANCELLED {
"cancelled"
} else {
"finished"
}
));
end(conn);
}
SOURCE_DND_DROP_PERFORMED => {}
_ => {}
}
true
}
#[cfg(test)]
mod tests {
use super::super::tests::{conn_pair, message, read_request};
use super::super::{O_CLOEXEC, pipe2, word};
use super::*;
use std::io::Read;
use std::os::fd::FromRawFd;
#[test]
fn offers_by_kind_and_converts() {
let files = DropData::Files(vec!["/tmp/a b".into()]);
assert_eq!(mimes(&files), &[URI_LIST]);
assert_eq!(
bytes(&files, URI_LIST).unwrap(),
b"file:///tmp/a%20b\r\n".to_vec()
);
assert_eq!(bytes(&files, "text/plain"), None);
let text = DropData::Text("h\u{e9}".into());
assert_eq!(bytes(&text, "UTF8_STRING").unwrap(), "h\u{e9}".as_bytes());
assert_eq!(bytes(&text, "STRING").unwrap(), vec![b'h', 0xe9]);
assert!(mimes(&DropData::Internal("x".into())).is_empty());
}
#[test]
fn starts_a_drag_serves_a_send_and_ends_on_finish() {
let (mut conn, compositor) = conn_pair();
conn.clip.device = 10;
conn.clip.manager = 9;
conn.clip.version = 3;
let files = DropData::Files(vec!["/tmp/out.txt".into()]);
assert!(!start(&mut conn, 3, &files));
conn.clip.press_serial = Some(42);
assert!(start(&mut conn, 3, &files));
let source = conn.clip.drag_out.as_ref().unwrap().source;
let (object, opcode, args, _) = read_request(&compositor);
assert_eq!(
(object, opcode, word(&args, 0)),
(9, MANAGER_CREATE_DATA_SOURCE, source)
);
let (object, opcode, args, _) = read_request(&compositor);
assert_eq!((object, opcode), (source, SOURCE_OFFER));
assert_eq!(read_str(&args, 0).unwrap().0, URI_LIST);
let (object, opcode, args, _) = read_request(&compositor);
assert_eq!(
(object, opcode, word(&args, 0)),
(source, SOURCE_SET_ACTIONS, 1)
);
let (object, opcode, args, _) = read_request(&compositor);
assert_eq!((object, opcode), (10, DEVICE_START_DRAG));
assert_eq!([0, 4, 8, 12].map(|i| word(&args, i)), [source, 3, 0, 42]);
let mut ends = [0i32; 2];
assert_eq!(unsafe { pipe2(ends.as_mut_ptr(), O_CLOEXEC) }, 0);
let mut args = Vec::new();
arg_str(&mut args, URI_LIST);
let sent = message(source, SOURCE_SEND, &args);
crate::platform::scm::send_with_fds(&compositor, &sent, &[ends[1]]).unwrap();
unsafe { close(ends[1]) };
let (object, opcode, args) = conn.recv().unwrap();
assert!(super::super::handle(&mut conn, object, opcode, &args));
let mut got = String::new();
let mut reader = unsafe { std::fs::File::from_raw_fd(ends[0]) };
reader.read_to_string(&mut got).unwrap();
assert_eq!(got, "file:///tmp/out.txt\r\n");
(&compositor)
.write_all(&message(source, SOURCE_DND_FINISHED, &[]))
.unwrap();
let (object, opcode, args) = conn.recv().unwrap();
assert!(super::super::handle(&mut conn, object, opcode, &args));
assert!(conn.clip.drag_out.is_none());
assert!(conn.clip.dead_sources.contains(&source));
let (object, opcode, _, _) = read_request(&compositor);
assert_eq!((object, opcode), (source, SOURCE_DESTROY));
}
}