Skip to content

Commit a8cf319

Browse files
fix: absolute cursor position on scaled outputs
1 parent 2b2a7b6 commit a8cf319

3 files changed

Lines changed: 93 additions & 7 deletions

File tree

src/remote_desktop.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub(crate) struct RemoteDesktopData {
3030
pub(crate) screen_cast_enabled: bool,
3131
restore: Option<PersistedRemoteDesktop>,
3232
pub(crate) ei_sender: Option<EiSender>,
33+
pub(crate) stream_offsets: Vec<(u32, (i32, i32))>,
3334
}
3435

3536
impl Default for RemoteDesktopData {
@@ -42,6 +43,7 @@ impl Default for RemoteDesktopData {
4243
screen_cast_enabled: false,
4344
restore: None,
4445
ei_sender: None,
46+
stream_offsets: Vec::new(),
4547
}
4648
}
4749
}
@@ -166,6 +168,32 @@ impl RemoteDesktop {
166168
}
167169
}
168170

171+
/// The global logical offset of a stream (the captured output's position).
172+
/// Defaults to (0, 0) if the stream is unknown.
173+
async fn stream_offset(
174+
&self,
175+
connection: &zbus::Connection,
176+
session_handle: &zvariant::ObjectPath<'_>,
177+
stream: u32,
178+
) -> (i32, i32) {
179+
let Some(interface) =
180+
crate::session_interface::<SessionData>(connection, session_handle).await
181+
else {
182+
return (0, 0);
183+
};
184+
let session_data = interface.get().await;
185+
session_data
186+
.remote_desktop
187+
.as_ref()
188+
.and_then(|rd| {
189+
rd.stream_offsets
190+
.iter()
191+
.find(|(node, _)| *node == stream)
192+
.map(|(_, off)| *off)
193+
})
194+
.unwrap_or((0, 0))
195+
}
196+
169197
async fn notify(
170198
&self,
171199
connection: &zbus::Connection,
@@ -377,6 +405,16 @@ impl RemoteDesktop {
377405
.into()
378406
});
379407

408+
// Record each stream's global logical offset so absolute pointer input
409+
// (sent in a stream's local space) can be mapped to global coordinates.
410+
let stream_offsets: Vec<(u32, (i32, i32))> = streams
411+
.iter()
412+
.map(|(node, props)| (*node, props.position().unwrap_or((0, 0))))
413+
.collect();
414+
if let Some(remote_desktop) = interface.get_mut().await.remote_desktop.as_mut() {
415+
remote_desktop.stream_offsets = stream_offsets;
416+
}
417+
380418
PortalResponse::Success(StartResult {
381419
devices: device_types,
382420
clipboard_enabled,
@@ -442,14 +480,19 @@ impl RemoteDesktop {
442480
#[zbus(connection)] connection: &zbus::Connection,
443481
session_handle: zvariant::ObjectPath<'_>,
444482
_options: HashMap<String, zvariant::OwnedValue>,
445-
_stream: u32,
483+
stream: u32,
446484
x: f64,
447485
y: f64,
448486
) {
487+
// The coordinates are in the chosen stream's (output's) local space. Resolve
488+
// that output's global offset so the EI sender can produce a global position.
489+
let offset = self
490+
.stream_offset(connection, &session_handle, stream)
491+
.await;
449492
self.notify(
450493
connection,
451494
&session_handle,
452-
Command::PointerMotionAbsolute { x, y },
495+
Command::PointerMotionAbsolute { x, y, offset },
453496
)
454497
.await;
455498
}

src/remote_desktop_ei.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::remote_desktop::{DEVICE_KEYBOARD, DEVICE_POINTER, DEVICE_TOUCHSCREEN}
1616
#[derive(Debug)]
1717
pub enum Command {
1818
PointerMotion { dx: f64, dy: f64 },
19-
PointerMotionAbsolute { x: f64, y: f64 },
19+
PointerMotionAbsolute { x: f64, y: f64, offset: (i32, i32) },
2020
PointerButton { button: i32, state: u32 },
2121
PointerAxis { dx: f64, dy: f64 },
2222
PointerAxisDiscrete { axis: u32, steps: i32 },
@@ -115,6 +115,7 @@ struct State {
115115
connection: reis::event::Connection,
116116
caps: BitFlags<DeviceCapability>,
117117
devices: HashMap<Device, DeviceState>,
118+
abs_regions: Vec<(i32, i32, f64)>,
118119
}
119120

120121
async fn run(
@@ -127,6 +128,7 @@ async fn run(
127128
connection,
128129
caps,
129130
devices: HashMap::new(),
131+
abs_regions: Vec::new(),
130132
};
131133
loop {
132134
tokio::select! {
@@ -155,6 +157,16 @@ impl State {
155157
let _ = self.connection.flush();
156158
}
157159
EiEvent::DeviceAdded(evt) => {
160+
let mut regions = Vec::new();
161+
for r in evt.device.regions() {
162+
regions.push((r.x as i32, r.y as i32, r.scale as f64));
163+
}
164+
if !regions.is_empty() {
165+
// The compositor recreates the absolute-pointer device with a fresh
166+
// region per output whenever an output's scale or geometry changes, so
167+
// replace the stale set rather than accumulating across re-adds.
168+
self.abs_regions = regions;
169+
}
158170
self.devices.insert(
159171
evt.device,
160172
DeviceState {
@@ -207,10 +219,22 @@ impl State {
207219
p.motion_relative(dx as f32, dy as f32);
208220
});
209221
}
210-
// `stream` (which output) is ignored for now; treat as a single space.
211-
Command::PointerMotionAbsolute { x, y } => {
222+
Command::PointerMotionAbsolute { x, y, offset } => {
223+
// `x`/`y` arrive in the stream's *physical* pixel space (we advertise the
224+
// physical size to the consumer so it maps 1:1 over the video). Convert to
225+
// the compositor's logical space by dividing by the output scale, taken from
226+
// the EI absolute-pointer region whose offset matches this stream, then add
227+
// the stream's global logical offset.
228+
let scale = self
229+
.abs_regions
230+
.iter()
231+
.find(|(rx, ry, _)| *rx == offset.0 && *ry == offset.1)
232+
.map(|(_, _, s)| *s)
233+
.unwrap_or(1.0);
234+
let ex = (offset.0 as f64 + x / scale) as f32;
235+
let ey = (offset.1 as f64 + y / scale) as f32;
212236
self.emit::<ei::PointerAbsolute, _>(serial, time, |p| {
213-
p.motion_absolute(x as f32, y as f32);
237+
p.motion_absolute(ex, ey);
214238
});
215239
}
216240
Command::PointerButton { button, state } => {

src/screencast.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ pub struct StreamProps {
149149
mapping_id: Option<String>,
150150
}
151151

152+
impl StreamProps {
153+
/// The stream's global logical offset (the captured output's position in the
154+
/// compositor layout), used to map absolute input to global coordinates.
155+
pub fn position(&self) -> Option<(i32, i32)> {
156+
self.position
157+
}
158+
}
159+
152160
#[derive(zvariant::SerializeDict, zvariant::Type)]
153161
#[zvariant(signature = "a{sv}")]
154162
struct StartResult {
@@ -270,7 +278,18 @@ pub(crate) async fn capture_from_sources(
270278
for output in &capture_sources.outputs {
271279
let info = wayland_helper.output_info(output);
272280
let (position, size) = if let Some(info) = info {
273-
(info.logical_position, info.logical_size.unwrap_or((0, 0)))
281+
// Advertise the *physical* (current-mode) size, matching the actual video
282+
// buffer, so the consumer maps absolute pointer input 1:1 over the video.
283+
// The portal converts these physical coordinates back to the compositor's
284+
// logical space on input
285+
let physical = info
286+
.modes
287+
.iter()
288+
.find(|m| m.current)
289+
.map(|m| m.dimensions)
290+
.or(info.logical_size)
291+
.unwrap_or((0, 0));
292+
(info.logical_position, physical)
274293
} else {
275294
(Some((0, 0)), (0, 0))
276295
};

0 commit comments

Comments
 (0)