This allows for most GTK and Qt apps to be scaled properly. In the case of mixed DPI, it will default to using the smallest monitor scale.
1894 lines
59 KiB
Rust
1894 lines
59 KiB
Rust
use rustix::event::{poll, PollFd, PollFlags};
|
|
use rustix::process::{Pid, Signal, WaitOptions};
|
|
use std::collections::HashMap;
|
|
use std::io::Write;
|
|
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::{
|
|
decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server::xdg_toplevel,
|
|
};
|
|
use wayland_server::Resource;
|
|
use xcb::{x, Xid};
|
|
use xwayland_satellite as xwls;
|
|
use xwayland_satellite::xstate::{WmSizeHintsFlags, WmState};
|
|
|
|
#[derive(Default)]
|
|
struct TestDataInner {
|
|
server_created: AtomicBool,
|
|
server_connected: AtomicBool,
|
|
display: Mutex<Option<String>>,
|
|
server: Mutex<Option<UnixStream>>,
|
|
pid: Mutex<Option<u32>>,
|
|
quit_rx: Mutex<Option<UnixStream>>,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct TestData(Arc<TestDataInner>);
|
|
|
|
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<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 quit_rx(&self) -> Option<UnixStream> {
|
|
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 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,
|
|
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;
|
|
self.quit_tx.write_all(&return_ptr.to_ne_bytes()).unwrap();
|
|
thread.join().expect("Main thread panicked");
|
|
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(|| {
|
|
env_logger::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);
|
|
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();
|
|
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();
|
|
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.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.last_created_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",
|
|
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",
|
|
motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false,
|
|
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",
|
|
}
|
|
}
|
|
|
|
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 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]
|
|
#[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;
|
|
}
|
|
assert!(
|
|
poll(&mut [self.pollfd.clone()], 100).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<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()
|
|
}
|
|
|
|
#[track_caller]
|
|
fn set_selection_owner(&self, window: x::Window) {
|
|
self.send_and_check_request(&x::SetSelectionOwner {
|
|
owner: window,
|
|
selection: self.atoms.clipboard,
|
|
time: x::CURRENT_TIME,
|
|
})
|
|
.unwrap();
|
|
let owner = self.get_reply(&x::GetSelectionOwner {
|
|
selection: self.atoms.clipboard,
|
|
});
|
|
|
|
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 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();
|
|
}
|
|
}
|
|
|
|
#[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(),
|
|
);
|
|
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.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::<x::Window>());
|
|
};
|
|
|
|
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::<u32>()
|
|
.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(1));
|
|
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);
|
|
|
|
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],
|
|
},
|
|
},
|
|
];
|
|
|
|
let advertised_mimes = f.testwl.data_source_mimes();
|
|
assert_eq!(
|
|
advertised_mimes.len(),
|
|
mimes_truth.len(),
|
|
"Wrong number of advertised mimes: {advertised_mimes:?}"
|
|
);
|
|
for MimeData { data, .. } in &mimes_truth {
|
|
assert!(
|
|
advertised_mimes.contains(&data.mime_type),
|
|
"Missing mime type {}",
|
|
data.mime_type
|
|
);
|
|
}
|
|
|
|
let data = f.testwl.paste_data(|mime, _| {
|
|
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 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<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);
|
|
}
|
|
}
|
|
|
|
// 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 });
|
|
println!("reply: {reply:?}");
|
|
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);
|
|
|
|
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.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<_>>(), 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);
|
|
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.targets, connection.atoms.mime1],
|
|
);
|
|
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
|
|
.send_and_check_request(&x::ChangeWindowAttributes {
|
|
window: request.requestor(),
|
|
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
|
|
})
|
|
.unwrap();
|
|
connection.set_property(
|
|
request.requestor(),
|
|
connection.atoms.incr,
|
|
request.property(),
|
|
&[3000u32],
|
|
);
|
|
connection.send_selection_notify(&request);
|
|
// skip NewValue
|
|
let notify = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
|
|
other => panic!("Didn't get property notify event, instead got {other:?}"),
|
|
};
|
|
assert_eq!(notify.atom(), request.property());
|
|
assert_eq!(notify.state(), x::Property::NewValue);
|
|
request.property()
|
|
});
|
|
|
|
let data: Vec<u8> = 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.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 = match connection.await_event() {
|
|
xcb::Event::X(x::Event::PropertyNotify(p)) => p,
|
|
other => panic!("Didn't get property notify event, instead got {other:?}"),
|
|
};
|
|
|
|
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 = match connection.poll_for_event().unwrap() {
|
|
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
|
|
other => panic!("Didn't get property notify event, instead got {other:?}"),
|
|
};
|
|
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::<u8>(
|
|
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);
|
|
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 SDD
|
|
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],
|
|
);
|
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
f.testwl.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],
|
|
);
|
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
f.testwl.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.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);
|
|
}
|
|
|
|
#[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();
|
|
|
|
struct Settings {
|
|
serial: u32,
|
|
data: HashMap<String, Setting>,
|
|
}
|
|
struct Setting {
|
|
value: i32,
|
|
last_change: u32,
|
|
}
|
|
|
|
let owner = connection
|
|
.get_reply(&x::GetSelectionOwner {
|
|
selection: connection.atoms.xsettings,
|
|
})
|
|
.owner();
|
|
|
|
let get_settings = || -> Settings {
|
|
let reply = connection.get_reply(&x::GetProperty {
|
|
delete: false,
|
|
window: owner,
|
|
property: connection.atoms.xsettings_setting,
|
|
r#type: connection.atoms.xsettings_setting,
|
|
long_offset: 0,
|
|
long_length: 60,
|
|
});
|
|
assert_eq!(reply.r#type(), connection.atoms.xsettings_setting);
|
|
|
|
let data = reply.value::<u8>();
|
|
|
|
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,
|
|
}
|
|
};
|
|
|
|
let settings = get_settings();
|
|
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 = get_settings();
|
|
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_eq!(
|
|
settings.data["Gdk/UnscaledDPI"].last_change,
|
|
unscaled_serial
|
|
);
|
|
|
|
let output2 = f.create_output(0, 0);
|
|
let settings = get_settings();
|
|
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 = get_settings();
|
|
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_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
|
|
);
|
|
}
|