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