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)
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,
}

View file

@ -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<String>, state: &mut
}
}
"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 {
state.common.config.cosmic_conf.descale_xwayland = new;
state.common.update_xwayland_scale();

View file

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

View file

@ -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<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 {
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::<XWaylandClientData>()
.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() {