use rustix::event::{PollFd, PollFlags, poll}; use rustix::process::{Pid, Signal, WaitOptions}; use std::collections::HashMap; use std::io::Write; use std::mem::ManuallyDrop; use std::os::fd::{AsRawFd, BorrowedFd, OwnedFd}; use std::os::unix::net::UnixStream; use std::sync::{ Arc, Mutex, Once, atomic::{AtomicBool, Ordering}, }; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use wayland_protocols::xdg::{ decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server::xdg_toplevel, }; use wayland_server::Resource; use wayland_server::protocol::{wl_output, wl_pointer}; use xcb::{Xid, x}; use xwayland_satellite as xwls; use xwayland_satellite::xstate::{MoveResizeDirection, WmSizeHintsFlags, WmState}; use xwls::timespec_from_millis; #[derive(Default)] struct TestDataInner { server_created: AtomicBool, server_connected: AtomicBool, display: Mutex>, server: Mutex>, pid: Mutex>, quit_rx: Mutex>, } #[derive(Default, Clone)] struct TestData(Arc); impl TestData { fn new(server: UnixStream, quit_rx: UnixStream) -> Self { Self(Arc::new(TestDataInner { server: Mutex::new(server.into()), quit_rx: Mutex::new(Some(quit_rx)), ..Default::default() })) } } impl std::ops::Deref for TestData { type Target = Arc; 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 quit_rx(&self) -> Option { self.quit_rx.lock().unwrap().take() } fn xwayland_ready(&self, display: String, pid: u32) { *self.display.lock().unwrap() = Some(display); *self.pid.lock().unwrap() = Some(pid); } fn display(&self) -> Option<&str> { None } fn listenfds(&mut self) -> Vec { Vec::new() } fn server(&self) -> Option { let mut server = self.server.lock().unwrap(); assert!(server.is_some()); server.take() } fn max_req_len_bytes(&self) -> Option { Some(500) } } struct Fixture { testwl: testwl::Server, thread: ManuallyDrop>>, pollfd: PollFd<'static>, display: String, pid: Pid, quit_tx: UnixStream, } impl Drop for Fixture { fn drop(&mut self) { let thread = unsafe { ManuallyDrop::take(&mut self.thread) }; // Sending anything to the quit receiver to stop the main loop. Then we guarantee a main // thread does not use file descriptors which outlive the Fixture's BorrowedFd let return_ptr = Box::into_raw(Box::new(0_usize)) as usize; // If the receiver end of the pipe closed, the main thread dropped it, which means that // thread already terminated if self .quit_tx .write_all(&return_ptr.to_ne_bytes()) .is_err_and(|e| e.kind() != std::io::ErrorKind::BrokenPipe) { panic!("could not message the main thread to terminate"); } if thread.join().is_err() { log::error!("main thread panicked"); } if rustix::process::test_kill_process(self.pid).is_ok() { rustix::process::kill_process(self.pid, Signal::TERM).unwrap(); rustix::process::waitpid(Some(self.pid), WaitOptions::NOHANG).unwrap(); } } } impl Fixture { fn new_preset(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self { static INIT: Once = Once::new(); INIT.call_once(|| { pretty_env_logger::formatted_timed_builder() .is_test(true) .filter_level(log::LevelFilter::Debug) .parse_default_env() .init(); }); let (quit_tx, quit_rx) = UnixStream::pair().unwrap(); let (a, b) = UnixStream::pair().unwrap(); let mut testwl = testwl::Server::new(false); pre_connect(&mut testwl); testwl.connect(a); let our_data = TestData::new(b, quit_rx); 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); let timeout = timespec_from_millis(1000); assert!(poll(&mut [pollfd.clone()], Some(&timeout)).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 timeout = timespec_from_millis(100); let n = poll(&mut f, Some(&timeout)).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(); let pid = our_data.pid.lock().unwrap().take().unwrap(); Self { testwl, thread: ManuallyDrop::new(thread), pollfd, display, pid: Pid::from_raw(pid as _).expect("Xwayland PID was invalid?"), quit_tx, } } fn new() -> Self { Self::new_preset(|_| {}) } #[track_caller] fn wait_and_dispatch(&mut self) { let mut pollfd = [self.pollfd.clone()]; self.testwl.dispatch(); let timeout = timespec_from_millis(50); assert!( poll(&mut pollfd, Some(&timeout)).unwrap() > 0, "Did not receive any events" ); self.pollfd.clear_revents(); self.testwl.dispatch(); while poll(&mut pollfd, Some(&timeout)).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.testwl.focus_toplevel(surface); self.wait_and_dispatch(); let geometry = connection.get_reply(&x::GetGeometry { drawable: x::Drawable::Window(window), }); assert_eq!(geometry.x(), 0); assert_eq!(geometry.y(), 0); assert_eq!(geometry.width(), 100); assert_eq!(geometry.height(), 100); } #[track_caller] fn map_as_toplevel( &mut self, connection: &mut Connection, window: x::Window, ) -> testwl::SurfaceId { connection.map_window(window); self.wait_and_dispatch(); let surface = self .testwl .last_created_surface_id() .expect("No surface created"); self.configure_and_verify_new_toplevel(connection, window, surface); surface } #[track_caller] fn map_as_popup( &mut self, connection: &mut Connection, window: x::Window, ) -> testwl::SurfaceId { connection.map_window(window); self.wait_and_dispatch(); let surface = self .testwl .last_created_surface_id() .expect("No surface created"); let data = self.testwl.get_surface_data(surface).unwrap(); assert!( matches!(data.role, Some(testwl::SurfaceRole::Popup(_))), "surface role was wrong: {:?}", data.role ); self.testwl.configure_popup(surface); self.wait_and_dispatch(); surface } /// Triggers a Wayland side toplevel Close event and processes the corresponding /// X11 side WM_DELETE_WINDOW client message fn wm_delete_window( &mut self, connection: &mut Connection, window: x::Window, surface: testwl::SurfaceId, ) { connection.set_property( window, x::ATOM_ATOM, connection.atoms.wm_protocols, &[connection.atoms.wm_delete_window], ); self.testwl.close_toplevel(surface); let event = connection.await_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:?}"), } } fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput { self.testwl.new_output(x, y); self.wait_and_dispatch(); self.testwl.finalize_output() } } xcb::atoms_struct! { struct Atoms { wm_protocols => b"WM_PROTOCOLS", net_active_window => b"_NET_ACTIVE_WINDOW", wm_delete_window => b"WM_DELETE_WINDOW", net_wm_state => b"_NET_WM_STATE", skip_taskbar => b"_NET_WM_STATE_SKIP_TASKBAR", transient_for => b"WM_TRANSIENT_FOR", clipboard => b"CLIPBOARD", primary => b"PRIMARY", targets => b"TARGETS", multiple => b"MULTIPLE", wm_state => b"WM_STATE", wm_check => b"_NET_SUPPORTING_WM_CHECK", win_type => b"_NET_WM_WINDOW_TYPE", win_type_normal => b"_NET_WM_WINDOW_TYPE_NORMAL", win_type_menu => b"_NET_WM_WINDOW_TYPE_MENU", win_type_popup_menu => b"_NET_WM_WINDOW_TYPE_POPUP_MENU", win_type_dropdown_menu => b"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", win_type_tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP", win_type_utility => b"_NET_WM_WINDOW_TYPE_UTILITY", win_type_dnd => b"_NET_WM_WINDOW_TYPE_DND", win_type_combo => b"_NET_WM_WINDOW_TYPE_COMBO", motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false, wm_hints => b"WM_HINTS", mime1 => b"text/plain" only_if_exists = false, mime2 => b"blah/blah" only_if_exists = false, incr => b"INCR", xsettings => b"_XSETTINGS_S0", xsettings_setting => b"_XSETTINGS_SETTINGS", moveresize => b"_NET_WM_MOVERESIZE", } } struct Settings { serial: u32, data: HashMap, } struct Setting { value: i32, last_change: u32, } struct Connection { inner: xcb::Connection, pollfd: PollFd<'static>, atoms: Atoms, root: x::Window, visual: u32, wm_window: x::Window, } 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_with_extensions(Some(display), &[xcb::Extension::XFixes], &[]) .unwrap(); // xfixes init let reply = inner .wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion { client_major_version: 1, client_minor_version: 0, })) .unwrap(); assert_eq!(reply.major_version(), 1); 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(); let wm_window: x::Window = inner .wait_for_reply(inner.send_request(&x::GetProperty { delete: false, window: root, property: atoms.wm_check, r#type: x::ATOM_WINDOW, long_offset: 0, long_length: 1, })) .expect("Couldn't get WM window") .value()[0]; Self { inner, pollfd, atoms, root, visual, wm_window, } } 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 unmap_window(&self, window: x::Window) { self.send_and_check_request(&x::UnmapWindow { 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( &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] #[must_use] fn await_event(&mut self) -> xcb::Event { if let Some(event) = self.poll_for_event().expect("Failed to poll for event") { return event; } let timeout = timespec_from_millis(100); assert!( poll(&mut [self.pollfd.clone()], Some(&timeout)).expect("poll failed") > 0, "Did not get any X11 events" ); self.pollfd.clear_revents(); self.poll_for_event() .expect("Failed to poll for event after pollfd") .unwrap() } #[track_caller] fn get_reply( &self, req: &R, ) -> ::Reply where R::Cookie: xcb::CookieWithReplyChecked, { self.wait_for_reply(self.send_request(req)).unwrap() } #[track_caller] fn set_selection_owner(&self, window: x::Window, selection: x::Atom) { self.send_and_check_request(&x::SetSelectionOwner { owner: window, selection, time: x::CURRENT_TIME, }) .unwrap(); let owner = self.get_reply(&x::GetSelectionOwner { selection }); assert_eq!(window, owner.owner(), "Unexpected selection owner"); } #[track_caller] fn await_selection_request(&mut self) -> x::SelectionRequestEvent { match self.await_event() { xcb::Event::X(x::Event::SelectionRequest(r)) => r, other => panic!("Didn't get selection request event, instead got {other:?}"), } } #[track_caller] fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent { match self.await_event() { xcb::Event::X(x::Event::SelectionNotify(r)) => r, other => panic!("Didn't get selection notify event, instead got {other:?}"), } } #[track_caller] fn send_selection_notify(&self, request: &x::SelectionRequestEvent) { self.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(); } #[track_caller] fn await_property_notify(&mut self) -> x::PropertyNotifyEvent { match self.await_event() { xcb::Event::X(x::Event::PropertyNotify(r)) => r, other => panic!("Didn't get property notify event, instead got {other:?}"), } } #[track_caller] fn get_property_change_events(&self, window: x::Window) { self.send_and_check_request(&x::ChangeWindowAttributes { window, value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)], }) .unwrap(); } #[track_caller] fn verify_clipboard_owner(&self, window: x::Window) { let owner = self.get_reply(&x::GetSelectionOwner { selection: self.atoms.clipboard, }); assert_eq!(owner.owner(), window, "Clipboard owner does not match"); } #[track_caller] fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent { match self.await_event() { xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => e, other => panic!("Expected XFixes SelectionNotify, got {other:?}"), } } #[track_caller] fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) { let event_mask = if enable { xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER } else { xcb::xfixes::SelectionEventMask::empty() }; self.send_and_check_request(&xcb::xfixes::SelectSelectionInput { window, selection: self.atoms.clipboard, event_mask, }) .unwrap(); } #[track_caller] fn send_client_message(&self, request: &x::ClientMessageEvent) { self.send_and_check_request(&x::SendEvent { propagate: false, destination: x::SendEventDest::Window(self.root), event_mask: x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::SUBSTRUCTURE_REDIRECT, event: request, }) .unwrap(); } fn get_xsettings(&self) -> Settings { let owner = self .get_reply(&x::GetSelectionOwner { selection: self.atoms.xsettings, }) .owner(); let reply = self.get_reply(&x::GetProperty { delete: false, window: owner, property: self.atoms.xsettings_setting, r#type: self.atoms.xsettings_setting, long_offset: 0, long_length: 60, }); assert_eq!(reply.r#type(), self.atoms.xsettings_setting); let data = reply.value::(); let byte_order = data[0]; assert_eq!(byte_order, 0); let serial = u32::from_le_bytes(data[4..8].try_into().unwrap()); let num_settings = u32::from_le_bytes(data[8..12].try_into().unwrap()); let mut current_idx = 12; let mut settings = HashMap::new(); for _ in 0..num_settings { assert_eq!(&data[current_idx..current_idx + 2], &[0, 0]); let name_len = u16::from_le_bytes(data[current_idx + 2..current_idx + 4].try_into().unwrap()); let padding_start = current_idx + 4 + name_len as usize; let name = String::from_utf8(data[current_idx + 4..padding_start].to_vec()).unwrap(); let num_padding_bytes = (4 - (name_len as usize % 4)) % 4; let data_start = padding_start + num_padding_bytes; let last_change = u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap()); let value = i32::from_le_bytes(data[data_start + 4..data_start + 8].try_into().unwrap()); settings.insert(name, Setting { value, last_change }); current_idx = data_start + 8; } Settings { serial, data: settings, } } } #[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 reply = connection.get_reply(&x::GetProperty { delete: false, window, property: connection.atoms.wm_state, r#type: connection.atoms.wm_state, long_offset: 0, long_length: 1, }); assert_eq!(reply.value::(), &[WmState::Normal as u32]); 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(), ); 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 }) ); connection.unmap_window(window); f.wait_and_dispatch(); let reply = connection.get_reply(&x::GetProperty { delete: false, window, property: connection.atoms.wm_state, r#type: connection.atoms.wm_state, long_offset: 0, long_length: 1, }); assert_eq!(reply.value::(), &[WmState::Withdrawn as u32]); 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); f.wm_delete_window(&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 window_properties_after_reparent() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let child = connection.new_window(connection.root, 0, 0, 1, 1, true); connection.map_window(child); f.wait_and_dispatch(); let child_surface = f .testwl .last_created_surface_id() .expect("No surface created!"); f.configure_and_verify_new_toplevel(&mut connection, child, child_surface); let other = connection.new_window(connection.root, 0, 0, 100, 100, false); connection.map_window(other); f.wait_and_dispatch(); let other_surface = f .testwl .last_created_surface_id() .expect("No surface created!"); f.configure_and_verify_new_toplevel(&mut connection, other, other_surface); connection.send_request(&x::UnmapWindow { window: child }); let parent = connection.new_window(connection.root, 0, 0, 20, 20, false); connection.send_request(&x::ReparentWindow { window: child, parent, x: 0, y: 0, }); // The server should get the notifications for these properties and shouldn't crash connection.set_property( child, x::ATOM_WM_SIZE_HINTS, x::ATOM_WM_NORMAL_HINTS, &[16u32, 0, 0, 0, 0, 200, 400], ); connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0"); connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_CLASS, c"class".to_bytes()); f.wait_and_dispatch(); } #[test] fn input_focus() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let wm_state = connection.atoms.wm_state; let conn = std::cell::RefCell::new(&mut connection); let check_focus = |win: x::Window| { let connection = conn.borrow(); let focus = connection.get_reply(&x::GetInputFocus {}).focus(); assert_eq!(win, focus); let reply = connection.get_reply(&x::GetProperty { delete: false, window: connection.root, property: connection.atoms.net_active_window, r#type: x::ATOM_WINDOW, long_offset: 0, long_length: 1, }); assert_eq!(&[win], reply.value::()); }; let mut create_win = || { let mut connection = conn.borrow_mut(); let win = connection.new_window(connection.root, 0, 0, 20, 20, false); 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(&mut connection, win, surface); (win, surface) }; let (win1, surface1) = create_win(); check_focus(win1); let (win2, surface2) = create_win(); check_focus(win2); // Simulate exclusive fullscreen clients that set window state to mimized on focus loss conn.borrow() .set_property(win1, wm_state, wm_state, &[WmState::Iconic as u32, 0]); f.testwl.focus_toplevel(surface1); // Seems the event doesn't get caught by wait_and_dispatch... std::thread::sleep(std::time::Duration::from_millis(10)); check_focus(win1); assert_eq!( conn.borrow() .get_reply(&x::GetProperty { delete: false, window: win1, property: wm_state, r#type: wm_state, long_offset: 0, long_length: 1, }) .value::() .first() .and_then(|state| WmState::try_from(*state).ok()), Some(WmState::Normal) ); f.testwl.unfocus_toplevel(); std::thread::sleep(std::time::Duration::from_millis(10)); check_focus(x::WINDOW_NONE); f.testwl.focus_toplevel(surface2); std::thread::sleep(std::time::Duration::from_millis(10)); check_focus(win2); conn.borrow().destroy_window(win2); f.wait_and_dispatch(); check_focus(x::WINDOW_NONE); f.wm_delete_window(&mut connection, win1, surface1); } #[test] fn activation_x11_to_x11() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let window1 = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface1 = f.map_as_toplevel(&mut connection, window1); let window2 = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface2 = f.map_as_toplevel(&mut connection, window2); f.testwl.focus_toplevel(surface2); std::thread::sleep(Duration::from_millis(10)); connection.send_client_message(&x::ClientMessageEvent::new( window1, connection.atoms.net_active_window, x::ClientMessageData::Data32([2, x::CURRENT_TIME, 0, 0, 0]), )); f.wait_and_dispatch(); assert_eq!(f.testwl.get_focused(), Some(surface1)); } #[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_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); } #[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); f.map_as_toplevel(&mut connection, window); connection.set_selection_owner(window, connection.atoms.primary); connection.set_selection_owner(window, connection.atoms.clipboard); for _ in [connection.atoms.primary, connection.atoms.clipboard] { let request = connection.await_selection_request(); 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_selection_notify(&request); } f.wait_and_dispatch(); struct MimeData { mime: x::Atom, data: testwl::PasteData, } let mimes_truth = [ MimeData { mime: connection.atoms.mime1, data: testwl::PasteData { mime_type: "text/plain".to_string(), data: b"hello world".to_vec(), }, }, MimeData { mime: connection.atoms.mime2, data: testwl::PasteData { mime_type: "blah/blah".to_string(), data: vec![1, 2, 3, 4], }, }, ]; // When requesting both primary and clipboard simultaneously, the first to be requested erases // its TARGETS (which is the correct behavior of a GetProperty request), but the second still // tried to use this data and would come up empty. let primary_mimes = f.testwl.primary_source_mimes(); let clipboard_mimes = f.testwl.data_source_mimes(); assert_eq!( primary_mimes.len(), mimes_truth.len(), "Wrong number of advertised primary mimes: {primary_mimes:?}" ); assert_eq!( clipboard_mimes.len(), mimes_truth.len(), "Wrong number of advertised clipboard mimes: {clipboard_mimes:?}" ); for MimeData { data, .. } in &mimes_truth { assert!( primary_mimes.contains(&data.mime_type), "Missing mime type {}", data.mime_type ); assert!( clipboard_mimes.contains(&data.mime_type), "Missing mime type {}", data.mime_type ); } // Type annotations hint the compiler to use HRTBs (needed since this closure is reused). // See: https://users.rust-lang.org/t/implementation-of-fnonce-is-not-general-enough/68294/3 let mut send_data_for_mime = |mime: &str, _: &mut testwl::Server| { let request = connection.await_selection_request(); let data = mimes_truth .iter() .find(|data| data.data.mime_type == mime) .unwrap_or_else(|| panic!("Asked for unknown mime: {mime}")); connection.set_property( request.requestor(), data.mime, request.property(), &data.data.data, ); connection.send_selection_notify(&request); true }; let clipboard_data = f.testwl.clipboard_paste_data(&mut send_data_for_mime); let primary_data = f.testwl.primary_paste_data(&mut send_data_for_mime); for data in [primary_data, clipboard_data] { let mut found_mimes = Vec::new(); for testwl::PasteData { mime_type, data } in data { match &mime_type { x if x == "text/plain" => { assert_eq!(&data, b"hello world"); } x if x == "blah/blah" => { assert_eq!(&data, &[1, 2, 3, 4]); } other => panic!("unexpected mime type: {other} ({data:?})"), } found_mimes.push(mime_type); } assert!( found_mimes.contains(&"text/plain".to_string()), "Didn't get mime data for text/plain" ); assert!( found_mimes.contains(&"blah/blah".to_string()), "Didn't get mime data for blah/blah" ); } } #[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.get_selection_owner_change_events(true, window); f.map_as_toplevel(&mut connection, window); 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()); connection.await_selection_owner_change(); connection.verify_clipboard_owner(connection.wm_window); connection.get_selection_owner_change_events(false, window); let dest1_atom = connection .get_reply(&x::InternAtom { name: b"dest1", only_if_exists: false, }) .atom(); 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(); let request = connection.await_selection_notify(); 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 = connection.await_selection_notify(); 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 = 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); } } #[test] fn incr_copy_from_wayland() { const BYTES: usize = 3000; 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.get_selection_owner_change_events(true, window); f.map_as_toplevel(&mut connection, window); let mut offer = vec![testwl::PasteData { mime_type: "text/plain".into(), data: std::iter::successors(Some(0u8), |n| Some(n.wrapping_add(1))) .take(BYTES) .collect(), }]; f.testwl.create_data_offer(offer.clone()); connection.await_selection_owner_change(); connection.verify_clipboard_owner(connection.wm_window); connection.get_selection_owner_change_events(false, window); let dest1_atom = connection .get_reply(&x::InternAtom { name: b"dest1", only_if_exists: false, }) .atom(); 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(); let request = connection.await_selection_notify(); 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(), 1); let offer_data = offer.pop().unwrap(); let (mime_type, data) = (offer_data.mime_type, offer_data.data); 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)); 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 = connection.await_selection_notify(); assert_eq!(request.requestor(), window); assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.target(), atom); assert_eq!(request.property(), dest1_atom); connection.get_property_change_events(window); let reply = connection.get_reply(&x::GetProperty { delete: true, window, property: dest1_atom, r#type: x::ATOM_ANY, long_offset: 0, long_length: (BYTES / 4 + BYTES % 4) as u32, }); assert_eq!(reply.r#type(), connection.atoms.incr); assert_eq!(reply.value::().len(), 1); assert!(reply.value::()[0] <= data.len() as u32); let delete_property = connection.await_property_notify(); assert_eq!(delete_property.state(), x::Property::Delete); assert_eq!(delete_property.atom(), dest1_atom); for (idx, chunk) in data.chunks(500).chain([]).enumerate() { let new_property = connection.await_property_notify(); assert_eq!(new_property.state(), x::Property::NewValue, "chunk {idx}"); assert_eq!(new_property.atom(), dest1_atom, "chunk {idx}"); let incr_reply = connection.get_reply(&x::GetProperty { delete: true, window, property: dest1_atom, r#type: x::ATOM_ANY, long_offset: 0, long_length: (BYTES / 4 + BYTES % 4) as u32, }); assert_eq!(incr_reply.r#type(), atom, "chunk {idx}"); assert_eq!(incr_reply.value::(), chunk, "chunk {idx}"); let delete_property = connection.await_property_notify(); assert_eq!(delete_property.state(), x::Property::Delete, "chunk {idx}"); assert_eq!(delete_property.atom(), dest1_atom, "chunk {idx}"); } } // TODO: this test doesn't actually match real behavior for some reason... #[test] fn different_output_position() { let mut f = Fixture::new(); //f.testwl.enable_xdg_output_manager(); let mut connection = Connection::new(&f.display); let window = connection.new_window(connection.root, 0, 0, 200, 200, 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 output = f.create_output(0, 0); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 10.0, 10.0); f.wait_and_dispatch(); let reply = connection.get_reply(&x::QueryPointer { window }); assert!(reply.same_screen()); assert_eq!(reply.win_x(), 10); assert_eq!(reply.win_y(), 10); let output = f.create_output(100, 0); //f.testwl.move_xdg_output(&output, 100, 0); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 150.0, 12.0); f.wait_and_dispatch(); let reply = connection.get_reply(&x::QueryPointer { window }); assert!(reply.same_screen()); assert_eq!(reply.win_x(), 150); assert_eq!(reply.win_y(), 12); } #[test] fn bad_clipboard_data() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let window = connection.new_window(connection.root, 0, 0, 20, 20, false); f.map_as_toplevel(&mut connection, window); connection.set_selection_owner(window, connection.atoms.clipboard); let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); connection.set_property( request.requestor(), x::ATOM_ATOM, request.property(), &[connection.atoms.mime2], ); connection.send_selection_notify(&request); f.wait_and_dispatch(); let mut data = f.testwl.clipboard_paste_data(|_, _| { let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.mime2); // Don't actually set any data as requested - just report success connection.send_selection_notify(&request); true }); connection.verify_clipboard_owner(window); assert_eq!(data.len(), 1, "Unexpected data: {data:?}"); let data = data.pop().unwrap(); assert_eq!(data.mime_type, "blah/blah"); assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data); } // issue #42 #[test] fn funny_window_title() { 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.set_property(window, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0\0\0\0"); 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("title".into())); connection.set_property( window, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0irrelevantdata\0", ); f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!(data.toplevel().title, Some("title".into())); } #[test] fn close_window() { 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); f.wm_delete_window(&mut connection, window, surface); connection .send_and_check_request(&x::DeleteProperty { window, property: connection.atoms.wm_protocols, }) .unwrap(); f.testwl.close_toplevel(surface); f.wait_and_dispatch(); // Connection should no longer work (KillClient) assert!(connection.poll_for_event().is_err()); } // TODO: figure out if the sleeps in this test can be dealt with... #[test] fn primary_output() { let mut f = Fixture::new_preset(|testwl| { testwl.new_output(0, 0); // WL-1 testwl.new_output(500, 500); // WL-2 }); let mut conn = Connection::new(&f.display); let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); let config_timestamp = reply.config_timestamp(); let mut it = reply.outputs().iter().copied().map(|output| { let reply = conn.get_reply(&xcb::randr::GetOutputInfo { output, config_timestamp, }); let name = std::str::from_utf8(reply.name()).unwrap(); ( f.testwl .get_output(name) .unwrap_or_else(|| panic!("Couldn't find output {name}")), output, ) }); let (wl_output1, output1) = it.next().expect("Couldn't find first output"); let (wl_output2, output2) = it.next().expect("Couldn't find second output"); assert_eq!(it.collect::>(), vec![]); let window1 = conn.new_window(conn.root, 0, 0, 20, 20, false); let surface1 = f.map_as_toplevel(&mut conn, window1); f.testwl.move_surface_to_output(surface1, &wl_output1); let window2 = conn.new_window(conn.root, 0, 0, 20, 20, false); std::thread::sleep(std::time::Duration::from_millis(10)); let surface2 = f.map_as_toplevel(&mut conn, window2); f.testwl.move_surface_to_output(surface2, &wl_output2); assert_ne!(surface1, surface2); f.wait_and_dispatch(); f.testwl.focus_toplevel(surface1); std::thread::sleep(std::time::Duration::from_millis(10)); let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); assert_eq!(reply.output(), output1); f.testwl.focus_toplevel(surface2); std::thread::sleep(std::time::Duration::from_millis(10)); let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); assert_eq!(reply.output(), output2); let wl_output3 = f.create_output(24, 46); f.testwl.move_surface_to_output(surface2, &wl_output3); std::thread::sleep(std::time::Duration::from_millis(10)); let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); assert_eq!(reply.outputs().len(), 3); let output3 = reply .outputs() .iter() .copied() .find(|o| ![output1, output2].contains(o)) .unwrap(); let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root }); assert_eq!(reply.output(), output3); } #[test] fn incr_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); f.map_as_toplevel(&mut connection, window); connection.set_selection_owner(window, connection.atoms.clipboard); let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.selection(), connection.atoms.clipboard); connection.set_property( request.requestor(), x::ATOM_ATOM, request.property(), &[connection.atoms.targets, connection.atoms.mime1], ); connection.send_selection_notify(&request); f.wait_and_dispatch(); // Also give the window the primary selection. // Due to a bug introduced in primary selection support, `XState::selection_state` having both // a primary and clipboard X selection prevented clipboard INCR checks from occuring. connection.set_selection_owner(window, connection.atoms.primary); let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.selection(), connection.atoms.primary); connection.set_property( request.requestor(), x::ATOM_ATOM, request.property(), &[connection.atoms.targets, connection.atoms.mime2], ); connection.send_selection_notify(&request); f.wait_and_dispatch(); let mut destination_property = x::Atom::none(); let mut begin_incr = Some(|connection: &mut Connection| { let request = connection.await_selection_request(); assert_eq!(request.target(), connection.atoms.mime1); connection.get_property_change_events(request.requestor()); connection.set_property( request.requestor(), connection.atoms.incr, request.property(), &[3000u32], ); connection.send_selection_notify(&request); // skip NewValue let notify = connection.await_property_notify(); assert_eq!(notify.atom(), request.property()); assert_eq!(notify.state(), x::Property::NewValue); request.property() }); let data: Vec = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1))) .take(3000) .collect(); let mut it = data.chunks(500).enumerate(); let mut paste_data = f.testwl.clipboard_paste_data(|_, testwl| { if let Some(begin) = begin_incr.take() { destination_property = begin(&mut connection); testwl.dispatch(); return false; } assert_ne!(destination_property, x::Atom::none()); let notify = connection.await_property_notify(); match it.next() { Some((idx, chunk)) => { assert_eq!(notify.atom(), destination_property, "chunk {idx}"); assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}"); connection.set_property( notify.window(), connection.atoms.mime1, destination_property, chunk, ); testwl.dispatch(); // skip NewValue let notify = connection.await_property_notify(); assert_eq!(notify.atom(), destination_property, "chunk {idx}"); assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}"); false } None => { // INCR completed! assert_eq!(notify.atom(), destination_property); assert_eq!(notify.state(), x::Property::Delete); connection.set_property::( notify.window(), connection.atoms.mime1, destination_property, &[], ); true } } }); assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]); assert_eq!(paste_data.len(), 1); let paste_data = paste_data.swap_remove(0); assert_eq!(paste_data.mime_type, "text/plain"); assert_eq!(&paste_data.data, &data); } #[test] fn wayland_then_x11_clipboard_owner() { 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.get_selection_owner_change_events(true, window); f.map_as_toplevel(&mut connection, window); 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()); connection.await_selection_owner_change(); connection.verify_clipboard_owner(connection.wm_window); connection.get_selection_owner_change_events(false, window); connection.set_selection_owner(window, connection.atoms.clipboard); f.testwl.dispatch(); connection.verify_clipboard_owner(window); let request = connection.await_selection_request(); assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.target(), connection.atoms.targets); } #[test] fn fake_selection_targets() { 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.get_selection_owner_change_events(true, window); let data = b"boingloings"; f.map_as_toplevel(&mut connection, window); let offer = vec![testwl::PasteData { mime_type: "text/plain;charset=utf-8".into(), data: data.to_vec(), }]; f.testwl.create_data_offer(offer.clone()); connection.await_selection_owner_change(); connection.verify_clipboard_owner(connection.wm_window); connection.get_selection_owner_change_events(false, window); let utf8_string = connection .get_reply(&x::InternAtom { only_if_exists: false, name: b"UTF8_STRING", }) .atom(); connection .send_and_check_request(&x::ConvertSelection { requestor: window, selection: connection.atoms.clipboard, target: utf8_string, property: utf8_string, time: x::CURRENT_TIME, }) .unwrap(); f.wait_and_dispatch(); let notify = connection.await_selection_notify(); assert_eq!(notify.property(), utf8_string, "ConvertSelection failed"); let reply = connection.get_reply(&x::GetProperty { delete: false, window, property: utf8_string, r#type: utf8_string, long_offset: 0, long_length: data.len() as u32, }); let paste_data: &[u8] = reply.value(); assert_eq!( std::str::from_utf8(paste_data).unwrap(), std::str::from_utf8(data).unwrap() ); } #[test] fn popup_done() { let mut f = Fixture::new(); let mut conn = Connection::new(&f.display); let toplevel = conn.new_window(conn.root, 0, 0, 20, 20, false); f.map_as_toplevel(&mut conn, toplevel); let popup = conn.new_window(conn.root, 0, 0, 20, 20, true); let surface = f.map_as_popup(&mut conn, popup); let geometry = conn.get_reply(&x::GetGeometry { drawable: x::Drawable::Window(popup), }); assert_eq!(geometry.x(), 0); assert_eq!(geometry.y(), 0); assert_eq!(geometry.width(), 20); assert_eq!(geometry.height(), 20); f.testwl.popup_done(surface); f.wait_and_dispatch(); let reply = conn .wait_for_reply(conn.send_request(&x::GetWindowAttributes { window: popup })) .expect("Couldn't get window attributes"); assert_eq!(reply.map_state(), x::MapState::Unmapped); } #[test] fn negative_output_coordinates() { let mut f = Fixture::new(); let output = f.create_output(-500, -500); let mut connection = Connection::new(&f.display); let window = connection.new_window(connection.root, 0, 0, 200, 200, false); let surface = f.map_as_toplevel(&mut connection, window); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 30.0, 40.0); f.wait_and_dispatch(); let tree = connection.get_reply(&x::QueryTree { window }); let geo = connection.get_reply(&x::GetGeometry { drawable: x::Drawable::Window(window), }); let reply = connection.get_reply(&x::TranslateCoordinates { src_window: tree.parent(), dst_window: connection.root, src_x: geo.x(), src_y: geo.y(), }); assert!(reply.same_screen()); assert_eq!(reply.dst_x(), 0); assert_eq!(reply.dst_y(), 0); let ptr_reply = connection.get_reply(&x::QueryPointer { window: connection.root, }); assert!(ptr_reply.same_screen()); assert_eq!(ptr_reply.child(), window); assert_eq!(ptr_reply.win_x(), 30); assert_eq!(ptr_reply.win_y(), 40); } #[test] fn xdg_decorations() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, window); let data = f.testwl.get_surface_data(surface).unwrap(); // The default decoration mode in x11 is SSD assert_eq!( data.toplevel() .decoration .as_ref() .and_then(|(_, decoration)| *decoration), Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) ); // CSD connection.set_property( window, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[2u32, 0, 0, 0, 0], ); f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!( data.toplevel() .decoration .as_ref() .and_then(|(_, decoration)| *decoration), Some(zxdg_toplevel_decoration_v1::Mode::ClientSide) ); // SSD connection.set_property( window, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[2u32, 0, 1, 0, 0], ); f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert_eq!( data.toplevel() .decoration .as_ref() .and_then(|(_, decoration)| *decoration), Some(zxdg_toplevel_decoration_v1::Mode::ServerSide) ); } #[test] fn forced_1x_scale_consistent_x11_size() { let mut f = Fixture::new(); f.testwl.enable_xdg_output_manager(); let output = f.create_output(0, 0); output.scale(2); output.done(); let mut conn = Connection::new(&f.display); let window = conn.new_window(conn.root, 0, 0, 200, 200, false); let surface = f.map_as_toplevel(&mut conn, window); f.testwl.move_surface_to_output(surface, &output); f.testwl.move_pointer_to(surface, 30.0, 40.0); f.wait_and_dispatch(); let tree = conn.get_reply(&x::QueryTree { window }); let geo = conn.get_reply(&x::GetGeometry { drawable: x::Drawable::Window(window), }); let reply = conn.get_reply(&x::TranslateCoordinates { src_window: tree.parent(), dst_window: conn.root, src_x: geo.x(), src_y: geo.y(), }); assert!(reply.same_screen()); assert_eq!(reply.dst_x(), 0); assert_eq!(reply.dst_y(), 0); let ptr_reply = conn.get_reply(&x::QueryPointer { window: conn.root }); assert!(ptr_reply.same_screen()); assert_eq!(ptr_reply.child(), window); assert_eq!(ptr_reply.win_x(), 60); assert_eq!(ptr_reply.win_y(), 80); // Update scale output.scale(3); output.done(); f.wait_and_dispatch(); f.testwl .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]); f.testwl.focus_toplevel(surface); f.testwl.move_pointer_to(surface, 30.0, 40.0); f.wait_and_dispatch(); let ptr_reply = conn.get_reply(&x::QueryPointer { window: conn.root }); assert!(ptr_reply.same_screen()); assert_eq!(ptr_reply.child(), window); assert_eq!(ptr_reply.win_x(), 90); assert_eq!(ptr_reply.win_y(), 120); // Popup let popup = conn.new_window(conn.root, 60, 60, 30, 30, true); f.map_as_popup(&mut conn, popup); let tree = conn.get_reply(&x::QueryTree { window: popup }); let geo = conn.get_reply(&x::GetGeometry { drawable: x::Drawable::Window(popup), }); let reply = conn.get_reply(&x::TranslateCoordinates { src_window: tree.parent(), dst_window: conn.root, src_x: geo.x(), src_y: geo.y(), }); assert_eq!(reply.dst_x(), 60); assert_eq!(reply.dst_y(), 60); assert_eq!(geo.width(), 30); assert_eq!(geo.height(), 30); } #[test] fn popup_heuristics() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false); f.map_as_toplevel(&mut connection, win_toplevel); let ghidra_popup = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( ghidra_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( ghidra_popup, x::ATOM_ATOM, connection.atoms.net_wm_state, &[connection.atoms.skip_taskbar], ); connection.set_property( ghidra_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0b11_u32, 0, 0, 0, 0], ); f.map_as_popup(&mut connection, ghidra_popup); let reaper_dialog = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( ghidra_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( ghidra_popup, x::ATOM_ATOM, connection.atoms.net_wm_state, &[connection.atoms.skip_taskbar], ); connection.set_property( ghidra_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0x2a, 0, 0], ); f.map_as_toplevel(&mut connection, reaper_dialog); let chromium_menu = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( chromium_menu, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_menu], ); f.map_as_popup(&mut connection, chromium_menu); let chromium_tooltip = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( chromium_tooltip, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_tooltip], ); f.map_as_popup(&mut connection, chromium_tooltip); let discord_dnd = connection.new_window(connection.root, 20, 138, 48, 48, true); connection.set_property( discord_dnd, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_dnd], ); f.map_as_popup(&mut connection, discord_dnd); let git_gui_popup = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( git_gui_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_popup_menu], ); f.map_as_popup(&mut connection, git_gui_popup); let git_gui_dropdown = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( git_gui_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_dropdown_menu], ); f.map_as_popup(&mut connection, git_gui_dropdown); let wechat_popup = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( wechat_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_utility], ); connection.set_property( wechat_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0, 0, 0], ); f.map_as_popup(&mut connection, wechat_popup); let fcitx5_popup = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( fcitx5_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_combo], ); f.map_as_popup(&mut connection, fcitx5_popup); let godot_popup = connection.new_window(connection.root, 10, 10, 50, 50, true); connection.set_property( godot_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_utility], ); connection.set_property( godot_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0, 0, 0], ); f.map_as_popup(&mut connection, godot_popup); let material_maker_popup = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( material_maker_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_utility], ); connection.set_property( material_maker_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0, 0, 0], ); f.map_as_popup(&mut connection, material_maker_popup); let ardour_toplevel = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( ardour_toplevel, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_utility], ); f.map_as_toplevel(&mut connection, ardour_toplevel); let yabridge_popup = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( yabridge_popup, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( yabridge_popup, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0, 0, 0], ); connection.set_property( yabridge_popup, connection.atoms.wm_hints, connection.atoms.wm_hints, &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0], ); connection.set_property( yabridge_popup, x::ATOM_ATOM, connection.atoms.net_wm_state, &[connection.atoms.skip_taskbar], ); f.map_as_popup(&mut connection, yabridge_popup); let steam = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( steam, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( steam, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x2_u32, 0, 0, 0, 0], ); connection.set_property( steam, connection.atoms.wm_hints, connection.atoms.wm_hints, &[0x1_u32, 1, 0, 0, 0, 0, 0, 0, 0], ); f.map_as_toplevel(&mut connection, steam); let battle_net = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( battle_net, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( battle_net, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x3_u32, 0x2c, 0x0, 0x0, 0x0], ); connection.set_property( battle_net, connection.atoms.wm_hints, connection.atoms.wm_hints, &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0], ); f.map_as_toplevel(&mut connection, battle_net); let wallpaper_engine = connection.new_window(connection.root, 10, 10, 50, 50, false); connection.set_property( wallpaper_engine, x::ATOM_ATOM, connection.atoms.win_type, &[connection.atoms.win_type_normal], ); connection.set_property( wallpaper_engine, connection.atoms.motif_wm_hints, connection.atoms.motif_wm_hints, &[0x3_u32, 0x6, 0x0, 0x0, 0x0], ); connection.set_property( wallpaper_engine, connection.atoms.wm_hints, connection.atoms.wm_hints, &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0], ); f.map_as_toplevel(&mut connection, wallpaper_engine); } #[test] fn xsettings_scale() { let mut f = Fixture::new_preset(|testwl| { testwl.new_output(0, 0); // WL-1 }); let connection = Connection::new(&f.display); f.testwl.enable_xdg_output_manager(); let settings = connection.get_xsettings(); let settings_serial = settings.serial; assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024); let dpi_serial = settings.data["Xft/DPI"].last_change; assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1); let window_serial = settings.data["Gdk/WindowScalingFactor"].last_change; assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); let unscaled_serial = settings.data["Gdk/UnscaledDPI"].last_change; let output = f.testwl.get_output("WL-1").unwrap(); output.scale(2); output.done(); f.wait_and_dispatch(); let settings = connection.get_xsettings(); assert!(settings.serial > settings_serial); assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024); assert!(settings.data["Xft/DPI"].last_change > dpi_serial); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); assert!(settings.data["Gdk/WindowScalingFactor"].last_change > window_serial); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); assert!(settings.data["Gdk/UnscaledDPI"].last_change > unscaled_serial); let output2 = f.create_output(0, 0); let settings = connection.get_xsettings(); assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); output2.scale(2); output2.done(); f.testwl.dispatch(); std::thread::sleep(Duration::from_millis(1)); let settings = connection.get_xsettings(); assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); } #[test] fn xsettings_fractional_scale() { let mut f = Fixture::new_preset(|testwl| { testwl.new_output(0, 0); // WL-1 testwl.enable_fractional_scale(); }); let mut connection = Connection::new(&f.display); f.testwl.enable_xdg_output_manager(); let output = f.testwl.finalize_output(); let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, window); let data = f .testwl .get_surface_data(surface) .expect("Missing surface data"); let fractional = data .fractional .as_ref() .expect("No fractional scale for surface"); fractional.preferred_scale(180); // 1.5 scale f.testwl.move_surface_to_output(surface, &output); f.wait_and_dispatch(); let settings = connection.get_xsettings(); assert_eq!( settings.data["Xft/DPI"].value, (1.5 * 96_f64 * 1024_f64).round() as i32 ); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1); assert_eq!( settings.data["Gdk/UnscaledDPI"].value, (1.5 * 96_f64 * 1024_f64).round() as i32 ); let data = f.testwl.get_surface_data(surface).unwrap(); let fractional = data.fractional.as_ref().unwrap(); fractional.preferred_scale(300); // 2.5 scale f.wait_and_dispatch(); let settings = connection.get_xsettings(); assert_eq!( settings.data["Xft/DPI"].value, (2.5 * 96_f64 * 1024_f64).round() as i32 ); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); assert_eq!( settings.data["Gdk/UnscaledDPI"].value, (2.5 / 2.0 * 96_f64 * 1024_f64).round() as i32 ); } #[test] fn xsettings_switch_owner() { let f = Fixture::new(); let mut connection = Connection::new(&f.display); let owner = connection .get_reply(&x::GetSelectionOwner { selection: connection.atoms.xsettings, }) .owner(); let win = connection.generate_id(); connection .send_and_check_request(&x::CreateWindow { wid: win, x: 0, y: 0, parent: connection.root, depth: 0, width: 1, height: 1, border_width: 0, class: x::WindowClass::InputOnly, visual: x::COPY_FROM_PARENT, value_list: &[], }) .unwrap(); connection .send_and_check_request(&x::SetSelectionOwner { owner: win, selection: connection.atoms.xsettings, time: x::CURRENT_TIME, }) .unwrap(); assert_eq!( connection .get_reply(&x::GetSelectionOwner { selection: connection.atoms.xsettings, }) .owner(), win ); connection .send_and_check_request(&xcb::xfixes::SelectSelectionInput { window: connection.root, selection: connection.atoms.xsettings, event_mask: xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER | xcb::xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY | xcb::xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE, }) .unwrap(); connection .send_and_check_request(&x::DestroyWindow { window: win }) .unwrap(); match connection.await_event() { xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(x)) if x.subtype() == xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {} other => panic!("unexpected event {other:?}"), } match connection.await_event() { xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(x)) if x.subtype() == xcb::xfixes::SelectionEvent::SetSelectionOwner => {} other => panic!("unexpected event {other:?}"), } assert_eq!( connection .get_reply(&x::GetSelectionOwner { selection: connection.atoms.xsettings, }) .owner(), owner ); } #[test] fn rotated_output() { let mut f = Fixture::new_preset(|testwl| { testwl.enable_xdg_output_manager(); testwl.new_output(0, 0); }); let mut connection = Connection::new(&f.display); connection .send_and_check_request(&x::ChangeWindowAttributes { window: connection.root, value_list: &[x::Cw::EventMask(x::EventMask::STRUCTURE_NOTIFY)], }) .unwrap(); let output = f.testwl.get_output("WL-1").unwrap(); output.mode(wl_output::Mode::Current, 100, 1000, 60); output.geometry( 0, 0, 50, 50, wl_output::Subpixel::Unknown, "satellite".to_string(), "WL-1".to_string(), wl_output::Transform::_90, ); output.done(); f.testwl.dispatch(); match connection.await_event() { xcb::Event::X(x::Event::ConfigureNotify(e)) => { assert_eq!(e.window(), connection.root); assert_eq!(e.width(), 1000); assert_eq!(e.height(), 100); } other => panic!("unexpected event {other:?}"), } } const BTN_LEFT: u32 = 0x110; #[test] fn client_init_move() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, win_toplevel); f.testwl.move_pointer_to(surface, 10., 10.); let ptr = f.testwl.pointer(); ptr.motion(10, 10.0, 10.0); ptr.frame(); ptr.button(10, 20, BTN_LEFT, wl_pointer::ButtonState::Pressed); ptr.frame(); f.testwl.dispatch(); connection.send_client_message(&x::ClientMessageEvent::new( win_toplevel, connection.atoms.moveresize, x::ClientMessageData::Data32([0, 0, MoveResizeDirection::Move.into(), 1, 0]), )); f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert!(data.moving); } #[test] fn client_init_resize() { let mut f = Fixture::new(); let mut connection = Connection::new(&f.display); let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, win_toplevel); f.testwl.move_pointer_to(surface, 10., 10.); let ptr = f.testwl.pointer(); ptr.motion(10, 10.0, 10.0); ptr.frame(); ptr.button(10, 20, BTN_LEFT, wl_pointer::ButtonState::Pressed); ptr.frame(); f.testwl.dispatch(); connection.send_client_message(&x::ClientMessageEvent::new( win_toplevel, connection.atoms.moveresize, x::ClientMessageData::Data32([0, 0, MoveResizeDirection::SizeBottomRight.into(), 1, 0]), )); f.wait_and_dispatch(); let data = f.testwl.get_surface_data(surface).unwrap(); assert!( matches!(data.resizing, Some(xdg_toplevel::ResizeEdge::BottomRight)), "Got wrong resizing edge: {:?}", data.resizing ); }