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.
This commit is contained in:
En-En 2026-02-04 01:19:49 +00:00 committed by GitHub
parent 75c9f5e775
commit 0947c4685f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 153 additions and 33 deletions

View file

@ -1,6 +1,6 @@
use super::decoration::DecorationMarker; use super::decoration::DecorationMarker;
use super::ObjectEvent; use super::{GlobalName, ObjectEvent};
use hecs::{Entity, World}; use hecs::{Entity, World};
use smithay_client_toolkit::{ use smithay_client_toolkit::{
activation::{ActivationHandler, RequestData, RequestDataExt}, activation::{ActivationHandler, RequestData, RequestDataExt},
@ -115,6 +115,7 @@ pub(super) struct MyWorld {
pub world: World, pub world: World,
pub global_list: GlobalList, pub global_list: GlobalList,
pub new_globals: Vec<Global>, pub new_globals: Vec<Global>,
pub removed_globals: Vec<GlobalName>,
events: Vec<(Entity, ObjectEvent)>, events: Vec<(Entity, ObjectEvent)>,
queued_events: Vec<mpsc::Receiver<(Entity, ObjectEvent)>>, queued_events: Vec<mpsc::Receiver<(Entity, ObjectEvent)>>,
pub clipboard: SelectionEvents<SelectionOffer>, pub clipboard: SelectionEvents<SelectionOffer>,
@ -128,6 +129,7 @@ impl MyWorld {
world: World::new(), world: World::new(),
global_list, global_list,
new_globals: Vec::new(), new_globals: Vec::new(),
removed_globals: Vec::new(),
events: Vec::new(), events: Vec::new(),
queued_events: Vec::new(), queued_events: Vec::new(),
clipboard: Default::default(), clipboard: Default::default(),
@ -204,18 +206,23 @@ impl Dispatch<WlRegistry, GlobalListContents> for MyWorld {
_: &wayland_client::Connection, _: &wayland_client::Connection,
_: &wayland_client::QueueHandle<Self>, _: &wayland_client::QueueHandle<Self>,
) { ) {
if let Event::<WlRegistry>::Global { match event {
Event::<WlRegistry>::Global {
name, name,
interface, interface,
version, version,
} = event } => {
{
state.new_globals.push(Global { state.new_globals.push(Global {
name, name,
interface, interface,
version, version,
}); });
}; }
Event::<WlRegistry>::GlobalRemove { name } => {
state.removed_globals.push(GlobalName(name));
}
_ => {}
}
} }
} }

View file

@ -1479,6 +1479,7 @@ impl<S: X11Selection> GlobalDispatch<WlOutput, Global> for InnerServerState<S> {
client, client,
event::OutputScaleFactor::Output(1), event::OutputScaleFactor::Output(1),
event::OutputDimensions::default(), event::OutputDimensions::default(),
GlobalName(data.name),
), ),
); );
state.updated_outputs.push(entity); state.updated_outputs.push(entity);

View file

@ -65,6 +65,7 @@ use wayland_protocols::{
use wayland_server::protocol::wl_seat::WlSeat; use wayland_server::protocol::wl_seat::WlSeat;
use wayland_server::{ use wayland_server::{
Client, DisplayHandle, Resource, WEnum, Client, DisplayHandle, Resource, WEnum,
backend::GlobalId,
protocol::{ protocol::{
wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm, wl_callback::WlCallback, wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::WlShm,
wl_surface::WlSurface, 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<GlobalName, (Global, GlobalId)>,
dh: &DisplayHandle, dh: &DisplayHandle,
globals: impl IntoIterator<Item = &'a Global>, globals: impl IntoIterator<Item = &'a Global>,
) { ) {
@ -353,7 +355,8 @@ fn handle_globals<'a, S: X11Selection + 'static>(
$( $(
ref x if x == <$global>::interface().name => { ref x if x == <$global>::interface().name => {
let version = u32::min(global.version, <$global>::interface().version); let version = u32::min(global.version, <$global>::interface().version);
dh.create_global::<InnerServerState<S>, $global, Global>(version, global.clone()); let global_id = dh.create_global::<InnerServerState<S>, $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 { struct FocusData {
window: x::Window, window: x::Window,
output_name: Option<String>, output_name: Option<String>,
@ -446,6 +452,7 @@ pub struct InnerServerState<S: X11Selection> {
world: MyWorld, world: MyWorld,
queue: EventQueue<MyWorld>, queue: EventQueue<MyWorld>,
qh: QueueHandle<MyWorld>, qh: QueueHandle<MyWorld>,
globals_map: HashMap<GlobalName, (Global, GlobalId)>,
client: Client, client: Client,
to_focus: Option<FocusData>, to_focus: Option<FocusData>,
unfocus: bool, unfocus: bool,
@ -532,9 +539,10 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
dh.create_global::<InnerServerState<S>, XwaylandShellV1, _>(1, ()); dh.create_global::<InnerServerState<S>, XwaylandShellV1, _>(1, ());
let mut globals_map = HashMap::new();
global_list global_list
.contents() .contents()
.with_list(|globals| handle_globals::<S>(&dh, globals)); .with_list(|globals| handle_new_globals::<S>(&mut globals_map, &dh, globals));
let world = MyWorld::new(global_list); let world = MyWorld::new(global_list);
let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap(); let client = dh.insert_client(client, std::sync::Arc::new(())).unwrap();
@ -545,6 +553,7 @@ impl<S: X11Selection> ServerState<NoConnection<S>> {
client, client,
queue, queue,
qh, qh,
globals_map,
dh, dh,
to_focus: None, to_focus: None,
unfocus: false, unfocus: false,
@ -613,7 +622,7 @@ impl<C: XConnection> ServerState<C> {
} }
pub fn handle_clientside_events(&mut self) { pub fn handle_clientside_events(&mut self) {
self.handle_new_globals(); self.handle_globals();
for (target, event) in self.world.read_events() { for (target, event) in self.world.read_events() {
if !self.world.contains(target) { if !self.world.contains(target) {
@ -659,8 +668,10 @@ impl<C: XConnection> ServerState<C> {
} }
if !self.updated_outputs.is_empty() { if !self.updated_outputs.is_empty() {
for output in self.updated_outputs.iter() { for output in std::mem::take(&mut self.updated_outputs).iter() {
let output_scale = self.world.get::<&OutputScaleFactor>(*output).unwrap(); let Ok(output_scale) = self.world.get::<&OutputScaleFactor>(*output) else {
continue;
};
if matches!(*output_scale, OutputScaleFactor::Output(..)) { if matches!(*output_scale, OutputScaleFactor::Output(..)) {
let mut surface_query = self let mut surface_query = self
.world .world
@ -684,7 +695,6 @@ impl<C: XConnection> ServerState<C> {
} }
} }
} }
self.updated_outputs.clear();
let mut mixed_scale = false; let mut mixed_scale = false;
let mut scale; let mut scale;
@ -780,9 +790,33 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
self.queue.as_fd() 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); let globals = std::mem::take(&mut self.world.new_globals);
handle_globals::<S>(&self.dh, globals.iter()); handle_new_globals::<S>(&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::<Vec<_>>();
for global in globals {
let (global_struct, global_id) = self.globals_map.remove(&global).unwrap();
self.dh.disable_global::<InnerServerState<S>>(global_id);
if global_struct.interface == <WlOutput>::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( pub fn new_window(

View file

@ -530,7 +530,19 @@ impl<C: XConnection> TestFixture<C> {
); );
self.run(); self.run();
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::<WlRegistry>::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] #[test]
fn client_side_decorations() { fn client_side_decorations() {
let (mut f, compositor) = TestFixture::new_with_compositor(); let (mut f, compositor) = TestFixture::new_with_compositor();

View file

@ -319,7 +319,7 @@ impl Fixture {
fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput { fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput {
self.testwl.new_output(x, y); self.testwl.new_output(x, y);
self.wait_and_dispatch(); 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); let mut connection = Connection::new(&f.display);
f.testwl.enable_xdg_output_manager(); 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 window = connection.new_window(connection.root, 0, 0, 20, 20, false);
let surface = f.map_as_toplevel(&mut connection, window); let surface = f.map_as_toplevel(&mut connection, window);

View file

@ -233,6 +233,7 @@ struct DataSourceData {
struct Output { struct Output {
name: String, name: String,
xdg: Option<ZxdgOutputV1>, xdg: Option<ZxdgOutputV1>,
global_id: Option<GlobalId>,
} }
struct KeyboardState { struct KeyboardState {
@ -266,6 +267,8 @@ struct State {
last_surface_id: Option<SurfaceId>, last_surface_id: Option<SurfaceId>,
created_surfaces: Vec<SurfaceId>, created_surfaces: Vec<SurfaceId>,
last_output: Option<WlOutput>, last_output: Option<WlOutput>,
last_output_global: Option<GlobalId>,
output_counter: u32,
callbacks: Vec<WlCallback>, callbacks: Vec<WlCallback>,
seat: Option<WlSeat>, seat: Option<WlSeat>,
pointer: Option<PointerState>, pointer: Option<PointerState>,
@ -296,6 +299,8 @@ impl Default for State {
begin: Instant::now(), begin: Instant::now(),
last_surface_id: None, last_surface_id: None,
last_output: None, last_output: None,
last_output_global: None,
output_counter: 0,
callbacks: Vec::new(), callbacks: Vec::new(),
seat: None, seat: None,
pointer: None, pointer: None,
@ -572,13 +577,15 @@ impl Server {
&self.state.created_surfaces &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] #[track_caller]
pub fn last_created_output(&self) -> WlOutput { pub fn finalize_output(&mut self) -> WlOutput {
self.state let output_s = self.state.last_output.take().expect("No new outputs");
.last_output let output_data = self.state.outputs.get_mut(&output_s).unwrap();
.as_ref() output_data.global_id = self.state.last_output_global.take();
.expect("No outputs created!") output_s
.clone()
} }
pub fn get_object<T: Resource + 'static>( pub fn get_object<T: Resource + 'static>(
@ -845,7 +852,8 @@ impl Server {
} }
pub fn new_output(&mut self, x: i32, y: i32) { pub fn new_output(&mut self, x: i32, y: i32) {
self.dh.create_global::<State, WlOutput, _>(4, (x, y)); self.state.last_output_global =
Some(self.dh.create_global::<State, WlOutput, _>(4, (x, y)));
self.display.flush_clients().unwrap(); self.display.flush_clients().unwrap();
} }
@ -877,6 +885,12 @@ impl Server {
self.display.flush_clients().unwrap(); 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::<State>(output.global_id.unwrap());
self.display.flush_clients().unwrap();
}
pub fn enable_xdg_output_manager(&mut self) { pub fn enable_xdg_output_manager(&mut self) {
self.dh self.dh
.create_global::<State, ZxdgOutputManagerV1, _>(3, ()); .create_global::<State, ZxdgOutputManagerV1, _>(3, ());
@ -1126,13 +1140,19 @@ impl GlobalDispatch<WlOutput, (i32, i32)> for State {
"fake monitor".to_string(), "fake monitor".to_string(),
wl_output::Transform::Normal, 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.name(name.clone());
output.mode(wl_output::Mode::Current, 1000, 1000, 0); output.mode(wl_output::Mode::Current, 1000, 1000, 0);
output.done(); output.done();
state state.outputs.insert(
.outputs output.clone(),
.insert(output.clone(), Output { name, xdg: None }); Output {
name,
xdg: None,
global_id: None,
},
);
state.last_output = Some(output); state.last_output = Some(output);
} }
} }