diff --git a/crates/hearthspace-compositor/src/compositor/handlers.rs b/crates/hearthspace-compositor/src/compositor/handlers.rs index e5446cf..1e11a43 100644 --- a/crates/hearthspace-compositor/src/compositor/handlers.rs +++ b/crates/hearthspace-compositor/src/compositor/handlers.rs @@ -10,14 +10,16 @@ use smithay::{ pointer::{CursorImageStatus, Focus}, }, reexports::{calloop::Interest, wayland_server::protocol::wl_seat}, - utils::{Serial, Size}, + utils::{Point, Serial, Size}, wayland::{ buffer::BufferHandler, compositor::{ BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, - SurfaceAttributes, add_blocker, add_pre_commit_hook, with_states, + SurfaceAttributes, TraversalAction, add_blocker, add_pre_commit_hook, with_states, + with_surface_tree_downward, }, dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf}, + fractional_scale::{FractionalScaleHandler, with_fractional_scale}, output::OutputHandler, selection::{ SelectionHandler, @@ -475,3 +477,57 @@ impl SeatHandler for App { self.request_redraw(); } } + +impl FractionalScaleHandler for App { + fn new_fractional_scale(&mut self, surface: WlSurface) { + // Advertise the preferred fractional scale so HiDPI toolkits (GTK, + // Firefox) render their buffers at the monitor scale and declare their + // logical size through `wp_viewport`, instead of rendering an oversized + // integer-scaled buffer that the compositor would then scale again. + let scale = self.preferred_fractional_scale(&surface); + with_states(&surface, |states| { + with_fractional_scale(states, |fractional_scale| { + fractional_scale.set_preferred_scale(scale); + }); + }); + } +} + +impl App { + /// The fractional scale to advertise for `surface`: the integer scale of the + /// output its window sits on, or the primary output's scale when the surface + /// is not (yet) a managed window. + pub(super) fn preferred_fractional_scale(&self, surface: &WlSurface) -> f64 { + let scale = self + .window_index_for_surface(surface) + .map(|index| { + let position = self.windows[index].position; + let screen = self + .canvas_to_screen(Point::from((f64::from(position.x), f64::from(position.y)))); + self.output_scale_at(screen) + }) + .unwrap_or_else(|| self.primary_output_scale()); + f64::from(scale.max(1)) + } + + /// Re-advertise the preferred fractional scale for every managed window's + /// surface tree. Called when the output layout (and thus per-output scale) + /// changes so already-mapped clients update their buffers. + pub(super) fn refresh_fractional_scales(&self) { + for window in &self.windows { + let root = window.surface.wl_surface(); + let scale = self.preferred_fractional_scale(root); + with_surface_tree_downward( + root, + (), + |_, _, _| TraversalAction::DoChildren(()), + |_, states, _| { + with_fractional_scale(states, |fractional_scale| { + fractional_scale.set_preferred_scale(scale); + }); + }, + |_, _, _| true, + ); + } + } +} diff --git a/crates/hearthspace-compositor/src/compositor/mod.rs b/crates/hearthspace-compositor/src/compositor/mod.rs index 3782271..ba2dd61 100644 --- a/crates/hearthspace-compositor/src/compositor/mod.rs +++ b/crates/hearthspace-compositor/src/compositor/mod.rs @@ -71,11 +71,13 @@ use smithay::{ wayland::{ compositor::CompositorState, dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufState, ImportNotifier}, + fractional_scale::FractionalScaleManagerState, output::OutputManagerState, selection::data_device::DataDeviceState, shell::xdg::{ToplevelSurface, XdgShellState, decoration::XdgDecorationState}, shm::ShmState, socket::ListeningSocketSource, + viewporter::ViewporterState, }, }; use tracing::{debug, error, info}; @@ -160,6 +162,14 @@ struct App { xdg_shell_state: XdgShellState, _xdg_decoration_state: XdgDecorationState, _output_manager_state: OutputManagerState, + /// `wp_viewporter`: lets clients declare a surface's logical destination + /// size independently of its buffer, which HiDPI toolkits (GTK/Firefox) use + /// together with fractional scale to avoid rendering an oversized buffer. + _viewporter_state: ViewporterState, + /// `wp_fractional_scale_v1`: advertises the preferred scale to clients so + /// they render buffers at the monitor scale instead of falling back to a + /// mismatched integer buffer scale. + _fractional_scale_manager_state: FractionalScaleManagerState, shm_state: ShmState, seat_state: SeatState, data_device_state: DataDeviceState, @@ -229,6 +239,7 @@ impl App { if refresh.output_layout_changed { self.configure_shell_bars(); self.reconcile_pointer_after_output_geometry_change(); + self.refresh_fractional_scales(); } if self.background_mode != settings.background_mode { @@ -438,6 +449,8 @@ pub(in crate::compositor) fn initialize_app( let compositor_state = CompositorState::new::(&dh); let xdg_decoration_state = XdgDecorationState::new::(&dh); let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + let viewporter_state = ViewporterState::new::(&dh); + let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); let shm_state = ShmState::new::(&dh, vec![]); let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&dh, "hearthspace"); @@ -475,6 +488,8 @@ pub(in crate::compositor) fn initialize_app( ), _xdg_decoration_state: xdg_decoration_state, _output_manager_state: output_manager_state, + _viewporter_state: viewporter_state, + _fractional_scale_manager_state: fractional_scale_manager_state, shm_state, seat_state, data_device_state: DataDeviceState::new::(&dh), diff --git a/crates/hearthspace-compositor/src/compositor/output.rs b/crates/hearthspace-compositor/src/compositor/output.rs index f20094a..7682cd9 100644 --- a/crates/hearthspace-compositor/src/compositor/output.rs +++ b/crates/hearthspace-compositor/src/compositor/output.rs @@ -295,6 +295,17 @@ impl App { self.outputs.primary.scale.max(1) } + /// The integer scale of whichever output contains `point` (in logical + /// screen space), falling back to the primary output's scale. Used to pick + /// the preferred fractional scale advertised to a surface's clients. + pub(super) fn output_scale_at(&self, point: Point) -> i32 { + std::iter::once(&self.outputs.primary) + .chain(self.outputs.secondary.iter()) + .find(|output| point_in_rect(point, output.logical_rect())) + .map(|output| output.scale.max(1)) + .unwrap_or_else(|| self.primary_output_scale()) + } + pub(in crate::compositor) fn set_output_layout(&mut self, layout: OutputLayout) -> bool { self.outputs.set_layout(layout) } @@ -516,6 +527,13 @@ fn scaled_client_dimension(size: i32, scale: i32) -> i32 { ((size + scale - 1) / scale).max(1) } +fn point_in_rect(point: Point, rect: Rectangle) -> bool { + point.x >= f64::from(rect.loc.x) + && point.y >= f64::from(rect.loc.y) + && point.x < f64::from(rect.loc.x + rect.size.w) + && point.y < f64::from(rect.loc.y + rect.size.h) +} + fn rectangles_overlap(a: Rectangle, b: Rectangle) -> bool { a.loc.x < b.loc.x + b.size.w && a.loc.x + a.size.w > b.loc.x