linux: Fix mouse cursor size and blur on Wayland (#21373)

Closes #15788, #13258

This is a long-standing issue with a few previous attempts to fix it,
such as [this one](https://github.com/zed-industries/zed/pull/17496).
However, that fix was later reverted because it resolved the blur issue
but caused a size issue. Currently, both blur and size issues persist
when you set a custom cursor size from GNOME Settings and use fractional
scaling.

This PR addresses both issues.

---

### Context

A new Wayland protocol,
[cursor-shape-v1](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/194),
allows the compositor to handle rendering the cursor at the correct size
and shape. This protocol is implemented by KDE, wlroots (Sway-like
environments), etc. Zed supports this protocol, so there are no issues
on these desktop environments.

However, GNOME has not yet
[adopted](https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6212) this
protocol. As a result, apps must fall back to manually rendering the
cursor by specifying the theme, size, scale, etc., themselves. Zed also
implements this fallback but does not correctly account for the display
scale.

---

### Scale Fix

For example, if your cursor size is `64px` and you’re using fractional
scaling (e.g., `150%`), the display scale reported by the window query
will be an integer value, `2` in this case. Why `2` if the scale is
`150%`? That’s what the new protocol aims to improve. However, since
GNOME Wayland uses this integer scale everywhere, it’s sufficient for
our use case.

To fix the issue, we set the `buffer_scale` to this value. But that
alone doesn’t solve the problem. We also need to generate a matching
theme cursor size for this scaled version. This can be calculated as
`64px` * `2`, resulting in `128px` as the theme cursor size.

---

### Size Fix

The XDG Desktop Portal’s `cursor-size` event fails to read the cursor
size because it expects an `i32` but encounters a type error with `u32`.
Due to this, the cursor size was interpreted as the default `24px`
instead of the actual size set via user.

---

### Tested

This fix has been tested with all possible combinations of the
following:

- [x] GNOME Normal Scale (100%, 200%, etc.)
- [x] GNOME Fractional Scaling (125%, 150%, etc.)
- [x] GNOME Cursor Sizes (**Settings > Accessibility > Seeing**, e.g.,
`24px`, `64px`, etc.)
- [x] GNOME Experimental Feature `scale-monitor-framebuffer` (both
enabled and disabled)
- [x] KDE (`cursor-shape-v1` protocol)

---

**Result:**

64px custom cursor size + 150% Fractional Scale:


https://github.com/user-attachments/assets/cf3b1a0f-9a25-45d0-ab03-75059d3305e7

---

Release Notes:

- Fixed mouse cursor size and blur issues on Wayland
This commit is contained in:
tims 2024-12-01 02:49:44 +05:30 committed by GitHub
parent fd71801346
commit d609931e1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 43 deletions

View file

@ -496,7 +496,7 @@ impl WaylandClient {
XDPEvent::CursorTheme(theme) => {
if let Some(client) = client.0.upgrade() {
let mut client = client.borrow_mut();
client.cursor.set_theme(theme.as_str(), None);
client.cursor.set_theme(theme.as_str());
}
}
XDPEvent::CursorSize(size) => {
@ -649,15 +649,16 @@ impl LinuxClient for WaylandClient {
if let Some(cursor_shape_device) = &state.cursor_shape_device {
cursor_shape_device.set_shape(serial, style.to_shape());
} else if state.mouse_focused_window.is_some() {
} else if let Some(focused_window) = &state.mouse_focused_window {
// cursor-shape-v1 isn't supported, set the cursor using a surface.
let wl_pointer = state
.wl_pointer
.clone()
.expect("window is focused by pointer");
let scale = focused_window.primary_output_scale();
state
.cursor
.set_icon(&wl_pointer, serial, &style.to_icon_name());
.set_icon(&wl_pointer, serial, &style.to_icon_name(), scale);
}
}
}
@ -1439,9 +1440,13 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if let Some(cursor_shape_device) = &state.cursor_shape_device {
cursor_shape_device.set_shape(serial, style.to_shape());
} else {
state
.cursor
.set_icon(&wl_pointer, serial, &style.to_icon_name());
let scale = window.primary_output_scale();
state.cursor.set_icon(
&wl_pointer,
serial,
&style.to_icon_name(),
scale,
);
}
}
drop(state);

View file

@ -9,6 +9,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme};
pub(crate) struct Cursor {
theme: Option<CursorTheme>,
theme_name: Option<String>,
theme_size: u32,
surface: WlSurface,
size: u32,
shm: WlShm,
@ -27,6 +28,7 @@ impl Cursor {
Self {
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
theme_name: None,
theme_size: size,
surface: globals.compositor.create_surface(&globals.qh, ()),
shm: globals.shm.clone(),
connection: connection.clone(),
@ -34,26 +36,26 @@ impl Cursor {
}
}
pub fn set_theme(&mut self, theme_name: &str, size: Option<u32>) {
if let Some(size) = size {
self.size = size;
}
if let Some(theme) =
CursorTheme::load_from_name(&self.connection, self.shm.clone(), theme_name, self.size)
.log_err()
pub fn set_theme(&mut self, theme_name: &str) {
if let Some(theme) = CursorTheme::load_from_name(
&self.connection,
self.shm.clone(),
theme_name,
self.theme_size,
)
.log_err()
{
self.theme = Some(theme);
self.theme_name = Some(theme_name.to_string());
} else if let Some(theme) =
CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err()
CursorTheme::load(&self.connection, self.shm.clone(), self.theme_size).log_err()
{
self.theme = Some(theme);
self.theme_name = None;
}
}
pub fn set_size(&mut self, size: u32) {
self.size = size;
fn set_theme_size(&mut self, theme_size: u32) {
self.theme = self
.theme_name
.as_ref()
@ -62,14 +64,29 @@ impl Cursor {
&self.connection,
self.shm.clone(),
name.as_str(),
self.size,
theme_size,
)
.log_err()
})
.or_else(|| CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err());
.or_else(|| {
CursorTheme::load(&self.connection, self.shm.clone(), theme_size).log_err()
});
}
pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) {
pub fn set_size(&mut self, size: u32) {
self.size = size;
self.set_theme_size(size);
}
pub fn set_icon(
&mut self,
wl_pointer: &WlPointer,
serial_id: u32,
mut cursor_icon_name: &str,
scale: i32,
) {
self.set_theme_size(self.size * scale as u32);
if let Some(theme) = &mut self.theme {
let mut buffer: Option<&CursorImageBuffer>;
@ -91,7 +108,15 @@ impl Cursor {
let (width, height) = buffer.dimensions();
let (hot_x, hot_y) = buffer.hotspot();
wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32);
self.surface.set_buffer_scale(scale);
wl_pointer.set_cursor(
serial_id,
Some(&self.surface),
hot_x as i32 / scale,
hot_y as i32 / scale,
);
self.surface.attach(Some(&buffer), 0, 0);
self.surface.damage(0, 0, width as i32, height as i32);
self.surface.commit();

View file

@ -194,6 +194,23 @@ impl WaylandWindowState {
self.decorations == WindowDecorations::Client
|| self.background_appearance != WindowBackgroundAppearance::Opaque
}
pub fn primary_output_scale(&mut self) -> i32 {
let mut scale = 1;
let mut current_output = self.display.take();
for (id, output) in self.outputs.iter() {
if let Some((_, output_data)) = &current_output {
if output.scale > output_data.scale {
current_output = Some((id.clone(), output.clone()));
}
} else {
current_output = Some((id.clone(), output.clone()));
}
scale = scale.max(output.scale);
}
self.display = current_output;
scale
}
}
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
@ -560,7 +577,7 @@ impl WaylandWindowStatePtr {
state.outputs.insert(id, output.clone());
let scale = primary_output_scale(&mut state);
let scale = state.primary_output_scale();
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
@ -572,7 +589,7 @@ impl WaylandWindowStatePtr {
wl_surface::Event::Leave { output } => {
state.outputs.remove(&output.id());
let scale = primary_output_scale(&mut state);
let scale = state.primary_output_scale();
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
@ -719,6 +736,10 @@ impl WaylandWindowStatePtr {
(fun)()
}
}
pub fn primary_output_scale(&self) -> i32 {
self.state.borrow_mut().primary_output_scale()
}
}
fn extract_states<'a, S: TryFrom<u32> + 'a>(states: &'a [u8]) -> impl Iterator<Item = S> + 'a
@ -732,23 +753,6 @@ where
.flat_map(S::try_from)
}
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
let mut scale = 1;
let mut current_output = state.display.take();
for (id, output) in state.outputs.iter() {
if let Some((_, output_data)) = &current_output {
if output.scale > output_data.scale {
current_output = Some((id.clone(), output.clone()));
}
} else {
current_output = Some((id.clone(), output.clone()));
}
scale = scale.max(output.scale);
}
state.display = current_output;
scale
}
impl rwh::HasWindowHandle for WaylandWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
unimplemented!()

View file

@ -42,11 +42,13 @@ impl XDPEventSource {
{
sender.send(Event::CursorTheme(initial_theme))?;
}
// If u32 is used here, it throws invalid type error
if let Ok(initial_size) = settings
.read::<u32>("org.gnome.desktop.interface", "cursor-size")
.read::<i32>("org.gnome.desktop.interface", "cursor-size")
.await
{
sender.send(Event::CursorSize(initial_size))?;
sender.send(Event::CursorSize(initial_size as u32))?;
}
if let Ok(mut cursor_theme_changed) = settings
@ -69,7 +71,7 @@ impl XDPEventSource {
}
if let Ok(mut cursor_size_changed) = settings
.receive_setting_changed_with_args::<u32>(
.receive_setting_changed_with_args::<i32>(
"org.gnome.desktop.interface",
"cursor-size",
)
@ -80,7 +82,7 @@ impl XDPEventSource {
.spawn(async move {
while let Some(size) = cursor_size_changed.next().await {
let size = size?;
sender.send(Event::CursorSize(size))?;
sender.send(Event::CursorSize(size as u32))?;
}
anyhow::Ok(())
})