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

commitc2472de0b7a7e99c01c71b4b2bcc651a0946149e
parente28b02cbd3
authorLucas Galante <[email protected]>
date2026-07-15 12:29
feat: border segment_gap/corner_length config keys; fix resize jitter

New keys on the border node: 'segment_gap' (visual gap between the 8
zone segments, default 4) and 'corner_length' (corner zone length from
the outer corner, default 0 = auto max(2*width, 16), clamped to at
least the band width). Both feed draw_borders and get_border_zone from
Layout, so visuals and pointer zones stay aligned.

Resize jitter fix: the commit handler already derived the compensating
position from the committed geometry, but only wrote it into
rendering_requested/box_geom — the scene node moved on the NEXT render
pass, so a frame could composite the resized buffer at the old
position, wobbling the anchored edges on left/top resizes. The commit
handler now repositions the scene trees and redraws the borders at the
committed size in the same commit.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/config.rs       | 38 +++++++++++++++++++++++++++++++++++++-
 src/server/cursor.rs       |  2 +-
 src/server/window.rs       | 21 ++++++++++++---------
 src/server/xdg_toplevel.rs | 11 +++++++++++
 4 files changed, 61 insertions(+), 11 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 937e018..6442d39 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -27,6 +27,11 @@ pub struct Layout {
     /// defaults to a lightened `border_color_focused`.
     pub border_color_hover: [f32; 4],
     pub border_corner_radius: i32,
+    /// Visual gap between the 8 border zone segments.
+    pub border_segment_gap: i32,
+    /// Corner zone length measured from the outer corner along each band;
+    /// 0 = auto (max(2 * width, 16)).
+    pub border_corner_length: i32,
     pub background_r: u32,
     pub background_g: u32,
     pub background_b: u32,
@@ -80,6 +85,8 @@ impl Default for Layout {
             border_color_focused: [62.0 / 255.0, 62.0 / 255.0, 62.0 / 255.0, 1.0],
             border_color_hover: lighten_premultiplied([62.0 / 255.0, 62.0 / 255.0, 62.0 / 255.0, 1.0], HOVER_LIGHTEN),
             border_corner_radius: 0,
+            border_segment_gap: 4,
+            border_corner_length: 0,
             background_r: 0x1C1C1C1Cu32,
             background_g: 0x20202020u32,
             background_b: 0x20202020u32,
@@ -286,6 +293,11 @@ pub struct SurfaceConfig {
     pub border_color_hover: Option<String>,
     #[serde(default = "default_border_corner_radius")]
     pub border_corner_radius: i64,
+    #[serde(default = "default_border_segment_gap")]
+    pub border_segment_gap: i64,
+    /// 0 = auto (max(2 * width, 16)).
+    #[serde(default)]
+    pub border_corner_length: i64,
     #[serde(default = "default_cloud_position_default")]
     pub cloud_position_default: Option<[i32; 2]>,
 }
@@ -310,6 +322,8 @@ impl Default for SurfaceConfig {
             border_color_focused: None,
             border_color_hover: None,
             border_corner_radius: default_border_corner_radius(),
+            border_segment_gap: default_border_segment_gap(),
+            border_corner_length: 0,
             cloud_position_default: default_cloud_position_default(),
         }
      }
@@ -380,6 +394,10 @@ fn default_border_corner_radius() -> i64 {
     0
 }
 
+fn default_border_segment_gap() -> i64 {
+    4
+}
+
 /// How far the default hover color moves toward white.
 const HOVER_LIGHTEN: f32 = 0.35;
 
@@ -1486,6 +1504,16 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
                                             surface.border_corner_radius = val;
                                         }
                                     }
+                                    "segment_gap" => {
+                                        if let Some(val) = entry.value().as_i64() {
+                                            surface.border_segment_gap = val;
+                                        }
+                                    }
+                                    "corner_length" => {
+                                        if let Some(val) = entry.value().as_i64() {
+                                            surface.border_corner_length = val;
+                                        }
+                                    }
                                     _ => {}
                                 }
                             }
@@ -1520,6 +1548,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
             surface.border_color_focused = get_child_arg_string_opt(node, "border_color_focused");
             surface.border_color_hover = get_child_arg_string_opt(node, "border_color_hover");
             surface.border_corner_radius = get_child_arg_i64(node, "border_corner_radius", default_border_corner_radius());
+            surface.border_segment_gap = get_child_arg_i64(node, "border_segment_gap", default_border_segment_gap());
+            surface.border_corner_length = get_child_arg_i64(node, "border_corner_length", 0);
             surface.cloud_position_default = get_child_arg_vec2i_opt(node, "cloud_position_default");
         }
     }
@@ -1604,6 +1634,8 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         .map(parse_hex_color_rgba)
         .unwrap_or_else(|| lighten_premultiplied(state.layout.border_color_focused, HOVER_LIGHTEN));
     state.layout.border_corner_radius = config.surface.border_corner_radius as i32;
+    state.layout.border_segment_gap = config.surface.border_segment_gap.max(0) as i32;
+    state.layout.border_corner_length = config.surface.border_corner_length.max(0) as i32;
 
     state.layout.desktop_gap_color = config.surface.desktop_gap_color.clone();
 
@@ -2033,7 +2065,7 @@ mod tests {
         let content = r##"
             style {
                 surface {
-                    border width=2 color="#ff8800" color_focused="#00ff88" color_hover="#88ffcc" corner_radius=10
+                    border width=2 color="#ff8800" color_focused="#00ff88" color_hover="#88ffcc" corner_radius=10 segment_gap=6 corner_length=24
                 }
             }
         "##;
@@ -2043,6 +2075,8 @@ mod tests {
         assert_eq!(config.surface.border_color_focused, Some("#00ff88".to_string()));
         assert_eq!(config.surface.border_color_hover, Some("#88ffcc".to_string()));
         assert_eq!(config.surface.border_corner_radius, 10);
+        assert_eq!(config.surface.border_segment_gap, 6);
+        assert_eq!(config.surface.border_corner_length, 24);
 
         // Defaults keep borders off; the focused color falls back to `color`
         // and the hover color to a lightened focused color.
@@ -2051,5 +2085,7 @@ mod tests {
         assert_eq!(config.surface.border_corner_radius, 0);
         assert_eq!(config.surface.border_color_focused, None);
         assert_eq!(config.surface.border_color_hover, None);
+        assert_eq!(config.surface.border_segment_gap, 4);
+        assert_eq!(config.surface.border_corner_length, 0);
     }
 }
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index a0a76cc..58ef4c2 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2298,7 +2298,7 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
         // moves the window, everything else resizes. Corner squares of
         // `corner_len` (measured from the outer corners along the band)
         // resize on both adjacent edges, so the top corners still resize.
-        let corner_len = crate::window::border_corner_len(bw);
+        let corner_len = crate::window::border_corner_len(bw, (*(*window).server).wm.layout.border_corner_length);
 
         let dist_left = rx + bw;
         let dist_right = (content_w + bw) - rx;
diff --git a/src/server/window.rs b/src/server/window.rs
index 6a4acfd..918fc9a 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -105,15 +105,17 @@ pub enum BorderElement {
 
 /// Length of a corner zone, measured from the outer corner along each band.
 /// Shared by the visual segments (draw_borders) and the pointer zones
-/// (cursor.rs get_border_zone) so they always agree.
-pub fn border_corner_len(bw: f64) -> f64 {
-    (2.0 * bw).max(16.0)
+/// (cursor.rs get_border_zone) so they always agree. `configured` comes from
+/// `border { corner_length= }`; 0 picks the auto formula. Never shorter than
+/// the band width, so a corner is at least its diagonal square.
+pub fn border_corner_len(bw: f64, configured: i32) -> f64 {
+    if configured > 0 {
+        (configured as f64).max(bw)
+    } else {
+        (2.0 * bw).max(16.0)
+    }
 }
 
-/// Visual gap between border segments, so the 8 zones read as separate
-/// elements. Zones and hit-testing stay continuous across the gaps.
-pub const BORDER_SEGMENT_GAP: i32 = 4;
-
 // Indices into BorderRects.segments: 4 edge bars + 2 L-arm rects per corner.
 const SEG_TOP: usize = 0;
 const SEG_BOTTOM: usize = 1;
@@ -2216,8 +2218,9 @@ impl Window {
             }
 
             let bw = border.width as i32;
-            let cl = border_corner_len(bw as f64) as i32;
-            let g = BORDER_SEGMENT_GAP;
+            let layout = &(*self.server).wm.layout;
+            let cl = border_corner_len(bw as f64, layout.border_corner_length) as i32;
+            let g = layout.border_segment_gap;
             let arm = cl - bw;
             // Edge bars span between the corner zones, inset by the gap.
             let bar_x = cl - bw + g;
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index ef68362..d269d17 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -645,6 +645,17 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
         (*window).box_geom.x = final_x;
         (*window).box_geom.y = final_y;
 
+        // Apply the compensating position (and the borders) to the scene in
+        // this same commit: the resized buffer is already part of the scene,
+        // and waiting for the next render pass lets a frame composite the
+        // new size at the old position — visible as jitter on the anchored
+        // edges during left/top resizes.
+        ffi::river_scene_node_set_position_if_changed((*window).tree as *mut ffi::wlr_scene_node, final_x, final_y);
+        ffi::river_scene_node_set_position_if_changed((*window).popup_tree as *mut ffi::wlr_scene_node, final_x, final_y);
+        (*window).box_geom.width = geometry.width;
+        (*window).box_geom.height = geometry.height;
+        (*window).draw_borders();
+
         if !resize_active {
             (*window).resize_edges = None;
         }