From 0947c4685f6237d4f8045482ce0c62feab40b6c4 Mon Sep 17 00:00:00 2001 From: En-En <39373446+En-En-Code@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:19:49 +0000 Subject: [PATCH] feat: handle global removals, recalc output scale (#367) All `GlobalRemove` events sent from the server are now handled by recording them in a new clientside `vec` and passing the identifier returned by `create_global` (now stored by a map in the state) to `disable_global`. `handle_globals` (the top-level function) and `handle_new_globals` (the `InnerServerState` member function) have swapped names to better represent their new purposes. This enables action to be taken when globals are removed. In this case, the desired action is to forward output removal, so that the scaling calculation does not account for disconnected monitors in its logic. Resolves #351. --- src/server/clientside.rs | 27 +++++++++++------- src/server/dispatch.rs | 1 + src/server/mod.rs | 52 ++++++++++++++++++++++++++++------ src/server/tests.rs | 60 +++++++++++++++++++++++++++++++++++++++- tests/integration.rs | 4 +-- testwl/src/lib.rs | 42 ++++++++++++++++++++-------- 6 files changed, 153 insertions(+), 33 deletions(-) diff --git a/src/server/clientside.rs b/src/server/clientside.rs index 05c8cbe..664c909 100644 --- a/src/server/clientside.rs +++ b/src/server/clientside.rs @@ -1,6 +1,6 @@ use super::decoration::DecorationMarker; -use super::ObjectEvent; +use super::{GlobalName, ObjectEvent}; use hecs::{Entity, World}; use smithay_client_toolkit::{ activation::{ActivationHandler, RequestData, RequestDataExt}, @@ -115,6 +115,7 @@ pub(super) struct MyWorld { pub world: World, pub global_list: GlobalList, pub new_globals: Vec, + pub removed_globals: Vec, events: Vec<(Entity, ObjectEvent)>, queued_events: Vec>, pub clipboard: SelectionEvents, @@ -128,6 +129,7 @@ impl MyWorld { world: World::new(), global_list, new_globals: Vec::new(), + removed_globals: Vec::new(), events: Vec::new(), queued_events: Vec::new(), clipboard: Default::default(), @@ -204,18 +206,23 @@ impl Dispatch for MyWorld { _: &wayland_client::Connection, _: &wayland_client::QueueHandle, ) { - if let Event::::Global { - name, - interface, - version, - } = event - { - state.new_globals.push(Global { + match event { + Event::::Global { name, interface, version, - }); - }; + } => { + state.new_globals.push(Global { + name, + interface, + version, + }); + } + Event::::GlobalRemove { name } => { + state.removed_globals.push(GlobalName(name)); + } + _ => {} + } } } diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs index 21acf44..9704f25 100644 --- a/src/server/dispatch.rs +++ b/src/server/dispatch.rs @@ -1479,6 +1479,7 @@ impl GlobalDispatch for InnerServerState { client, event::OutputScaleFactor::Output(1), event::OutputDimensions::default(), + GlobalName(data.name), ), ); state.updated_outputs.push(entity); diff --git a/src/server/mod.rs b/src/server/mod.rs index a110fab..662d1a9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -65,6 +65,7 @@ use wayland_protocols::{ use wayland_server::protocol::wl_seat::WlSeat; use wayland_server::{ Client, DisplayHandle, Resource, WEnum, + backend::GlobalId, protocol::{ wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm, wl_surface::WlSurface, @@ -342,7 +343,8 @@ enum ObjectEvent { } } -fn handle_globals<'a, S: X11Selection + 'static>( +fn handle_new_globals<'a, S: X11Selection + 'static>( + globals_map: &mut HashMap, dh: &DisplayHandle, globals: impl IntoIterator, ) { @@ -353,7 +355,8 @@ fn handle_globals<'a, S: X11Selection + 'static>( $( ref x if x == <$global>::interface().name => { let version = u32::min(global.version, <$global>::interface().version); - dh.create_global::, $global, Global>(version, global.clone()); + let global_id = dh.create_global::, $global, Global>(version, global.clone()); + globals_map.insert(GlobalName(global.name), (global.clone(), global_id)); } )+ _ => {} @@ -377,6 +380,9 @@ fn handle_globals<'a, S: X11Selection + 'static>( } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(super) struct GlobalName(pub u32); + struct FocusData { window: x::Window, output_name: Option, @@ -446,6 +452,7 @@ pub struct InnerServerState { world: MyWorld, queue: EventQueue, qh: QueueHandle, + globals_map: HashMap, client: Client, to_focus: Option, unfocus: bool, @@ -532,9 +539,10 @@ impl ServerState> { dh.create_global::, XwaylandShellV1, _>(1, ()); + let mut globals_map = HashMap::new(); global_list .contents() - .with_list(|globals| handle_globals::(&dh, globals)); + .with_list(|globals| handle_new_globals::(&mut globals_map, &dh, globals)); let world = MyWorld::new(global_list); let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap(); @@ -545,6 +553,7 @@ impl ServerState> { client, queue, qh, + globals_map, dh, to_focus: None, unfocus: false, @@ -613,7 +622,7 @@ impl ServerState { } pub fn handle_clientside_events(&mut self) { - self.handle_new_globals(); + self.handle_globals(); for (target, event) in self.world.read_events() { if !self.world.contains(target) { @@ -659,8 +668,10 @@ impl ServerState { } if !self.updated_outputs.is_empty() { - for output in self.updated_outputs.iter() { - let output_scale = self.world.get::<&OutputScaleFactor>(*output).unwrap(); + for output in std::mem::take(&mut self.updated_outputs).iter() { + let Ok(output_scale) = self.world.get::<&OutputScaleFactor>(*output) else { + continue; + }; if matches!(*output_scale, OutputScaleFactor::Output(..)) { let mut surface_query = self .world @@ -684,7 +695,6 @@ impl ServerState { } } } - self.updated_outputs.clear(); let mut mixed_scale = false; let mut scale; @@ -780,9 +790,33 @@ impl InnerServerState { self.queue.as_fd() } - fn handle_new_globals(&mut self) { + fn handle_globals(&mut self) { let globals = std::mem::take(&mut self.world.new_globals); - handle_globals::(&self.dh, globals.iter()); + handle_new_globals::(&mut self.globals_map, &self.dh, &globals); + + let globals = std::mem::take(&mut self.world.removed_globals); + if globals.is_empty() { + return; + } + let query = self + .world + .query_mut::<(&WlOutput, &GlobalName)>() + .into_iter() + .map(|(e, (_, name))| (e, *name)) + .collect::>(); + for global in globals { + let (global_struct, global_id) = self.globals_map.remove(&global).unwrap(); + self.dh.disable_global::>(global_id); + if global_struct.interface == ::interface().name { + for (entity, name) in query.iter() { + if *name == global { + self.updated_outputs.push(*entity); + self.world.despawn(*entity).unwrap(); + break; + } + } + } + } } pub fn new_window( diff --git a/src/server/tests.rs b/src/server/tests.rs index da176cb..8f398e6 100644 --- a/src/server/tests.rs +++ b/src/server/tests.rs @@ -530,7 +530,19 @@ impl TestFixture { ); self.run(); self.run(); - (output, self.testwl.last_created_output()) + (output, self.testwl.finalize_output()) + } + + fn remove_output(&mut self, output_s: wayland_server::protocol::wl_output::WlOutput) { + self.testwl.remove_output(output_s); + self.run(); + self.run(); + let mut events = std::mem::take(&mut *self.registry.data.events.lock().unwrap()); + assert_eq!(events.len(), 1); + let event = events.pop().unwrap(); + let Ev::::GlobalRemove { .. } = event else { + panic!("Unexpected event: {event:?}"); + }; } } @@ -2725,6 +2737,52 @@ fn scaled_pointer_lock_position_hint() { ); } +#[test] +fn disconnected_output_rescaling() { + let mut f = TestFixture::new_pre_connect(|testwl| { + testwl.enable_fractional_scale(); + }); + let comp = f.compositor(); + let (_, output_main) = f.new_output(0, 0); + let (_, output_ext) = f.new_output(1000, 0); + + let window = Window::new(1); + let (_, id) = f.create_toplevel(&comp, window); + + let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); + let fractional = surface_data + .fractional + .as_ref() + .expect("No fractional scale for surface"); + fractional.preferred_scale(240); // 2.0 scale + f.testwl.move_surface_to_output(id, &output_main); + f.run(); + + let surface_data = f.testwl.get_surface_data(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.testwl.move_surface_to_output(id, &output_ext); + f.run(); + // Multiple monitors with different scaling will select the lowest scale across monitors + assert_eq!(f.satellite.inner.new_scale, Some(1.5)); + + f.remove_output(output_ext); + f.testwl.move_surface_to_output(id, &output_main); + let surface_data = f.testwl.get_surface_data(id).expect("No surface data"); + let fractional = surface_data + .fractional + .as_ref() + .expect("No fractional scale for surface"); + fractional.preferred_scale(240); // 2.0 scale + f.run(); + f.run(); + // Afteer the output is disconnected, only the 2x scale output remains, so use that scale + assert_eq!(f.satellite.inner.new_scale, Some(2.0)); +} + #[test] fn client_side_decorations() { let (mut f, compositor) = TestFixture::new_with_compositor(); diff --git a/tests/integration.rs b/tests/integration.rs index 06615bd..f7bbdf1 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -319,7 +319,7 @@ impl Fixture { fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput { self.testwl.new_output(x, y); self.wait_and_dispatch(); - self.testwl.last_created_output() + self.testwl.finalize_output() } } @@ -2233,7 +2233,7 @@ fn xsettings_fractional_scale() { let mut connection = Connection::new(&f.display); f.testwl.enable_xdg_output_manager(); - let output = f.testwl.last_created_output(); + let output = f.testwl.finalize_output(); let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let surface = f.map_as_toplevel(&mut connection, window); diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 2304415..f8d850f 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -233,6 +233,7 @@ struct DataSourceData { struct Output { name: String, xdg: Option, + global_id: Option, } struct KeyboardState { @@ -266,6 +267,8 @@ struct State { last_surface_id: Option, created_surfaces: Vec, last_output: Option, + last_output_global: Option, + output_counter: u32, callbacks: Vec, seat: Option, pointer: Option, @@ -296,6 +299,8 @@ impl Default for State { begin: Instant::now(), last_surface_id: None, last_output: None, + last_output_global: None, + output_counter: 0, callbacks: Vec::new(), seat: None, pointer: None, @@ -572,13 +577,15 @@ impl Server { &self.state.created_surfaces } + /// Finish the initialization of an output created by `new_output`. + /// This function must be called after the globals have been dispatched in order to use the + /// output on the server side created by `new_output` (this function's return value). #[track_caller] - pub fn last_created_output(&self) -> WlOutput { - self.state - .last_output - .as_ref() - .expect("No outputs created!") - .clone() + pub fn finalize_output(&mut self) -> WlOutput { + let output_s = self.state.last_output.take().expect("No new outputs"); + let output_data = self.state.outputs.get_mut(&output_s).unwrap(); + output_data.global_id = self.state.last_output_global.take(); + output_s } pub fn get_object( @@ -845,7 +852,8 @@ impl Server { } pub fn new_output(&mut self, x: i32, y: i32) { - self.dh.create_global::(4, (x, y)); + self.state.last_output_global = + Some(self.dh.create_global::(4, (x, y))); self.display.flush_clients().unwrap(); } @@ -877,6 +885,12 @@ impl Server { self.display.flush_clients().unwrap(); } + pub fn remove_output(&mut self, output: WlOutput) { + let output = self.state.outputs.remove(&output).unwrap(); + self.dh.remove_global::(output.global_id.unwrap()); + self.display.flush_clients().unwrap(); + } + pub fn enable_xdg_output_manager(&mut self) { self.dh .create_global::(3, ()); @@ -1126,13 +1140,19 @@ impl GlobalDispatch for State { "fake monitor".to_string(), wl_output::Transform::Normal, ); - let name = format!("WL-{}", state.outputs.len() + 1); + state.output_counter += 1; + let name = format!("WL-{}", state.output_counter); output.name(name.clone()); output.mode(wl_output::Mode::Current, 1000, 1000, 0); output.done(); - state - .outputs - .insert(output.clone(), Output { name, xdg: None }); + state.outputs.insert( + output.clone(), + Output { + name, + xdg: None, + global_id: None, + }, + ); state.last_output = Some(output); } }