srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/udev')
-rw-r--r--crates/wayland/src/udev/mod.rs17
-rw-r--r--crates/wayland/src/udev/outputs.rs54
-rw-r--r--crates/wayland/src/udev/platform.rs165
-rw-r--r--crates/wayland/src/udev/render.rs70
-rw-r--r--crates/wayland/src/udev/session.rs7
5 files changed, 303 insertions, 10 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs
index d22666d..27a7c2c 100644
--- a/crates/wayland/src/udev/mod.rs
+++ b/crates/wayland/src/udev/mod.rs
@@ -204,6 +204,21 @@ pub(crate) struct UdevState {
/// Pointer position in the *global* space, so it can cross between
/// monitors; clamped to the union of all head rectangles.
pub(crate) pointer_pos: Point<f64, Logical>,
+ /// Multi-cursor mode, Phase 1: every physical pointer/trackpad's own
+ /// last-known position, keyed by its real libinput device identity
+ /// (`smithay::backend::input::Event::device()`, confirmed `Device:
+ /// PartialEq + Eq + Hash` by reading smithay's own trait definition).
+ /// Purely a *visual* addition - `pointer_pos` above is still the one
+ /// position that actually drives clicks/drags/hit-testing, updated by
+ /// whichever device moved most recently exactly as before, so nothing
+ /// about existing interactive behaviour changes. This is what lets a
+ /// mouse and a trackpad each show their own live cursor sprite instead
+ /// of only the most-recently-moved device having a visible pointer at
+ /// all - see `docs/TODO.md`'s "Multi-cursor" plan for what later
+ /// phases would still need (per-device *interaction*, not just
+ /// per-device *rendering*, and the real `wl_seat` ecosystem wall a
+ /// second seat runs into for arbitrary client content).
+ pub(crate) secondary_cursors: HashMap<smithay::reexports::input::Device, Point<f64, Logical>>,
/// A clone of the same `LibSeatSession` `platform.rs` opened the DRM
/// device with (`LibSeatSession` is cheaply `Clone` - see its own
/// derive - all clones share the same underlying seat connection).
@@ -267,7 +282,7 @@ impl UdevState {
/// this from outside, but srdwm's own pointer clamp assuming an origin
/// no other part of this backend actually enforces is the real bug --
/// fixed here instead of just left for every future caller to avoid.
- fn bounds(&self) -> (f64, f64, f64, f64) {
+ pub(crate) fn bounds(&self) -> (f64, f64, f64, f64) {
bounds_of(self.heads.iter().map(|h| (h.location.x, h.location.y, h.size.0, h.size.1)))
}
}
diff --git a/crates/wayland/src/udev/outputs.rs b/crates/wayland/src/udev/outputs.rs
index c3291d8..a0169a4 100644
--- a/crates/wayland/src/udev/outputs.rs
+++ b/crates/wayland/src/udev/outputs.rs
@@ -371,7 +371,7 @@ impl CompState {
head.output.change_current_state(None, None, None, Some((x_logical, 0).into()));
placed.push((head.output.clone(), head.location));
x_physical += head.size.0;
- x_logical += (head.size.0 as f64 / scale).round() as i32;
+ x_logical = next_logical_x(x_logical, head.size.0, scale);
}
for (output, location) in placed {
if let Some(entry) = self.outputs.iter_mut().find(|e| e.output == output) {
@@ -385,3 +385,55 @@ impl CompState {
}
}
+/// The actual arithmetic behind [`CompState::relayout_outputs`]'s logical-x
+/// accumulation - pulled out so it's testable without a real `Output`/DRM
+/// head, the same reasoning `udev/mod.rs::bounds_of` already applies to
+/// `UdevState::bounds`. Takes the previous head's own resulting logical x,
+/// this head's physical width, and this head's fractional scale; returns
+/// the *next* head's logical x.
+fn next_logical_x(prev_logical_x: i32, physical_width: i32, scale: f64) -> i32 {
+ prev_logical_x + (physical_width as f64 / scale).round() as i32
+}
+
+#[cfg(test)]
+mod relayout_tests {
+ use super::next_logical_x;
+
+ /// The exact scenario this function exists to fix, using the exact
+ /// figures a peer session measured live from inside GTK
+ /// (`Gdk.Display.get_monitors()`) before the fix: `HDMI-A-1` at 1920
+ /// physical / ~0.843 scale (2276 logical), `eDP-1` at 1920 physical /
+ /// 1.0 scale placed after it. The bug this guards against: passing the
+ /// raw physical accumulator straight into `change_current_state`
+ /// advertised `eDP-1` at logical x=1920 - inside `HDMI-A-1`'s own
+ /// logical extent (0..2276), a real, measured ~356px overlap.
+ #[test]
+ fn a_sub_one_scale_head_is_not_overrun_by_the_next_heads_logical_x() {
+ let hdmi_logical_end = next_logical_x(0, 1920, 1920.0 / 2276.0);
+ assert_eq!(hdmi_logical_end, 2276);
+ let edp_logical_x = next_logical_x(hdmi_logical_end, 1920, 1.0);
+ assert!(edp_logical_x >= hdmi_logical_end, "eDP-1 logical x ({edp_logical_x}) must not land inside HDMI-A-1's own logical extent (0..{hdmi_logical_end})");
+ assert_eq!(edp_logical_x, 2276 + 1920);
+ }
+
+ /// Every output at `scale == 1.0` (this machine's actual current,
+ /// user-chosen configuration - see docs/TODO.md's "HDMI-A-1 forced to
+ /// scale 1.0" entry) must reduce to plain physical accumulation, byte
+ /// for byte - this is the case that was already correct before the
+ /// fix and must stay that way.
+ #[test]
+ fn every_output_at_unit_scale_reduces_to_plain_physical_accumulation() {
+ assert_eq!(next_logical_x(0, 1920, 1.0), 1920);
+ assert_eq!(next_logical_x(1920, 1920, 1.0), 3840);
+ }
+
+ /// A scale above 1.0 (a HiDPI output) narrows logical space relative to
+ /// physical - the next head's logical x must land *before* its own
+ /// physical offset would suggest, not after.
+ #[test]
+ fn a_scale_above_one_narrows_the_next_heads_logical_x() {
+ let logical_end = next_logical_x(0, 3840, 2.0);
+ assert_eq!(logical_end, 1920);
+ }
+}
+
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index 6f6cbe8..9023cb9 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -16,6 +16,12 @@ pub struct UdevPlatform {
/// Last time the unconditional end-of-cycle `render_udev_frame()` call
/// actually ran - see its own call site for why.
last_render: Instant,
+ /// Sticky designation of which connector `monitors()` reports as
+ /// primary - see that function's own doc comment on `primary_name`
+ /// for why this can't be recomputed from `udev.heads`' own iteration
+ /// order every call. `None` until the first `monitors()` call ever
+ /// runs.
+ primary_connector: Option<String>,
}
impl UdevPlatform {
@@ -158,6 +164,7 @@ impl UdevPlatform {
heads,
active: true,
pointer_pos: (width as f64 / 2.0, height as f64 / 2.0).into(),
+ secondary_cursors: std::collections::HashMap::new(),
session: session.clone(),
disabled_connectors: std::collections::HashSet::new(),
last_rendered_workspace: None,
@@ -190,6 +197,7 @@ impl UdevPlatform {
|_| true,
),
_screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
+ _virtual_pointer_state: crate::virtual_pointer::VirtualPointerState::new::<CompState>(&display_handle),
screencopy_pending: Vec::new(),
_appmenu_state: crate::appmenu::AppmenuManagerState::new::<CompState>(&display_handle),
_virtual_keyboard_state: smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState::new::<CompState, _>(&display_handle, |_client| true),
@@ -233,6 +241,8 @@ impl UdevPlatform {
desktop_icons: None,
desktop_icon_buffers: HashMap::new(),
desktop_icon_drag: None,
+ desktop_marquee: None,
+ marquee_buffers: Default::default(),
desktop_menu: None,
desktop_menu_buffer: None,
last_icon_click: None,
@@ -240,6 +250,7 @@ impl UdevPlatform {
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
+ virtual_pointers: Vec::new(),
dead_layer_surfaces: HashSet::new(),
hidden_layer_surfaces: HashMap::new(),
layer_surfaces_shown_once: HashSet::new(),
@@ -280,6 +291,17 @@ impl UdevPlatform {
// the socket a client would need to connect to doesn't exist yet.
state.restore_monitor_layout();
+ // Per-app remembered window position/size (`window_memory.rs`) --
+ // no ordering requirement as strict as the layout restore just
+ // above (a window can't map before a client connects, and the
+ // socket isn't even bound yet), but seeded here anyway, at the
+ // same "before anything else can possibly run" point, so there's
+ // no window in this compositor's own startup where a first window
+ // could map before this table is populated.
+ for (app_id, g) in crate::window_memory::load() {
+ state.wm.borrow_mut().set_remembered_geometry(app_id, (g.x, g.y, g.width, g.height));
+ }
+
let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
if let Some(name) = listener.socket_name() {
std::env::set_var("WAYLAND_DISPLAY", name);
@@ -326,11 +348,52 @@ impl UdevPlatform {
if let Err(e) = register_udev_monitor(&handle, &seat_name) {
log::warn!("udev: connector hotplug unavailable ({e}); monitors are fixed at startup");
}
- if let Err(e) = crate::xwayland::spawn(&handle, &display_handle) {
- log::warn!("XWayland unavailable ({e}); X11-only clients will not run");
- }
-
- Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending, ipc, last_ipc_poll: Instant::now(), last_render: Instant::now() })
+ // Deferred to the loop's first idle tick, not called here directly.
+ // This function still runs inside `connect()`, before the caller
+ // ever calls `event_loop.run()` - so a direct call here forks
+ // XWayland while nothing is actually dispatching this process's own
+ // Wayland socket yet. XWayland connects immediately (`WAYLAND_SOCKET`
+ // is already a live fd, no accept() to wait for) and starts its own
+ // registry/seat/keyboard handshake right away; if that handshake's
+ // response - specifically the `wl_keyboard.keymap` event carrying
+ // this compositor's real `pc105+inet`-derived keymap - doesn't get
+ // serviced before XWayland's own internal timeout, XWayland falls
+ // back to compiling a keymap of its own with no real RMLVO behind
+ // it, which is exactly the "Failed to load keymap. Loading default
+ // keymap instead" line seen in `xwayland.log` right before "Fatal
+ // server error: Failed to activate virtual core keyboard: 2" --
+ // confirmed to reproduce on every single real startup (53 identical
+ // crashes across one session's restarts) while an external XWayland
+ // spawned against this exact same, already-*running* compositor
+ // (same socket, same keymap, same env-clearing, same `-wm`/
+ // `-displayfd` fd-passing - checked by replicating smithay's own
+ // `XWayland::spawn` byte for byte in a standalone harness) never
+ // once reproduced it. `insert_idle` runs its callback on the loop's
+ // own first dispatch pass, which only happens once `event_loop.run`
+ // is actually pumping this process's sockets - moving the fork
+ // there closes the exact gap between "child process exists and
+ // starts talking" and "someone is listening," which nothing else
+ // about this fix changes.
+ let handle_for_xwayland = handle.clone();
+ let idle_display_handle = display_handle.clone();
+ handle.insert_idle(move |_state| {
+ if let Err(e) = crate::xwayland::spawn(&handle_for_xwayland, &idle_display_handle) {
+ log::warn!("XWayland unavailable ({e}); X11-only clients will not run");
+ }
+ });
+
+ Ok(Self {
+ event_loop,
+ display: dh,
+ state,
+ listener,
+ clients: Vec::new(),
+ pending,
+ ipc,
+ last_ipc_poll: Instant::now(),
+ last_render: Instant::now(),
+ primary_connector: None,
+ })
}
fn accept_clients(&mut self) -> PlatformResult<()> {
@@ -550,6 +613,14 @@ impl Platform for UdevPlatform {
self.state.disable_connector_by_name(&name);
}
}
+ // Applies any `srd dispatch pin input`/`unpin input` IPC requests
+ // queued since the last poll - Phase 2 of the multi-cursor plan,
+ // see `virtual_pointer.rs`'s module doc comment and `CompState::
+ // set_virtual_pointer_pin`'s own doc comment for the full design.
+ let pin_requests = self.state.wm.borrow_mut().drain_pin_input_requests();
+ for (pid, window) in pin_requests {
+ self.state.set_virtual_pointer_pin(pid, window);
+ }
// Throttled the same way and for the same underlying reason as the
// `ipc.poll()` call above - this is the *other*, larger half of
// this cycle's needless work at the dead-pipe-driven spin rate.
@@ -597,6 +668,50 @@ impl Platform for UdevPlatform {
let wm = wm.borrow();
let mut out = Vec::new();
let mut next_id: u32 = 0;
+ // Sticky by connector name, not "whichever head is first in `udev.
+ // heads` this call" - that positional rule looked harmless (heads
+ // are only ever appended, in probe order, at startup) but
+ // `enable_connector_by_name` pushes a re-enabled connector back
+ // onto the *end* of the vec, same as a fresh hotplug - so cycling
+ // any non-first connector's own enabled state (confirmed live: a
+ // peer session repeatedly toggling one monitor for unrelated
+ // testing) never moves it, but disabling the connector that
+ // currently sits first and re-enabling it does, silently handing
+ // "primary" to whatever was second. Reported live as this
+ // session's own desktop icons (pinned to whichever monitor `Platform
+ // ::monitors()` calls primary) "sometimes showing on the other
+ // monitor" with no action anyone took that looked related. Once a
+ // primary connector name is chosen, it keeps that designation
+ // across every later call as long as it's still connected --
+ // falling back to the first head only when it genuinely isn't
+ // (unplugged, or the very first call this process ever makes).
+ // The *first* fallback pick (when nothing is sticky yet) used to be
+ // `udev.heads.first()` - whichever connector DRM happened to probe
+ // first, which has no relationship to the user's actual layout.
+ // Reported live on this machine: with an "extend left" saved layout
+ // (external monitor at negative x, laptop panel at x=0), the
+ // external monitor still got "primary" at boot whenever it happened
+ // to probe before the panel, dragging desktop icons and every
+ // primary-monitor-anchored window placement onto it - exactly the
+ // "apps open on the wrong monitor" and "icons not showing" symptoms
+ // reported live, on the very first call this process ever makes,
+ // before stickiness has anything to preserve. `relayout_outputs`/
+ // `output_management::apply_output_position` already keep the
+ // user's actual anchor monitor at physical `(0, 0)` - that IS the
+ // position-based definition of "primary" every desktop convention
+ // (xrandr, wlr-output-management) already uses, and unlike
+ // enumeration order it's driven by the same saved layout the user
+ // configured. Preferred over the origin-search only as the initial
+ // pick; once chosen, `primary_connector` stays sticky exactly as
+ // before, so a later `relayout_outputs` call temporarily putting a
+ // different head at `(0, 0)` mid-drag doesn't itself flip primary.
+ let primary_name = self
+ .primary_connector
+ .clone()
+ .filter(|name| udev.heads.iter().any(|h| &h.output.name() == name))
+ .or_else(|| udev.heads.iter().find(|h| h.location == Point::from((0, 0))).map(|h| h.output.name()))
+ .or_else(|| udev.heads.first().map(|h| h.output.name()));
+ self.primary_connector = primary_name.clone();
for head in udev.heads.iter() {
// Shrunk by whatever a layer-shell surface (bar, dock) has
// reserved via `set_exclusive_zone` - reporting the full
@@ -630,12 +745,48 @@ impl Platform for UdevPlatform {
let zone = layer_map_for_output(&head.output).non_exclusive_zone();
let scale = head.output.current_scale().fractional_scale();
let zone_physical = |v: i32| (v as f64 * scale).round() as i32;
- let usable = srdwm_core::Rect::new(
+ let mut usable = srdwm_core::Rect::new(
head.location.x + zone_physical(zone.loc.x),
head.location.y + zone_physical(zone.loc.y),
zone_physical(zone.size.w).max(0) as u32,
zone_physical(zone.size.h).max(0) as u32,
);
+ // `general.reserve_top`/`_bottom`/`_left`/`_right` - a static
+ // floor under the real exclusive zone above, not a competing
+ // claim: only shrinks `usable` further if the configured
+ // reservation is *larger* than what's already reserved for
+ // that edge, so a real bar/dock that has actually connected
+ // and registered its own (equal or bigger) zone always wins.
+ // See `WindowManager::reserve_top`'s own doc comment for the
+ // startup-race this exists to close.
+ let (rt, rb, rl, rr) =
+ (zone_physical(wm.reserve_top as i32), zone_physical(wm.reserve_bottom as i32), zone_physical(wm.reserve_left as i32), zone_physical(wm.reserve_right as i32));
+ let full_top = head.location.y;
+ let full_left = head.location.x;
+ let full_bottom = head.location.y + head.size.1;
+ let full_right = head.location.x + head.size.0;
+ let want_top = full_top + rt;
+ let want_left = full_left + rl;
+ let want_bottom = full_bottom - rb;
+ let want_right = full_right - rr;
+ if want_top > usable.y {
+ let shrink = want_top - usable.y;
+ usable.y = want_top;
+ usable.height = usable.height.saturating_sub(shrink.max(0) as u32);
+ }
+ if want_left > usable.x {
+ let shrink = want_left - usable.x;
+ usable.x = want_left;
+ usable.width = usable.width.saturating_sub(shrink.max(0) as u32);
+ }
+ let usable_bottom = usable.y + usable.height as i32;
+ if want_bottom < usable_bottom {
+ usable.height = (want_bottom - usable.y).max(0) as u32;
+ }
+ let usable_right = usable.x + usable.width as i32;
+ if want_right < usable_right {
+ usable.width = (want_right - usable.x).max(0) as u32;
+ }
// The head's true full rect, ignoring any exclusive zone --
// deliberately *not* defaulted from `usable` the way `Monitor::
// new` alone would (see the fullscreen note below).
@@ -668,7 +819,7 @@ impl Platform for UdevPlatform {
// erasing the split it was placed to respect.
m.full_geometry = srdwm_core::monitor::split_rect(full, part, parts, rows);
m.maximize_geometry = srdwm_core::monitor::split_rect(maximize, part, parts, rows);
- m.primary = next_id == 0;
+ m.primary = primary_name.as_deref() == Some(name.as_str());
m.split = parts > 1;
m.scale = scale;
out.push(m);
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 2d0a063..4bfe04d 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -7,6 +7,19 @@ impl CompState {
/// monitors on different refresh rates each run at their own pace
/// instead of the slowest one gating the rest.
pub(crate) fn render_udev_frame(&mut self) {
+ // Real perf instrumentation, not a guess-fix: reported live as
+ // "resizing seems slow", and this session's own investigation
+ // (checked decoration-buffer caching, motion-path logging levels,
+ // GPU-path config) found no smoking gun without an actual
+ // measurement. Cheap when nothing's slow (one `Instant::now()` and
+ // one comparison per frame, no allocation, no formatting unless
+ // the threshold trips) - logs only when a frame actually misses a
+ // 60fps budget, tagged with whether a resize/drag was in progress
+ // at the time, so the next real resize either produces real
+ // evidence this is a genuine per-frame cost during resize
+ // specifically, or rules that out in favor of something else
+ // (input latency, client-side redraw cost, a specific app).
+ let frame_start = Instant::now();
self.tick_animations();
self.tick_hover_glyph_animation();
self.tick_dirty_broadcasts();
@@ -299,6 +312,23 @@ impl CompState {
origin,
hsize,
));
+ // Multi-cursor mode, Phase 1: one extra sprite per *other*
+ // physical pointer device's own last-known position (see
+ // `UdevState::secondary_cursors`'s own doc comment) - the
+ // device that drove `pointer_pos` itself is skipped so its
+ // cursor isn't drawn twice at the same spot. All secondary
+ // sprites share the one real cursor image/theme
+ // (`cursor_status`/`cursor_buffers`) rather than each
+ // device getting its own - a real visual distinction
+ // between devices is a later-phase refinement, not needed
+ // to prove multiple live positions render at all.
+ let active_device = udev.secondary_cursors.iter().find(|&(_, &p)| p == pointer_pos).map(|(d, _)| d.clone());
+ for (device, &pos) in &udev.secondary_cursors {
+ if Some(device) == active_device.as_ref() {
+ continue;
+ }
+ custom_elements.extend(crate::cursor::render_elements(&cursor_status, &cursor_buffers, &mut udev.renderer, pos, origin, hsize));
+ }
// Night light/reading mode - a translucent full-output
// overlay, pushed right after the cursor so it colours
// everything else (windows, bars, menus) but never the
@@ -415,7 +445,7 @@ impl CompState {
// the actual content position still reads `geom`/`band`
// directly, since that's already correctly anchored via
// `content_offset` below regardless of this correction.
- let frame = crate::state::CompState::effective_frame_of(&self.wm, &self.id_to_window, id, geom);
+ let frame = crate::state::CompState::effective_frame_of(&self.wm, &self.id_to_window, &self.pending_size_configure, id, geom);
// Computed here, ahead of the border strips below,
// purely so they can know it - the actual content
// element that reads this same masked buffer is still
@@ -924,6 +954,34 @@ impl CompState {
// background-layer push just below (so the wallpaper still
// shows through everywhere an icon doesn't draw). See
// `desktop_icons.rs`'s own module doc comment.
+ // The rubber-band marquee outline, above the icons it's
+ // selecting - four thin solid-colour strips (the same
+ // `border_side_render_element` primitive window borders
+ // already use), not a translucent fill: `SolidColorRender
+ // Element` has no alpha-blend path, and a plain accent-
+ // coloured outline is still a real, visible selection
+ // indicator without needing a new element type for one
+ // feature.
+ if let Some((start, current)) = self.desktop_marquee {
+ let (x0, y0) = (start.0.min(current.0), start.1.min(current.1));
+ let (x1, y1) = (start.0.max(current.0), start.1.max(current.1));
+ let color = self.wm.borrow().theme.default_border_color;
+ const T: i32 = 1;
+ let strips = [
+ srdwm_core::Rect::new(x0, y0, (x1 - x0).max(0) as u32, T as u32),
+ srdwm_core::Rect::new(x0, y1 - T, (x1 - x0).max(0) as u32, T as u32),
+ srdwm_core::Rect::new(x0, y0, T as u32, (y1 - y0).max(0) as u32),
+ srdwm_core::Rect::new(x1 - T, y0, T as u32, (y1 - y0).max(0) as u32),
+ ];
+ for (strip, buf) in strips.into_iter().zip(self.marquee_buffers.iter_mut()) {
+ custom_elements.push(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(
+ buf,
+ strip,
+ color,
+ (origin.x, origin.y),
+ )));
+ }
+ }
for (pos, buffer) in &desktop_icon_render_list {
let local_pos = ((pos.0 - origin.x) as f64, (pos.1 - origin.y) as f64);
match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, local_pos, buffer, None, None, None, Kind::Unspecified) {
@@ -1190,6 +1248,16 @@ impl CompState {
// otherwise never come (see docs/PANEL_SUPPORT_TODO.md, P1).
self.screencopy_pending.extend(captures);
}
+ const FRAME_BUDGET: Duration = Duration::from_millis(16);
+ let frame_time = frame_start.elapsed();
+ if frame_time > FRAME_BUDGET {
+ let wm = self.wm.borrow();
+ log::warn!(
+ "PERF-RESIZE render_udev_frame took {frame_time:?} (budget {FRAME_BUDGET:?}) - resizing={} dragging={}",
+ wm.resizing_window().is_some(),
+ wm.is_dragging()
+ );
+ }
}
/// Sets a connector's DPMS mode via the generic KMS "DPMS" property --
diff --git a/crates/wayland/src/udev/session.rs b/crates/wayland/src/udev/session.rs
index bf6eb67..c80592e 100644
--- a/crates/wayland/src/udev/session.rs
+++ b/crates/wayland/src/udev/session.rs
@@ -335,6 +335,12 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
udev.pointer_pos.x = (udev.pointer_pos.x + delta.x).clamp(min_x, (max_x - 1.0).max(min_x));
udev.pointer_pos.y = (udev.pointer_pos.y + delta.y).clamp(min_y, (max_y - 1.0).max(min_y));
let pos = udev.pointer_pos;
+ // Multi-cursor mode, Phase 1 (see `UdevState::secondary_
+ // cursors`'s own doc comment): records this specific physical
+ // device's own position too, purely for rendering its own
+ // cursor sprite - `pos`/`handle_pointer_position` below are
+ // still the one interactive position, unchanged.
+ udev.secondary_cursors.insert(event.device(), pos);
handle_pointer_position(state, pos, event.time_msec());
}
// Absolute-positioning devices (a touchscreen, a drawing tablet,
@@ -362,6 +368,7 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
udev.pointer_pos.x = (pos.x + min_x).clamp(min_x, (max_x - 1.0).max(min_x));
udev.pointer_pos.y = (pos.y + min_y).clamp(min_y, (max_y - 1.0).max(min_y));
let pos = udev.pointer_pos;
+ udev.secondary_cursors.insert(event.device(), pos);
handle_pointer_position(state, pos, event.time_msec());
}
InputEvent::PointerButton { event } => {