diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index c5f24e3..4eacc87 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -912,15 +912,31 @@ impl Dispatch for ServerState { _: &DisplayHandle, _: &mut wayland_server::DataInit<'_, Self>, ) { - let client = state.world.get::<&LockedPointerClient>(*entity).unwrap(); - simple_event_shunt! { - client, request: lp::Request => [ - SetCursorPositionHint { surface_x, surface_y }, - SetRegion { - |region| region.as_ref().map(|r| r.data().unwrap()) - }, - Destroy - ] + match request { + lp::Request::SetCursorPositionHint { + surface_x, + surface_y, + } => { + let (client, scale) = state + .world + .query_one_mut::<(&LockedPointerClient, &SurfaceScaleFactor)>(*entity) + .unwrap(); + + // Xwayland believes that the surface is actually times bigger + // than it currently is, and therefore that the cursor position is also scaled up by the same + // amount. So we need to divide the cursor position from Xwayland by the surface scale + // to get where the cursor should actually be positioned. + + client.set_cursor_position_hint(surface_x / scale.0, surface_y / scale.0); + } + lp::Request::Destroy => { + { + let client = state.world.get::<&LockedPointerClient>(*entity).unwrap(); + client.destroy(); + } + state.world.despawn(*entity).unwrap(); + } + _ => warn!("unhandled locked pointer request: {request:?}"), } } } @@ -983,8 +999,8 @@ impl } => { let surf_key: Entity = surface.data().copied().unwrap(); let ptr_key: Entity = pointer.data().copied().unwrap(); - let entity = state.world.reserve_entity(); + let client = { let c_surface = state .world @@ -1004,8 +1020,16 @@ impl ) }; let server = data_init.init(id, entity); + let surface_scale = state + .world + .get::<&SurfaceScaleFactor>(surf_key) + .as_deref() + .copied() + .unwrap(); - state.world.spawn_at(entity, (client, server)); + state + .world + .spawn_at(entity, (client, server, surface_scale)); } Request::Destroy => { client.destroy(); diff --git a/src/server/tests.rs b/src/server/tests.rs index b6728ca..2908c9b 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -27,7 +27,10 @@ use wayland_client::{ use wayland_protocols::{ wp::{ linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, - pointer_constraints::zv1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, + pointer_constraints::zv1::client::{ + zwp_locked_pointer_v1::ZwpLockedPointerV1, + zwp_pointer_constraints_v1::{self, ZwpPointerConstraintsV1}, + }, relative_pointer::zv1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, tablet::zv2::client::{ zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}, @@ -104,7 +107,8 @@ struct Compositor { shm: TestObject, shell: TestObject, seat: TestObject, - tablet_man: TestObject + tablet_man: TestObject, + pointer_constraints: TestObject, } => CompositorOptional } @@ -190,14 +194,12 @@ impl crate::X11Selection for Vec { mime: &str, mut pipe: smithay_client_toolkit::data_device_manager::WritePipe, ) { - println!("writing"); let data = self .iter() .find(|data| data.mime_type == mime) .unwrap_or_else(|| panic!("Couldn't find mime type {mime}")); pipe.write_all(&data.data) .expect("Couldn't write paste data"); - println!("goodbye pipe {mime}"); } } @@ -475,6 +477,9 @@ impl TestFixture { x if x == XwaylandShellV1::interface().name => bind!(shell), x if x == WlSeat::interface().name => bind!(seat), x if x == ZwpTabletManagerV2::interface().name => bind!(tablet_man), + x if x == ZwpPointerConstraintsV1::interface().name => { + bind!(pointer_constraints) + } _ => {} } } @@ -2248,8 +2253,6 @@ fn subpopup_positioning() { f.testwl.move_pointer_to(id_popup, 1.0, 1.0); f.run(); - println!("{:?}", f.satellite.last_hovered); - let win_subpopup = unsafe { Window::new(3) }; f.create_popup( @@ -2493,6 +2496,53 @@ fn quick_destroy_window_with_serial() { surface_data.role ); } + +#[test] +fn scaled_pointer_lock_position_hint() { + let mut f = TestFixture::new_pre_connect(|testwl| { + testwl.enable_fractional_scale(); + }); + let comp = f.compositor(); + let pointer = + TestObject::::from_request(&comp.seat.obj, wl_seat::Request::GetPointer {}); + + let (_, output) = f.new_output(0, 0); + let win = unsafe { Window::new(1) }; + let (surface, id) = f.create_toplevel(&comp, win); + 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); + f.run(); + f.run(); + + let locked_pointer = TestObject::::from_request( + &comp.pointer_constraints.obj, + zwp_pointer_constraints_v1::Request::LockPointer { + surface: surface.obj.clone(), + pointer: pointer.obj.clone(), + region: None, + lifetime: WEnum::Value(zwp_pointer_constraints_v1::Lifetime::Persistent), + }, + ); + locked_pointer.set_cursor_position_hint(75.0, 75.0); + f.run(); + f.run(); + + let lock_data = f + .testwl + .locked_pointer() + .expect("Missing locked pointer data"); + assert_eq!(lock_data.surface, id); + assert_eq!( + lock_data.cursor_hint, + Some(testwl::Vec2f { x: 50.0, y: 50.0 }) + ); +} + /// See Pointer::handle_event for an explanation. #[test] fn popup_pointer_motion_workaround() {} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 06197f1..e9ba96a 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -12,7 +12,10 @@ use wayland_protocols::{ wp_fractional_scale_v1::{self, WpFractionalScaleV1}, }, linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, - pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, + pointer_constraints::zv1::server::{ + zwp_locked_pointer_v1::{self, ZwpLockedPointerV1}, + zwp_pointer_constraints_v1::{self, ZwpPointerConstraintsV1}, + }, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, tablet::zv2::server::{ zwp_tablet_manager_v2::ZwpTabletManagerV2, @@ -165,6 +168,12 @@ pub struct Vec2 { pub y: i32, } +#[derive(Debug, Copy, Clone, PartialEq, Default)] +pub struct Vec2f { + pub x: f64, + pub y: f64, +} + #[derive(Debug, PartialEq, Eq)] pub struct XdgSurfaceData { pub surface: XdgSurface, @@ -187,6 +196,11 @@ impl XdgSurfaceData { #[derive(Debug, Hash, Clone, Copy, Eq, PartialEq)] pub struct SurfaceId(u32); +impl From<&WlSurface> for SurfaceId { + fn from(value: &WlSurface) -> Self { + Self(value.id().protocol_id()) + } +} #[derive(Hash, Clone, Copy, Eq, PartialEq)] struct PositionerId(u32); @@ -214,6 +228,15 @@ struct ActivationTokenData { constructed: bool, } +pub struct LockedPointer { + pub surface: SurfaceId, + pub cursor_hint: Option, +} +struct PointerState { + pointer: WlPointer, + locked: Option, +} + struct State { surfaces: HashMap, outputs: HashMap, @@ -224,7 +247,7 @@ struct State { last_output: Option, callbacks: Vec, seat: Option, - pointer: Option, + pointer: Option, keyboard: Option, touch: Option, tablet: Option, @@ -417,9 +440,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!(ZwpPointerConstraintsV1); struct HandlerData; impl ObjectData for HandlerData { @@ -577,7 +600,12 @@ impl Server { #[track_caller] pub fn pointer(&self) -> &WlPointer { - self.state.pointer.as_ref().unwrap() + self.state.pointer.as_ref().map(|p| &p.pointer).unwrap() + } + + #[track_caller] + pub fn locked_pointer(&self) -> Option<&LockedPointer> { + self.state.pointer.as_ref().unwrap().locked.as_ref() } #[track_caller] @@ -714,8 +742,8 @@ impl Server { let pointer = self.state.pointer.as_ref().expect("No pointer created"); let data = self.state.surfaces.get(&surface).expect("No such surface"); - pointer.enter(24, &data.surface, x, y); - pointer.frame(); + pointer.pointer.enter(24, &data.surface, x, y); + pointer.pointer.frame(); self.display.flush_clients().unwrap(); } @@ -807,6 +835,7 @@ simple_global_dispatch!(ZwpTabletManagerV2); simple_global_dispatch!(ZxdgDecorationManagerV1); simple_global_dispatch!(WpViewporter); simple_global_dispatch!(WpFractionalScaleManagerV1); +simple_global_dispatch!(ZwpPointerConstraintsV1); impl Dispatch for State { fn request( @@ -1100,7 +1129,10 @@ impl Dispatch for State { ) { match request { wl_seat::Request::GetPointer { id } => { - state.pointer = Some(data_init.init(id, ())); + state.pointer = Some(PointerState { + pointer: data_init.init(id, ()), + locked: None, + }); } wl_seat::Request::GetKeyboard { id } => { state.keyboard = Some(KeyboardState { @@ -1130,10 +1162,7 @@ impl Dispatch for State { match request { wl_pointer::Request::SetCursor { surface, .. } => { if let Some(surface) = surface { - let data = state - .surfaces - .get_mut(&SurfaceId(surface.id().protocol_id())) - .unwrap(); + let data = state.surfaces.get_mut(&SurfaceId::from(&surface)).unwrap(); assert!( matches!( @@ -2031,3 +2060,64 @@ impl Dispatch for State { } } } + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &ZwpPointerConstraintsV1, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + zwp_pointer_constraints_v1::Request::LockPointer { + id, + surface, + pointer, + region: _, + lifetime: _, + } => { + let pointer_state = state.pointer.as_mut().unwrap(); + assert_eq!(pointer, pointer_state.pointer); + let surface_id = SurfaceId::from(&surface); + + assert!(pointer_state.locked.is_none()); + data_init.init(id, ()); + pointer_state.locked = Some(LockedPointer { + surface: surface_id, + cursor_hint: None, + }); + } + _ => todo!("{request:?}"), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &ZwpLockedPointerV1, + request: ::Request, + _: &(), + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + zwp_locked_pointer_v1::Request::SetCursorPositionHint { + surface_x, + surface_y, + } => { + let state = state.pointer.as_mut().unwrap(); + let lock = state.locked.as_mut().unwrap(); + lock.cursor_hint = Some(Vec2f { + x: surface_x, + y: surface_y, + }); + } + _ => todo!("{request:?}"), + } + } +}