diff --git a/README.md b/README.md index ce7bf2f..6bbf70e 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,4 @@ Additionally, satellite can *optionally* take advantage of the following protoco - XDG foreign - Pointer constraints - Tablet input +- Fractional scale diff --git a/src/clientside/mod.rs b/src/clientside/mod.rs index cfe8a1f..3b9bf00 100644 --- a/src/clientside/mod.rs +++ b/src/clientside/mod.rs @@ -23,6 +23,10 @@ use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1: use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1; use wayland_protocols::{ wp::{ + fractional_scale::v1::client::{ + wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, + wp_fractional_scale_v1::WpFractionalScaleV1, + }, linux_dmabuf::zv1::client::{ self as dmabuf, zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1 as DmabufFeedback, @@ -143,6 +147,7 @@ delegate_noop!(Globals: ZwpPointerConstraintsV1); delegate_noop!(Globals: ZwpTabletManagerV2); delegate_noop!(Globals: XdgActivationV1); delegate_noop!(Globals: ZxdgDecorationManagerV1); +delegate_noop!(Globals: WpFractionalScaleManagerV1); delegate_noop!(Globals: ignore ZxdgToplevelDecorationV1); impl Dispatch for Globals { @@ -232,6 +237,7 @@ push_events!(XdgOutput); push_events!(WlTouch); push_events!(ZwpConfinedPointerV1); push_events!(ZwpLockedPointerV1); +push_events!(WpFractionalScaleV1); pub(crate) struct LateInitObjectKey { key: OnceLock, diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index c416b3a..2c86717 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -230,6 +230,10 @@ impl let viewport = state.viewporter.get_viewport(&client, &state.qh, ()); surface_id = Some(server.id().protocol_id()); debug!("new surface with key {key:?} ({surface_id:?})"); + let fractional = state + .fractional_scale + .as_ref() + .map(|f| f.get_fractional_scale(&client, &state.qh, key)); SurfaceData { client, @@ -242,8 +246,9 @@ impl xwl: None, window: None, output_key: None, - scale_factor: 1, + scale_factor: 1.0, viewport, + fractional, } .into() }); diff --git a/src/server/event.rs b/src/server/event.rs index 2541f27..9f3b6f6 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -7,6 +7,7 @@ use std::os::fd::AsFd; use wayland_client::{protocol as client, Proxy}; use wayland_protocols::{ wp::{ + fractional_scale::v1::client::wp_fractional_scale_v1, pointer_constraints::zv1::{ client::{ zwp_confined_pointer_v1::{self, ZwpConfinedPointerV1 as ConfinedPointerClient}, @@ -63,6 +64,7 @@ pub(crate) enum SurfaceEvents { XdgSurface(xdg_surface::Event), Toplevel(xdg_toplevel::Event), Popup(xdg_popup::Event), + FractionalScale(wp_fractional_scale_v1::Event), } macro_rules! impl_from { ($type:ty, $variant:ident) => { @@ -77,6 +79,7 @@ impl_from!(client::wl_surface::Event, WlSurface); impl_from!(xdg_surface::Event, XdgSurface); impl_from!(xdg_toplevel::Event, Toplevel); impl_from!(xdg_popup::Event, Popup); +impl_from!(wp_fractional_scale_v1::Event, FractionalScale); impl HandleEvent for SurfaceData { type Event = SurfaceEvents; @@ -86,6 +89,20 @@ impl HandleEvent for SurfaceData { SurfaceEvents::XdgSurface(event) => self.xdg_event(event, state), SurfaceEvents::Toplevel(event) => self.toplevel_event(event, state), SurfaceEvents::Popup(event) => self.popup_event(event, state), + SurfaceEvents::FractionalScale(event) => match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => { + self.scale_factor = scale as f64 / 120.0; + log::debug!("{} scale factor: {}", self.server.id(), self.scale_factor); + if let Some(win_data) = self + .window + .as_ref() + .and_then(|win| state.windows.get_mut(win)) + { + self.update_viewport(win_data.attrs.dims, win_data.attrs.size_hints); + } + } + _ => unreachable!(), + }, } } } @@ -109,8 +126,8 @@ impl SurfaceData { } fn update_viewport(&self, dims: WindowDims, size_hints: Option) { - let width = dims.width as i32 / self.scale_factor; - let height = dims.height as i32 / self.scale_factor; + let width = (dims.width as f64 / self.scale_factor) as i32; + let height = (dims.height as f64 / self.scale_factor) as i32; self.viewport.set_destination(width, height); debug!("{} viewport: {width}x{height}", self.server.id()); if let Some(hints) = size_hints { @@ -121,16 +138,17 @@ impl SurfaceData { ); return; }; + if let Some(min) = hints.min_size { data.toplevel.set_min_size( - min.width / self.scale_factor, - min.height / self.scale_factor, + (min.width as f64 / self.scale_factor) as i32, + (min.height as f64 / self.scale_factor) as i32, ); } if let Some(max) = hints.max_size { data.toplevel.set_max_size( - max.width / self.scale_factor, - max.height / self.scale_factor, + (max.width as f64 / self.scale_factor) as i32, + (max.height as f64 / self.scale_factor) as i32, ); } } @@ -151,13 +169,17 @@ impl SurfaceData { }; let output: &mut Output = object.as_mut(); self.server.enter(&output.server); - self.scale_factor = output.scale; + if state.fractional_scale.is_none() { + self.scale_factor = output.scale as f64; + } self.output_key = Some(key); debug!("{} entered {}", self.server.id(), output.server.id()); let windows = &mut state.windows; if let Some(win_data) = self.window.as_ref().and_then(|win| windows.get_mut(win)) { - self.update_viewport(win_data.attrs.dims, win_data.attrs.size_hints); + if state.fractional_scale.is_none() { + self.update_viewport(win_data.attrs.dims, win_data.attrs.size_hints); + } win_data.update_output_offset( key, WindowOutputOffset { @@ -205,15 +227,15 @@ impl SurfaceData { if let Some(pending) = xdg.pending.take() { let window = state.associated_windows[self.key]; let window = state.windows.get_mut(&window).unwrap(); - let x = pending.x * self.scale_factor + window.output_offset.x; - let y = pending.y * self.scale_factor + window.output_offset.y; + let x = (pending.x as f64 * self.scale_factor) as i32 + window.output_offset.x; + let y = (pending.y as f64 * self.scale_factor) as i32 + window.output_offset.y; let width = if pending.width > 0 { - (pending.width * self.scale_factor) as u16 + (pending.width as f64 * self.scale_factor) as u16 } else { window.attrs.dims.width }; let height = if pending.height > 0 { - (pending.height * self.scale_factor) as u16 + (pending.height as f64 * self.scale_factor) as u16 } else { window.attrs.dims.height }; @@ -394,7 +416,7 @@ pub struct Pointer { server: WlPointer, pub client: client::wl_pointer::WlPointer, pending_enter: PendingEnter, - scale: i32, + scale: f64, } impl Pointer { @@ -403,7 +425,7 @@ impl Pointer { server, client, pending_enter: PendingEnter(None), - scale: 1, + scale: 1.0, } } } @@ -445,8 +467,8 @@ impl HandleEvent for Pointer { self.server.enter( serial, &surface_data.server, - surface_x * self.scale as f64, - surface_y * self.scale as f64, + surface_x * self.scale, + surface_y * self.scale, ); let window = surface_data.window.unwrap(); state.connection.as_mut().unwrap().raise_to_top(window); @@ -524,11 +546,8 @@ impl HandleEvent for Pointer { warn!("could not move pointer to surface ({serial}): stale surface"); } } else { - self.server.motion( - time, - surface_x * self.scale as f64, - surface_y * self.scale as f64, - ); + self.server + .motion(time, surface_x * self.scale, surface_y * self.scale); } } _ => simple_event_shunt! { @@ -983,24 +1002,26 @@ impl Output { Event::Scale { factor } => { debug!("{} scale: {factor}", self.server.id()); self.scale = factor; - self.windows.retain(|window| { - let Some(data): Option<&WindowData> = state.windows.get(window) else { - return false; - }; + if state.fractional_scale.is_none() { + self.windows.retain(|window| { + let Some(data): Option<&WindowData> = state.windows.get(window) else { + return false; + }; - if let Some::<&mut SurfaceData>(surface) = data - .surface_key - .and_then(|key| state.objects.get_mut(key)) - .map(AsMut::as_mut) - { - surface.scale_factor = factor; - surface.update_viewport(data.attrs.dims, data.attrs.size_hints); - } + if let Some::<&mut SurfaceData>(surface) = data + .surface_key + .and_then(|key| state.objects.get_mut(key)) + .map(AsMut::as_mut) + { + surface.scale_factor = factor as f64; + surface.update_viewport(data.attrs.dims, data.attrs.size_hints); + } - true - }); + true + }); - self.server.scale(factor); + self.server.scale(factor); + } } _ => simple_event_shunt! { self.server, event: Event => [ diff --git a/src/server/mod.rs b/src/server/mod.rs index 99c4fb9..0d00346 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -22,12 +22,14 @@ use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; use std::rc::{Rc, Weak}; use wayland_client::{globals::Global, protocol as client, Proxy}; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::{ self, ZxdgToplevelDecorationV1, }; use wayland_protocols::{ wp::{ + fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, @@ -188,8 +190,9 @@ pub struct SurfaceData { xwl: Option, window: Option, output_key: Option, - scale_factor: i32, + scale_factor: f64, viewport: WpViewport, + fractional: Option, } impl SurfaceData { @@ -515,6 +518,7 @@ pub struct ServerState { xdg_wm_base: XdgWmBase, viewporter: WpViewporter, + fractional_scale: Option, clipboard_data: Option>, last_kb_serial: Option<(client::wl_seat::WlSeat, u32)>, activation_state: Option, @@ -542,6 +546,10 @@ impl ServerState { .bind::(&qh, 1..=1, ()) .expect("Could not bind wp_viewporter"); + let fractional_scale = clientside.global_list.bind::(&qh, 1..=1, ()) + .inspect_err(|e| warn!("Couldn't bind fractional scale manager: {e}. Fractional scaling will not work.")) + .ok(); + let manager = DataDeviceManagerState::bind(&clientside.global_list, &qh) .inspect_err(|e| { warn!("Could not bind data device manager ({e:?}). Clipboard will not work.") @@ -590,6 +598,7 @@ impl ServerState { associated_windows: Default::default(), xdg_wm_base, viewporter, + fractional_scale, clipboard_data, last_kb_serial: None, activation_state, @@ -731,14 +740,14 @@ impl ServerState { if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role { if let Some(min_size) = &hints.min_size { data.toplevel.set_min_size( - min_size.width / surface.scale_factor, - min_size.height / surface.scale_factor, + (min_size.width as f64 / surface.scale_factor) as i32, + (min_size.height as f64 / surface.scale_factor) as i32, ); } if let Some(max_size) = &hints.max_size { data.toplevel.set_max_size( - max_size.width / surface.scale_factor, - max_size.height / surface.scale_factor, + (max_size.width as f64 / surface.scale_factor) as i32, + (max_size.height as f64 / surface.scale_factor) as i32, ); } } @@ -831,12 +840,12 @@ impl ServerState { match &data.role { Some(SurfaceRole::Popup(Some(popup))) => { popup.positioner.set_offset( - (event.x() as i32 - win.output_offset.x) / data.scale_factor, - (event.y() as i32 - win.output_offset.y) / data.scale_factor, + ((event.x() as i32 - win.output_offset.x) as f64 / data.scale_factor) as i32, + ((event.y() as i32 - win.output_offset.y) as f64 / data.scale_factor) as i32, ); popup.positioner.set_size( - event.width() as i32 / data.scale_factor, - event.height() as i32 / data.scale_factor, + (event.width() as f64 / data.scale_factor) as i32, + (event.height() as f64 / data.scale_factor) as i32, ); popup.popup.reposition(&popup.positioner, 0); } @@ -1199,19 +1208,19 @@ impl ServerState { let positioner = self.xdg_wm_base.create_positioner(&self.qh, ()); positioner.set_size( - 1.max(window.attrs.dims.width as i32 / initial_scale), - 1.max(window.attrs.dims.height as i32 / initial_scale), + 1.max((window.attrs.dims.width as f64 / initial_scale) as i32), + 1.max((window.attrs.dims.height as f64 / initial_scale) as i32), ); - let x = (window.attrs.dims.x - parent_dims.x) as i32 / initial_scale; - let y = (window.attrs.dims.y - parent_dims.y) as i32 / initial_scale; + let x = ((window.attrs.dims.x - parent_dims.x) as f64 / initial_scale) as i32; + let y = ((window.attrs.dims.y - parent_dims.y) as f64 / initial_scale) as i32; positioner.set_offset(x, y); positioner.set_anchor(Anchor::TopLeft); positioner.set_gravity(Gravity::BottomRight); positioner.set_anchor_rect( 0, 0, - parent_window.attrs.dims.width as i32 / initial_scale, - parent_window.attrs.dims.height as i32 / initial_scale, + (parent_window.attrs.dims.width as f64 / initial_scale) as i32, + (parent_window.attrs.dims.height as f64 / initial_scale) as i32, ); let popup = xdg_surface.get_popup( Some(&parent_surface.xdg().unwrap().surface), @@ -1230,7 +1239,7 @@ impl ServerState { }; SurfaceRole::Popup(Some(popup)) } else { - initial_scale = 1; + initial_scale = 1.0; let data = self.create_toplevel(window, surface_key, xdg_surface, fullscreen); SurfaceRole::Toplevel(Some(data)) }; diff --git a/src/server/tests.rs b/src/server/tests.rs index 5e197f1..44bc07b 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -1,6 +1,7 @@ use super::{ServerState, WindowDims}; use crate::xstate::{SetState, WinSize, WmName}; use rustix::event::{poll, PollFd, PollFlags}; +use smithay_client_toolkit::compositor::Surface; use std::collections::HashMap; use std::io::Write; use std::os::fd::{AsRawFd, BorrowedFd}; @@ -23,6 +24,7 @@ use wayland_client::{ }, Connection, Proxy, WEnum, }; +use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; use wayland_protocols::{ wp::{ @@ -269,7 +271,7 @@ struct PopupBuilder { parent_window: Window, parent_surface: testwl::SurfaceId, dims: WindowDims, - scale: i32, + scale: f64, check_size_and_pos: bool, } @@ -285,7 +287,7 @@ impl PopupBuilder { width: 50, height: 50, }, - scale: 1, + scale: 1.0, check_size_and_pos: true, } } @@ -315,14 +317,14 @@ impl PopupBuilder { self } - fn scale(mut self, scale: i32) -> Self { - self.scale = scale; + fn scale(mut self, scale: impl Into) -> Self { + self.scale = scale.into(); self } } impl TestFixture { - fn new() -> Self { + fn new_pre_connect(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self { INIT.call_once(|| { env_logger::builder() .is_test(true) @@ -332,6 +334,7 @@ impl TestFixture { let (client_s, server_s) = UnixStream::pair().unwrap(); let mut testwl = testwl::Server::new(true); + pre_connect(&mut testwl); let display = Display::::new().unwrap(); testwl.connect(server_s); // Handle initial globals roundtrip setup requirement @@ -369,6 +372,10 @@ impl TestFixture { f } + fn new() -> Self { + Self::new_pre_connect(|_| {}) + } + fn new_with_compositor() -> (Self, Compositor) { let mut f = Self::new(); let compositor = f.compositor(); @@ -512,6 +519,39 @@ impl TestFixture { (output, self.testwl.last_created_output()) } + fn enable_fractional_scale(&mut self) -> TestObject { + self.testwl.enable_fractional_scale(); + self.run(); + self.run(); + + let mut events = std::mem::take(&mut *self.registry.data.events.lock().unwrap()); + assert_eq!( + events.len(), + 1, + "Unexpected number of global events after enabling fractional scale" + ); + let event = events.pop().unwrap(); + let Ev::::Global { + name, + interface, + version, + } = event + else { + panic!("Unexpected event: {event:?}"); + }; + + assert_eq!(interface, WpFractionalScaleManagerV1::interface().name); + let man = TestObject::::from_request( + &self.registry.obj, + Req::::Bind { + name, + id: (WpFractionalScaleManagerV1::interface(), version), + }, + ); + self.run(); + man + } + fn enable_xdg_output(&mut self) -> TestObject { self.testwl.enable_xdg_output_manager(); self.run(); @@ -755,8 +795,8 @@ impl TestFixture { assert_eq!( pos.size.as_ref().unwrap(), &testwl::Vec2 { - x: 50 / scale, - y: 50 / scale + x: (dims.width as f64 / scale) as i32, + y: (dims.height as f64 / scale) as i32 } ); @@ -765,8 +805,8 @@ impl TestFixture { pos.anchor_rect.as_ref().unwrap(), &testwl::Rect { size: testwl::Vec2 { - x: parent_win.dims.width as i32 / scale, - y: parent_win.dims.height as i32 / scale + x: (parent_win.dims.width as f64 / scale) as i32, + y: (parent_win.dims.height as f64 / scale) as i32 }, offset: testwl::Vec2::default() } @@ -774,8 +814,8 @@ impl TestFixture { assert_eq!( pos.offset, testwl::Vec2 { - x: (dims.x - parent_win.dims.x) as i32 / scale, - y: (dims.y - parent_win.dims.y) as i32 / scale + x: ((dims.x - parent_win.dims.x) as f64 / scale) as i32, + y: ((dims.y - parent_win.dims.y) as f64 / scale) as i32 } ); } @@ -1955,6 +1995,45 @@ fn scaled_output_popup() { ); } +#[test] +fn fractional_scale_popup() { + let mut f = TestFixture::new_pre_connect(|testwl| { + testwl.enable_fractional_scale(); + }); + let comp = f.compositor(); + + let toplevel = unsafe { Window::new(1) }; + let (_, toplevel_id) = f.create_toplevel(&comp, toplevel); + let surface_data = f + .testwl + .get_surface_data(toplevel_id) + .expect("No surface data"); + let fractional = surface_data + .fractional + .as_ref() + .expect("No fractional scale for surface"); + + fractional.preferred_scale(180); // 1.5 scale + f.run(); + f.run(); + + let popup = unsafe { Window::new(2) }; + let builder = PopupBuilder::new(popup, toplevel, toplevel_id) + .x(60) + .y(60) + .width(60) + .height(60) + .scale(1.5); + let initial_dims = builder.dims; + f.create_popup(&comp, builder); + f.run(); + assert_eq!( + initial_dims, + f.connection().window(popup).dims, + "X11 dimensions changed after configure" + ); +} + #[test] fn scaled_output_small_popup() { let (mut f, comp) = TestFixture::new_with_compositor(); diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index a315763..d5aed16 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -7,6 +7,10 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::time::Instant; use wayland_protocols::{ wp::{ + fractional_scale::v1::server::{ + wp_fractional_scale_manager_v1::{self, WpFractionalScaleManagerV1}, + wp_fractional_scale_v1::{self, WpFractionalScaleV1}, + }, linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, pointer_constraints::zv1::server::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, @@ -88,6 +92,7 @@ pub struct SurfaceData { pub last_damage: Option, pub role: Option, pub last_enter_serial: Option, + pub fractional: Option, } impl SurfaceData { @@ -732,6 +737,12 @@ impl Server { xdg.done(); self.display.flush_clients().unwrap(); } + + pub fn enable_fractional_scale(&mut self) { + self.dh + .create_global::(1, ()); + self.display.flush_clients().unwrap(); + } } #[derive(Clone, Eq, PartialEq, Debug)] @@ -746,6 +757,8 @@ simple_global_dispatch!(XdgWmBase); simple_global_dispatch!(ZxdgOutputManagerV1); simple_global_dispatch!(ZwpTabletManagerV2); simple_global_dispatch!(ZxdgDecorationManagerV1); +simple_global_dispatch!(WpViewporter); +simple_global_dispatch!(WpFractionalScaleManagerV1); impl Dispatch for State { fn request( @@ -1505,6 +1518,7 @@ impl Dispatch for State { last_damage: None, role: None, last_enter_serial: None, + fractional: None, }, ); state.last_surface_id = Some(SurfaceId(id)); @@ -1812,19 +1826,6 @@ impl Dispatch for State { } } -impl GlobalDispatch for State { - fn bind( - _: &mut Self, - _: &DisplayHandle, - _: &Client, - resource: wayland_server::New, - _: &(), - data_init: &mut wayland_server::DataInit<'_, Self>, - ) { - data_init.init(resource, ()); - } -} - impl Dispatch for State { fn request( _: &mut Self, @@ -1858,3 +1859,46 @@ impl Dispatch for State { //todo!() } } + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WpFractionalScaleManagerV1, + request: ::Request, + _: &(), + _: &DisplayHandle, + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wp_fractional_scale_manager_v1::Request::GetFractionalScale { id, surface } => { + let surface_id = SurfaceId(surface.id().protocol_id()); + let fractional = data_init.init(id, surface_id); + let surface_data = state.surfaces.get_mut(&surface_id).unwrap(); + surface_data.fractional = Some(fractional); + } + _ => unreachable!(), + } + } +} + +impl Dispatch for State { + fn request( + state: &mut Self, + _: &Client, + _: &WpFractionalScaleV1, + request: ::Request, + data: &SurfaceId, + _: &DisplayHandle, + _: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wp_fractional_scale_v1::Request::Destroy => { + if let Some(surface_data) = state.surfaces.get_mut(data) { + surface_data.fractional.take(); + } + } + _ => unreachable!(), + } + } +}