diff --git a/src/lib.rs b/src/lib.rs index cd9e2d7..d70be79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,7 @@ pub fn main(data: impl RunData) -> Option<()> { let mut xwayland = xwayland .args([ "-rootless", + "-force-xrandr-emulation", "-wm", &xsock_xwl.as_raw_fd().to_string(), "-displayfd", diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 07a84d2..c416b3a 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -23,7 +23,6 @@ use wayland_protocols::{ server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1 as RelativePointerManServer, }, tablet::zv2::{client as c_tablet, server as s_tablet}, - viewporter::{client as c_vp, server as s_vp}, }, xdg::xdg_output::zv1::{ client::zxdg_output_manager_v1::ZxdgOutputManagerV1 as OutputManClient, @@ -228,6 +227,7 @@ impl state.objects.insert_with_key(|key| { let client = client.create_surface(&state.qh, key); let server = data_init.init(id, key); + let viewport = state.viewporter.get_viewport(&client, &state.qh, ()); surface_id = Some(server.id().protocol_id()); debug!("new surface with key {key:?} ({surface_id:?})"); @@ -242,6 +242,8 @@ impl xwl: None, window: None, output_key: None, + scale_factor: 1, + viewport, } .into() }); @@ -775,60 +777,6 @@ impl Dispatch for ServerState { } } -impl Dispatch - for ServerState -{ - fn request( - _: &mut Self, - _: &wayland_server::Client, - _: &s_vp::wp_viewport::WpViewport, - request: ::Request, - c_viewport: &c_vp::wp_viewport::WpViewport, - _: &DisplayHandle, - _: &mut wayland_server::DataInit<'_, Self>, - ) { - simple_event_shunt! { - c_viewport, request: s_vp::wp_viewport::Request => [ - SetSource { x, y, width, height }, - SetDestination { width, height }, - Destroy - ] - } - } -} - -impl - Dispatch< - s_vp::wp_viewporter::WpViewporter, - ClientGlobalWrapper, - > for ServerState -{ - fn request( - state: &mut Self, - _: &wayland_server::Client, - _: &s_vp::wp_viewporter::WpViewporter, - request: ::Request, - client: &ClientGlobalWrapper, - _: &DisplayHandle, - data_init: &mut wayland_server::DataInit<'_, Self>, - ) { - use s_vp::wp_viewporter; - match request { - wp_viewporter::Request::GetViewport { id, surface } => 'get_viewport: { - let Some(c_surface) = state.get_client_surface_from_server(surface) else { - break 'get_viewport; - }; - let c_viewport = client.get_viewport(c_surface, &state.qh, ()); - data_init.init(id, c_viewport); - } - wp_viewporter::Request::Destroy => { - client.destroy(); - } - _ => unreachable!(), - } - } -} - impl Dispatch for ServerState { fn request( state: &mut Self, @@ -1255,10 +1203,6 @@ global_dispatch_no_events!( c_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1 ); global_dispatch_no_events!(OutputManServer, OutputManClient); -global_dispatch_no_events!( - s_vp::wp_viewporter::WpViewporter, - c_vp::wp_viewporter::WpViewporter -); global_dispatch_no_events!(PointerConstraintsServer, PointerConstraintsClient); global_dispatch_no_events!( s_tablet::zwp_tablet_manager_v2::ZwpTabletManagerV2, diff --git a/src/server/event.rs b/src/server/event.rs index 8c9b861..2541f27 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -108,6 +108,34 @@ impl SurfaceData { output_name } + fn update_viewport(&self, dims: WindowDims, size_hints: Option) { + let width = dims.width as i32 / self.scale_factor; + let height = dims.height as i32 / self.scale_factor; + self.viewport.set_destination(width, height); + debug!("{} viewport: {width}x{height}", self.server.id()); + if let Some(hints) = size_hints { + let Some(SurfaceRole::Toplevel(Some(data))) = &self.role else { + warn!( + "Trying to update size hints on {}, but toplevel role data is missing", + self.server.id() + ); + return; + }; + if let Some(min) = hints.min_size { + data.toplevel.set_min_size( + min.width / self.scale_factor, + min.height / self.scale_factor, + ); + } + if let Some(max) = hints.max_size { + data.toplevel.set_max_size( + max.width / self.scale_factor, + max.height / self.scale_factor, + ); + } + } + } + fn surface_event( &mut self, event: client::wl_surface::Event, @@ -122,12 +150,14 @@ impl SurfaceData { return; }; let output: &mut Output = object.as_mut(); - self.server.enter(&output.server); + self.scale_factor = output.scale; self.output_key = Some(key); debug!("{} entered {}", self.server.id(), output.server.id()); + let windows = &mut state.windows; if let Some(win_data) = self.window.as_ref().and_then(|win| windows.get_mut(win)) { + self.update_viewport(win_data.attrs.dims, win_data.attrs.size_hints); win_data.update_output_offset( key, WindowOutputOffset { @@ -157,7 +187,7 @@ impl SurfaceData { self.output_key = None; } } - Event::PreferredBufferScale { factor } => self.server.preferred_buffer_scale(factor), + Event::PreferredBufferScale { .. } => {} other => warn!("unhandled surface request: {other:?}"), } } @@ -175,19 +205,23 @@ impl SurfaceData { if let Some(pending) = xdg.pending.take() { let window = state.associated_windows[self.key]; let window = state.windows.get_mut(&window).unwrap(); - let x = pending.x + window.output_offset.x; - let y = pending.y + window.output_offset.y; + let x = pending.x * self.scale_factor + window.output_offset.x; + let y = pending.y * self.scale_factor + window.output_offset.y; let width = if pending.width > 0 { - pending.width as u16 + (pending.width * self.scale_factor) as u16 } else { window.attrs.dims.width }; let height = if pending.height > 0 { - pending.height as u16 + (pending.height * self.scale_factor) as u16 } else { window.attrs.dims.height }; - debug!("configuring {:?}: {x}x{y}, {width}x{height}", window.window); + debug!( + "configuring {} ({:?}): {x}x{y}, {width}x{height}", + self.server.id(), + window.window + ); connection.set_window_dims( window.window, PendingSurfaceState { @@ -203,6 +237,7 @@ impl SurfaceData { width, height, }; + self.update_viewport(window.attrs.dims, window.attrs.size_hints); } if let Some(SurfaceAttach { buffer, x, y }) = self.attach.take() { @@ -225,7 +260,10 @@ impl SurfaceData { height, states, } => { - debug!("configuring toplevel {width}x{height}, {states:?}"); + debug!( + "configuring toplevel {} {width}x{height}, {states:?}", + self.server.id() + ); if let Some(SurfaceRole::Toplevel(Some(toplevel))) = &mut self.role { let prev_fs = toplevel.fullscreen; toplevel.fullscreen = @@ -250,6 +288,9 @@ impl SurfaceData { let window = state.associated_windows[self.key]; state.close_x_window(window); } + // TODO: support capabilities (minimize, maximize, etc) + xdg_toplevel::Event::WmCapabilities { .. } => {} + xdg_toplevel::Event::ConfigureBounds { .. } => {} ref other => warn!("unhandled xdgtoplevel event: {other:?}"), } } @@ -262,7 +303,10 @@ impl SurfaceData { width, height, } => { - trace!("popup configure: {x}x{y}, {width}x{height}"); + trace!( + "popup configure {}: {x}x{y}, {width}x{height}", + self.server.id() + ); self.xdg_mut().unwrap().pending = Some(PendingSurfaceState { x, y, @@ -350,6 +394,7 @@ pub struct Pointer { server: WlPointer, pub client: client::wl_pointer::WlPointer, pending_enter: PendingEnter, + scale: i32, } impl Pointer { @@ -358,6 +403,7 @@ impl Pointer { server, client, pending_enter: PendingEnter(None), + scale: 1, } } } @@ -393,10 +439,15 @@ impl HandleEvent for Pointer { break 'enter; }; + self.scale = surface_data.scale_factor; let mut do_enter = || { - debug!("entering surface ({serial})"); - self.server - .enter(serial, &surface_data.server, surface_x, surface_y); + debug!("pointer entering {} ({serial})", surface_data.server.id()); + self.server.enter( + serial, + &surface_data.server, + surface_x * self.scale as f64, + surface_y * self.scale as f64, + ); let window = surface_data.window.unwrap(); state.connection.as_mut().unwrap().raise_to_top(window); state.last_hovered = Some(window); @@ -473,7 +524,11 @@ impl HandleEvent for Pointer { warn!("could not move pointer to surface ({serial}): stale surface"); } } else { - self.server.motion(time, surface_x, surface_y); + self.server.motion( + time, + surface_x * self.scale as f64, + surface_y * self.scale as f64, + ); } } _ => simple_event_shunt! { @@ -715,6 +770,7 @@ pub struct Output { windows: HashSet, pub(super) dimensions: OutputDimensions, name: String, + scale: i32, } impl Output { @@ -739,6 +795,7 @@ impl Output { height: 0, }, name: "".to_string(), + scale: 1, } } } @@ -873,72 +930,90 @@ impl Output { event: client::wl_output::Event, state: &mut ServerState, ) { - if let client::wl_output::Event::Geometry { - x, - y, - physical_width, - physical_height, - subpixel, - make, - model, - transform, - } = &event - { - self.update_offset( - OutputDimensionsSource::Wl { - physical_width: *physical_width, - physical_height: *physical_height, - subpixel: *subpixel, - make: make.clone(), - model: model.clone(), - transform: *transform, - }, - *x, - *y, - state, - ); - } - - if let client::wl_output::Event::Mode { width, height, .. } = event { - if matches!(self.dimensions.source, OutputDimensionsSource::Wl { .. }) { - self.dimensions.width = width; - self.dimensions.height = height; - debug!("{} dimensions: {width}x{height} (wl)", self.server.id()); - } - } - - simple_event_shunt! { - self.server, event: client::wl_output::Event => [ - Name { - |name| { - self.name = name.clone(); - name - } - }, - Description { description }, - Mode { - |flags| convert_wenum(flags), - width, - height, - refresh - }, - Scale { factor }, - Geometry { - |x| { - x - state.global_output_offset.x.value - }, - |y| { - y - state.global_output_offset.y.value + use client::wl_output::Event; + match event { + Event::Geometry { + x, + y, + physical_width, + physical_height, + subpixel, + make, + model, + transform, + } => { + self.update_offset( + OutputDimensionsSource::Wl { + physical_width, + physical_height, + subpixel, + make: make.clone(), + model: model.clone(), + transform, }, + x, + y, + state, + ); + self.server.geometry( + x - state.global_output_offset.x.value, + y - state.global_output_offset.y.value, physical_width, physical_height, - |subpixel| convert_wenum(subpixel), + convert_wenum(subpixel), make, model, - |transform| convert_wenum(transform) - }, - Done - ] + convert_wenum(transform), + ); + } + Event::Mode { + flags, + width, + height, + refresh, + } => { + if matches!(self.dimensions.source, OutputDimensionsSource::Wl { .. }) { + self.dimensions.width = width; + self.dimensions.height = height; + debug!("{} dimensions: {width}x{height} (wl)", self.server.id()); + } + self.server + .mode(convert_wenum(flags), width, height, refresh); + } + Event::Scale { factor } => { + debug!("{} scale: {factor}", self.server.id()); + self.scale = factor; + self.windows.retain(|window| { + let Some(data): Option<&WindowData> = state.windows.get(window) else { + return false; + }; + + if let Some::<&mut SurfaceData>(surface) = data + .surface_key + .and_then(|key| state.objects.get_mut(key)) + .map(AsMut::as_mut) + { + surface.scale_factor = factor; + surface.update_viewport(data.attrs.dims, data.attrs.size_hints); + } + + true + }); + + self.server.scale(factor); + } + _ => simple_event_shunt! { + self.server, event: Event => [ + Name { + |name| { + self.name = name.clone(); + name + } + }, + Description { description }, + Done + ] + }, } } @@ -947,31 +1022,27 @@ impl Output { event: zxdg_output_v1::Event, state: &mut ServerState, ) { - if let zxdg_output_v1::Event::LogicalPosition { x, y } = event { - self.update_offset(OutputDimensionsSource::Xdg, x, y, state); - } + use zxdg_output_v1::Event; + let xdg = &self.xdg.as_ref().unwrap().server; - if let zxdg_output_v1::Event::LogicalSize { width, height } = event { - self.dimensions.source = OutputDimensionsSource::Xdg; - self.dimensions.width = width; - self.dimensions.height = height; - debug!("{} dimensions: {width}x{height} (xdg)", self.server.id()); - } - simple_event_shunt! { - xdg, event: zxdg_output_v1::Event => [ - LogicalPosition { - |x| { - x - state.global_output_offset.x.value - }, - |y| { - y - state.global_output_offset.y.value - } - }, - LogicalSize { width, height }, - Done, - Name { name }, - Description { description } - ] + match event { + Event::LogicalPosition { x, y } => { + self.update_offset(OutputDimensionsSource::Xdg, x, y, state); + self.xdg.as_ref().unwrap().server.logical_position( + x - state.global_output_offset.x.value, + y - state.global_output_offset.y.value, + ); + } + Event::LogicalSize { .. } => { + xdg.logical_size(self.dimensions.width, self.dimensions.height); + } + _ => simple_event_shunt! { + xdg, event: zxdg_output_v1::Event => [ + Done, + Name { name }, + Description { description } + ] + }, } } } diff --git a/src/server/mod.rs b/src/server/mod.rs index c0ce7ca..99c4fb9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -32,7 +32,7 @@ use wayland_protocols::{ pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, tablet::zv2::server::zwp_tablet_manager_v2::ZwpTabletManagerV2, - viewporter::server as s_vp, + viewporter::client::{wp_viewport::WpViewport, wp_viewporter::WpViewporter}, }, xdg::{ shell::client::{ @@ -188,6 +188,8 @@ pub struct SurfaceData { xwl: Option, window: Option, output_key: Option, + scale_factor: i32, + viewport: WpViewport, } impl SurfaceData { @@ -467,7 +469,6 @@ fn handle_globals<'a, C: XConnection>( WlDrmServer, s_dmabuf::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, ZxdgOutputManagerV1, - s_vp::wp_viewporter::WpViewporter, ZwpPointerConstraintsV1, ZwpTabletManagerV2 ]; @@ -513,6 +514,7 @@ pub struct ServerState { pub connection: Option, xdg_wm_base: XdgWmBase, + viewporter: WpViewporter, clipboard_data: Option>, last_kb_serial: Option<(client::wl_seat::WlSeat, u32)>, activation_state: Option, @@ -535,6 +537,11 @@ impl ServerState { warn!("xdg_wm_base version 2 detected. Popup repositioning will not work, and some popups may not work correctly."); } + let viewporter = clientside + .global_list + .bind::(&qh, 1..=1, ()) + .expect("Could not bind wp_viewporter"); + let manager = DataDeviceManagerState::bind(&clientside.global_list, &qh) .inspect_err(|e| { warn!("Could not bind data device manager ({e:?}). Clipboard will not work.") @@ -582,6 +589,7 @@ impl ServerState { output_keys: Default::default(), associated_windows: Default::default(), xdg_wm_base, + viewporter, clipboard_data, last_kb_serial: None, activation_state, @@ -722,10 +730,16 @@ impl ServerState { let surface: &SurfaceData = object.as_ref(); if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role { if let Some(min_size) = &hints.min_size { - data.toplevel.set_min_size(min_size.width, min_size.height); + data.toplevel.set_min_size( + min_size.width / surface.scale_factor, + min_size.height / surface.scale_factor, + ); } if let Some(max_size) = &hints.max_size { - data.toplevel.set_max_size(max_size.width, max_size.height); + data.toplevel.set_max_size( + max_size.width / surface.scale_factor, + max_size.height / surface.scale_factor, + ); } } } else { @@ -817,12 +831,13 @@ impl ServerState { match &data.role { Some(SurfaceRole::Popup(Some(popup))) => { popup.positioner.set_offset( - event.x() as i32 - win.output_offset.x, - event.y() as i32 - win.output_offset.y, + (event.x() as i32 - win.output_offset.x) / data.scale_factor, + (event.y() as i32 - win.output_offset.y) / data.scale_factor, + ); + popup.positioner.set_size( + event.width() as i32 / data.scale_factor, + event.height() as i32 / data.scale_factor, ); - popup - .positioner - .set_size(event.width().into(), event.height().into()); popup.popup.reposition(&popup.positioner, 0); } other => warn!("Non popup ({other:?}) being reconfigured, behavior may be off."), @@ -1169,30 +1184,34 @@ impl ServerState { self.objects[surface_key].0 = Some(surface.into()); let window = self.windows.get(&window).unwrap(); + let initial_scale; let role = if let Some(parent) = window.attrs.popup_for { - debug!( - "creating popup ({:?}) {:?} {:?} {:?} {surface_key:?}", - window.window, parent, window.attrs.dims, surface_id - ); - let parent_window = self.windows.get(&parent).unwrap(); let parent_surface: &SurfaceData = self.objects[parent_window.surface_key.unwrap()].as_ref(); let parent_dims = parent_window.attrs.dims; - let x = window.attrs.dims.x - parent_dims.x; - let y = window.attrs.dims.y - parent_dims.y; + initial_scale = parent_surface.scale_factor; + debug!( + "creating popup ({:?}) {:?} {:?} {:?} {surface_key:?} (scale: {initial_scale})", + window.window, parent, window.attrs.dims, surface_id + ); let positioner = self.xdg_wm_base.create_positioner(&self.qh, ()); - positioner.set_size(window.attrs.dims.width as _, window.attrs.dims.height as _); - positioner.set_offset(x as i32, y as i32); + positioner.set_size( + 1.max(window.attrs.dims.width as i32 / initial_scale), + 1.max(window.attrs.dims.height as i32 / initial_scale), + ); + let x = (window.attrs.dims.x - parent_dims.x) as i32 / initial_scale; + let y = (window.attrs.dims.y - parent_dims.y) as i32 / initial_scale; + positioner.set_offset(x, y); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); positioner.set_anchor_rect( 0, 0, - parent_window.attrs.dims.width as _, - parent_window.attrs.dims.height as _, + parent_window.attrs.dims.width as i32 / initial_scale, + parent_window.attrs.dims.height as i32 / initial_scale, ); let popup = xdg_surface.get_popup( Some(&parent_surface.xdg().unwrap().surface), @@ -1211,11 +1230,13 @@ impl ServerState { }; SurfaceRole::Popup(Some(popup)) } else { + initial_scale = 1; let data = self.create_toplevel(window, surface_key, xdg_surface, fullscreen); SurfaceRole::Toplevel(Some(data)) }; let surface: &mut SurfaceData = self.objects[surface_key].as_mut(); + surface.scale_factor = initial_scale; let new_role_type = std::mem::discriminant(&role); let prev = surface.role.replace(role); diff --git a/src/server/tests.rs b/src/server/tests.rs index 473de59..5e197f1 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -1,5 +1,5 @@ use super::{ServerState, WindowDims}; -use crate::xstate::{SetState, WmName}; +use crate::xstate::{SetState, WinSize, WmName}; use rustix::event::{poll, PollFd, PollFlags}; use std::collections::HashMap; use std::io::Write; @@ -44,7 +44,6 @@ use wayland_protocols::{ zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, zwp_tablet_v2::{self, ZwpTabletV2}, }, - viewporter::client::wp_viewporter::WpViewporter, }, xdg::{ shell::server::{xdg_positioner, xdg_toplevel}, @@ -157,11 +156,18 @@ struct FakeXConnection { impl FakeXConnection { #[track_caller] - fn window(&mut self, window: Window) -> &mut WindowData { + fn window_mut(&mut self, window: Window) -> &mut WindowData { self.windows .get_mut(&window) .unwrap_or_else(|| panic!("Unknown window: {window:?}")) } + + #[track_caller] + fn window(&self, window: Window) -> &WindowData { + self.windows + .get(&window) + .unwrap_or_else(|| panic!("Unknown window: {window:?}")) + } } impl Default for FakeXConnection { @@ -204,17 +210,17 @@ impl super::XConnection for FakeXConnection { #[track_caller] fn close_window(&mut self, window: Window) { log::debug!("closing window {window:?}"); - self.window(window).mapped = false; + self.window_mut(window).mapped = false; } #[track_caller] fn set_fullscreen(&mut self, window: xcb::x::Window, fullscreen: bool) { - self.window(window).fullscreen = fullscreen; + self.window_mut(window).fullscreen = fullscreen; } #[track_caller] fn set_window_dims(&mut self, window: Window, state: super::PendingSurfaceState) { - self.window(window).dims = WindowDims { + self.window_mut(window).dims = WindowDims { x: state.x as _, y: state.y as _, width: state.width as _, @@ -258,6 +264,63 @@ struct TestFixture { static INIT: std::sync::Once = std::sync::Once::new(); +struct PopupBuilder { + window: Window, + parent_window: Window, + parent_surface: testwl::SurfaceId, + dims: WindowDims, + scale: i32, + check_size_and_pos: bool, +} + +impl PopupBuilder { + fn new(window: Window, parent_window: Window, parent_surface: testwl::SurfaceId) -> Self { + Self { + window, + parent_window, + parent_surface, + dims: WindowDims { + x: 0, + y: 0, + width: 50, + height: 50, + }, + scale: 1, + check_size_and_pos: true, + } + } + + fn x(mut self, x: i16) -> Self { + self.dims.x = x; + self + } + + fn y(mut self, y: i16) -> Self { + self.dims.y = y; + self + } + + fn width(mut self, width: u16) -> Self { + self.dims.width = width; + self + } + + fn height(mut self, height: u16) -> Self { + self.dims.height = height; + self + } + + fn check_size_and_pos(mut self, check: bool) -> Self { + self.check_size_and_pos = check; + self + } + + fn scale(mut self, scale: i32) -> Self { + self.scale = scale; + self + } +} + impl TestFixture { fn new() -> Self { INIT.call_once(|| { @@ -639,24 +702,23 @@ impl TestFixture { fn create_popup( &mut self, comp: &Compositor, - window: Window, - parent_window: Window, - parent_surface: testwl::SurfaceId, - x: i16, - y: i16, + builder: PopupBuilder, ) -> (TestObject, testwl::SurfaceId) { let (buffer, surface) = comp.create_surface(); + let PopupBuilder { + window, + parent_window, + parent_surface, + dims, + scale, + check_size_and_pos, + } = builder; + let data = WindowData { mapped: true, - dims: WindowDims { - x, - y, - width: 50, - height: 50, - }, + dims, fullscreen: false, }; - let dims = data.dims; self.new_window(window, true, data, None); self.map_window(comp, window, &surface.obj, &buffer); self.run(); @@ -689,26 +751,34 @@ impl TestFixture { assert_eq!(&surface_data.popup().parent, toplevel_xdg); let pos = &surface_data.popup().positioner_state; - assert_eq!(pos.size.as_ref().unwrap(), &testwl::Vec2 { x: 50, y: 50 }); + if check_size_and_pos { + assert_eq!( + pos.size.as_ref().unwrap(), + &testwl::Vec2 { + x: 50 / scale, + y: 50 / scale + } + ); - let parent_win = &self.connection().windows[&parent_window]; - assert_eq!( - pos.anchor_rect.as_ref().unwrap(), - &testwl::Rect { - size: testwl::Vec2 { - x: parent_win.dims.width as _, - y: parent_win.dims.height as _ - }, - offset: testwl::Vec2::default() - } - ); - assert_eq!( - pos.offset, - testwl::Vec2 { - x: (dims.x - parent_win.dims.x) as _, - y: (dims.y - parent_win.dims.y) as _ - } - ); + let parent_win = &self.connection().windows[&parent_window]; + assert_eq!( + pos.anchor_rect.as_ref().unwrap(), + &testwl::Rect { + size: testwl::Vec2 { + x: parent_win.dims.width as i32 / scale, + y: parent_win.dims.height as i32 / scale + }, + offset: testwl::Vec2::default() + } + ); + assert_eq!( + pos.offset, + testwl::Vec2 { + x: (dims.x - parent_win.dims.x) as i32 / scale, + y: (dims.y - parent_win.dims.y) as i32 / scale + } + ); + } assert_eq!(pos.anchor, xdg_positioner::Anchor::TopLeft); assert_eq!(pos.gravity, xdg_positioner::Gravity::BottomRight); } @@ -850,8 +920,10 @@ fn popup_flow_simple() { let (_, toplevel_id) = f.create_toplevel(&compositor, win_toplevel); let win_popup = unsafe { Window::new(2) }; - let (popup_surface, popup_id) = - f.create_popup(&compositor, win_popup, win_toplevel, toplevel_id, 10, 10); + let (popup_surface, popup_id) = f.create_popup( + &compositor, + PopupBuilder::new(win_popup, win_toplevel, toplevel_id), + ); f.satellite.unmap_window(win_popup); f.satellite.destroy_window(win_popup); @@ -910,7 +982,6 @@ fn pass_through_globals() { ZwpLinuxDmabufV1, ZwpRelativePointerManagerV1, ZxdgOutputManagerV1, - WpViewporter, WlDrm, ZwpPointerConstraintsV1, XwaylandShellV1, @@ -968,7 +1039,7 @@ fn popup_window_changes_surface() { let (_, toplevel_id) = f.create_toplevel(&comp, t_win); let win = unsafe { Window::new(2) }; - let (surface, old_id) = f.create_popup(&comp, win, t_win, toplevel_id, 0, 0); + let (surface, old_id) = f.create_popup(&comp, PopupBuilder::new(win, t_win, toplevel_id)); f.satellite.unmap_window(win); surface.obj.destroy(); @@ -1328,7 +1399,19 @@ fn override_redirect_choose_hover_window() { let win3 = unsafe { Window::new(3) }; let (buffer, surface) = comp.create_surface(); - f.new_window(win3, true, WindowData::default(), None); + f.new_window( + win3, + true, + WindowData { + dims: WindowDims { + width: 1, + height: 1, + ..Default::default() + }, + ..Default::default() + }, + None, + ); f.map_window(&comp, win3, &surface.obj, &buffer); f.run(); let id3 = f.check_new_surface(); @@ -1385,7 +1468,8 @@ fn output_offset() { } let popup = unsafe { Window::new(2) }; - let (p_surface, p_id) = f.create_popup(&comp, popup, window, t_id, 510, 110); + 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(); @@ -1456,7 +1540,7 @@ fn reposition_popup() { let (_, t_id) = f.create_toplevel(&comp, toplevel); let popup = unsafe { Window::new(2) }; - let (_, p_id) = f.create_popup(&comp, popup, toplevel, t_id, 20, 40); + let (_, p_id) = f.create_popup(&comp, PopupBuilder::new(popup, toplevel, t_id).x(20).y(40)); f.satellite.reconfigure_window(x::ConfigureNotifyEvent::new( popup, @@ -1839,6 +1923,143 @@ fn negative_output_position_remove_offset() { check_output_position_event(&c_output, 500, 500); } +#[test] +fn scaled_output_popup() { + let (mut f, comp) = TestFixture::new_with_compositor(); + + let (_, output) = f.new_output(0, 0); + let scale = 2; + output.scale(scale); + output.done(); + f.run(); + f.run(); + + let toplevel = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); + f.testwl.move_surface_to_output(toplevel_id, &output); + f.run(); + + let popup = unsafe { Window::new(2) }; + let builder = PopupBuilder::new(popup, toplevel, toplevel_id) + .x(50) + .y(50) + .scale(scale); + let initial_dims = builder.dims; + let (_, popup_id) = f.create_popup(&comp, builder); + f.testwl.move_surface_to_output(popup_id, &output); + f.run(); + assert_eq!( + initial_dims, + f.connection().window(popup).dims, + "X11 dimensions changed after configure" + ); +} + +#[test] +fn scaled_output_small_popup() { + let (mut f, comp) = TestFixture::new_with_compositor(); + + let (_, output) = f.new_output(0, 0); + output.scale(2); + output.done(); + f.run(); + f.run(); + + let toplevel = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); + f.testwl.move_surface_to_output(toplevel_id, &output); + f.run(); + + let popup = unsafe { Window::new(2) }; + let builder = PopupBuilder::new(popup, toplevel, toplevel_id) + .x(50) + .y(50) + .width(1) + .height(1) + .scale(2) + .check_size_and_pos(false); + + let (_, popup_id) = f.create_popup(&comp, builder); + f.testwl.move_surface_to_output(popup_id, &output); + f.run(); + + let dims = f.connection().window(popup).dims; + + assert!(dims.width > 0); + assert!(dims.height > 0); +} + +#[test] +fn toplevel_size_limits_scaled() { + let (mut f, comp) = TestFixture::new_with_compositor(); + + let (_, output) = f.new_output(0, 0); + output.scale(2); + output.done(); + f.run(); + f.run(); + + let window = unsafe { Window::new(1) }; + let (buffer, surface) = comp.create_surface(); + let data = WindowData { + mapped: true, + dims: WindowDims { + width: 50, + height: 50, + ..Default::default() + }, + fullscreen: false, + }; + f.new_window(window, false, data, None); + f.satellite.set_size_hints( + window, + super::WmNormalHints { + min_size: Some(WinSize { + width: 20, + height: 20, + }), + max_size: Some(WinSize { + width: 100, + height: 100, + }), + }, + ); + + f.map_window(&comp, window, &surface.obj, &buffer); + f.run(); + + let id = f.check_new_surface(); + f.testwl.configure_toplevel(id, 50, 50, vec![]); + f.run(); + + f.testwl.move_surface_to_output(id, &output); + f.run(); + + let data = f.testwl.get_surface_data(id).unwrap(); + let toplevel = data.toplevel(); + assert_eq!(toplevel.min_size, Some(testwl::Vec2 { x: 10, y: 10 })); + assert_eq!(toplevel.max_size, Some(testwl::Vec2 { x: 50, y: 50 })); + + f.satellite.set_size_hints( + window, + super::WmNormalHints { + min_size: Some(WinSize { + width: 40, + height: 40, + }), + max_size: Some(WinSize { + width: 200, + height: 200, + }), + }, + ); + f.run(); + let data = f.testwl.get_surface_data(id).unwrap(); + let toplevel = data.toplevel(); + assert_eq!(toplevel.min_size, Some(testwl::Vec2 { x: 20, y: 20 })); + assert_eq!(toplevel.max_size, Some(testwl::Vec2 { x: 100, y: 100 })); +} + /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index 74b45fa..5c59216 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -842,13 +842,13 @@ bitflags! { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct WinSize { pub width: i32, pub height: i32, } -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct WmNormalHints { pub min_size: Option, pub max_size: Option, diff --git a/tests/integration.rs b/tests/integration.rs index 74d8193..a86649c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -239,10 +239,6 @@ impl Fixture { &mut self, connection: &mut Connection, window: x::Window, - x: i16, - y: i16, - width: u16, - height: u16, ) -> testwl::SurfaceId { connection.map_window(window); self.wait_and_dispatch(); @@ -258,15 +254,6 @@ impl Fixture { ); self.testwl.configure_popup(surface); self.wait_and_dispatch(); - let geometry = connection.get_reply(&x::GetGeometry { - drawable: x::Drawable::Window(window), - }); - - assert_eq!(geometry.x(), x); - assert_eq!(geometry.y(), y); - assert_eq!(geometry.width(), width); - assert_eq!(geometry.height(), height); - surface } @@ -796,8 +783,7 @@ fn input_focus() { long_offset: 0, long_length: 1, }) - .value::() - .get(0) + .value::().first() .and_then(|state| WmState::try_from(*state).ok()), Some(WmState::Normal) ); @@ -1475,7 +1461,15 @@ fn popup_done() { 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, 0, 0, 20, 20); + 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(); @@ -1576,3 +1570,74 @@ fn xdg_decorations() { 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); +} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 014b52b..a315763 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -20,7 +20,10 @@ use wayland_protocols::{ zwp_tablet_tool_v2::{self, ZwpTabletToolV2}, zwp_tablet_v2::ZwpTabletV2, }, - viewporter::server::wp_viewporter::WpViewporter, + viewporter::server::{ + wp_viewport::WpViewport, + wp_viewporter::{self, WpViewporter}, + }, }, xdg::{ activation::v1::server::{ @@ -392,9 +395,9 @@ impl Server { dh.create_global::(1, ()); dh.create_global::(1, ()); dh.create_global::(1, ()); + dh.create_global::(1, ()); global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpRelativePointerManagerV1); - global_noop!(WpViewporter); global_noop!(ZwpPointerConstraintsV1); struct HandlerData; @@ -1239,14 +1242,33 @@ impl Dispatch for State { parent, positioner, } => { + let positioner_state = + state.positioners[&PositionerId(positioner.id().protocol_id())].clone(); + if positioner_state + .size + .is_none_or(|size| size.x <= 0 || size.y <= 0) + { + // TODO: figure out why the client.kill here doesn't make satellite print the error message + let message = + format!("positioner had an invalid size {:?}", positioner_state.size); + eprintln!("{message}"); + client.kill( + dh, + ProtocolError { + code: xdg_surface::Error::InvalidSize.into(), + object_id: resource.id().protocol_id(), + object_interface: XdgSurface::interface().name.to_string(), + message, + }, + ); + return; + } let popup = data_init.init(id, *surface_id); let p = Popup { xdg: XdgSurfaceData::new(resource.clone()), popup, parent: parent.unwrap(), - positioner_state: state.positioners - [&PositionerId(positioner.id().protocol_id())] - .clone(), + positioner_state, }; let data = state.surfaces.get_mut(surface_id).unwrap(); data.role = Some(SurfaceRole::Popup(p)); @@ -1789,3 +1811,50 @@ impl Dispatch for State { } } } + +impl GlobalDispatch for State { + fn bind( + _: &mut Self, + _: &DisplayHandle, + _: &Client, + resource: wayland_server::New, + _: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + data_init.init(resource, ()); + } +} + +impl Dispatch for State { + fn request( + _: &mut Self, + _: &Client, + _: &WpViewporter, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wp_viewporter::Request::GetViewport { surface: _, id } => { + data_init.init(id, ()); + } + wp_viewporter::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch for State { + fn request( + _: &mut Self, + _: &Client, + _: &WpViewport, + _: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + //todo!() + } +}