From b98fa84524e52a2051382b3ea2cbbaeea287b0e3 Mon Sep 17 00:00:00 2001 From: Shawn Wallace Date: Thu, 19 Jun 2025 17:02:42 -0400 Subject: [PATCH] Use fractional scaling when setting scale through xsettings Fixes #168 --- src/server/dispatch.rs | 4 +- src/server/event.rs | 109 ++++++++++++++++-------- src/server/mod.rs | 92 +++++++++++--------- src/server/tests.rs | 4 + src/xstate/settings.rs | 22 +++-- tests/integration.rs | 189 ++++++++++++++++++++++++++--------------- 6 files changed, 269 insertions(+), 151 deletions(-) diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 5171020..f6dabfc 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -1350,11 +1350,11 @@ impl GlobalDispatch for ServerState { ( server, client, - event::OutputScaleFactor(1), + event::OutputScaleFactor::Output(1), event::OutputDimensions::default(), ), ); - state.output_scales_updated = true; + state.updated_outputs.push(entity); } } global_dispatch_with_events!(WlDrmServer, WlDrmClient); diff --git a/src/server/event.rs b/src/server/event.rs index 7b33264..5a4f684 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -44,12 +44,15 @@ use wayland_server::protocol::{ wl_seat::WlSeat, wl_touch::WlTouch, }; +#[derive(Copy, Clone)] +pub(super) struct SurfaceScaleFactor(pub f64); + #[derive(hecs::Bundle)] pub(super) struct SurfaceBundle { pub client: client::wl_surface::WlSurface, pub server: WlSurface, - pub scale: SurfaceScaleFactor, pub viewport: WpViewport, + pub scale: SurfaceScaleFactor, } #[derive(Debug)] @@ -86,12 +89,22 @@ impl Event for SurfaceEvents { wp_fractional_scale_v1::Event::PreferredScale { scale } => { let entity = state.world.entity(target).unwrap(); let factor = scale as f64 / 120.0; - entity.get::<&mut SurfaceScaleFactor>().unwrap().0 = factor; - log::debug!( + debug!( "{} scale factor: {}", entity.get::<&WlSurface>().unwrap().id(), factor ); + + entity.get::<&mut SurfaceScaleFactor>().unwrap().0 = factor; + + if let Some(OnOutput(output)) = entity.get::<&OnOutput>().as_deref().copied() { + if update_output_scale( + state.world.query_one(output).unwrap(), + OutputScaleFactor::Fractional(factor), + ) { + state.updated_outputs.push(output); + } + } if entity.has::() { update_surface_viewport(state.world.query_one(target).unwrap()); } @@ -126,11 +139,6 @@ impl SurfaceEvents { surface.enter(&output); let on_output = OnOutput(output_entity); - if state.fractional_scale.is_none() { - data.get::<&mut SurfaceScaleFactor>().unwrap().0 = - output_data.get::<&OutputScaleFactor>().unwrap().0 as f64; - } - debug!("{} entered {}", surface.id(), output.id()); let mut query = data.query::<(&x::Window, &mut WindowData)>(); @@ -151,9 +159,19 @@ impl SurfaceEvents { conn.focus_window(*window, output); } - drop(query); if state.fractional_scale.is_none() { + let output_scale = output_data.get::<&OutputScaleFactor>().unwrap().get(); + data.get::<&mut SurfaceScaleFactor>().unwrap().0 = output_scale; + drop(query); update_surface_viewport(state.world.query_one(target).unwrap()); + } else { + let scale = data.get::<&SurfaceScaleFactor>().unwrap(); + if update_output_scale( + state.world.query_one(on_output.0).unwrap(), + OutputScaleFactor::Fractional(scale.0), + ) { + state.updated_outputs.push(on_output.0); + } } } cmd.insert_one(target, on_output); @@ -200,20 +218,19 @@ impl SurfaceEvents { drop(xdg); if let Some(pending) = pending { - let mut query = data.query::<(&mut SurfaceScaleFactor, &x::Window, &mut WindowData)>(); + let mut query = data.query::<(&SurfaceScaleFactor, &x::Window, &mut WindowData)>(); let (scale_factor, window, window_data) = query.get().unwrap(); - let scale_factor = scale_factor.0; let window = *window; - let x = (pending.x as f64 * scale_factor) as i32 + window_data.output_offset.x; - let y = (pending.y as f64 * scale_factor) as i32 + window_data.output_offset.y; + let x = (pending.x as f64 * scale_factor.0) as i32 + window_data.output_offset.x; + let y = (pending.y as f64 * scale_factor.0) as i32 + window_data.output_offset.y; let width = if pending.width > 0 { - (pending.width as f64 * scale_factor) as u16 + (pending.width as f64 * scale_factor.0) as u16 } else { window_data.attrs.dims.width }; let height = if pending.height > 0 { - (pending.height as f64 * scale_factor) as u16 + (pending.height as f64 * scale_factor.0) as u16 } else { window_data.attrs.dims.height }; @@ -452,7 +469,7 @@ impl Event for client::wl_pointer::Event { let surface_is_popup = matches!(role, SurfaceRole::Popup(_)); let mut do_enter = || { - debug!("pointer entering {} ({serial})", surface.id()); + debug!("pointer entering {} ({serial} {})", surface.id(), scale.0); server.enter(serial, surface, surface_x * scale.0, surface_y * scale.0); state.connection.as_mut().unwrap().raise_to_top(*window); if !surface_is_popup { @@ -488,6 +505,7 @@ impl Event for client::wl_pointer::Event { cmd.run_on(&mut state.world); } client::wl_pointer::Event::Leave { serial, surface } => { + let _ = state.world.remove_one::(target); if !surface.is_alive() { return; } @@ -720,13 +738,47 @@ impl Event for client::wl_touch::Event { } } +#[derive(Copy, Clone)] pub(super) struct OnOutput(pub Entity); struct OutputName(String); fn get_output_name(output: Option<&OnOutput>, world: &World) -> Option { output.map(|o| world.get::<&OutputName>(o.0).unwrap().0.clone()) } -pub(super) struct OutputScaleFactor(pub i32); +#[derive(Copy, Clone, PartialEq, Debug)] +pub(super) enum OutputScaleFactor { + Output(i32), + Fractional(f64), +} + +impl OutputScaleFactor { + pub(super) fn get(&self) -> f64 { + match *self { + Self::Output(o) => o as _, + Self::Fractional(f) => f, + } + } +} + +#[must_use] +fn update_output_scale( + mut output_scale: hecs::QueryOne<&mut OutputScaleFactor>, + factor: OutputScaleFactor, +) -> bool { + let output_scale = output_scale.get().unwrap(); + if matches!(output_scale, OutputScaleFactor::Fractional(..)) + && matches!(factor, OutputScaleFactor::Output(..)) + { + return false; + } + + if *output_scale != factor { + *output_scale = factor; + return true; + } + + false +} enum OutputDimensionsSource { // The data in this variant is the values needed for the wl_output.geometry event. @@ -1012,26 +1064,15 @@ impl OutputEvent { "{} scale: {factor}", state.world.get::<&WlOutput>(target).unwrap().id() ); - state.world.get::<&mut OutputScaleFactor>(target).unwrap().0 = factor; + if update_output_scale( + state.world.query_one(target).unwrap(), + OutputScaleFactor::Output(factor), + ) { + state.updated_outputs.push(target); + } if state.fractional_scale.is_none() { - let mut surfaces = vec![]; - for (entity, (scale, _)) in state - .world - .query_mut::<(&mut SurfaceScaleFactor, &OnOutput)>() - .with::<&WindowData>() - .into_iter() - .filter(|(_, (_, o))| o.0 == target) - { - surfaces.push(entity); - scale.0 = factor as f64; - } - for entity in surfaces { - update_surface_viewport(state.world.query_one(entity).unwrap()); - } - state.world.get::<&WlOutput>(target).unwrap().scale(factor); } - state.output_scales_updated = true; } Event::Name { name } => { state diff --git a/src/server/mod.rs b/src/server/mod.rs index 34e1e61..296021f 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -173,9 +173,6 @@ struct SurfaceAttach { #[derive(PartialEq, Eq)] struct SurfaceSerial([u32; 2]); -#[derive(Copy, Clone)] -struct SurfaceScaleFactor(f64); - #[derive(Debug)] enum SurfaceRole { Toplevel(Option), @@ -418,8 +415,8 @@ pub struct ServerState { activation_state: Option, global_output_offset: GlobalOutputOffset, global_offset_updated: bool, - output_scales_updated: bool, - new_scale: Option, + updated_outputs: Vec, + new_scale: Option, } impl ServerState { @@ -507,7 +504,7 @@ impl ServerState { }, }, global_offset_updated: false, - output_scales_updated: false, + updated_outputs: Vec::new(), new_scale: None, decoration_manager, world: MyWorld::new(global_list), @@ -999,19 +996,42 @@ impl ServerState { self.global_offset_updated = false; } - if self.output_scales_updated { + if !self.updated_outputs.is_empty() { + for output in self.updated_outputs.drain(..) { + let output_scale = self.world.get::<&OutputScaleFactor>(output).unwrap(); + if matches!(*output_scale, OutputScaleFactor::Output(..)) { + let mut surface_query = self + .world + .query::<(&OnOutput, &mut SurfaceScaleFactor)>() + .with::<(&WindowData, &WlSurface)>(); + + let mut surfaces = vec![]; + for (surface, (OnOutput(s_output), surface_scale)) in surface_query.iter() { + if *s_output == output { + surface_scale.0 = output_scale.get(); + surfaces.push(surface); + } + } + + drop(surface_query); + for surface in surfaces { + update_surface_viewport(self.world.query_one(surface).unwrap()); + } + } + } + let mut mixed_scale = false; let mut scale; let mut outputs = self.world.query_mut::<&OutputScaleFactor>().into_iter(); let (_, output_scale) = outputs.next().unwrap(); - scale = output_scale.0; + scale = output_scale.get(); for (_, output_scale) in outputs { - if output_scale.0 != scale { + if output_scale.get() != scale { mixed_scale = true; - scale = scale.min(output_scale.0); + scale = scale.min(output_scale.get()); } } @@ -1021,8 +1041,6 @@ impl ServerState { debug!("Using new scale {scale}"); self.new_scale = Some(scale); - - self.output_scales_updated = false; } { @@ -1049,7 +1067,7 @@ impl ServerState { .expect("Failed flushing clientside events"); } - pub fn new_global_scale(&mut self) -> Option { + pub fn new_global_scale(&mut self) -> Option { self.new_scale.take() } @@ -1156,26 +1174,18 @@ impl ServerState { } } - let initial_scale; let role = if let Some(parent) = popup_for { - let data; - (initial_scale, data) = self.create_popup(entity, xdg_surface, parent); + let data = self.create_popup(entity, xdg_surface, parent); SurfaceRole::Popup(Some(data)) } else { - initial_scale = 1.0; let data = self.create_toplevel(entity, xdg_surface, fullscreen); SurfaceRole::Toplevel(Some(data)) }; - let (surface_role, scale_factor, client) = self + let (surface_role, client) = self .world - .query_one_mut::<( - Option<&SurfaceRole>, - &mut SurfaceScaleFactor, - &client::wl_surface::WlSurface, - )>(entity) + .query_one_mut::<(Option<&SurfaceRole>, &client::wl_surface::WlSurface)>(entity) .unwrap(); - scale_factor.0 = initial_scale; let new_role_type = std::mem::discriminant(&role); if let Some(role) = surface_role { @@ -1294,13 +1304,13 @@ impl ServerState { } } - fn create_popup( - &mut self, - entity: Entity, - xdg: XdgSurface, - parent: x::Window, - ) -> (f64, PopupData) { - let window = self.world.get::<&WindowData>(entity).unwrap(); + fn create_popup(&mut self, entity: Entity, xdg: XdgSurface, parent: x::Window) -> PopupData { + let mut query = self + .world + .query_one::<(&WindowData, &mut SurfaceScaleFactor)>(entity) + .unwrap(); + + let (window, scale) = query.get().unwrap(); let mut parent_query = self .world .query_one::<(&WindowData, &SurfaceScaleFactor, &SurfaceRole)>(self.windows[&parent]) @@ -1308,6 +1318,7 @@ impl ServerState { let (parent_window, parent_scale, parent_role) = parent_query.get().unwrap(); let parent_dims = parent_window.attrs.dims; let initial_scale = parent_scale.0; + *scale = *parent_scale; debug!( "creating popup ({:?}) {:?} {:?} {:?} {entity:?} (scale: {initial_scale})", @@ -1340,18 +1351,15 @@ impl ServerState { entity, ); - ( - initial_scale, - PopupData { - popup, - positioner, - xdg: XdgSurfaceData { - surface: xdg, - configured: false, - pending: None, - }, + PopupData { + popup, + positioner, + xdg: XdgSurfaceData { + surface: xdg, + configured: false, + pending: None, }, - ) + } } fn close_x_window(&mut self, window: x::Window) { diff --git a/src/server/tests.rs b/src/server/tests.rs index fa3fad2..428b105 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -1965,6 +1965,7 @@ fn fractional_scale_popup() { testwl.enable_fractional_scale(); }); let comp = f.compositor(); + let (_, output) = f.new_output(0, 0); let toplevel = unsafe { Window::new(1) }; let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); @@ -1978,6 +1979,7 @@ fn fractional_scale_popup() { .expect("No fractional scale for surface"); fractional.preferred_scale(180); // 1.5 scale + f.testwl.move_surface_to_output(toplevel_id, &output); f.run(); f.run(); @@ -2038,6 +2040,7 @@ fn fractional_scale_small_popup() { }); let comp = f.compositor(); + let (_, output) = f.new_output(0, 0); let toplevel = unsafe { Window::new(1) }; let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); let data = f.testwl.get_surface_data(toplevel_id).unwrap(); @@ -2046,6 +2049,7 @@ fn fractional_scale_small_popup() { .as_ref() .expect("Missing fracitonal scale data"); fractional.preferred_scale(180); // 1.5 scale + f.testwl.move_surface_to_output(toplevel_id, &output); f.run(); f.run(); diff --git a/src/xstate/settings.rs b/src/xstate/settings.rs index 4c0d65c..4c3426b 100644 --- a/src/xstate/settings.rs +++ b/src/xstate/settings.rs @@ -27,7 +27,7 @@ impl XState { } } - pub(crate) fn update_global_scale(&mut self, scale: i32) { + pub(crate) fn update_global_scale(&mut self, scale: f64) { self.settings.set_scale(scale); self.connection .send_and_check_request(&x::ChangeProperty { @@ -57,6 +57,7 @@ pub(super) struct Settings { settings: HashMap<&'static str, IntSetting>, } +#[derive(Copy, Clone)] struct IntSetting { value: i32, last_change_serial: u32, @@ -160,17 +161,26 @@ impl Settings { data } - fn set_scale(&mut self, scale: i32) { + fn set_scale(&mut self, scale: f64) { self.serial += 1; - self.settings.entry(XFT_DPI).insert_entry(IntSetting { - value: scale * DEFAULT_DPI * DPI_SCALE_FACTOR, + let setting = IntSetting { + value: (scale * DEFAULT_DPI as f64 * DPI_SCALE_FACTOR as f64).round() as i32, last_change_serial: self.serial, - }); + }; + self.settings.entry(XFT_DPI).insert_entry(setting); + // Gdk/WindowScalingFactor + Gdk/UnscaledDPI is identical to setting + // GDK_SCALE = scale and then GDK_DPI_SCALE = 1 / scale. + self.settings + .entry(GDK_UNSCALED_DPI) + .insert_entry(IntSetting { + value: setting.value / scale as i32, + last_change_serial: self.serial, + }); self.settings .entry(GDK_WINDOW_SCALE) .insert_entry(IntSetting { - value: scale, + value: scale as i32, last_change_serial: self.serial, }); } diff --git a/tests/integration.rs b/tests/integration.rs index 99774fa..8d5ea73 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -329,6 +329,15 @@ xcb::atoms_struct! { } } +struct Settings { + serial: u32, + data: HashMap, +} +struct Setting { + value: i32, + last_change: u32, +} + struct Connection { inner: xcb::Connection, pollfd: PollFd<'static>, @@ -564,6 +573,56 @@ impl Connection { }) .unwrap(); } + + fn get_xsettings(&self) -> Settings { + let owner = self + .get_reply(&x::GetSelectionOwner { + selection: self.atoms.xsettings, + }) + .owner(); + + let reply = self.get_reply(&x::GetProperty { + delete: false, + window: owner, + property: self.atoms.xsettings_setting, + r#type: self.atoms.xsettings_setting, + long_offset: 0, + long_length: 60, + }); + assert_eq!(reply.r#type(), self.atoms.xsettings_setting); + + let data = reply.value::(); + + let byte_order = data[0]; + assert_eq!(byte_order, 0); + let serial = u32::from_le_bytes(data[4..8].try_into().unwrap()); + let num_settings = u32::from_le_bytes(data[8..12].try_into().unwrap()); + + let mut current_idx = 12; + let mut settings = HashMap::new(); + for _ in 0..num_settings { + assert_eq!(&data[current_idx..current_idx + 2], &[0, 0]); + let name_len = + u16::from_le_bytes(data[current_idx + 2..current_idx + 4].try_into().unwrap()); + + let padding_start = current_idx + 4 + name_len as usize; + let name = String::from_utf8(data[current_idx + 4..padding_start].to_vec()).unwrap(); + let num_padding_bytes = (4 - (name_len as usize % 4)) % 4; + let data_start = padding_start + num_padding_bytes; + let last_change = + u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap()); + let value = + i32::from_le_bytes(data[data_start + 4..data_start + 8].try_into().unwrap()); + + settings.insert(name, Setting { value, last_change }); + current_idx = data_start + 8; + } + + Settings { + serial, + data: settings, + } + } } #[test] @@ -1627,6 +1686,8 @@ fn forced_1x_scale_consistent_x11_size() { // Update scale output.scale(3); output.done(); + f.wait_and_dispatch(); + f.testwl .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]); f.testwl.focus_toplevel(surface); @@ -1736,66 +1797,7 @@ fn xsettings_scale() { let connection = Connection::new(&f.display); f.testwl.enable_xdg_output_manager(); - struct Settings { - serial: u32, - data: HashMap, - } - struct Setting { - value: i32, - last_change: u32, - } - - let owner = connection - .get_reply(&x::GetSelectionOwner { - selection: connection.atoms.xsettings, - }) - .owner(); - - let get_settings = || -> Settings { - let reply = connection.get_reply(&x::GetProperty { - delete: false, - window: owner, - property: connection.atoms.xsettings_setting, - r#type: connection.atoms.xsettings_setting, - long_offset: 0, - long_length: 60, - }); - assert_eq!(reply.r#type(), connection.atoms.xsettings_setting); - - let data = reply.value::(); - - let byte_order = data[0]; - assert_eq!(byte_order, 0); - let serial = u32::from_le_bytes(data[4..8].try_into().unwrap()); - let num_settings = u32::from_le_bytes(data[8..12].try_into().unwrap()); - - let mut current_idx = 12; - let mut settings = HashMap::new(); - for _ in 0..num_settings { - assert_eq!(&data[current_idx..current_idx + 2], &[0, 0]); - let name_len = - u16::from_le_bytes(data[current_idx + 2..current_idx + 4].try_into().unwrap()); - - let padding_start = current_idx + 4 + name_len as usize; - let name = String::from_utf8(data[current_idx + 4..padding_start].to_vec()).unwrap(); - let num_padding_bytes = (4 - (name_len as usize % 4)) % 4; - let data_start = padding_start + num_padding_bytes; - let last_change = - u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap()); - let value = - i32::from_le_bytes(data[data_start + 4..data_start + 8].try_into().unwrap()); - - settings.insert(name, Setting { value, last_change }); - current_idx = data_start + 8; - } - - Settings { - serial, - data: settings, - } - }; - - let settings = get_settings(); + let settings = connection.get_xsettings(); let settings_serial = settings.serial; assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024); let dpi_serial = settings.data["Xft/DPI"].last_change; @@ -1809,20 +1811,17 @@ fn xsettings_scale() { output.done(); f.wait_and_dispatch(); - let settings = get_settings(); + let settings = connection.get_xsettings(); assert!(settings.serial > settings_serial); assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024); assert!(settings.data["Xft/DPI"].last_change > dpi_serial); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); assert!(settings.data["Gdk/WindowScalingFactor"].last_change > window_serial); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); - assert_eq!( - settings.data["Gdk/UnscaledDPI"].last_change, - unscaled_serial - ); + assert!(settings.data["Gdk/UnscaledDPI"].last_change > unscaled_serial); let output2 = f.create_output(0, 0); - let settings = get_settings(); + let settings = connection.get_xsettings(); assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); @@ -1832,12 +1831,68 @@ fn xsettings_scale() { f.testwl.dispatch(); std::thread::sleep(Duration::from_millis(1)); - let settings = get_settings(); + let settings = connection.get_xsettings(); assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024); assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024); } +#[test] +fn xsettings_fractional_scale() { + let mut f = Fixture::new_preset(|testwl| { + testwl.new_output(0, 0); // WL-1 + testwl.enable_fractional_scale(); + }); + let mut connection = Connection::new(&f.display); + f.testwl.enable_xdg_output_manager(); + + let output = f.testwl.last_created_output(); + + let window = connection.new_window(connection.root, 0, 0, 20, 20, false); + let surface = f.map_as_toplevel(&mut connection, window); + + let data = f + .testwl + .get_surface_data(surface) + .expect("Missing surface data"); + let fractional = data + .fractional + .as_ref() + .expect("No fractional scale for surface"); + + fractional.preferred_scale(180); // 1.5 scale + f.testwl.move_surface_to_output(surface, &output); + + f.wait_and_dispatch(); + let settings = connection.get_xsettings(); + + assert_eq!( + settings.data["Xft/DPI"].value, + (1.5 * 96_f64 * 1024_f64).round() as i32 + ); + assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1); + assert_eq!( + settings.data["Gdk/UnscaledDPI"].value, + (1.5 * 96_f64 * 1024_f64).round() as i32 + ); + + let data = f.testwl.get_surface_data(surface).unwrap(); + let fractional = data.fractional.as_ref().unwrap(); + fractional.preferred_scale(300); // 2.5 scale + f.wait_and_dispatch(); + + let settings = connection.get_xsettings(); + assert_eq!( + settings.data["Xft/DPI"].value, + (2.5 * 96_f64 * 1024_f64).round() as i32 + ); + assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2); + assert_eq!( + settings.data["Gdk/UnscaledDPI"].value, + (2.5 / 2.0 * 96_f64 * 1024_f64).round() as i32 + ); +} + #[test] fn xsettings_switch_owner() { let f = Fixture::new();