feat: Allow fractional xwayland client scale

This commit is contained in:
Victoria Brekenfeld 2025-04-04 18:49:45 +02:00 committed by Victoria Brekenfeld
parent 0159bce9db
commit 7472351de0
4 changed files with 160 additions and 37 deletions

View file

@ -45,7 +45,7 @@ pub struct CosmicCompConfig {
/// The delay in milliseconds before focus follows mouse (if enabled) /// The delay in milliseconds before focus follows mouse (if enabled)
pub focus_follows_cursor_delay: u64, pub focus_follows_cursor_delay: u64,
/// Let X11 applications scale themselves /// 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 /// Let X11 applications snoop on certain key-presses to allow for global shortcuts
pub xwayland_eavesdropping: XwaylandEavesdropping, pub xwayland_eavesdropping: XwaylandEavesdropping,
/// The threshold before windows snap themselves to output edges /// The threshold before windows snap themselves to output edges
@ -80,7 +80,7 @@ impl Default for CosmicCompConfig {
focus_follows_cursor: false, focus_follows_cursor: false,
cursor_follows_focus: false, cursor_follows_focus: false,
focus_follows_cursor_delay: 250, focus_follows_cursor_delay: 250,
descale_xwayland: false, descale_xwayland: XwaylandDescaling::Fractional,
xwayland_eavesdropping: XwaylandEavesdropping::default(), xwayland_eavesdropping: XwaylandEavesdropping::default(),
edge_snap_threshold: 0, edge_snap_threshold: 0,
accessibility_zoom: ZoomConfig::default(), accessibility_zoom: ZoomConfig::default(),
@ -172,3 +172,14 @@ pub enum EavesdroppingKeyboardMode {
Combinations, Combinations,
All, 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,
}

View file

@ -45,7 +45,7 @@ pub use self::types::*;
use cosmic::config::CosmicTk; use cosmic::config::CosmicTk;
use cosmic_comp_config::{ use cosmic_comp_config::{
input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior,
XkbConfig, XwaylandEavesdropping, ZoomConfig, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -917,7 +917,7 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
} }
} }
"descale_xwayland" => { "descale_xwayland" => {
let new = get_config::<bool>(&config, "descale_xwayland"); let new = get_config::<XwaylandDescaling>(&config, "descale_xwayland");
if new != state.common.config.cosmic_conf.descale_xwayland { if new != state.common.config.cosmic_conf.descale_xwayland {
state.common.config.cosmic_conf.descale_xwayland = new; state.common.config.cosmic_conf.descale_xwayland = new;
state.common.update_xwayland_scale(); state.common.update_xwayland_scale();

View file

@ -243,7 +243,7 @@ pub struct Common {
pub xdg_activation_state: XdgActivationState, pub xdg_activation_state: XdgActivationState,
pub xdg_foreign_state: XdgForeignState, pub xdg_foreign_state: XdgForeignState,
pub workspace_state: WorkspaceState<State>, pub workspace_state: WorkspaceState<State>,
pub xwayland_scale: Option<i32>, pub xwayland_scale: Option<f64>,
pub xwayland_state: Option<XWaylandState>, pub xwayland_state: Option<XWaylandState>,
pub xwayland_shell_state: XWaylandShellState, pub xwayland_shell_state: XWaylandShellState,
pub pointer_focus_state: Option<PointerFocusState>, pub pointer_focus_state: Option<PointerFocusState>,

View file

@ -11,16 +11,28 @@ use crate::{
toplevel_management::minimize_rectangle, xdg_activation::ActivationContext, toplevel_management::minimize_rectangle, xdg_activation::ActivationContext,
}, },
}; };
use cosmic_comp_config::EavesdroppingKeyboardMode; use cosmic_comp_config::{EavesdroppingKeyboardMode, XwaylandDescaling};
use smithay::{ use smithay::{
backend::{ backend::{
allocator::Fourcc,
drm::DrmNode, drm::DrmNode,
input::{ButtonState, KeyState, Keycode}, input::{ButtonState, KeyState, Keycode},
renderer::{
element::{
memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement},
Kind,
},
pixman::{PixmanError, PixmanRenderer},
utils::draw_render_elements,
Bind, Frame, Offscreen, Renderer,
},
}, },
desktop::space::SpaceElement, desktop::space::SpaceElement,
input::{keyboard::ModifiersState, pointer::CursorIcon}, input::{keyboard::ModifiersState, pointer::CursorIcon},
reexports::{wayland_server::Client, x11rb::protocol::xproto::Window as X11Window}, 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::{ wayland::{
selection::{ selection::{
data_device::{ data_device::{
@ -41,6 +53,7 @@ use smithay::{
}, },
}; };
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use xcursor::parser::Image;
use xkbcommon::xkb::Keysym; use xkbcommon::xkb::Keysym;
#[derive(Debug)] #[derive(Debug)]
@ -97,7 +110,7 @@ impl State {
last_modifier_state: None, last_modifier_state: None,
}); });
let mut wm = match X11Wm::start_wm( let wm = match X11Wm::start_wm(
data.common.event_loop_handle.clone(), data.common.event_loop_handle.clone(),
x11_socket, x11_socket,
client.clone(), 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(); let xwayland_state = data.common.xwayland_state.as_mut().unwrap();
xwayland_state.xwm = Some(wm); xwayland_state.xwm = Some(wm);
xwayland_state.reload_cursor(1.);
data.notify_ready(); data.notify_ready();
data.common.update_xwayland_scale(); data.common.update_xwayland_scale();
@ -147,6 +146,102 @@ impl State {
} }
} }
fn scale_cursor(
scale: f64,
cursor_size: u32,
image: &Image,
) -> Result<(Vec<u8>, Size<u16, Logical>, Point<u16, Logical>), 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::<i32, BufferCoords>::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::<i32, BufferCoords>::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::<i32>();
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 { impl Common {
fn has_x_keyboard_focus(&self, xwmid: XwmId) -> bool { fn has_x_keyboard_focus(&self, xwmid: XwmId) -> bool {
let keyboard = self let keyboard = self
@ -461,15 +556,23 @@ impl Common {
} }
pub fn update_xwayland_scale(&mut self) { pub fn update_xwayland_scale(&mut self) {
let new_scale = if self.config.cosmic_conf.descale_xwayland { let new_scale = match self.config.cosmic_conf.descale_xwayland {
XwaylandDescaling::Disabled => 1.,
XwaylandDescaling::Enabled => {
let shell = self.shell.read().unwrap(); let shell = self.shell.read().unwrap();
shell shell
.outputs() .outputs()
.map(|o| o.current_scale().integer_scale()) .map(|o| o.current_scale().integer_scale())
.max() .max()
.unwrap_or(1) .unwrap_or(1) as f64
} else { }
1 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 // compare with current scale
@ -487,12 +590,18 @@ impl Common {
// update xorg dpi // update xorg dpi
if let Some(xwm) = xwayland.xwm.as_mut() { 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( if let Err(err) = xwm.set_xsettings(
[ [
("Xft/DPI".into(), dpi.into()), ("Xft/DPI".into(), (dpi.round() as i32).into()),
("Gdk/UnscaledDPI".into(), (dpi / new_scale).into()), (
("Gdk/WindowScalingFactor".into(), new_scale.into()), "Gdk/UnscaledDPI".into(),
((dpi / new_scale).round() as i32).into(),
),
(
"Gdk/WindowScalingFactor".into(),
(new_scale.round() as i32).into(),
),
] ]
.into_iter(), .into_iter(),
) { ) {
@ -500,13 +609,16 @@ impl Common {
} }
} }
// update cursor
xwayland.reload_cursor(new_scale);
// update client scale // update client scale
xwayland xwayland
.client .client
.get_data::<XWaylandClientData>() .get_data::<XWaylandClientData>()
.unwrap() .unwrap()
.compositor_state .compositor_state
.set_client_scale(new_scale as f64); .set_client_scale(new_scale);
// update wl/xdg_outputs // update wl/xdg_outputs
for output in self.shell.read().unwrap().outputs() { for output in self.shell.read().unwrap().outputs() {