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