Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(handles): longer corner runs, clamped per side
corner_length 120 -> 160 in the user's config makes the corner zones span
38% of a side, at the edge bars' expense — but doing that surfaced a latent
hazard and a coupling, both fixed here.
The hazard: nothing capped the corner run against the window, so on a
window short enough for one side's two corner runs to meet, every point on
that side became a corner zone and its MIDPOINT resized diagonally.
The coupling: the obvious cap (against the window's shorter side) binds
both axes to the short one — measured on an 840x520 window, it silently
held the top edge's corner run at 28% when the config asked for 38%.
So the clamp is per SIDE, 0.45 of that side's own length, applied
identically in the shader's zone cut and the hit test's corner squares. A
long side carries the full configured run; a short one shortens; a 10%
edge bar always survives in the middle. Verified by gap-notch positions:
top edge at 38%/62% (the configured length), left edge at 45%/54% (its
cap), where the single cap had pinned both to 28%.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 6 +++++-
scenefx/render/fx_renderer/shaders/frame.frag | 11 ++++++++---
src/server/cursor.rs | 13 +++++++++----
src/server/window.rs | 6 +++++-
4 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 178f47c..b2e001a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -331,7 +331,11 @@ where the old band sat outside them.
The gap notches sever only the band; a groove across a pad would read as
damage. Both `get_border_zone` and the drawing treat a pad as its corner's
zone — its tip reaches past the band, so the hit test carries a matching
- corner-disc check. The profile is continuous along
+ corner-disc check. The corner run (`corner_length`) clamps PER SIDE at 0.45
+ of that side's length, in the shader and the hit test alike: two corner
+ zones on one side must never meet, or its midpoint would resize diagonally
+ — and per-side (rather than against the window's short side) lets a long
+ side carry the full configured run while a short one shortens. The profile is continuous along
a side, which a rect cannot express: its only shaping tool is a clipped
region whose corner radius is a single scalar, capped by the thickness
change (tens of px) while a side is hundreds long, so it reads as a bump
diff --git a/scenefx/render/fx_renderer/shaders/frame.frag b/scenefx/render/fx_renderer/shaders/frame.frag
index 26a0f1f..40edfdb 100644
--- a/scenefx/render/fx_renderer/shaders/frame.frag
+++ b/scenefx/render/fx_renderer/shaders/frame.frag
@@ -123,13 +123,18 @@ void main() {
// Zones: anything on the pad is its corner's, then the band cut as
// before. The gap only severs the BAND — a groove across the pad would
// read as damage, so it skips fragments the disc owns.
+ // The corner run clamps against ITS OWN side: two corner zones on one
+ // side must never meet, or the side's midpoint would resize diagonally —
+ // and clamping per side rather than to the window's short side lets a
+ // long side carry the full configured run while a short one shortens.
+ float cl = min(corner_len, 0.45 * len);
float zone;
- if (u <= corner_len || f_disc > 0.0) {
+ if (u <= cl || f_disc > 0.0) {
zone = top ? (left ? ZONE_TL : ZONE_TR) : (left ? ZONE_BL : ZONE_BR);
- if (u > corner_len && u <= corner_len + gap && f_disc <= 0.0) {
+ if (u > cl && u <= cl + gap && f_disc <= 0.0) {
discard;
}
- } else if (u <= corner_len + gap) {
+ } else if (u <= cl + gap) {
discard;
} else if (vertical) {
zone = (dl <= dr) ? ZONE_LEFT : ZONE_RIGHT;
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 70d8ccf..ad951b7 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2890,16 +2890,21 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
// Corner squares of `corner_len`, measured from the content corners —
// the same length draw_borders gives the corner handles.
+ // Floored at the band, then clamped per AXIS to 0.45 of that side, the
+ // same rule the shader draws with: two corner runs on one side must
+ // never meet, or its midpoint would resize diagonally.
let corner_len = (crate::window::border_corner_len(
bw_unscaled,
(*(*window).server).wm.layout.border_corner_length,
0.0,
) * scale)
.max(bw);
- let corner_l = rx < corner_len;
- let corner_r = rx >= content_w - corner_len;
- let corner_t = ry < corner_len;
- let corner_b = ry >= content_h - corner_len;
+ let cl_x = corner_len.min(content_w * 0.45);
+ let cl_y = corner_len.min(content_h * 0.45);
+ let corner_l = rx < cl_x;
+ let corner_r = rx >= content_w - cl_x;
+ let corner_t = ry < cl_y;
+ let corner_b = ry >= content_h - cl_y;
if (corner_l || corner_r) && (corner_t || corner_b) {
return BorderZone::Resize(crate::window::Edges {
diff --git a/src/server/window.rs b/src/server/window.rs
index be916a0..4a029b6 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3815,7 +3815,11 @@ impl Window {
self.border.frame,
band_screen as f32,
(band_screen as f32 * layout.border_taper.clamp(0.0, 1.0)).max(2.0),
- (px(cl) as f32).max(band_screen as f32),
+ // Floored at the band; the shader clamps the run per SIDE
+ // (0.45 of that side), so a long side carries the full
+ // configured length while a short one shortens. The hit test
+ // applies the same per-axis clamp.
+ (px(cl) as f64).max(band_screen) as f32,
px(g) as f32,
layout.border_swell_curve,
(layout.border_corner_bulge as f64).min(short_side * 0.3) as f32,