diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index df71d94..c9932a1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,16 +34,13 @@ The code for the X11 portion of satellite lives in `src/xstate`. Satellite must as any other standard X11 window manager. This includes: - Setting SubstructureRedirect and SubstructureNotify on the root window, to get notifications for when new windows are being created -- Following (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs +- Follwing (most of) the [ICCCM](https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html) and [EWMH](https://specifications.freedesktop.org/wm-spec/latest/) specs In addition, satellite must do some other things that a normal X11 window manager wouldn't - but a compositor integrating Xwayland would - such as synchronize X11 and Wayland selections. This is explained further in the Wayland server section. The way that satellite manages windows from the X11 point of view is as follows: -- All monitors maintain their relative positions to one another. Their absolute position is such that - the top-most monitor's top edge is on the X-axis and the left-most monitor's left edge is on the Y-axis. - - All monitors are on non-negative coordinates with no gaps between the screen and any monitor, matching what `xrandr` does. - All toplevels on a monitor are positioned at 0x0 on that monitor. So if you have one monitor at 0x0, all the windows are located at 0x0. If you have a monitor at 300x600, all the windows on that monitor are at 300x600. - This offset is needed because all monitors rest in the same coordinate plane in X11, so missing this offset would diff --git a/Cargo.lock b/Cargo.lock index 32f850e..60e76c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,7 +1241,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xwayland-satellite" -version = "0.8.1" +version = "0.8.0" dependencies = [ "ab_glyph", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 400c210..ece7236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ edition = "2024" [package] name = "xwayland-satellite" -version = "0.8.1" +version = "0.8.0" authors = ["Shawn Wallace"] license = "MPL-2.0" description = "xwayland-satellite grants rootless Xwayland integration to any Wayland compositor implementing xdg_wm_base and viewporter. This is particularly useful for compositors that (understandably) do not want to go through implementing support for rootless Xwayland themselves." diff --git a/src/lib.rs b/src/lib.rs index 8c27f2d..611c959 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use server::selection::{Clipboard, Primary}; use smithay_client_toolkit::data_device_manager::WritePipe; use std::io::{BufRead, BufReader, Read, Write}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; -use std::os::unix::{net::UnixStream, process::ExitStatusExt}; +use std::os::unix::net::UnixStream; use std::process::{Command, ExitStatus, Stdio}; use wayland_server::{Display, ListeningSocket}; use xcb::x; @@ -121,9 +121,9 @@ pub fn main(mut data: impl RunData) -> Option<()> { let line = line.unwrap(); info!(target: "xwayland_process", "{line}"); } - let status = xwayland.wait().unwrap().into_raw(); - // On a successful integration test, the rx will be dropped, so keep logs/GDB clean - let _ = finish_tx.write_all(&status.to_ne_bytes()); + let status = Box::new(xwayland.wait().unwrap()); + let status = Box::into_raw(status) as usize; + finish_tx.write_all(&status.to_ne_bytes()).unwrap(); }); let mut ready_fds = [ @@ -131,10 +131,11 @@ pub fn main(mut data: impl RunData) -> Option<()> { PollFd::new(&finish_rx, PollFlags::IN), ]; - fn xwayland_exit_code(rx: &mut UnixStream) -> ExitStatus { - let mut data = [0; std::mem::size_of::()]; + fn xwayland_exit_code(rx: &mut UnixStream) -> Box { + let mut data = [0; (usize::BITS / 8) as usize]; rx.read_exact(&mut data).unwrap(); - ExitStatus::from_raw(i32::from_ne_bytes(data)) + let data = usize::from_ne_bytes(data); + unsafe { Box::from_raw(data as *mut _) } } let connection = match poll(&mut ready_fds, None) { @@ -178,7 +179,7 @@ pub fn main(mut data: impl RunData) -> Option<()> { Ok(_) => { if !fds[3].revents().is_empty() { let status = xwayland_exit_code(&mut quit_rx); - if status != ExitStatus::default() { + if *status != ExitStatus::default() { error!("Xwayland exited early with {status}"); } return None; @@ -245,7 +246,7 @@ pub fn main(mut data: impl RunData) -> Option<()> { Ok(_) => { if !fds[3].revents().is_empty() { let status = xwayland_exit_code(&mut quit_rx); - if status != ExitStatus::default() { + if *status != ExitStatus::default() { error!("Xwayland exited early with {status}"); } return None; diff --git a/src/server/clientside.rs b/src/server/clientside.rs index 664c909..05c8cbe 100644 --- a/src/server/clientside.rs +++ b/src/server/clientside.rs @@ -1,6 +1,6 @@ use super::decoration::DecorationMarker; -use super::{GlobalName, ObjectEvent}; +use super::ObjectEvent; use hecs::{Entity, World}; use smithay_client_toolkit::{ activation::{ActivationHandler, RequestData, RequestDataExt}, @@ -115,7 +115,6 @@ pub(super) struct MyWorld { pub world: World, pub global_list: GlobalList, pub new_globals: Vec, - pub removed_globals: Vec, events: Vec<(Entity, ObjectEvent)>, queued_events: Vec>, pub clipboard: SelectionEvents, @@ -129,7 +128,6 @@ impl MyWorld { world: World::new(), global_list, new_globals: Vec::new(), - removed_globals: Vec::new(), events: Vec::new(), queued_events: Vec::new(), clipboard: Default::default(), @@ -206,23 +204,18 @@ impl Dispatch for MyWorld { _: &wayland_client::Connection, _: &wayland_client::QueueHandle, ) { - match event { - Event::::Global { + if let Event::::Global { + name, + interface, + version, + } = event + { + state.new_globals.push(Global { name, interface, version, - } => { - state.new_globals.push(Global { - name, - interface, - version, - }); - } - Event::::GlobalRemove { name } => { - state.removed_globals.push(GlobalName(name)); - } - _ => {} - } + }); + }; } } diff --git a/src/server/decoration.rs b/src/server/decoration.rs index 0df8abb..db37dc2 100644 --- a/src/server/decoration.rs +++ b/src/server/decoration.rs @@ -173,7 +173,7 @@ impl DecorationsDataSatellite { // Draw the bar and its components let mut bar = Pixmap::new(drawn_width as u32, drawn_height as u32).unwrap(); - bar.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap()); + bar.fill(Color::WHITE); if let Some(title) = title { bar.draw_pixmap( @@ -350,9 +350,9 @@ fn draw_pixmap_to_buffer(pixmap: &Pixmap, buffer: &mut [u8]) { fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap { let mut x = Pixmap::new(bar_height, bar_height).unwrap(); if hovered { - x.fill(Color::from_rgba(1.0, 0.2, 0.2, 0.8).unwrap()); + x.fill(Color::from_rgba(1.0, 0.0, 0.0, 0.8).unwrap()); } else { - x.fill(Color::from_rgba(0.2, 0.2, 0.2, 1.0).unwrap()); + x.fill(Color::WHITE); } let size = x.width() as f32; let margin = 8.4 * scale; @@ -363,11 +363,9 @@ fn x_pixmap(bar_height: u32, scale: f32, hovered: bool) -> Pixmap { line.move_to(size - margin, margin); line.line_to(margin, size - margin); let line = line.finish().unwrap(); - let mut paint = Paint::default(); - paint.set_color(Color::WHITE); x.stroke_path( &line, - &paint, + &Default::default(), &Stroke { width: scale + 0.5, ..Default::default() @@ -402,7 +400,7 @@ fn title_pixmap(title: &str, max_width: u32, height: u32, scale: f32) -> Option< ((bounds.min.x as u32 + x) + (bounds.min.y as u32 + y) * width) as usize; data[pixel_idx] = - ColorU8::from_rgba(255, 255, 255, (coverage * 255.0) as u8).premultiply(); + ColorU8::from_rgba(0, 0, 0, (coverage * 255.0) as u8).premultiply(); }); } } diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 9704f25..21acf44 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -1479,7 +1479,6 @@ impl GlobalDispatch for InnerServerState { client, event::OutputScaleFactor::Output(1), event::OutputDimensions::default(), - GlobalName(data.name), ), ); state.updated_outputs.push(entity); diff --git a/src/server/event.rs b/src/server/event.rs index 3731103..2e2afc1 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -212,9 +212,7 @@ impl SurfaceEvents { let mut query = data.query::<(&x::Window, &mut WindowData)>(); if let Some((window, win_data)) = query.get() { - let Some(dimensions) = output_data.get::<&OutputDimensions>() else { - return; - }; + let dimensions = output_data.get::<&OutputDimensions>().unwrap(); win_data.update_output_offset( *window, WindowOutputOffset { @@ -474,8 +472,8 @@ pub(super) fn update_surface_viewport( let dims = &window_data.attrs.dims; let size_hints = &window_data.attrs.size_hints; - let width = (dims.width as f64 / scale_factor.0).ceil() as i32; - let height = (dims.height as f64 / scale_factor.0).ceil() as i32; + let width = (dims.width as f64 / scale_factor.0) as i32; + let height = (dims.height as f64 / scale_factor.0) as i32; if width > 0 && height > 0 { viewport.set_destination(width, height); } @@ -985,7 +983,7 @@ impl Event for client::wl_touch::Event { } } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone)] pub(super) struct OnOutput(pub Entity); struct OutputName(String); fn get_output_name(output: Option<&OnOutput>, world: &World) -> Option { @@ -1079,9 +1077,7 @@ fn update_output_offset( let connection = &mut state.connection; let state = &mut state.inner; { - let Ok(mut dimensions) = state.world.get::<&mut OutputDimensions>(output) else { - return; - }; + let mut dimensions = state.world.get::<&mut OutputDimensions>(output).unwrap(); if matches!(source, OutputDimensionsSource::Wl { .. }) && matches!(dimensions.source, OutputDimensionsSource::Xdg) { @@ -1097,8 +1093,7 @@ fn update_output_offset( }; state.global_offset_updated = true; } else if dim.owner == Some(output) && value > dim.value { - // Another output's position could be less than the new value, so recalculate - dim.owner = None; + *dim = Default::default(); state.global_offset_updated = true; } }; @@ -1129,9 +1124,7 @@ fn update_window_output_offsets( world: &World, connection: &mut impl XConnection, ) { - let Ok(dimensions) = world.get::<&OutputDimensions>(output) else { - return; - }; + let dimensions = world.get::<&OutputDimensions>(output).unwrap(); let mut query = world.query::<(&x::Window, &mut WindowData, &OnOutput)>(); for (_, (window, data, _)) in query @@ -1157,9 +1150,7 @@ pub(super) fn update_global_output_offset( ) { let entity = world.entity(output).unwrap(); let mut query = entity.query::<(&OutputDimensions, &WlOutput)>(); - let Some((dimensions, server)) = query.get() else { - return; - }; + let (dimensions, server) = query.get().unwrap(); let x = dimensions.x - global_output_offset.x.value; let y = dimensions.y - global_output_offset.y.value; @@ -1258,28 +1249,24 @@ impl OutputEvent { state, ); let global_output_offset = state.global_output_offset; - let global_offset_updated = state.global_offset_updated; - let Ok((output, dimensions, xdg)) = state.world.query_one_mut::<( - &WlOutput, - &mut OutputDimensions, - Option<&XdgOutputServer>, - )>(target) else { - return; - }; + let (output, dimensions, xdg) = state + .world + .query_one_mut::<(&WlOutput, &mut OutputDimensions, Option<&XdgOutputServer>)>( + target, + ) + .unwrap(); - if !global_offset_updated { - output.geometry( - x - global_output_offset.x.value, - y - global_output_offset.y.value, - physical_width, - physical_height, - convert_wenum(subpixel), - make, - model, - convert_wenum(transform), - ); - } + output.geometry( + x - global_output_offset.x.value, + y - global_output_offset.y.value, + physical_width, + physical_height, + convert_wenum(subpixel), + make, + model, + convert_wenum(transform), + ); dimensions.rotated_90 = transform.into_result().is_ok_and(|t| { matches!( t, @@ -1303,12 +1290,10 @@ impl OutputEvent { height, refresh, } => { - let Ok((output, dimensions)) = state + let (output, dimensions) = state .world .query_one_mut::<(&WlOutput, &mut OutputDimensions)>(target) - else { - return; - }; + .unwrap(); if flags .into_result() @@ -1363,24 +1348,20 @@ impl OutputEvent { match event { Event::LogicalPosition { x, y } => { update_output_offset(target, OutputDimensionsSource::Xdg, x, y, state); - if !state.global_offset_updated { - state - .world - .get::<&XdgOutputServer>(target) - .unwrap() - .logical_position( - x - state.global_output_offset.x.value, - y - state.global_output_offset.y.value, - ); - } + state + .world + .get::<&XdgOutputServer>(target) + .unwrap() + .logical_position( + x - state.global_output_offset.x.value, + y - state.global_output_offset.y.value, + ); } Event::LogicalSize { .. } => { - let Ok((xdg, dimensions)) = state + let (xdg, dimensions) = state .world .query_one_mut::<(&XdgOutputServer, &OutputDimensions)>(target) - else { - return; - }; + .unwrap(); if dimensions.rotated_90 { xdg.logical_size(dimensions.height, dimensions.width); } else { diff --git a/src/server/mod.rs b/src/server/mod.rs index ab6f7a4..a110fab 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -65,7 +65,6 @@ use wayland_protocols::{ use wayland_server::protocol::wl_seat::WlSeat; use wayland_server::{ Client, DisplayHandle, Resource, WEnum, - backend::GlobalId, protocol::{ wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm, wl_surface::WlSurface, @@ -143,7 +142,7 @@ impl WindowData { offset: WindowOutputOffset, connection: &mut C, ) { - log::trace!(target: "output_offset", "offset: {offset:?}"); + log::trace!("offset: {offset:?}"); if offset == self.output_offset { return; } @@ -162,7 +161,7 @@ impl WindowData { height: self.attrs.dims.height as _, }, ) { - debug!(target: "output_offset", "set {:?} offset to {:?}", window, self.output_offset); + debug!("set {:?} offset to {:?}", window, self.output_offset); } } } @@ -343,8 +342,7 @@ enum ObjectEvent { } } -fn handle_new_globals<'a, S: X11Selection + 'static>( - globals_map: &mut HashMap, +fn handle_globals<'a, S: X11Selection + 'static>( dh: &DisplayHandle, globals: impl IntoIterator, ) { @@ -355,8 +353,7 @@ fn handle_new_globals<'a, S: X11Selection + 'static>( $( ref x if x == <$global>::interface().name => { let version = u32::min(global.version, <$global>::interface().version); - let global_id = dh.create_global::, $global, Global>(version, global.clone()); - globals_map.insert(GlobalName(global.name), (global.clone(), global_id)); + dh.create_global::, $global, Global>(version, global.clone()); } )+ _ => {} @@ -380,9 +377,6 @@ fn handle_new_globals<'a, S: X11Selection + 'static>( } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(super) struct GlobalName(pub u32); - struct FocusData { window: x::Window, output_name: Option, @@ -452,7 +446,6 @@ pub struct InnerServerState { world: MyWorld, queue: EventQueue, qh: QueueHandle, - globals_map: HashMap, client: Client, to_focus: Option, unfocus: bool, @@ -539,10 +532,9 @@ impl ServerState> { dh.create_global::, XwaylandShellV1, _>(1, ()); - let mut globals_map = HashMap::new(); global_list .contents() - .with_list(|globals| handle_new_globals::(&mut globals_map, &dh, globals)); + .with_list(|globals| handle_globals::(&dh, globals)); let world = MyWorld::new(global_list); let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap(); @@ -553,7 +545,6 @@ impl ServerState> { client, queue, qh, - globals_map, dh, to_focus: None, unfocus: false, @@ -622,7 +613,7 @@ impl ServerState { } pub fn handle_clientside_events(&mut self) { - self.handle_globals(); + self.handle_new_globals(); for (target, event) in self.world.read_events() { if !self.world.contains(target) { @@ -644,15 +635,14 @@ impl ServerState { .unwrap(); } - if self.global_output_offset.x.owner.is_none() - || self.global_output_offset.y.owner.is_none() - { - self.calc_global_output_offset(); - self.global_offset_updated = true; - } if self.global_offset_updated { + if self.global_output_offset.x.owner.is_none() + || self.global_output_offset.y.owner.is_none() + { + self.calc_global_output_offset(); + } + debug!( - target: "output_offset", "updated global output offset: {}x{}", self.global_output_offset.x.value, self.global_output_offset.y.value ); @@ -669,10 +659,8 @@ impl ServerState { } if !self.updated_outputs.is_empty() { - for output in std::mem::take(&mut self.updated_outputs).iter() { - let Ok(output_scale) = self.world.get::<&OutputScaleFactor>(*output) else { - continue; - }; + for output in self.updated_outputs.iter() { + let output_scale = self.world.get::<&OutputScaleFactor>(*output).unwrap(); if matches!(*output_scale, OutputScaleFactor::Output(..)) { let mut surface_query = self .world @@ -696,30 +684,31 @@ impl ServerState { } } } + self.updated_outputs.clear(); let mut mixed_scale = false; let mut scale; let mut outputs = self.world.query_mut::<&OutputScaleFactor>().into_iter(); - if let Some((_, output_scale)) = outputs.next() { - scale = output_scale.get(); + let (_, output_scale) = outputs.next().unwrap(); - for (_, output_scale) in outputs { - if output_scale.get() != scale { - mixed_scale = true; - scale = scale.min(output_scale.get()); - } + scale = output_scale.get(); + + for (_, output_scale) in outputs { + if output_scale.get() != scale { + mixed_scale = true; + scale = scale.min(output_scale.get()); } - - if mixed_scale { - warn!( - "Mixed output scales detected, choosing to give apps the smallest detected scale ({scale}x)" - ); - } - - debug!("Using new scale {scale}"); - self.new_scale = Some(scale); } + + if mixed_scale { + warn!( + "Mixed output scales detected, choosing to give apps the smallest detected scale ({scale}x)" + ); + } + + debug!("Using new scale {scale}"); + self.new_scale = Some(scale); } { @@ -791,55 +780,9 @@ impl InnerServerState { self.queue.as_fd() } - fn handle_globals(&mut self) { + fn handle_new_globals(&mut self) { let globals = std::mem::take(&mut self.world.new_globals); - handle_new_globals::(&mut self.globals_map, &self.dh, &globals); - - let globals = std::mem::take(&mut self.world.removed_globals); - for global in globals { - let (global_struct, global_id) = self.globals_map.remove(&global).unwrap(); - self.dh.disable_global::>(global_id); - if global_struct.interface == ::interface().name { - self.remove_output(global); - } - } - } - - fn remove_output(&mut self, global: GlobalName) { - let query = self - .world - .query_mut::<(&WlOutput, &GlobalName)>() - .into_iter() - .map(|(e, (_, name))| (e, *name)) - .collect::>(); - for (entity, name) in query.iter() { - if *name == global { - self.updated_outputs.push(*entity); - self.world - .remove::<(OutputScaleFactor, OutputDimensions)>(*entity) - .unwrap(); - let query = self - .world - .query_mut::<&OnOutput>() - .into_iter() - .map(|(e, on_out)| (e, *on_out)) - .collect::>(); - for (e, on_out) in query.iter() { - if *on_out == OnOutput(*entity) { - self.world.remove_one::(*e).unwrap(); - } - } - if self.global_output_offset.x.owner == Some(*entity) { - self.global_offset_updated = true; - self.global_output_offset.x.owner = None; - } - if self.global_output_offset.y.owner == Some(*entity) { - self.global_offset_updated = true; - self.global_output_offset.y.owner = None; - } - break; - } - } + handle_globals::(&self.dh, globals.iter()); } pub fn new_window( @@ -1334,8 +1277,6 @@ impl InnerServerState { } fn calc_global_output_offset(&mut self) { - self.global_output_offset.x.value = i32::MAX; - self.global_output_offset.y.value = i32::MAX; for (entity, dimensions) in self.world.query_mut::<&OutputDimensions>() { if dimensions.x < self.global_output_offset.x.value { self.global_output_offset.x = GlobalOutputOffsetDimension { diff --git a/src/server/tests.rs b/src/server/tests.rs index 25db5b7..da176cb 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -530,19 +530,7 @@ impl TestFixture { ); self.run(); self.run(); - (output, self.testwl.finalize_output()) - } - - fn remove_output(&mut self, output_s: wayland_server::protocol::wl_output::WlOutput) { - self.testwl.remove_output(output_s); - self.run(); - self.run(); - let mut events = std::mem::take(&mut *self.registry.data.events.lock().unwrap()); - assert_eq!(events.len(), 1); - let event = events.pop().unwrap(); - let Ev::::GlobalRemove { .. } = event else { - panic!("Unexpected event: {event:?}"); - }; + (output, self.testwl.last_created_output()) } } @@ -864,6 +852,13 @@ impl TestFixture { let data = self.testwl.get_surface_data(surface_id).unwrap(); match data.role { Some(SurfaceRole::Popup(_)) => { + assert_eq!( + data.popup().positioner_state.offset, + testwl::Vec2 { + x: dims.x as _, + y: dims.y as _ + } + ); assert_eq!( data.popup().positioner_state.size, Some(testwl::Vec2 { @@ -1613,209 +1608,56 @@ fn override_redirect_choose_hover_window() { assert_eq!(&popup_data.popup().parent, win1_xdg); } -#[track_caller] -fn check_output_position_event(output: &TestObject, pos: (i32, i32)) { - let mut geo = None; - let events = std::mem::take(&mut *output.data.events.lock().unwrap()); - log::debug!("events: {events:?}"); - for event in events { - match event { - wl_output::Event::Geometry { x, y, .. } => { - geo = Some((x, y)); - } - wl_output::Event::Done => { - if let Some(geo) = geo { - assert_eq!(geo, pos); - return; - } - } - _ => {} - } - } - if geo.is_none() { - panic!("Did not receive any geometry events"); - } else { - panic!("Did not receive a done event"); - } -} - -#[track_caller] -fn check_output_position_event_xdg( - xdg_out: &TestObject, - out: &TestObject, - pos: (i32, i32), - goo_updated: bool, -) { - let mut done = false; - let events = std::mem::take(&mut *xdg_out.data.events.lock().unwrap()) - .into_iter() - .rev(); - for event in events { - if let zxdg_output_v1::Event::LogicalPosition { x, y } = event { - assert_eq!(pos, (x, y)); - done = true; - break; - } - } - assert!(done, "Did not get zxdg_output_v1 logical_position"); - let events = std::mem::take(&mut *out.data.events.lock().unwrap()); - assert_eq!( - events - .into_iter() - .filter(|e| matches!(*e, wl_output::Event::Done)) - .count(), - goo_updated as usize, - "Did not get expected wl_output done event" - ); -} - #[test] -fn output_offset_one_output() { - // If there is only one output, that output is always positioned at 0x0 - let (mut f, _) = TestFixture::new_with_compositor(); - let (output_obj, output) = f.new_output(0, 0); - f.run(); - f.run(); - check_output_position_event(&output_obj, (0, 0)); - - f.testwl.move_output(&output, 500, 100); - f.run(); - f.run(); - check_output_position_event(&output_obj, (0, 0)); - - f.testwl.move_output(&output, -500, -100); - f.run(); - f.run(); - check_output_position_event(&output_obj, (0, 0)); -} - -#[test] -fn output_offset_multi_output() { - // With multiple outputs, the top-most output is on the X-axis, the left-most output is on the - // Y-axis, and they always maintain relative positioning. - let (mut f, _) = TestFixture::new_with_compositor(); - - let (output_obj_1, output_1) = f.new_output(1000, 0); - f.run(); - check_output_position_event(&output_obj_1, (0, 0)); - - let (output_obj_2, _) = f.new_output(0, 1000); - f.run(); - check_output_position_event(&output_obj_1, (1000, 0)); - check_output_position_event(&output_obj_2, (0, 1000)); - - f.testwl.move_output(&output_1, 1000, 2000); - f.run(); - f.run(); - check_output_position_event(&output_obj_1, (1000, 1000)); - check_output_position_event(&output_obj_2, (0, 0)); - - // Global output offset does not change - f.testwl.move_output(&output_1, 1000, 1000); - f.run(); - f.run(); - check_output_position_event(&output_obj_1, (1000, 0)); - assert!(&output_obj_2.data.events.lock().unwrap().is_empty()); -} - -#[test] -fn output_offset_multi_output_xdg() { - let (mut f, _) = TestFixture::new_with_compositor(); - let man = f.enable_xdg_output(); - - let (output_obj_1, output_1) = f.new_output(0, 0); - f.run(); - std::mem::take(&mut *output_obj_1.data.events.lock().unwrap()); - let output_xdg_1 = f.create_xdg_output(&man, output_obj_1.obj.clone()); - f.testwl.move_xdg_output(&output_1, 1000, 0); - f.run(); - f.run(); - check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (0, 0), true); - - let (output_obj_2, output_2) = f.new_output(1000, 1000); - f.run(); - std::mem::take(&mut *output_obj_2.data.events.lock().unwrap()); - let output_xdg_2 = f.create_xdg_output(&man, output_obj_2.obj.clone()); - f.testwl.move_xdg_output(&output_2, 0, 1000); - f.run(); - f.run(); - check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 0), true); - check_output_position_event_xdg(&output_xdg_2, &output_obj_2, (0, 1000), true); - - f.testwl.move_xdg_output(&output_1, 1000, 2000); - f.run(); - f.run(); - check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 1000), true); - check_output_position_event_xdg(&output_xdg_2, &output_obj_2, (0, 0), true); - - f.testwl.move_xdg_output(&output_1, 1000, 1000); - f.run(); - f.run(); - check_output_position_event_xdg(&output_xdg_1, &output_obj_1, (1000, 0), false); - assert!(output_xdg_2.data.events.lock().unwrap().is_empty()); - assert!(output_obj_2.data.events.lock().unwrap().is_empty()); -} - -#[test] -fn output_offset_remove_output() { - let (mut f, _) = TestFixture::new_with_compositor(); - - let (output_ext_c, output_ext) = f.new_output(0, 0); - let (output_main_c, _) = f.new_output(1000, 500); - f.run(); - - check_output_position_event(&output_ext_c, (0, 0)); - check_output_position_event(&output_main_c, (1000, 500)); - - f.remove_output(output_ext); - f.run(); - f.run(); - check_output_position_event(&output_main_c, (0, 0)); -} - -#[test] -fn remove_all_outputs() { - let (mut f, _) = TestFixture::new_with_compositor(); - - let (_, output) = f.new_output(0, 0); - f.run(); - - f.remove_output(output); - f.run(); -} - -#[test] -fn output_offset_surface_positioning() { +fn output_offset() { let (mut f, comp) = TestFixture::new_with_compositor(); - - f.new_output(0, 0); - let (_, output) = f.new_output(500, 100); + let (output_obj, output) = f.new_output(0, 0); + let man = f.enable_xdg_output(); + f.create_xdg_output(&man, output_obj.obj); + f.testwl.move_xdg_output(&output, 500, 100); f.run(); - let window = Window::new(1); - let (_, toplevel_id) = f.create_toplevel(&comp, window); - f.testwl.move_surface_to_output(toplevel_id, &output); + + { + let (surface, surface_id) = f.create_toplevel(&comp, window); + f.testwl.move_surface_to_output(surface_id, &output); + f.run(); + let data = &f.connection().windows[&window]; + assert_eq!(data.dims.x, 500); + assert_eq!(data.dims.y, 100); + + f.satellite.unmap_window(window); + surface.obj.destroy(); + f.run(); + } + + let (t_buffer, t_surface) = comp.create_surface(); + f.map_window(&comp, window, &t_surface.obj, &t_buffer); + f.run(); + let t_id = f.testwl.last_created_surface_id().unwrap(); + f.testwl.move_surface_to_output(t_id, &output); + f.run(); + { + let data = f.testwl.get_surface_data(t_id).unwrap(); + assert!( + matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))), + "surface role: {:?}", + data.role + ); + } + f.testwl.configure_toplevel(t_id, 100, 100, vec![]); + f.testwl.focus_toplevel(t_id); f.run(); - let mut toplevel_pos = WindowDims { - x: 500, - y: 100, - width: 100, - height: 100, - }; - f.assert_window_dimensions(window, toplevel_id, toplevel_pos); + { + let data = &f.connection().windows[&window]; + assert_eq!(data.dims.x, 500); + assert_eq!(data.dims.y, 100); + } let popup = Window::new(2); - let (_, p_id) = f.create_popup( - &comp, - PopupBuilder::new(popup, window, toplevel_id).x(510).y(110), - ); - let mut popup_dims = WindowDims { - x: 510, - y: 110, - width: 50, - height: 50, - }; + let (p_surface, p_id) = + f.create_popup(&comp, PopupBuilder::new(popup, window, t_id).x(510).y(110)); f.testwl.move_surface_to_output(p_id, &output); f.run(); let data = f.testwl.get_surface_data(p_id).unwrap(); @@ -1823,46 +1665,28 @@ fn output_offset_surface_positioning() { data.popup().positioner_state.offset, testwl::Vec2 { x: 10, y: 10 } ); - f.assert_window_dimensions(popup, p_id, popup_dims); - f.testwl.move_output(&output, 600, 200); - f.run(); + f.satellite.unmap_window(popup); + p_surface.obj.destroy(); f.run(); - toplevel_pos.x = 600; - toplevel_pos.y = 200; - f.assert_window_dimensions(window, toplevel_id, toplevel_pos); + let (buffer, surface) = comp.create_surface(); + f.map_window(&comp, popup, &surface.obj, &buffer); + f.run(); + let p_id = f.testwl.last_created_surface_id().unwrap(); + f.testwl.move_surface_to_output(p_id, &output); + f.testwl.configure_popup(p_id); + f.run(); let data = f.testwl.get_surface_data(p_id).unwrap(); assert_eq!( data.popup().positioner_state.offset, testwl::Vec2 { x: 10, y: 10 } ); - popup_dims.x = 610; - popup_dims.y = 210; - f.assert_window_dimensions(popup, p_id, popup_dims); - - f.testwl.move_output(&output, -100, -200); - f.run(); - f.run(); - - toplevel_pos.x = 0; - toplevel_pos.y = 0; - f.assert_window_dimensions(window, toplevel_id, toplevel_pos); - let data = f.testwl.get_surface_data(p_id).unwrap(); - assert_eq!( - data.popup().positioner_state.offset, - testwl::Vec2 { x: 10, y: 10 } - ); - popup_dims.x = 10; - popup_dims.y = 10; - f.assert_window_dimensions(popup, p_id, popup_dims); } #[test] -fn output_offset_xdg_override() { +fn output_offset_change() { let (mut f, comp) = TestFixture::new_with_compositor(); - f.new_output(0, 0); - f.run(); let (output_obj, output) = f.new_output(500, 100); let window = Window::new(1); @@ -1877,8 +1701,13 @@ fn output_offset_xdg_override() { }; test_position(&f, 500, 100); + f.testwl.move_output(&output, 600, 200); + f.run(); + f.run(); + test_position(&f, 600, 200); + let man = f.enable_xdg_output(); - f.create_xdg_output(&man, output_obj.obj.clone()); + f.create_xdg_output(&man, output_obj.obj); // testwl inits xdg output position to 0, and it should take priority over wl_output position test_position(&f, 0, 0); @@ -1892,82 +1721,6 @@ fn output_offset_xdg_override() { test_position(&f, 1000, 22); } -#[test] -fn output_offset_negative_position() { - let mut f = TestFixture::new(); - std::mem::take(&mut *f.registry.data.events.lock().unwrap()); - let (output, _) = f.new_output(-500, -500); - f.run(); - f.run(); - check_output_position_event(&output, (0, 0)); - - let (output2, _) = f.new_output(0, 0); - f.run(); - f.run(); - check_output_position_event(&output2, (500, 500)); - assert!(output.data.events.lock().unwrap().is_empty()); - - let (output3, _) = f.new_output(500, 500); - f.run(); - f.run(); - check_output_position_event(&output3, (1000, 1000)); - assert!(output.data.events.lock().unwrap().is_empty()); - assert!(output2.data.events.lock().unwrap().is_empty()); -} - -#[test] -fn output_offset_negative_position_update() { - let mut f = TestFixture::new(); - std::mem::take(&mut *f.registry.data.events.lock().unwrap()); - - let (output, _) = f.new_output(-500, -500); - f.run(); - f.run(); - check_output_position_event(&output, (0, 0)); - - let (output2, _) = f.new_output(0, -1000); - f.run(); - f.run(); - check_output_position_event(&output, (0, 500)); - check_output_position_event(&output2, (500, 0)); - - let (output3, o3) = f.new_output(-1000, 0); - f.run(); - f.run(); - check_output_position_event(&output, (500, 500)); - check_output_position_event(&output2, (1000, 0)); - check_output_position_event(&output3, (0, 1000)); - - f.testwl.move_output(&o3, 0, 0); - f.run(); - f.run(); - check_output_position_event(&output, (0, 500)); - check_output_position_event(&output2, (500, 0)); - check_output_position_event(&output3, (500, 1000)); -} - -#[test] -fn output_offset_negative_position_update_xdg() { - let mut f = TestFixture::new(); - std::mem::take(&mut *f.registry.data.events.lock().unwrap()); - let xdg = f.enable_xdg_output(); - - let (output, _) = f.new_output(-500, -500); - f.run(); - f.run(); - check_output_position_event(&output, (0, 0)); - - let (output2, output_s) = f.new_output(0, 0); - f.run(); - std::mem::take(&mut *output2.data.events.lock().unwrap()); - let xdg_output = f.create_xdg_output(&xdg, output2.obj.clone()); - f.testwl.move_xdg_output(&output_s, 0, -1000); - f.run(); - f.run(); - check_output_position_event(&output, (0, 500)); - check_output_position_event_xdg(&xdg_output, &output2, (500, 0), true); -} - #[test] fn reconfigure_popup() { let (mut f, comp) = TestFixture::new_with_compositor(); @@ -2272,6 +2025,140 @@ fn fullscreen_heuristic() { check_fullscreen(3, true); } +#[track_caller] +fn check_output_position_event(output: &TestObject, x: i32, y: i32) { + let events = std::mem::take(&mut *output.data.events.lock().unwrap()); + assert!(!events.is_empty()); + let mut done = false; + let mut geo = false; + for event in events { + match event { + wl_output::Event::Geometry { + x: geo_x, y: geo_y, .. + } => { + assert_eq!(geo_x, x); + assert_eq!(geo_y, y); + geo = true; + } + wl_output::Event::Done => { + done = true; + } + _ => {} + } + } + + assert!(geo, "Didn't get geometry event"); + assert!(done, "Didn't get done event"); +} + +#[test] +fn negative_output_position() { + let mut f = TestFixture::new(); + std::mem::take(&mut *f.registry.data.events.lock().unwrap()); + let (output, _) = f.new_output(-500, -500); + f.run(); + f.run(); + check_output_position_event(&output, 0, 0); + + let (output2, _) = f.new_output(0, 0); + f.run(); + f.run(); + check_output_position_event(&output2, 500, 500); + assert!(output.data.events.lock().unwrap().is_empty()); + + let (output3, _) = f.new_output(500, 500); + f.run(); + f.run(); + check_output_position_event(&output3, 1000, 1000); + assert!(output.data.events.lock().unwrap().is_empty()); + assert!(output2.data.events.lock().unwrap().is_empty()); +} + +#[test] +fn negative_output_position_update_offset() { + let mut f = TestFixture::new(); + std::mem::take(&mut *f.registry.data.events.lock().unwrap()); + + let (output, _) = f.new_output(-500, -500); + f.run(); + f.run(); + check_output_position_event(&output, 0, 0); + + let (output2, _) = f.new_output(0, -1000); + f.run(); + f.run(); + check_output_position_event(&output, 0, 500); + check_output_position_event(&output2, 500, 0); + + let (output3, _) = f.new_output(-1000, 0); + f.run(); + f.run(); + check_output_position_event(&output, 500, 500); + check_output_position_event(&output2, 1000, 0); + check_output_position_event(&output3, 0, 1000); +} + +#[test] +fn negative_output_xdg_position_update_offset() { + let mut f = TestFixture::new(); + std::mem::take(&mut *f.registry.data.events.lock().unwrap()); + let xdg = f.enable_xdg_output(); + + let (output, _) = f.new_output(-500, -500); + f.run(); + f.run(); + check_output_position_event(&output, 0, 0); + + let (output2, output_s) = f.new_output(0, 0); + let xdg_output = f.create_xdg_output(&xdg, output2.obj); + f.testwl.move_xdg_output(&output_s, 0, -1000); + f.run(); + f.run(); + check_output_position_event(&output, 0, 500); + + let mut found = false; + let mut first = false; + for event in std::mem::take(&mut *xdg_output.data.events.lock().unwrap()) { + if let zxdg_output_v1::Event::LogicalPosition { x, y } = event { + // Testwl sends a logical position event when the output is first created + // We are interested in the second one generated by satellite + if !first { + first = true; + continue; + } + assert_eq!(x, 500); + assert_eq!(y, 0); + found = true; + break; + } + } + assert!(found, "Did not get xdg output logical position"); + found = false; + for event in std::mem::take(&mut *output2.data.events.lock().unwrap()) { + if let wl_output::Event::Done = event { + found = true; + break; + } + } + assert!(found, "Did not get done event"); +} + +#[test] +fn negative_output_position_remove_offset() { + let mut f = TestFixture::new(); + std::mem::take(&mut *f.registry.data.events.lock().unwrap()); + + let (c_output, s_output) = f.new_output(-500, -500); + f.run(); + f.run(); + check_output_position_event(&c_output, 0, 0); + + f.testwl.move_output(&s_output, 500, 500); + f.run(); + f.run(); + check_output_position_event(&c_output, 500, 500); +} + #[test] fn scaled_output_popup() { let (mut f, comp) = TestFixture::new_with_compositor(); @@ -2401,8 +2288,8 @@ fn fractional_scale_small_popup() { { let data = f.testwl.get_surface_data(toplevel_id).unwrap(); let viewport = data.viewport.as_ref().expect("Missing viewport"); - assert_eq!(viewport.width, 67); - assert_eq!(viewport.height, 67); + assert_eq!(viewport.width, 66); + assert_eq!(viewport.height, 66); } let popup = Window::new(2); @@ -2838,63 +2725,6 @@ fn scaled_pointer_lock_position_hint() { ); } -#[test] -fn disconnected_output_rescaling() { - let mut f = TestFixture::new_pre_connect(|testwl| { - testwl.enable_fractional_scale(); - }); - let comp = f.compositor(); - let (_, output_main) = f.new_output(0, 0); - let (_, output_ext) = f.new_output(1000, 0); - - let window = Window::new(1); - let (_, id) = f.create_toplevel(&comp, window); - - let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); - let fractional = surface_data - .fractional - .as_ref() - .expect("No fractional scale for surface"); - fractional.preferred_scale(240); // 2.0 scale - f.testwl.move_surface_to_output(id, &output_main); - f.run(); - - let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); - let fractional = surface_data - .fractional - .as_ref() - .expect("No fractional scale for surface"); - fractional.preferred_scale(180); // 1.5 scale - f.testwl.move_surface_to_output(id, &output_ext); - f.run(); - // Multiple monitors with different scaling will select the lowest scale across monitors - assert_eq!(f.satellite.inner.new_scale, Some(1.5)); - - f.remove_output(output_ext); - let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); - let fractional = surface_data - .fractional - .as_ref() - .expect("No fractional scale for surface"); - fractional.preferred_scale(120); // 1.0 scale - f.run(); - f.run(); - // An fractional scale change done while the surface is on a removed output is ignored - assert_eq!(f.satellite.inner.new_scale, Some(1.5)); - - f.testwl.move_surface_to_output(id, &output_main); - let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); - let fractional = surface_data - .fractional - .as_ref() - .expect("No fractional scale for surface"); - fractional.preferred_scale(240); // 2.0 scale - f.run(); - f.run(); - // After the output is disconnected, only the 2x scale output remains, so use that scale - assert_eq!(f.satellite.inner.new_scale, Some(2.0)); -} - #[test] fn client_side_decorations() { let (mut f, compositor) = TestFixture::new_with_compositor(); diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index 0fd91d1..cb6f9b0 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -333,14 +333,6 @@ impl XState { time: x::CURRENT_TIME, }) .unwrap(); - - self.connection - .send_and_check_request(&x::SetSelectionOwner { - owner: self.wm_window, - selection: self.atoms.net_wm_cm_s0, - time: x::CURRENT_TIME, - }) - .unwrap(); } pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { @@ -731,12 +723,9 @@ impl XState { wmhint_popup = motif_popup && wm_hints.is_some_and(|h| !h.acquire_input_via_wm) && !hints.functions.as_ref().is_some_and(|f| { - f.intersects( - motif::Functions::Minimize - | motif::Functions::Maximize - | motif::Functions::Resize - | motif::Functions::All, - ) + f.contains(motif::Functions::Minimize) + || f.contains(motif::Functions::Maximize) + || f.contains(motif::Functions::All) }); // If the motif hints indicate the user shouldn't be able to do anything // to the window at all, it stands to reason it's probably a popup. @@ -1042,7 +1031,6 @@ xcb::atoms_struct! { wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false, wm_state => b"WM_STATE" only_if_exists = false, wm_s0 => b"WM_S0" only_if_exists = false, - net_wm_cm_s0 => b"_NET_WM_CM_S0" only_if_exists = false, wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false, net_wm_name => b"_NET_WM_NAME" only_if_exists = false, wm_pid => b"_NET_WM_PID" only_if_exists = false, diff --git a/tests/integration.rs b/tests/integration.rs index df08a07..06615bd 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -102,11 +102,12 @@ impl Drop for Fixture { 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(&0_i32.to_ne_bytes()) + .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"); @@ -318,7 +319,7 @@ impl Fixture { 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() + self.testwl.last_created_output() } } @@ -2173,27 +2174,6 @@ fn popup_heuristics() { &[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] @@ -2253,7 +2233,7 @@ fn xsettings_fractional_scale() { let mut connection = Connection::new(&f.display); f.testwl.enable_xdg_output_manager(); - let output = f.testwl.finalize_output(); + let output = f.testwl.last_created_output(); let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, window); diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index f8d850f..2304415 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -233,7 +233,6 @@ struct DataSourceData { struct Output { name: String, xdg: Option, - global_id: Option, } struct KeyboardState { @@ -267,8 +266,6 @@ struct State { last_surface_id: Option, created_surfaces: Vec, last_output: Option, - last_output_global: Option, - output_counter: u32, callbacks: Vec, seat: Option, pointer: Option, @@ -299,8 +296,6 @@ impl Default for State { begin: Instant::now(), last_surface_id: None, last_output: None, - last_output_global: None, - output_counter: 0, callbacks: Vec::new(), seat: None, pointer: None, @@ -577,15 +572,13 @@ impl Server { &self.state.created_surfaces } - /// Finish the initialization of an output created by `new_output`. - /// This function must be called after the globals have been dispatched in order to use the - /// output on the server side created by `new_output` (this function's return value). #[track_caller] - pub fn finalize_output(&mut self) -> WlOutput { - let output_s = self.state.last_output.take().expect("No new outputs"); - let output_data = self.state.outputs.get_mut(&output_s).unwrap(); - output_data.global_id = self.state.last_output_global.take(); - output_s + pub fn last_created_output(&self) -> WlOutput { + self.state + .last_output + .as_ref() + .expect("No outputs created!") + .clone() } pub fn get_object( @@ -852,8 +845,7 @@ impl Server { } pub fn new_output(&mut self, x: i32, y: i32) { - self.state.last_output_global = - Some(self.dh.create_global::(4, (x, y))); + self.dh.create_global::(4, (x, y)); self.display.flush_clients().unwrap(); } @@ -885,12 +877,6 @@ impl Server { self.display.flush_clients().unwrap(); } - pub fn remove_output(&mut self, output: WlOutput) { - let output = self.state.outputs.remove(&output).unwrap(); - self.dh.remove_global::(output.global_id.unwrap()); - self.display.flush_clients().unwrap(); - } - pub fn enable_xdg_output_manager(&mut self) { self.dh .create_global::(3, ()); @@ -1140,19 +1126,13 @@ impl GlobalDispatch for State { "fake monitor".to_string(), wl_output::Transform::Normal, ); - state.output_counter += 1; - let name = format!("WL-{}", state.output_counter); + let name = format!("WL-{}", state.outputs.len() + 1); output.name(name.clone()); output.mode(wl_output::Mode::Current, 1000, 1000, 0); output.done(); - state.outputs.insert( - output.clone(), - Output { - name, - xdg: None, - global_id: None, - }, - ); + state + .outputs + .insert(output.clone(), Output { name, xdg: None }); state.last_output = Some(output); } }