diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index f11878a4..9db89fe2 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -45,7 +45,7 @@ pub struct CosmicCompConfig { /// The delay in milliseconds before focus follows mouse (if enabled) pub focus_follows_cursor_delay: u64, /// Let X11 applications scale themselves - pub descale_xwayland: bool, + pub descale_xwayland: XwaylandDescaling, /// Let X11 applications snoop on certain key-presses to allow for global shortcuts pub xwayland_eavesdropping: XwaylandEavesdropping, /// The threshold before windows snap themselves to output edges @@ -80,7 +80,7 @@ impl Default for CosmicCompConfig { focus_follows_cursor: false, cursor_follows_focus: false, focus_follows_cursor_delay: 250, - descale_xwayland: false, + descale_xwayland: XwaylandDescaling::Fractional, xwayland_eavesdropping: XwaylandEavesdropping::default(), edge_snap_threshold: 0, accessibility_zoom: ZoomConfig::default(), @@ -172,3 +172,14 @@ pub enum EavesdroppingKeyboardMode { Combinations, All, } + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum XwaylandDescaling { + #[serde(rename = "true")] + Enabled, + #[serde(rename = "false")] + Disabled, + #[default] + Fractional, +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 5842520e..d48b7f88 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -45,7 +45,7 @@ pub use self::types::*; use cosmic::config::CosmicTk; use cosmic_comp_config::{ input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, - XkbConfig, XwaylandEavesdropping, ZoomConfig, + XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, }; #[derive(Debug)] @@ -917,7 +917,7 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut } } "descale_xwayland" => { - let new = get_config::(&config, "descale_xwayland"); + let new = get_config::(&config, "descale_xwayland"); if new != state.common.config.cosmic_conf.descale_xwayland { state.common.config.cosmic_conf.descale_xwayland = new; state.common.update_xwayland_scale(); diff --git a/src/state.rs b/src/state.rs index 133174c6..bf8dfb41 100644 --- a/src/state.rs +++ b/src/state.rs @@ -243,7 +243,7 @@ pub struct Common { pub xdg_activation_state: XdgActivationState, pub xdg_foreign_state: XdgForeignState, pub workspace_state: WorkspaceState, - pub xwayland_scale: Option, + pub xwayland_scale: Option, pub xwayland_state: Option, pub xwayland_shell_state: XWaylandShellState, pub pointer_focus_state: Option, diff --git a/src/xwayland.rs b/src/xwayland.rs index cbb4875a..f59e1654 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -11,16 +11,28 @@ use crate::{ toplevel_management::minimize_rectangle, xdg_activation::ActivationContext, }, }; -use cosmic_comp_config::EavesdroppingKeyboardMode; +use cosmic_comp_config::{EavesdroppingKeyboardMode, XwaylandDescaling}; use smithay::{ backend::{ + allocator::Fourcc, drm::DrmNode, input::{ButtonState, KeyState, Keycode}, + renderer::{ + element::{ + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + Kind, + }, + pixman::{PixmanError, PixmanRenderer}, + utils::draw_render_elements, + Bind, Frame, Offscreen, Renderer, + }, }, desktop::space::SpaceElement, input::{keyboard::ModifiersState, pointer::CursorIcon}, reexports::{wayland_server::Client, x11rb::protocol::xproto::Window as X11Window}, - utils::{Logical, Point, Rectangle, Serial, Size, SERIAL_COUNTER}, + utils::{ + Buffer as BufferCoords, Logical, Point, Rectangle, Serial, Size, Transform, SERIAL_COUNTER, + }, wayland::{ selection::{ data_device::{ @@ -41,6 +53,7 @@ use smithay::{ }, }; use tracing::{error, trace, warn}; +use xcursor::parser::Image; use xkbcommon::xkb::Keysym; #[derive(Debug)] @@ -97,7 +110,7 @@ impl State { last_modifier_state: None, }); - let mut wm = match X11Wm::start_wm( + let wm = match X11Wm::start_wm( data.common.event_loop_handle.clone(), x11_socket, client.clone(), @@ -109,23 +122,9 @@ impl State { } }; - let (theme, size) = load_cursor_theme(); - let cursor = Cursor::load(&theme, CursorIcon::Default, size); - let image = cursor.get_image(1, 0); - if let Err(err) = wm.set_cursor( - &image.pixels_rgba, - Size::from((image.width as u16, image.height as u16)), - Point::from((image.xhot as u16, image.yhot as u16)), - ) { - warn!( - id = ?wm.id(), - ?err, - "Failed to set default cursor for Xwayland WM", - ); - } - let xwayland_state = data.common.xwayland_state.as_mut().unwrap(); xwayland_state.xwm = Some(wm); + xwayland_state.reload_cursor(1.); data.notify_ready(); data.common.update_xwayland_scale(); @@ -147,6 +146,102 @@ impl State { } } +fn scale_cursor( + scale: f64, + cursor_size: u32, + image: &Image, +) -> Result<(Vec, Size, Point), PixmanError> { + let mut renderer = PixmanRenderer::new()?; + let image_scale = (image.size / cursor_size).max(1); + let pixel_size = (cursor_size as f64 * scale).round() as i32; + let buffer_size = Size::::from((pixel_size, pixel_size)); + let output_size = buffer_size.to_logical(1, Transform::Normal).to_physical(1); + + let image_buffer = MemoryRenderBuffer::from_slice( + &image.pixels_rgba, + Fourcc::Abgr8888, + (image.width as i32, image.height as i32), + image_scale as i32, + Transform::Normal, + None, + ); + let element = MemoryRenderBufferRenderElement::from_buffer( + &mut renderer, + (0., 0.), + &image_buffer, + None, + None, + None, + Kind::Unspecified, + )?; + + let mut buffer = renderer.create_buffer(Fourcc::Abgr8888, buffer_size)?; + let mut fb = renderer.bind(&mut buffer)?; + let mut frame = renderer.render(&mut fb, output_size, Transform::Normal)?; + draw_render_elements( + &mut frame, + scale, + &[element], + &[Rectangle::new((0, 0).into(), output_size)], + )?; + let sync = frame.finish()?; + while let Err(_) = sync.wait() {} + + let len = (buffer_size.w * buffer_size.h * 4) as usize; + let mut data = Vec::with_capacity(len); + assert_eq!(buffer.stride(), (buffer_size.w * 4) as usize); + data.extend_from_slice(unsafe { std::slice::from_raw_parts(buffer.data() as *mut u8, len) }); + + let hotspot = Point::::from((image.xhot as i32, image.yhot as i32)) + .to_f64() + .to_logical( + image_scale as f64, + Transform::Normal, + &Size::from((image.width as f64, image.height as f64)), + ) + .to_buffer( + scale, + Transform::Normal, + &output_size.to_logical(1).to_f64(), + ) + .to_i32_round::(); + Ok(( + data, + Size::from((buffer_size.w as u16, buffer_size.h as u16)), + Point::from((hotspot.x as u16, hotspot.y as u16)), + )) +} + +impl XWaylandState { + pub fn reload_cursor(&mut self, scale: f64) { + if let Some(wm) = self.xwm.as_mut() { + let (theme, size) = load_cursor_theme(); + let cursor = Cursor::load(&theme, CursorIcon::Default, size); + let image = cursor.get_image(scale.ceil() as u32, 0); + + let (pixels_rgba, size, hotspot) = match scale_cursor(scale, size, &image) { + Ok(x) => x, + Err(err) => { + warn!("Failed to scale Xwayland cursor image: {}", err); + ( + image.pixels_rgba, + Size::from((image.width as u16, image.height as u16)), + Point::from((image.xhot as u16, image.yhot as u16)), + ) + } + }; + + if let Err(err) = wm.set_cursor(&pixels_rgba, size, hotspot) { + warn!( + id = ?wm.id(), + ?err, + "Failed to set default cursor for Xwayland WM", + ); + } + } + } +} + impl Common { fn has_x_keyboard_focus(&self, xwmid: XwmId) -> bool { let keyboard = self @@ -461,15 +556,23 @@ impl Common { } pub fn update_xwayland_scale(&mut self) { - let new_scale = if self.config.cosmic_conf.descale_xwayland { - let shell = self.shell.read().unwrap(); - shell - .outputs() - .map(|o| o.current_scale().integer_scale()) - .max() - .unwrap_or(1) - } else { - 1 + let new_scale = match self.config.cosmic_conf.descale_xwayland { + XwaylandDescaling::Disabled => 1., + XwaylandDescaling::Enabled => { + let shell = self.shell.read().unwrap(); + shell + .outputs() + .map(|o| o.current_scale().integer_scale()) + .max() + .unwrap_or(1) as f64 + } + XwaylandDescaling::Fractional => { + let shell = self.shell.read().unwrap(); + shell + .outputs() + .map(|o| o.current_scale().fractional_scale()) + .fold(1f64, |acc, val| acc.max(val)) + } }; // compare with current scale @@ -487,12 +590,18 @@ impl Common { // update xorg dpi if let Some(xwm) = xwayland.xwm.as_mut() { - let dpi = new_scale.abs() * 96 * 1024; + let dpi = new_scale * 96. * 1024.; if let Err(err) = xwm.set_xsettings( [ - ("Xft/DPI".into(), dpi.into()), - ("Gdk/UnscaledDPI".into(), (dpi / new_scale).into()), - ("Gdk/WindowScalingFactor".into(), new_scale.into()), + ("Xft/DPI".into(), (dpi.round() as i32).into()), + ( + "Gdk/UnscaledDPI".into(), + ((dpi / new_scale).round() as i32).into(), + ), + ( + "Gdk/WindowScalingFactor".into(), + (new_scale.round() as i32).into(), + ), ] .into_iter(), ) { @@ -500,13 +609,16 @@ impl Common { } } + // update cursor + xwayland.reload_cursor(new_scale); + // update client scale xwayland .client .get_data::() .unwrap() .compositor_state - .set_client_scale(new_scale as f64); + .set_client_scale(new_scale); // update wl/xdg_outputs for output in self.shell.read().unwrap().outputs() {