Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(fullscreen): honour a client's fullscreen request in-process, including one set before its first commit
A client's fullscreen request — xdg_toplevel.set_fullscreen, the cce
protocol's set_fullscreen, Xwayland's _NET_WM_STATE — only ever set
wm_scheduled.fullscreen_requested, whose one consumer posts a
zcce_window_v1 fullscreen_requested event to a window-manager client.
Nothing in the session listens for that event, so every client request was
silently dropped: the only way into Fullscreen mode was the keyed action or
a mode_rule. A state set before the first commit was doubly lost, since
wlroots records that one instead of signalling it.
WindowManager::apply_client_fullscreen(win, enter) applies the request
in-process, on a mapped window whose mode disagrees with it, by dispatching
the policy's own Fullscreen action with the window as the ActionCtx focus,
so the mode, the lock and the exit's pre_fullscreen restore are exactly the
keyed toggle's. Every request site calls it after scheduling the event as
before; handle_map reads wlroots' recorded request for the pre-commit case.
A fresh floating spawn has no box until its first acked commit is adopted,
and after a map-time entry that commit is the fullscreen one, so handle_map
seeds the box with the size the window mapped at first — otherwise leaving
fullscreen restored an output-sized window.
Verified in a shadow session on this build: a toplevel that sets fullscreen
before its first commit maps Fullscreen at the output size, the keyed
toggle exits it to its natural 400x250 and re-enters, and a plain floating
toplevel is unaffected.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/server/cce_window_management.rs | 2 ++
src/server/window_manager.rs | 25 +++++++++++++++++++++++++
src/server/xdg_toplevel.rs | 18 ++++++++++++++++++
src/server/xwayland_window.rs | 1 +
4 files changed, 46 insertions(+)
diff --git a/src/server/cce_window_management.rs b/src/server/cce_window_management.rs
index 5d43b40..3665b76 100644
--- a/src/server/cce_window_management.rs
+++ b/src/server/cce_window_management.rs
@@ -255,6 +255,7 @@ unsafe extern "C" fn toplevel_set_fullscreen(
if let Some(window) = resolve_window(server, window_key) {
(*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
(*server).wm.dirty_windowing();
+ (*server).wm.apply_client_fullscreen(window, true);
}
}
@@ -271,6 +272,7 @@ unsafe extern "C" fn toplevel_unset_fullscreen(
if let Some(window) = resolve_window(server, window_key) {
(*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Exit;
(*server).wm.dirty_windowing();
+ (*server).wm.apply_client_fullscreen(window, false);
}
}
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index fdaacad..9cfbc2c 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3662,6 +3662,31 @@ impl WindowManager {
}
}
+ /// A client asked for fullscreen, or to leave it, on one of its own windows:
+ /// xdg_toplevel.set_fullscreen, the cce protocol's set_fullscreen, Xwayland's
+ /// _NET_WM_STATE, or a state a toplevel set before its first commit (read at
+ /// map, since wlroots stores that one instead of signalling it). Applied here,
+ /// in-process, through the policy's fullscreen toggle, so the mode, the lock
+ /// and the exit's restore are exactly the keyed action's. The scheduled
+ /// zcce_window_v1 event still goes out to a window-manager client as before;
+ /// until this, that event was the request's only consumer, and nothing in the
+ /// session listens for it, so client fullscreen was silently dropped.
+ pub unsafe fn apply_client_fullscreen(&mut self, win: *mut Window, enter: bool) {
+ if win.is_null() || (*win).closed || (*win).state != crate::window::WindowState::Mapped {
+ return;
+ }
+ let is_fullscreen = (*win).tiling_mode == crate::tiling::TilingMode::Fullscreen;
+ if enter == is_fullscreen {
+ return;
+ }
+ use crate::policy::api::{Compositor, Policy, WindowId};
+ let mut ctx = self.build_action_ctx();
+ ctx.focused = Some(WindowId((*win).ref_key));
+ for cmd in crate::policy::actions::DefaultPolicy.action(&ctx, crate::config::Action::Fullscreen, None) {
+ self.apply(&cmd);
+ }
+ }
+
pub unsafe fn execute_action(&mut self, action: &crate::config::Action, command: Option<&str>) {
use crate::config::Action;
self.stop_panning_animation();
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 3afdbf6..20667aa 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -459,6 +459,22 @@ unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std
let mut new_geometry = std::mem::zeroed();
ffi::river_wlr_xdg_surface_get_geometry(base, &mut new_geometry);
(*toplevel).geometry = new_geometry;
+
+ // A fullscreen state the client set before its first commit never raises
+ // request_fullscreen (wlroots only records it); honour it now that the
+ // window is mapped. A fresh floating spawn has no box yet — the arrange
+ // pass leaves it 0x0 so the acked-commit path adopts the natural size —
+ // but that next commit will be the fullscreen one, so seed the box with
+ // the size the window mapped at, or leaving fullscreen restores an
+ // output-sized window.
+ if ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel) {
+ let window = (*toplevel).window;
+ if (*window).box_geom.width <= 0 && (*window).box_geom.height <= 0 && new_geometry.width > 0 && new_geometry.height > 0 {
+ (*window).box_geom.width = new_geometry.width;
+ (*window).box_geom.height = new_geometry.height;
+ }
+ (*(*window).server).wm.apply_client_fullscreen(window, true);
+ }
// Status segments and Utility windows are SELF-sizing: their bounds
// track their own box, so the committed geometry is adopted as the box.
let is_self_sized = matches!(
@@ -942,6 +958,8 @@ unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener,
(*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Exit;
}
(*(*window).server).wm.dirty_windowing();
+ let enter = ffi::river_wlr_xdg_toplevel_get_requested_fullscreen((*toplevel).wlr_toplevel);
+ (*(*window).server).wm.apply_client_fullscreen(window, enter);
}
unsafe extern "C" fn handle_request_maximize(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
diff --git a/src/server/xwayland_window.rs b/src/server/xwayland_window.rs
index dc80477..5f2b50f 100644
--- a/src/server/xwayland_window.rs
+++ b/src/server/xwayland_window.rs
@@ -571,6 +571,7 @@ unsafe extern "C" fn handle_request_fullscreen(listener: *mut ffi::wl_listener,
crate::window::FullscreenRequest::Exit
};
(*(*(*xwindow).window).server).wm.dirty_windowing();
+ (*(*(*xwindow).window).server).wm.apply_client_fullscreen((*xwindow).window, fullscreen);
}
unsafe extern "C" fn handle_request_minimize(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {