git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit1d90ac20d4eeac0dda83e207d4b71bd9f780f0ef
parentbccd836c36
authorLucas Galante <[email protected]>
date2026-09-06 13:14
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) {