872 lines
25 KiB
Rust
872 lines
25 KiB
Rust
use rustix::event::{poll, PollFd, PollFlags};
|
|
use std::mem::ManuallyDrop;
|
|
use std::os::fd::{AsRawFd, BorrowedFd};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, Mutex, Once,
|
|
};
|
|
use std::thread::JoinHandle;
|
|
use std::time::{Duration, Instant};
|
|
use wayland_protocols::xdg::shell::server::xdg_toplevel;
|
|
use wayland_server::Resource;
|
|
use xcb::{x, Xid};
|
|
use xwayland_satellite as xwls;
|
|
use xwayland_satellite::xstate::{WmHintsFlags, WmSizeHintsFlags};
|
|
|
|
#[derive(Default)]
|
|
struct TestDataInner {
|
|
server_created: AtomicBool,
|
|
server_connected: AtomicBool,
|
|
display: Mutex<Option<String>>,
|
|
server: Mutex<Option<UnixStream>>,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct TestData(Arc<TestDataInner>);
|
|
|
|
impl TestData {
|
|
fn new(server: UnixStream) -> Self {
|
|
Self(Arc::new(TestDataInner {
|
|
server: Mutex::new(server.into()),
|
|
..Default::default()
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for TestData {
|
|
type Target = Arc<TestDataInner>;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl xwls::RunData for TestData {
|
|
fn created_server(&self) {
|
|
self.server_created.store(true, Ordering::Relaxed);
|
|
}
|
|
|
|
fn connected_server(&self) {
|
|
self.server_connected.store(true, Ordering::Relaxed);
|
|
}
|
|
|
|
fn xwayland_ready(&self, display: String) {
|
|
*self.display.lock().unwrap() = Some(display);
|
|
}
|
|
|
|
fn display(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
fn server(&self) -> Option<UnixStream> {
|
|
let mut server = self.server.lock().unwrap();
|
|
assert!(server.is_some());
|
|
server.take()
|
|
}
|
|
}
|
|
|
|
struct Fixture {
|
|
testwl: testwl::Server,
|
|
thread: ManuallyDrop<JoinHandle<Option<()>>>,
|
|
pollfd: PollFd<'static>,
|
|
display: String,
|
|
}
|
|
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let thread = unsafe { ManuallyDrop::take(&mut self.thread) };
|
|
if thread.is_finished() {
|
|
thread.join().expect("Main thread panicked");
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
static INIT: Once = Once::new();
|
|
INIT.call_once(|| {
|
|
env_logger::builder()
|
|
.is_test(true)
|
|
.filter_level(log::LevelFilter::Debug)
|
|
.parse_default_env()
|
|
.init();
|
|
});
|
|
|
|
let (a, b) = UnixStream::pair().unwrap();
|
|
let mut testwl = testwl::Server::new(false);
|
|
testwl.connect(a);
|
|
let our_data = TestData::new(b);
|
|
let data = our_data.clone();
|
|
let thread = std::thread::spawn(move || xwls::main(data));
|
|
|
|
// wait for connection
|
|
let fd = unsafe { BorrowedFd::borrow_raw(testwl.poll_fd().as_raw_fd()) };
|
|
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
|
|
assert!(poll(&mut [pollfd.clone()], 100).unwrap() > 0);
|
|
testwl.dispatch();
|
|
|
|
let try_bool_timeout = |b: &AtomicBool| {
|
|
let timeout = Duration::from_secs(1);
|
|
let mut res = b.load(Ordering::Relaxed);
|
|
let start = Instant::now();
|
|
while !res && start.elapsed() < timeout {
|
|
res = b.load(Ordering::Relaxed);
|
|
}
|
|
|
|
res
|
|
};
|
|
assert!(
|
|
try_bool_timeout(&our_data.server_created),
|
|
"creating server"
|
|
);
|
|
assert!(
|
|
try_bool_timeout(&our_data.server_connected),
|
|
"connecting to server"
|
|
);
|
|
|
|
let mut f = [pollfd.clone()];
|
|
let start = std::time::Instant::now();
|
|
// Give Xwayland time to do its thing
|
|
|
|
let mut ready = our_data.display.lock().unwrap().is_some();
|
|
while !ready && start.elapsed() < Duration::from_millis(2000) {
|
|
let n = poll(&mut f, 100).unwrap();
|
|
if n > 0 {
|
|
testwl.dispatch();
|
|
}
|
|
ready = our_data.display.lock().unwrap().is_some();
|
|
}
|
|
|
|
assert!(ready, "connecting to xwayland failed");
|
|
|
|
let display = our_data.display.lock().unwrap().take().unwrap();
|
|
Self {
|
|
testwl,
|
|
thread: ManuallyDrop::new(thread),
|
|
pollfd,
|
|
display,
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn wait_and_dispatch(&mut self) {
|
|
let mut pollfd = [self.pollfd.clone()];
|
|
assert!(
|
|
poll(&mut pollfd, 50).unwrap() > 0,
|
|
"Did not receive any events"
|
|
);
|
|
self.pollfd.clear_revents();
|
|
self.testwl.dispatch();
|
|
|
|
while poll(&mut pollfd, 50).unwrap() > 0 {
|
|
self.testwl.dispatch();
|
|
self.pollfd.clear_revents();
|
|
}
|
|
}
|
|
|
|
fn configure_and_verify_new_toplevel(
|
|
&mut self,
|
|
connection: &mut Connection,
|
|
window: x::Window,
|
|
surface: testwl::SurfaceId,
|
|
) {
|
|
let data = self.testwl.get_surface_data(surface).unwrap();
|
|
assert!(
|
|
matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))),
|
|
"surface role was wrong: {:?}",
|
|
data.role
|
|
);
|
|
|
|
self.testwl
|
|
.configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
|
|
self.wait_and_dispatch();
|
|
let geometry = connection
|
|
.wait_for_reply(connection.send_request(&x::GetGeometry {
|
|
drawable: x::Drawable::Window(window),
|
|
}))
|
|
.unwrap();
|
|
|
|
assert_eq!(geometry.x(), 0);
|
|
assert_eq!(geometry.y(), 0);
|
|
assert_eq!(geometry.width(), 100);
|
|
assert_eq!(geometry.height(), 100);
|
|
}
|
|
|
|
/// Triggers a Wayland side toplevel Close event and processes the corresponding
|
|
/// X11 side WM_DELETE_WINDOW client message
|
|
fn close_toplevel(
|
|
&mut self,
|
|
connection: &mut Connection,
|
|
window: x::Window,
|
|
surface: testwl::SurfaceId,
|
|
) {
|
|
self.testwl.close_toplevel(surface);
|
|
connection.await_event();
|
|
let event = connection
|
|
.inner
|
|
.poll_for_event()
|
|
.unwrap()
|
|
.expect("No close event");
|
|
|
|
let xcb::Event::X(x::Event::ClientMessage(event)) = event else {
|
|
panic!("Expected ClientMessage event, got {event:?}");
|
|
};
|
|
|
|
assert_eq!(event.window(), window);
|
|
assert_eq!(event.format(), 32);
|
|
assert_eq!(event.r#type(), connection.atoms.wm_protocols);
|
|
match event.data() {
|
|
x::ClientMessageData::Data32(d) => {
|
|
assert_eq!(d[0], connection.atoms.wm_delete_window.resource_id())
|
|
}
|
|
other => panic!("wrong data type: {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
xcb::atoms_struct! {
|
|
struct Atoms {
|
|
wm_protocols => b"WM_PROTOCOLS",
|
|
wm_delete_window => b"WM_DELETE_WINDOW",
|
|
clipboard => b"CLIPBOARD",
|
|
targets => b"TARGETS",
|
|
multiple => b"MULTIPLE",
|
|
wm_check => b"_NET_SUPPORTING_WM_CHECK",
|
|
mime1 => b"text/plain" only_if_exists = false,
|
|
mime2 => b"blah/blah" only_if_exists = false,
|
|
}
|
|
}
|
|
|
|
struct Connection {
|
|
inner: xcb::Connection,
|
|
pollfd: PollFd<'static>,
|
|
atoms: Atoms,
|
|
root: x::Window,
|
|
visual: u32,
|
|
}
|
|
|
|
impl std::ops::Deref for Connection {
|
|
type Target = xcb::Connection;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.inner
|
|
}
|
|
}
|
|
|
|
impl Connection {
|
|
fn new(display: &str) -> Self {
|
|
let (inner, _) = xcb::Connection::connect(Some(display)).unwrap();
|
|
let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) };
|
|
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
|
|
let atoms = Atoms::intern_all(&inner).unwrap();
|
|
let screen = inner.get_setup().roots().next().unwrap();
|
|
let root = screen.root();
|
|
let visual = screen.root_visual();
|
|
|
|
Self {
|
|
inner,
|
|
pollfd,
|
|
atoms,
|
|
root,
|
|
visual,
|
|
}
|
|
}
|
|
|
|
fn new_window(
|
|
&self,
|
|
parent: x::Window,
|
|
x: i16,
|
|
y: i16,
|
|
width: u16,
|
|
height: u16,
|
|
override_redirect: bool,
|
|
) -> x::Window {
|
|
let wid = self.inner.generate_id();
|
|
let req = x::CreateWindow {
|
|
depth: 0,
|
|
wid,
|
|
parent,
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
border_width: 0,
|
|
class: x::WindowClass::InputOutput,
|
|
visual: self.visual,
|
|
value_list: &[x::Cw::OverrideRedirect(override_redirect)],
|
|
};
|
|
self.inner
|
|
.send_and_check_request(&req)
|
|
.expect("creating window failed");
|
|
|
|
wid
|
|
}
|
|
|
|
#[track_caller]
|
|
fn map_window(&self, window: x::Window) {
|
|
self.send_and_check_request(&x::MapWindow { window })
|
|
.unwrap();
|
|
}
|
|
|
|
#[track_caller]
|
|
fn destroy_window(&self, window: x::Window) {
|
|
self.send_and_check_request(&x::DestroyWindow { window })
|
|
.unwrap();
|
|
}
|
|
|
|
#[track_caller]
|
|
fn set_property<P: x::PropEl>(
|
|
&self,
|
|
window: x::Window,
|
|
r#type: x::Atom,
|
|
property: x::Atom,
|
|
data: &[P],
|
|
) {
|
|
self.send_and_check_request(&x::ChangeProperty {
|
|
mode: x::PropMode::Replace,
|
|
window,
|
|
r#type,
|
|
property,
|
|
data,
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[track_caller]
|
|
fn await_event(&mut self) {
|
|
assert!(
|
|
poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0,
|
|
"Did not get any X11 events"
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn get_reply<R: xcb::Request>(
|
|
&self,
|
|
req: &R,
|
|
) -> <R::Cookie as xcb::CookieWithReplyChecked>::Reply
|
|
where
|
|
R::Cookie: xcb::CookieWithReplyChecked,
|
|
{
|
|
self.wait_for_reply(self.send_request(req)).unwrap()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn toplevel_flow() {
|
|
let mut f = Fixture::new();
|
|
let mut connection = Connection::new(&f.display);
|
|
let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
|
|
|
|
// Pre-map properties
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_NAME,
|
|
c"window".to_bytes(),
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_CLASS,
|
|
&[
|
|
c"instance".to_bytes_with_nul(),
|
|
c"class".to_bytes_with_nul(),
|
|
]
|
|
.concat(),
|
|
);
|
|
|
|
let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_WM_SIZE_HINTS,
|
|
x::ATOM_WM_NORMAL_HINTS,
|
|
&[flags, 0, 0, 0, 0, 50, 100, 300, 400],
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_NAME,
|
|
c"window".to_bytes(),
|
|
);
|
|
connection.map_window(window);
|
|
f.wait_and_dispatch();
|
|
|
|
let surface = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created!");
|
|
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
|
|
|
let data = f.testwl.get_surface_data(surface).unwrap();
|
|
assert_eq!(data.toplevel().title, Some("window".into()));
|
|
assert_eq!(data.toplevel().app_id, Some("class".into()));
|
|
assert_eq!(
|
|
data.toplevel().min_size,
|
|
Some(testwl::Vec2 { x: 50, y: 100 })
|
|
);
|
|
assert_eq!(
|
|
data.toplevel().max_size,
|
|
Some(testwl::Vec2 { x: 300, y: 400 })
|
|
);
|
|
|
|
// Post map properties
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_NAME,
|
|
c"bindow".to_bytes(),
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_CLASS,
|
|
&[c"boink".to_bytes()].concat(),
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_WM_SIZE_HINTS,
|
|
x::ATOM_WM_NORMAL_HINTS,
|
|
&[flags, 1, 2, 3, 4, 25, 50, 150, 200],
|
|
);
|
|
f.wait_and_dispatch();
|
|
let data = f.testwl.get_surface_data(surface).unwrap();
|
|
let toplevel = data.toplevel().toplevel.clone();
|
|
assert_eq!(data.toplevel().title, Some("bindow".into()));
|
|
assert_eq!(data.toplevel().app_id, Some("boink".into()));
|
|
assert_eq!(
|
|
data.toplevel().min_size,
|
|
Some(testwl::Vec2 { x: 25, y: 50 })
|
|
);
|
|
assert_eq!(
|
|
data.toplevel().max_size,
|
|
Some(testwl::Vec2 { x: 150, y: 200 })
|
|
);
|
|
|
|
f.close_toplevel(&mut connection, window, surface);
|
|
|
|
// Simulate killing client
|
|
drop(connection);
|
|
f.wait_and_dispatch();
|
|
|
|
assert!(!toplevel.is_alive());
|
|
}
|
|
|
|
#[test]
|
|
fn reparent() {
|
|
let mut f = Fixture::new();
|
|
let mut connection = Connection::new(&f.display);
|
|
|
|
let parent = connection.new_window(connection.root, 0, 0, 1, 1, false);
|
|
let child = connection.new_window(parent, 0, 0, 20, 20, false);
|
|
|
|
connection
|
|
.send_and_check_request(&x::ReparentWindow {
|
|
window: child,
|
|
parent: connection.root,
|
|
x: 0,
|
|
y: 0,
|
|
})
|
|
.unwrap();
|
|
|
|
connection.map_window(child);
|
|
f.wait_and_dispatch();
|
|
let surface = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created!");
|
|
f.configure_and_verify_new_toplevel(&mut connection, child, surface);
|
|
}
|
|
|
|
#[test]
|
|
fn input_focus() {
|
|
let mut f = Fixture::new();
|
|
let mut connection = Connection::new(&f.display);
|
|
|
|
fn tst(
|
|
f: &mut Fixture,
|
|
connection: &mut Connection,
|
|
set_props: impl FnOnce(&mut Connection, x::Window),
|
|
check: impl FnOnce(/* win: */ x::Window, /* focus: */ x::Window),
|
|
) {
|
|
let win = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
|
set_props(connection, win);
|
|
connection.map_window(win);
|
|
f.wait_and_dispatch();
|
|
let surface = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created!");
|
|
f.configure_and_verify_new_toplevel(connection, win, surface);
|
|
|
|
let focus = connection
|
|
.wait_for_reply(connection.send_request(&x::GetInputFocus {}))
|
|
.unwrap()
|
|
.focus();
|
|
check(win, focus);
|
|
|
|
f.close_toplevel(connection, win, surface);
|
|
}
|
|
|
|
// Input field unset
|
|
tst(
|
|
&mut f,
|
|
&mut connection,
|
|
|_, _| {},
|
|
|win, focus| assert_eq!(win, focus),
|
|
);
|
|
|
|
// Input field set to false
|
|
tst(
|
|
&mut f,
|
|
&mut connection,
|
|
|connection, win| {
|
|
connection.set_property(
|
|
win,
|
|
x::ATOM_WM_HINTS,
|
|
x::ATOM_WM_HINTS,
|
|
&[WmHintsFlags::Input.bits(), 0],
|
|
);
|
|
},
|
|
|win, focus| assert_ne!(win, focus),
|
|
);
|
|
|
|
// Input field set to true
|
|
tst(
|
|
&mut f,
|
|
&mut connection,
|
|
|connection, win| {
|
|
connection.set_property(
|
|
win,
|
|
x::ATOM_WM_HINTS,
|
|
x::ATOM_WM_HINTS,
|
|
&[WmHintsFlags::Input.bits(), 1],
|
|
);
|
|
},
|
|
|win, focus| assert_eq!(win, focus),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quick_delete() {
|
|
let mut f = Fixture::new();
|
|
let connection = Connection::new(&f.display);
|
|
|
|
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
|
connection.map_window(window);
|
|
f.wait_and_dispatch();
|
|
let surf = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created");
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_WM_HINTS,
|
|
x::ATOM_WM_HINTS,
|
|
&[WmHintsFlags::Input.bits(), 0],
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_NAME,
|
|
c"bindow".to_bytes(),
|
|
);
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_STRING,
|
|
x::ATOM_WM_CLASS,
|
|
&[c"f".to_bytes_with_nul(), c"ssalc".to_bytes_with_nul()].concat(),
|
|
);
|
|
let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
|
|
connection.set_property(
|
|
window,
|
|
x::ATOM_WM_SIZE_HINTS,
|
|
x::ATOM_WM_NORMAL_HINTS,
|
|
&[flags, 1, 2, 3, 4, 25, 50, 150, 200],
|
|
);
|
|
connection
|
|
.send_and_check_request(&x::ConfigureWindow {
|
|
window,
|
|
value_list: &[x::ConfigWindow::X(10), x::ConfigWindow::Y(40)],
|
|
})
|
|
.unwrap();
|
|
f.testwl
|
|
.configure_toplevel(surf, 100, 100, vec![xdg_toplevel::State::Activated]);
|
|
connection.destroy_window(window);
|
|
f.wait_and_dispatch();
|
|
|
|
assert_eq!(f.testwl.get_surface_data(surf), None);
|
|
}
|
|
|
|
// aaaaaaaaaa
|
|
#[test]
|
|
fn copy_from_x11() {
|
|
let mut f = Fixture::new();
|
|
let mut connection = Connection::new(&f.display);
|
|
|
|
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
|
connection.map_window(window);
|
|
f.wait_and_dispatch();
|
|
let surface = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created");
|
|
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
|
|
|
// set data
|
|
connection
|
|
.send_and_check_request(&x::SetSelectionOwner {
|
|
owner: window,
|
|
selection: connection.atoms.clipboard,
|
|
time: x::CURRENT_TIME,
|
|
})
|
|
.unwrap();
|
|
let owner = connection
|
|
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
|
|
selection: connection.atoms.clipboard,
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(window, owner.owner());
|
|
|
|
// wait for request to come through
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
let request = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
|
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
|
};
|
|
|
|
assert_eq!(request.target(), connection.atoms.targets);
|
|
connection.set_property(
|
|
request.requestor(),
|
|
x::ATOM_ATOM,
|
|
request.property(),
|
|
&[connection.atoms.mime1, connection.atoms.mime2],
|
|
);
|
|
connection
|
|
.send_and_check_request(&x::SendEvent {
|
|
propagate: false,
|
|
destination: x::SendEventDest::Window(request.requestor()),
|
|
event_mask: x::EventMask::empty(),
|
|
event: &x::SelectionNotifyEvent::new(
|
|
request.time(),
|
|
request.requestor(),
|
|
request.selection(),
|
|
request.target(),
|
|
request.property(),
|
|
),
|
|
})
|
|
.unwrap();
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
let request = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
|
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
|
};
|
|
|
|
assert_eq!(request.target(), connection.atoms.multiple);
|
|
let pairs = connection
|
|
.wait_for_reply(connection.send_request(&x::GetProperty {
|
|
delete: true,
|
|
window: request.requestor(),
|
|
property: request.property(),
|
|
r#type: x::ATOM_ATOM,
|
|
long_offset: 0,
|
|
long_length: 4,
|
|
}))
|
|
.unwrap();
|
|
|
|
let pairs: &[x::Atom] = pairs.value();
|
|
assert_eq!(pairs.len(), 4);
|
|
assert!(pairs.contains(&connection.atoms.mime1));
|
|
assert!(pairs.contains(&connection.atoms.mime2));
|
|
|
|
let mime1data = b"hello world";
|
|
let mime2data = &[1u8, 2, 3, 4];
|
|
for [target, property] in pairs
|
|
.chunks_exact(2)
|
|
.map(|pair| <[x::Atom; 2]>::try_from(pair).unwrap())
|
|
{
|
|
match target {
|
|
x if x == connection.atoms.mime1 => {
|
|
connection.set_property(request.requestor(), x::ATOM_STRING, property, mime1data);
|
|
}
|
|
x if x == connection.atoms.mime2 => {
|
|
connection.set_property(request.requestor(), x::ATOM_INTEGER, property, mime2data);
|
|
}
|
|
_ => panic!("unexpected target: {target:?}"),
|
|
}
|
|
}
|
|
|
|
connection
|
|
.send_and_check_request(&x::SendEvent {
|
|
propagate: false,
|
|
destination: x::SendEventDest::Window(request.requestor()),
|
|
event_mask: x::EventMask::empty(),
|
|
event: &x::SelectionNotifyEvent::new(
|
|
request.time(),
|
|
request.requestor(),
|
|
request.selection(),
|
|
request.target(),
|
|
request.property(),
|
|
),
|
|
})
|
|
.unwrap();
|
|
|
|
f.wait_and_dispatch();
|
|
|
|
let owner = connection
|
|
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
|
|
selection: connection.atoms.clipboard,
|
|
}))
|
|
.unwrap();
|
|
assert_ne!(window, owner.owner());
|
|
|
|
let mimes = f.testwl.data_source_mimes();
|
|
assert!(mimes.contains(&"text/plain".into())); // mime1
|
|
assert!(mimes.contains(&"blah/blah".into())); // mime2
|
|
|
|
let data = f.testwl.paste_data();
|
|
f.testwl.dispatch();
|
|
let data = data.resolve();
|
|
for testwl::PasteData { mime_type, data } in data {
|
|
match mime_type {
|
|
x if x == "text/plain" => {
|
|
assert_eq!(&data, mime1data);
|
|
}
|
|
x if x == "blah/blah" => {
|
|
assert_eq!(&data, mime2data);
|
|
}
|
|
other => panic!("unexpected mime type: {other} ({data:?})"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn copy_from_wayland() {
|
|
let mut f = Fixture::new();
|
|
let mut connection = Connection::new(&f.display);
|
|
|
|
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
|
connection.map_window(window);
|
|
f.wait_and_dispatch();
|
|
let surface = f
|
|
.testwl
|
|
.last_created_surface_id()
|
|
.expect("No surface created");
|
|
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
|
let offer = vec![
|
|
testwl::PasteData {
|
|
mime_type: "text/plain".into(),
|
|
data: b"boingloings".to_vec(),
|
|
},
|
|
testwl::PasteData {
|
|
mime_type: "yah/hah".into(),
|
|
data: vec![1, 2, 3, 2, 1],
|
|
},
|
|
];
|
|
|
|
f.testwl.create_data_offer(offer.clone());
|
|
|
|
let wm_window: x::Window = connection
|
|
.get_reply(&x::GetProperty {
|
|
delete: false,
|
|
window: connection.root,
|
|
property: connection.atoms.wm_check,
|
|
r#type: x::ATOM_WINDOW,
|
|
long_offset: 0,
|
|
long_length: 1,
|
|
})
|
|
.value()[0];
|
|
|
|
let reply = connection.get_reply(&x::GetSelectionOwner {
|
|
selection: connection.atoms.clipboard,
|
|
});
|
|
assert_eq!(reply.owner(), wm_window);
|
|
let dest1_atom = connection
|
|
.get_reply(&x::InternAtom {
|
|
name: b"dest1",
|
|
only_if_exists: false,
|
|
})
|
|
.atom();
|
|
|
|
// I don't know why, but omitting this little sleep prevents the SelectionRequest notification
|
|
// from being sent, and I don't have the heart to determine why.
|
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
connection
|
|
.send_and_check_request(&x::ConvertSelection {
|
|
requestor: window,
|
|
selection: connection.atoms.clipboard,
|
|
target: connection.atoms.targets,
|
|
property: dest1_atom,
|
|
time: x::CURRENT_TIME,
|
|
})
|
|
.unwrap();
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
let request = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
|
|
other => panic!("Didn't get selection notify event, instead got {other:?}"),
|
|
};
|
|
|
|
assert_eq!(request.requestor(), window);
|
|
assert_eq!(request.selection(), connection.atoms.clipboard);
|
|
assert_eq!(request.target(), connection.atoms.targets);
|
|
assert_eq!(request.property(), dest1_atom);
|
|
|
|
let reply = connection.get_reply(&x::GetProperty {
|
|
delete: true,
|
|
window,
|
|
property: dest1_atom,
|
|
r#type: x::ATOM_ATOM,
|
|
long_offset: 0,
|
|
long_length: 10,
|
|
});
|
|
let targets: &[x::Atom] = reply.value();
|
|
assert_eq!(targets.len(), 2);
|
|
|
|
for testwl::PasteData { mime_type, data } in offer {
|
|
let atom = connection
|
|
.get_reply(&x::InternAtom {
|
|
only_if_exists: true,
|
|
name: mime_type.as_bytes(),
|
|
})
|
|
.atom();
|
|
assert_ne!(atom, x::ATOM_NONE);
|
|
assert!(targets.contains(&atom));
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
connection
|
|
.send_and_check_request(&x::ConvertSelection {
|
|
requestor: window,
|
|
selection: connection.atoms.clipboard,
|
|
target: atom,
|
|
property: dest1_atom,
|
|
time: x::CURRENT_TIME,
|
|
})
|
|
.unwrap();
|
|
|
|
f.wait_and_dispatch();
|
|
let request = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
|
|
other => panic!("Didn't get selection notify event, instead got {other:?}"),
|
|
};
|
|
|
|
assert_eq!(request.requestor(), window);
|
|
assert_eq!(request.selection(), connection.atoms.clipboard);
|
|
assert_eq!(request.target(), atom);
|
|
assert_eq!(request.property(), dest1_atom);
|
|
|
|
let val: Vec<u8> = connection
|
|
.get_reply(&x::GetProperty {
|
|
delete: true,
|
|
window,
|
|
property: dest1_atom,
|
|
r#type: x::ATOM_ANY,
|
|
long_offset: 0,
|
|
long_length: 10,
|
|
})
|
|
.value()
|
|
.to_vec();
|
|
|
|
assert_eq!(val, data);
|
|
}
|
|
}
|