server: unscale locked pointer cursor hint

Fix #199, fix #202, fix #204
This commit is contained in:
Shawn Wallace 2025-07-12 12:20:21 -04:00
parent 557ebeb616
commit 07847e11d7
3 changed files with 192 additions and 28 deletions

View file

@ -912,15 +912,31 @@ impl<C: XConnection> Dispatch<LockedPointerServer, Entity> for ServerState<C> {
_: &DisplayHandle, _: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>, _: &mut wayland_server::DataInit<'_, Self>,
) { ) {
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 <surface scale factor> 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(); let client = state.world.get::<&LockedPointerClient>(*entity).unwrap();
simple_event_shunt! { client.destroy();
client, request: lp::Request => [ }
SetCursorPositionHint { surface_x, surface_y }, state.world.despawn(*entity).unwrap();
SetRegion { }
|region| region.as_ref().map(|r| r.data().unwrap()) _ => warn!("unhandled locked pointer request: {request:?}"),
},
Destroy
]
} }
} }
} }
@ -983,8 +999,8 @@ impl<C: XConnection>
} => { } => {
let surf_key: Entity = surface.data().copied().unwrap(); let surf_key: Entity = surface.data().copied().unwrap();
let ptr_key: Entity = pointer.data().copied().unwrap(); let ptr_key: Entity = pointer.data().copied().unwrap();
let entity = state.world.reserve_entity(); let entity = state.world.reserve_entity();
let client = { let client = {
let c_surface = state let c_surface = state
.world .world
@ -1004,8 +1020,16 @@ impl<C: XConnection>
) )
}; };
let server = data_init.init(id, entity); 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 => { Request::Destroy => {
client.destroy(); client.destroy();

View file

@ -27,7 +27,10 @@ use wayland_client::{
use wayland_protocols::{ use wayland_protocols::{
wp::{ wp::{
linux_dmabuf::zv1::client::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, 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, relative_pointer::zv1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
tablet::zv2::client::{ tablet::zv2::client::{
zwp_tablet_manager_v2::{self, ZwpTabletManagerV2}, zwp_tablet_manager_v2::{self, ZwpTabletManagerV2},
@ -104,7 +107,8 @@ struct Compositor {
shm: TestObject<WlShm>, shm: TestObject<WlShm>,
shell: TestObject<XwaylandShellV1>, shell: TestObject<XwaylandShellV1>,
seat: TestObject<WlSeat>, seat: TestObject<WlSeat>,
tablet_man: TestObject<ZwpTabletManagerV2> tablet_man: TestObject<ZwpTabletManagerV2>,
pointer_constraints: TestObject<ZwpPointerConstraintsV1>,
} => CompositorOptional } => CompositorOptional
} }
@ -190,14 +194,12 @@ impl crate::X11Selection for Vec<testwl::PasteData> {
mime: &str, mime: &str,
mut pipe: smithay_client_toolkit::data_device_manager::WritePipe, mut pipe: smithay_client_toolkit::data_device_manager::WritePipe,
) { ) {
println!("writing");
let data = self let data = self
.iter() .iter()
.find(|data| data.mime_type == mime) .find(|data| data.mime_type == mime)
.unwrap_or_else(|| panic!("Couldn't find mime type {mime}")); .unwrap_or_else(|| panic!("Couldn't find mime type {mime}"));
pipe.write_all(&data.data) pipe.write_all(&data.data)
.expect("Couldn't write paste 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 == XwaylandShellV1::interface().name => bind!(shell),
x if x == WlSeat::interface().name => bind!(seat), x if x == WlSeat::interface().name => bind!(seat),
x if x == ZwpTabletManagerV2::interface().name => bind!(tablet_man), 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.testwl.move_pointer_to(id_popup, 1.0, 1.0);
f.run(); f.run();
println!("{:?}", f.satellite.last_hovered);
let win_subpopup = unsafe { Window::new(3) }; let win_subpopup = unsafe { Window::new(3) };
f.create_popup( f.create_popup(
@ -2493,6 +2496,53 @@ fn quick_destroy_window_with_serial() {
surface_data.role 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::<WlPointer>::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::<ZwpLockedPointerV1>::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. /// See Pointer::handle_event for an explanation.
#[test] #[test]
fn popup_pointer_motion_workaround() {} fn popup_pointer_motion_workaround() {}

View file

@ -12,7 +12,10 @@ use wayland_protocols::{
wp_fractional_scale_v1::{self, WpFractionalScaleV1}, wp_fractional_scale_v1::{self, WpFractionalScaleV1},
}, },
linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, 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, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
tablet::zv2::server::{ tablet::zv2::server::{
zwp_tablet_manager_v2::ZwpTabletManagerV2, zwp_tablet_manager_v2::ZwpTabletManagerV2,
@ -165,6 +168,12 @@ pub struct Vec2 {
pub y: i32, pub y: i32,
} }
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct Vec2f {
pub x: f64,
pub y: f64,
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct XdgSurfaceData { pub struct XdgSurfaceData {
pub surface: XdgSurface, pub surface: XdgSurface,
@ -187,6 +196,11 @@ impl XdgSurfaceData {
#[derive(Debug, Hash, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Hash, Clone, Copy, Eq, PartialEq)]
pub struct SurfaceId(u32); 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)] #[derive(Hash, Clone, Copy, Eq, PartialEq)]
struct PositionerId(u32); struct PositionerId(u32);
@ -214,6 +228,15 @@ struct ActivationTokenData {
constructed: bool, constructed: bool,
} }
pub struct LockedPointer {
pub surface: SurfaceId,
pub cursor_hint: Option<Vec2f>,
}
struct PointerState {
pointer: WlPointer,
locked: Option<LockedPointer>,
}
struct State { struct State {
surfaces: HashMap<SurfaceId, SurfaceData>, surfaces: HashMap<SurfaceId, SurfaceData>,
outputs: HashMap<WlOutput, Output>, outputs: HashMap<WlOutput, Output>,
@ -224,7 +247,7 @@ struct State {
last_output: Option<WlOutput>, last_output: Option<WlOutput>,
callbacks: Vec<WlCallback>, callbacks: Vec<WlCallback>,
seat: Option<WlSeat>, seat: Option<WlSeat>,
pointer: Option<WlPointer>, pointer: Option<PointerState>,
keyboard: Option<KeyboardState>, keyboard: Option<KeyboardState>,
touch: Option<WlTouch>, touch: Option<WlTouch>,
tablet: Option<ZwpTabletV2>, tablet: Option<ZwpTabletV2>,
@ -417,9 +440,9 @@ impl Server {
dh.create_global::<State, XdgActivationV1, _>(1, ()); dh.create_global::<State, XdgActivationV1, _>(1, ());
dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ()); dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ());
dh.create_global::<State, WpViewporter, _>(1, ()); dh.create_global::<State, WpViewporter, _>(1, ());
dh.create_global::<State, ZwpPointerConstraintsV1, _>(1, ());
global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpLinuxDmabufV1);
global_noop!(ZwpRelativePointerManagerV1); global_noop!(ZwpRelativePointerManagerV1);
global_noop!(ZwpPointerConstraintsV1);
struct HandlerData; struct HandlerData;
impl ObjectData<State> for HandlerData { impl ObjectData<State> for HandlerData {
@ -577,7 +600,12 @@ impl Server {
#[track_caller] #[track_caller]
pub fn pointer(&self) -> &WlPointer { 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] #[track_caller]
@ -714,8 +742,8 @@ impl Server {
let pointer = self.state.pointer.as_ref().expect("No pointer created"); let pointer = self.state.pointer.as_ref().expect("No pointer created");
let data = self.state.surfaces.get(&surface).expect("No such surface"); let data = self.state.surfaces.get(&surface).expect("No such surface");
pointer.enter(24, &data.surface, x, y); pointer.pointer.enter(24, &data.surface, x, y);
pointer.frame(); pointer.pointer.frame();
self.display.flush_clients().unwrap(); self.display.flush_clients().unwrap();
} }
@ -807,6 +835,7 @@ simple_global_dispatch!(ZwpTabletManagerV2);
simple_global_dispatch!(ZxdgDecorationManagerV1); simple_global_dispatch!(ZxdgDecorationManagerV1);
simple_global_dispatch!(WpViewporter); simple_global_dispatch!(WpViewporter);
simple_global_dispatch!(WpFractionalScaleManagerV1); simple_global_dispatch!(WpFractionalScaleManagerV1);
simple_global_dispatch!(ZwpPointerConstraintsV1);
impl Dispatch<ZwpTabletManagerV2, ()> for State { impl Dispatch<ZwpTabletManagerV2, ()> for State {
fn request( fn request(
@ -1100,7 +1129,10 @@ impl Dispatch<WlSeat, ()> for State {
) { ) {
match request { match request {
wl_seat::Request::GetPointer { id } => { 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 } => { wl_seat::Request::GetKeyboard { id } => {
state.keyboard = Some(KeyboardState { state.keyboard = Some(KeyboardState {
@ -1130,10 +1162,7 @@ impl Dispatch<WlPointer, ()> for State {
match request { match request {
wl_pointer::Request::SetCursor { surface, .. } => { wl_pointer::Request::SetCursor { surface, .. } => {
if let Some(surface) = surface { if let Some(surface) = surface {
let data = state let data = state.surfaces.get_mut(&SurfaceId::from(&surface)).unwrap();
.surfaces
.get_mut(&SurfaceId(surface.id().protocol_id()))
.unwrap();
assert!( assert!(
matches!( matches!(
@ -2031,3 +2060,64 @@ impl Dispatch<WpFractionalScaleV1, SurfaceId> for State {
} }
} }
} }
impl Dispatch<ZwpPointerConstraintsV1, ()> for State {
fn request(
state: &mut Self,
_: &Client,
_: &ZwpPointerConstraintsV1,
request: <ZwpPointerConstraintsV1 as Resource>::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<ZwpLockedPointerV1, ()> for State {
fn request(
state: &mut Self,
_: &Client,
_: &ZwpLockedPointerV1,
request: <ZwpLockedPointerV1 as Resource>::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:?}"),
}
}
}