server: auto-fullscreen windows that match an output's dimensions

Fixes #93
This commit is contained in:
Shawn Wallace 2025-02-10 22:57:38 -05:00
parent 3944c9a0e4
commit 653391c7c9
4 changed files with 141 additions and 30 deletions

View file

@ -1296,7 +1296,7 @@ impl<C: XConnection> GlobalDispatch<WlOutput, Global> for ServerState<C> {
data: &Global, data: &Global,
data_init: &mut wayland_server::DataInit<'_, Self>, data_init: &mut wayland_server::DataInit<'_, Self>,
) { ) {
state.objects.insert_with_key(|key| { let key = state.objects.insert_with_key(|key| {
let server = data_init.init(resource, key); let server = data_init.init(resource, key);
let client = state let client = state
.clientside .clientside
@ -1310,6 +1310,7 @@ impl<C: XConnection> GlobalDispatch<WlOutput, Global> for ServerState<C> {
); );
Output::new(client, server).into() Output::new(client, server).into()
}); });
state.output_keys.insert(key, ());
} }
} }
global_dispatch_with_events!(WlDrmServer, WlDrmClient); global_dispatch_with_events!(WlDrmServer, WlDrmClient);

View file

@ -128,13 +128,12 @@ impl SurfaceData {
debug!("{} entered {}", self.server.id(), output.server.id()); debug!("{} entered {}", self.server.id(), output.server.id());
let windows = &mut state.windows; let windows = &mut state.windows;
if let Some(win_data) = self.window.as_ref().and_then(|win| windows.get_mut(win)) { if let Some(win_data) = self.window.as_ref().and_then(|win| windows.get_mut(win)) {
let (x, y) = match output.position {
OutputPosition::Xdg { x, y } => (x, y),
OutputPosition::Wl { x, y } => (x, y),
};
win_data.update_output_offset( win_data.update_output_offset(
key, key,
WindowOutputOffset { x, y }, WindowOutputOffset {
x: output.dimensions.x,
y: output.dimensions.y,
},
state.connection.as_mut().unwrap(), state.connection.as_mut().unwrap(),
); );
let window = win_data.window; let window = win_data.window;
@ -656,9 +655,18 @@ pub struct XdgOutput {
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum OutputPosition { enum OutputDimensionsSource {
Wl { x: i32, y: i32 }, Wl,
Xdg { x: i32, y: i32 }, Xdg,
}
#[derive(Copy, Clone)]
pub(super) struct OutputDimensions {
source: OutputDimensionsSource,
x: i32,
y: i32,
pub width: i32,
pub height: i32,
} }
pub struct Output { pub struct Output {
@ -666,7 +674,7 @@ pub struct Output {
pub server: WlOutput, pub server: WlOutput,
pub xdg: Option<XdgOutput>, pub xdg: Option<XdgOutput>,
windows: HashSet<x::Window>, windows: HashSet<x::Window>,
position: OutputPosition, pub(super) dimensions: OutputDimensions,
name: String, name: String,
} }
@ -677,7 +685,13 @@ impl Output {
server, server,
xdg: None, xdg: None,
windows: HashSet::new(), windows: HashSet::new(),
position: OutputPosition::Wl { x: 0, y: 0 }, dimensions: OutputDimensions {
source: OutputDimensionsSource::Wl,
x: 0,
y: 0,
width: 0,
height: 0,
},
name: "<unknown>".to_string(), name: "<unknown>".to_string(),
} }
} }
@ -715,19 +729,23 @@ impl HandleEvent for Output {
impl Output { impl Output {
fn update_offset<C: XConnection>( fn update_offset<C: XConnection>(
&mut self, &mut self,
offset: OutputPosition, source: OutputDimensionsSource,
x: i32,
y: i32,
state: &mut ServerState<C>, state: &mut ServerState<C>,
) { ) {
if matches!(offset, OutputPosition::Wl { .. }) if matches!(source, OutputDimensionsSource::Wl)
&& matches!(self.position, OutputPosition::Xdg { .. }) && matches!(self.dimensions.source, OutputDimensionsSource::Xdg)
{ {
return; return;
} }
self.position = offset; self.dimensions.source = source;
let (x, y, id) = match offset { self.dimensions.x = x;
OutputPosition::Xdg { x, y } => (x, y, self.xdg.as_ref().unwrap().server.id()), self.dimensions.y = y;
OutputPosition::Wl { x, y } => (x, y, self.server.id()), let id = match source {
OutputDimensionsSource::Xdg => self.xdg.as_ref().unwrap().server.id(),
OutputDimensionsSource::Wl => self.server.id(),
}; };
debug!("moving {id} to {x}x{y}"); debug!("moving {id} to {x}x{y}");
self.windows.retain(|window| { self.windows.retain(|window| {
@ -750,7 +768,15 @@ impl Output {
state: &mut ServerState<C>, state: &mut ServerState<C>,
) { ) {
if let client::wl_output::Event::Geometry { x, y, .. } = event { if let client::wl_output::Event::Geometry { x, y, .. } = event {
self.update_offset(OutputPosition::Wl { x, y }, state); self.update_offset(OutputDimensionsSource::Wl, x, y, state);
}
if let client::wl_output::Event::Mode { width, height, .. } = event {
if matches!(self.dimensions.source, OutputDimensionsSource::Wl) {
self.dimensions.width = width;
self.dimensions.height = height;
debug!("{} dimensions: {width}x{height} (wl)", self.server.id());
}
} }
simple_event_shunt! { simple_event_shunt! {
@ -790,9 +816,15 @@ impl Output {
state: &mut ServerState<C>, state: &mut ServerState<C>,
) { ) {
if let zxdg_output_v1::Event::LogicalPosition { x, y } = event { if let zxdg_output_v1::Event::LogicalPosition { x, y } = event {
self.update_offset(OutputPosition::Xdg { x, y }, state); self.update_offset(OutputDimensionsSource::Xdg, x, y, state);
} }
let xdg = &self.xdg.as_ref().unwrap().server; let xdg = &self.xdg.as_ref().unwrap().server;
if let zxdg_output_v1::Event::LogicalSize { width, height } = event {
self.dimensions.source = OutputDimensionsSource::Xdg;
self.dimensions.width = width;
self.dimensions.height = height;
debug!("{} dimensions: {width}x{height} (xdg)", self.server.id());
}
simple_event_shunt! { simple_event_shunt! {
xdg, event: zxdg_output_v1::Event => [ xdg, event: zxdg_output_v1::Event => [
LogicalPosition { x, y }, LogicalPosition { x, y },

View file

@ -480,6 +480,7 @@ pub struct ServerState<C: XConnection> {
clientside: ClientState, clientside: ClientState,
objects: ObjectMap, objects: ObjectMap,
associated_windows: SparseSecondaryMap<ObjectKey, x::Window>, associated_windows: SparseSecondaryMap<ObjectKey, x::Window>,
output_keys: SparseSecondaryMap<ObjectKey, ()>,
windows: HashMap<x::Window, WindowData>, windows: HashMap<x::Window, WindowData>,
qh: ClientQueueHandle, qh: ClientQueueHandle,
@ -539,6 +540,7 @@ impl<C: XConnection> ServerState<C> {
last_hovered: None, last_hovered: None,
connection: None, connection: None,
objects: Default::default(), objects: Default::default(),
output_keys: Default::default(),
associated_windows: Default::default(), associated_windows: Default::default(),
xdg_wm_base, xdg_wm_base,
clipboard_data, clipboard_data,
@ -922,15 +924,20 @@ impl<C: XConnection> ServerState<C> {
} }
fn create_role_window(&mut self, window: x::Window, surface_key: ObjectKey) { fn create_role_window(&mut self, window: x::Window, surface_key: ObjectKey) {
let surface: &mut SurfaceData = self.objects[surface_key].as_mut(); // Temporarily remove surface to placate borrow checker
let mut surface: SurfaceData = self.objects[surface_key]
.0
.take()
.unwrap()
.try_into()
.unwrap();
surface.window = Some(window); surface.window = Some(window);
let client = &surface.client; surface.client.attach(None, 0, 0);
client.attach(None, 0, 0); surface.client.commit();
client.commit();
let xdg_surface = self let xdg_surface = self
.xdg_wm_base .xdg_wm_base
.get_xdg_surface(client, &self.qh, surface_key); .get_xdg_surface(&surface.client, &self.qh, surface_key);
let window_data = self.windows.get_mut(&window).unwrap(); let window_data = self.windows.get_mut(&window).unwrap();
if window_data.attrs.override_redirect { if window_data.attrs.override_redirect {
@ -941,15 +948,27 @@ impl<C: XConnection> ServerState<C> {
window_data.attrs.popup_for = Some(win); window_data.attrs.popup_for = Some(win);
} }
} }
let mut fullscreen = false;
let (width, height) = (window_data.attrs.dims.width, window_data.attrs.dims.height);
for (key, _) in &self.output_keys {
let output: &Output = self.objects[key].as_ref();
if output.dimensions.width == width as i32 && output.dimensions.height == height as i32
{
fullscreen = true;
window_data.attrs.popup_for = None;
}
}
let surface_id = surface.client.id();
// Reinsert surface
self.objects[surface_key].0 = Some(surface.into());
let window = self.windows.get(&window).unwrap(); let window = self.windows.get(&window).unwrap();
let role = if let Some(parent) = window.attrs.popup_for { let role = if let Some(parent) = window.attrs.popup_for {
debug!( debug!(
"creating popup ({:?}) {:?} {:?} {:?} {surface_key:?}", "creating popup ({:?}) {:?} {:?} {:?} {surface_key:?}",
window.window, window.window, parent, window.attrs.dims, surface_id
parent,
window.attrs.dims,
client.id()
); );
let parent_window = self.windows.get(&parent).unwrap(); let parent_window = self.windows.get(&parent).unwrap();
@ -988,7 +1007,7 @@ impl<C: XConnection> ServerState<C> {
}; };
SurfaceRole::Popup(Some(popup)) SurfaceRole::Popup(Some(popup))
} else { } else {
let data = self.create_toplevel(window, surface_key, xdg_surface); let data = self.create_toplevel(window, surface_key, xdg_surface, fullscreen);
SurfaceRole::Toplevel(Some(data)) SurfaceRole::Toplevel(Some(data))
}; };
@ -1013,6 +1032,7 @@ impl<C: XConnection> ServerState<C> {
window: &WindowData, window: &WindowData,
surface_key: ObjectKey, surface_key: ObjectKey,
xdg: XdgSurface, xdg: XdgSurface,
fullscreen: bool,
) -> ToplevelData { ) -> ToplevelData {
debug!("creating toplevel for {:?}", window.window); debug!("creating toplevel for {:?}", window.window);
@ -1044,6 +1064,10 @@ impl<C: XConnection> ServerState<C> {
toplevel.set_title(title.name().to_string()); toplevel.set_title(title.name().to_string());
} }
if fullscreen {
toplevel.set_fullscreen(None);
}
ToplevelData { ToplevelData {
xdg: XdgSurfaceData { xdg: XdgSurfaceData {
surface: xdg, surface: xdg,

View file

@ -1651,6 +1651,60 @@ fn tablet_smoke_test() {
], ],
) )
} }
#[test]
fn fullscreen_heuristic() {
let (mut f, comp) = TestFixture::new_with_compositor();
let (_, output) = f.new_output(0, 0);
let window1 = unsafe { Window::new(1) };
let (_, id) = f.create_toplevel(&comp, window1);
f.testwl.move_surface_to_output(id, &output);
f.run();
let mut check_fullscreen = |id, override_redirect| {
let window = unsafe { Window::new(id) };
let (buffer, surface) = comp.create_surface();
let data = WindowData {
mapped: true,
dims: WindowDims {
x: 0,
y: 0,
// Outputs default to 1000x1000 in testwl
width: 1000,
height: 1000,
},
fullscreen: false,
};
f.new_window(window, override_redirect, data, None);
f.map_window(&comp, window, &surface.obj, &buffer);
f.run();
let id = f.check_new_surface();
let surface_data = f.testwl.get_surface_data(id).unwrap();
assert!(
surface_data.surface
== f.testwl
.get_object::<s_proto::wl_surface::WlSurface>(id)
.unwrap()
);
let Some(testwl::SurfaceRole::Toplevel(toplevel_data)) = &surface_data.role else {
panic!("Expected toplevel, got {:?}", surface_data.role);
};
assert!(
toplevel_data
.states
.contains(&xdg_toplevel::State::Fullscreen),
"states: {:?}",
toplevel_data.states
);
};
check_fullscreen(2, false);
check_fullscreen(3, true);
}
/// See Pointer::handle_event for an explanation. /// See Pointer::handle_event for an explanation.
#[test] #[test]
fn popup_pointer_motion_workaround() {} fn popup_pointer_motion_workaround() {}