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

commit7bc8de85663d7cbb9488c5416c3f041805f1c2c1
parent172006d880
authorLucas Galante <[email protected]>
date2026-07-18 12:58
fix: track a self-sizing overlay's border with its live geometry

An Overlay window that resizes itself (cce-cloud, which auto-sizes per
keystroke) left its border a size behind. Two defects, each masking the
other:

- The client-initiated branch in handle_commit never runs for it.
  size_changed compares new_geometry against (*toplevel).geometry, which
  already holds the new value by then, so it is never true — confirmed by
  tracing: zero "client initiated size change" hits across three sessions
  while geometry changed on nearly every commit.
- render_finish resets box_geom from rendering_sent, the render-start
  snapshot, which still holds the previous size and dragged the border
  back after anything else had corrected it.

Overlay commits now sync box_geom from the live surface geometry and draw
the border in the same commit that puts the new buffer on screen, without
depending on size_changed. Waiting for the WM cycle isn't an option here —
it round-trips out to the external window-manager client. A self_resized
flag stops render_finish from undoing it, mirroring how resize_edges
already gates that same sync for interactive resizes.

set_dimensions_hint also now marks windowing dirty for Overlay, so an
arrange pass runs to reconcile placement (an overlay's position is a
function of its size).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/server/window.rs       | 16 ++++++++++++++--
 src/server/xdg_toplevel.rs | 33 +++++++++++++++++++++++++++++++--
 2 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index da0cacb..753b18a 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -285,6 +285,11 @@ pub struct Window {
     pub resize_start_w: u32,
     pub resize_start_h: u32,
     pub resize_edges: Option<Edges>,
+    /// The client resized itself and the new-size buffer is already on screen, so
+    /// `render_finish` must take the size from the live commit rather than the
+    /// render-start snapshot (`rendering_sent`), which still holds the previous
+    /// size and would snap the border back. Cleared once consumed.
+    pub self_resized: bool,
     pub commit: ffi::wl_listener,
     pub was_fullscreen: bool,
     pub saved_width: i32,
@@ -464,6 +469,7 @@ impl Window {
             resize_start_w: 0,
             resize_start_h: 0,
             resize_edges: None,
+            self_resized: false,
             commit: std::mem::zeroed(),
             was_fullscreen: false,
             saved_width: 0,
@@ -1022,7 +1028,9 @@ impl Window {
     pub unsafe fn set_dimensions_hint(&mut self, hint: DimensionsHint) {
         self.wm_scheduled.dimensions_hint = hint;
         if self.wm_sent.dimensions_hint != hint {
-            if matches!(self.tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Status) {
+            // Overlay included: a self-sizing overlay (cce-cloud) changes its hint
+            // on every resize, and skipping it meant no arrange pass was scheduled.
+            if matches!(self.tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Status | crate::tiling::TilingMode::Overlay) {
                 (*self.server).wm.dirty_windowing();
             }
             self.wm_sent.dimensions_hint = hint;
@@ -1865,8 +1873,11 @@ impl Window {
         // updated by the commit handler) always tracks the newest commit.
         // Pairing it with the older snapshot size clips the surface short
         // and makes the anchored edge bounce every cycle.
+        // self_resized: same reasoning, for a client that resized itself without a
+        // configure — its newest buffer is already on screen, so rendering_sent is
+        // behind and would drag the border back to the previous size.
         let mut resize_synced = false;
-        if self.resize_edges.is_some() {
+        if self.resize_edges.is_some() || self.self_resized {
             if let WindowImpl::Toplevel(toplevel) = self.impl_type {
                 if !toplevel.is_null() {
                     self.box_geom.width = (*toplevel).geometry.width;
@@ -1883,6 +1894,7 @@ impl Window {
                 self.box_geom.height = self.rendering_sent.height as i32;
             }
         }
+        self.self_resized = false;
 
         let mut clip = requested.clip;
         let mut content_clip = requested.content_clip;
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index e0b1ca6..ca39871 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -503,6 +503,30 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
         return;
     }
 
+    // A self-sizing overlay (cce-cloud) repaints at a new size on its own, with no
+    // configure round trip. The size-change branches below can't catch it: they
+    // compare against (*toplevel).geometry, which already holds the new value by
+    // the time they run, so size_changed is never true. Track the live geometry
+    // here instead and move the border with it, in the same commit that puts the
+    // new buffer on screen — waiting for the WM cycle (which round-trips out to
+    // the external window-manager client) leaves the border a size behind.
+    if (*window).tiling_mode == crate::tiling::TilingMode::Overlay {
+        let mut live = std::mem::zeroed();
+        ffi::river_wlr_xdg_surface_get_geometry(base, &mut live);
+        if live.width > 0 && live.height > 0
+            && (live.width != (*window).box_geom.width || live.height != (*window).box_geom.height)
+        {
+            (*window).box_geom.width = live.width;
+            (*window).box_geom.height = live.height;
+            // render_finish would otherwise reset box_geom from the render-start
+            // snapshot (rendering_sent) and snap the border back to the old size.
+            (*window).self_resized = true;
+            (*window).draw_borders();
+            (*window).set_dimensions(live.width as u32, live.height as u32);
+            (*(*window).server).wm.dirty_windowing();
+        }
+    }
+
     match (*toplevel).configure_state {
         ConfigureState::Idle | ConfigureState::Committed | ConfigureState::TimedOut(..) => {
             let old_geometry = (*toplevel).geometry;
@@ -519,6 +543,8 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
                 );
                 let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status || 
                                 (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
+                // Overlays are handled by the live-geometry sync above, before this
+                // match — size_changed is never true for them.
                 if matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup) || is_status {
                     (*window).set_dimensions(new_geometry.width as u32, new_geometry.height as u32);
                     (*window).configure_sent.width = Some(new_geometry.width as u32);
@@ -546,9 +572,12 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
             (*window).rendering_scheduled.width = new_geometry.width as u32;
             (*window).rendering_scheduled.height = new_geometry.height as u32;
 
-            let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status || 
+            let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
                             (*window).get_app_id_string().map_or(false, |id| id.starts_with("cce-status"));
-            if matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup) || is_status {
+            // Overlay included so its scheduled size tracks the client's own; the
+            // border itself is handled by the live-geometry sync above.
+            let is_overlay = (*window).tiling_mode == crate::tiling::TilingMode::Overlay;
+            if matches!((*window).tiling_mode, crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Popup) || is_status || is_overlay {
                 (*window).set_dimensions(new_geometry.width as u32, new_geometry.height as u32);
                 if is_status {
                     (*window).box_geom.width = new_geometry.width;