Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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;