Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(handles): the ring's inner edge is a wave of eight hills and eight valleys
Redo the resize ring's inner profile. A hill in the middle of each side
and one on each corner, a valley between every two. The valleys sit R in
from every corner — a quarter of the window's shorter on-screen side — at
band_min. Between a side's two valleys the band swells to band at the
midpoint along a raised cosine (swell_curve shapes it, floored at 0.6 so
the valleys never cusp). Between the two valleys flanking a corner the
inner edge is a superellipse arc of radius R − band_min, tangent to both
valleys, whose exponent the shader solves so that its deepest point — on
the corner's 45° diagonal — is exactly band in from the silhouette: the
corner hill peaks on the diagonal at the same height as the side hills and
meets the valleys with no crease.
The previous profile (edge bars and corner arms pinching to seams at
corner_length, gap notches, a round bulge pad per corner) is gone. Those
three config keys still parse and are still passed to the node; the shader
ignores them.
The valleys are the zone seams now. In the shader a fragment belongs to the
side it is nearest and is a corner zone within R of that side's end; in
get_border_zone the corner zones are the R×R squares at the content
corners, replacing the corner_length run and the corner-disc check. Hover
verified per zone in a headless shadow.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 88 +++++------
scenefx/render/fx_renderer/shaders/frame.frag | 207 +++++++++++++-------------
src/server/cursor.rs | 40 +----
src/server/window.rs | 12 +-
4 files changed, 161 insertions(+), 186 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index c7eea99..539109a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -350,16 +350,22 @@ where the old band sat outside them.
to be spent on moving the way the outside band's did.
- The ring is drawn by **one scenefx node**, `wlr_scene_frame`
(`scenefx/render/fx_renderer/shaders/frame.frag`), not by rects. Its
- two elements make the frame. The BAND is thinnest at the SEAMS — the gaps
- between corner and edge pieces — and swells away from them both ways: an
- edge bar to `band` at its side's midpoint, a corner arm back toward the
- apex, where it flows into the round PAD (`bulge`, screen px, 0 disables) —
- a disc centred on the corner arc's centre, smooth-unioned onto the ring so
- its inner boundary bows inward with a fillet where it meets the moulding.
- Each gap therefore separates two thin tips: the frame pinches at every
- join and thickens toward every piece's middle.
- The gap notches sever only the band; a groove across a pad would read as
- damage. The shader's zone logic works in TOP-DOWN box-local coordinates
+ inner edge is a WAVE of eight hills and eight valleys: a hill in the
+ middle of each side, a hill on each corner, a valley between every two.
+ The valleys sit `R` in from every corner — a quarter of the window's
+ SHORTER side, on screen — and are `band_min` thick. Between a side's two
+ valleys the band swells to `band` at the midpoint along a raised cosine
+ (shaped by `swell_curve`). Between the two valleys flanking a corner the
+ inner edge is a SUPERELLIPSE arc — a squircle corner of radius
+ R − band_min, tangent to both valleys — whose exponent the shader solves
+ so its deepest point, on the corner's 45° diagonal, is exactly `band` in
+ from the silhouette: the corner hill peaks on the diagonal at the same
+ height as the side hills, and meets the valleys with no crease. The
+ earlier profile (edge bars and corner arms pinching to seams at
+ `corner_length`, with gap notches and a round `bulge` pad on each corner)
+ is gone; those three keys still parse and are passed to the node, and
+ the shader ignores them.
+ The shader's zone logic works in TOP-DOWN box-local coordinates
(`gl_FragCoord` minus the box position, unflipped): the `corner_dist` SDF
flips its own copy, and mirroring the zone coordinate the same way once
swapped every zone label vertically — the top edge lit the bottom. And a
@@ -367,43 +373,41 @@ where the old band sat outside them.
ring is already fully revealed, so `step_border_fade` compares the hovered
zone against the one last drawn (`border_hover_drawn`), or the shader
keeps showing the previous zone until an unrelated commit repaints.
- 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 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
- near the centre rather than a swell. `border.segments`' 12 rects are what it
- replaced; they stay allocated but disabled.
+ The valleys are the zone seams: in the shader, a fragment belongs to the
+ side it is nearest (so the split runs along the diagonals) and is a
+ corner zone when it lies within R of that side's end; in
+ `get_border_zone` the corner zones are the R×R squares at the content
+ corners. Both derive R the same way and must stay in step. 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 near the centre rather than a swell. `border.segments`'
+ 12 rects are what it replaced; they stay allocated but disabled.
- **The ring's thickness is a SCREEN width, not a world one**, floored at
`HOVER_BAND_MIN`. Handles exist only in overview, which is zoomed *out*, so
a band that scaled with the window would be at its thinnest exactly where it
is the only way to resize: 16px renders as 7 at a typical overview zoom and
- the thin corners as 2.5, which is neither visible nor clickable. The lengths
- along a side (`corner_len`, `gap`) still scale, so the composition holds at
- any zoom — only the thickness is pinned. `draw_borders` and
+ the thin corners as 2.5, which is neither visible nor clickable. R, the
+ valley position, is a fraction of the on-screen window, so the composition
+ holds at any zoom — only the thickness is pinned. `draw_borders` and
`cursor::get_border_zone` each derive it the same way and must stay in step.
-- `swell_curve` shapes the profile: below 1 the ring gains its thickness
- early — a corner that visibly swells, then a long creep to the peak at the
- midpoint — and above 1 does the reverse. It matters more than it sounds,
- because a plain eased ramp is nearly FLAT across a corner piece: a corner
- spanning an eighth of a side only reaches t=0.13, where smoothstep is 0.04,
- under a pixel of the whole range. That is why the corners read as a constant
- thin run without it, however long `corner_length` makes them.
-- Four knobs shape it, all under `border` in config.kdl. `handle_width` is
- the thickness at the middle of a side in screen px — **its own key, not
- derived from `width`**, because the ring must be thick enough to see and hit
- while the desktop is zoomed out, while the window's visible border is a much
- finer line; deriving one from the other meant you could not thicken the grip
- without thickening every border. `corner_length` sets how far the thin
- corner run extends before the swell begins, and `taper` is the corner
- thickness as a fraction of the middle's — 1.0 is an even ring, clamped to
- (0, 1] because past 1 the corners would be thicker than the middle, which is
- the moulding inside out.
+- `swell_curve` shapes the SIDE hills: the raised cosine's height, 0 at the
+ valleys and 1 at the midpoint, is raised to this power. Below 1 broadens
+ the hill (a flatter top, tighter valleys); above 1 sharpens it. The shader
+ floors it at 0.6 — below 0.5 the valleys turn into cusps. The corner
+ hills' shape is the superellipse's own and does not take it.
+- Three knobs shape it, all under `border` in config.kdl. `handle_width` is
+ the hill height — the thickness at the middle of a side and on each
+ corner's diagonal — in screen px, **its own key, not derived from
+ `width`**, because the ring must be thick enough to see and hit while the
+ desktop is zoomed out, while the window's visible border is a much finer
+ line; deriving one from the other meant you could not thicken the grip
+ without thickening every border. `taper` is the valley thickness as a
+ fraction of the hill's — 1.0 is an even ring, clamped to (0, 1] because
+ past 1 the valleys would be thicker than the hills, which is the moulding
+ inside out. `swell_curve` is above. `corner_length`, `segment_gap` and
+ `bulge` belonged to the retired seam-and-pad profile: still parsed, no
+ longer drawn.
- The thickness is capped at a fifth of the window's shorter on-screen side,
so a zoomed-out window is never mostly ring. That cap replaced a hard
cutoff which disabled the handles below a size threshold: a window you
diff --git a/scenefx/render/fx_renderer/shaders/frame.frag b/scenefx/render/fx_renderer/shaders/frame.frag
index 5d063ee..b1d4372 100644
--- a/scenefx/render/fx_renderer/shaders/frame.frag
+++ b/scenefx/render/fx_renderer/shaders/frame.frag
@@ -1,21 +1,24 @@
-// Window resize-handle frame: a ring inside a rounded rect whose thickness
-// swells from the corners toward the middle of each side, like the moulding
-// of a picture frame.
+// Window resize-handle frame: a ring inside a rounded rect whose INNER
+// edge is a wave of eight hills and eight valleys — a hill in the middle
+// of each side and one on each corner, with a valley between every two.
+// The corner hills are domes whose apex sits on the corner's 45° diagonal,
+// pointing into the window, at the same height as the side hills.
//
-// Two elements make the frame. The BAND is thinnest at the SEAMS — the gaps
-// between corner and edge pieces — and swells away from them in both
-// directions: an edge bar rises to `band` at its side's midpoint, a corner
-// arm rises back toward the apex, where it flows into the round BULGE that
-// sits on each corner (a disc smooth-unioned onto the ring, its inner
-// boundary bowing inward like a bead). So each gap separates two thin tips,
-// and every piece thickens toward its own middle. `corner_len` places the
-// seams and the corner zones.
+// The ring is `band_min` thick at the valleys, which sit `R` (a quarter of
+// the shorter side) in from every corner. Between the two valleys of a
+// side, the band swells to `band` at the midpoint with a raised cosine.
+// Between the two valleys that flank a corner, the ring's inner edge is a
+// SUPERELLIPSE arc — a squircle corner of radius R − band_min, tangent to
+// both valleys — whose exponent is solved so that its deepest point, on
+// the diagonal, is exactly `band` in from the silhouette. That arc is the
+// corner hill: it rises from each valley, peaks on the diagonal, and, being
+// tangent at both ends, meets the side hills' flat valleys with no crease.
//
-// Why a shader rather than rects: the swell is continuous along a side, and
-// a scene rect can only approximate it with a clipped region whose corner
-// radius is one scalar. That fillet is capped by the thickness change (tens
-// of px) while a side is hundreds long, so it reads as a bump near the
-// centre rather than a swell along the whole run.
+// Why a shader rather than rects: the swell is continuous along a side,
+// and a scene rect can only approximate it with a clipped region whose
+// corner radius is one scalar. That fillet is capped by the thickness
+// change (tens of px) while a side is hundreds long, so it reads as a bump
+// near the centre rather than a swell along the whole run.
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
@@ -29,18 +32,21 @@ varying vec2 v_texcoord;
uniform vec2 position;
uniform vec2 size;
uniform float corner_radius;
-// Thickness at a side's midpoint, and at the corner pieces.
+// Thickness at a hill's apex, and at a valley.
uniform float band;
uniform float band_min;
-// How far a corner piece runs along each of its sides, and the gap that
-// separates it from the neighbouring edge bar.
+// Retired by the wave profile and ignored: the valleys place the zone
+// seams now, and the corner hill's size and height follow from `band`
+// and the window's shorter side. Kept so the node API and its callers
+// need not change.
uniform float corner_len;
uniform float gap;
-// Shape of the swell along a side. Below 1 the ring gains its thickness
-// early and then creeps toward the peak; above 1 does the reverse.
-uniform float swell_curve;
-// Radius of the round pad on each corner, 0 to disable.
uniform float bulge;
+// Shape of each SIDE hill: the raised cosine's [0,1] height to this power.
+// Below 1 broadens the hill (flatter top, tighter valleys); above 1
+// sharpens it. Floored at 0.6 in the shader — below 0.5 the valleys would
+// turn into cusps. The corner hills' shape is the superellipse's own.
+uniform float swell_curve;
// Node-local rect (x, y, w, h) the ring must not draw over — the client is
// drawing an in-surface popover there and the menu must read as in FRONT of
// the chrome. w or h <= 0 disables.
@@ -72,28 +78,51 @@ const float ZONE_TR = 5.0;
const float ZONE_BL = 6.0;
const float ZONE_BR = 7.0;
-void main() {
- float dist = ring_dist(vec2(0.0));
- // Outside the rect, or deeper in than anything here reaches: the band at
- // its thickest, or a corner bulge (its centre already sits corner_radius
- // in from the silhouette).
- if (dist > 0.0 || dist < -(max(band, bulge + corner_radius) + 1.0)) {
- discard;
+const float PI = 3.14159265359;
+const float LN2 = 0.69314718056;
+
+// Height, 0..1, of a side's hill at distance `x` along it: a raised cosine
+// peaking at the midpoint and reaching zero — with zero slope — at the two
+// valleys, `R` in from either end. Zero beyond them.
+float side_hill(float x, float len, float R) {
+ float half_span = max(0.5 * len - R, 1e-3);
+ float d = abs(x - 0.5 * len);
+ if (d >= half_span) {
+ return 0.0;
}
- float depth = -dist; // 0 at the silhouette, growing inward
+ float h = 0.5 * (1.0 + cos(PI * d / half_span));
+ return pow(h, max(swell_curve, 0.6));
+}
+void main() {
+ float dist = ring_dist(vec2(0.0));
// Rect-local position, TOP-DOWN: gl_FragCoord minus the box position is
// already y-down box-local here (the pass renders under a FLIPPED_180
// projection, so fragment row 0 is the top of the buffer — see the same
- // note in droplet.frag). Everything below that names a side — dt/db,
- // the corner pads, the zones — reads y as "distance from the top", so
- // this must NOT be flipped the way corner_dist flips its own copy: the
- // SDF is symmetric under that flip (one radius for all four corners),
- // but the zone labels are not, and flipping here mirrored them
- // vertically — hovering the top edge lit the bottom one, and each top
- // corner lit the corner below it.
+ // note in droplet.frag). Everything below that names a side reads y as
+ // "distance from the top", so this must NOT be flipped the way
+ // corner_dist flips its own copy: the SDF is symmetric under that flip
+ // (one radius for all four corners), but the zone labels are not.
vec2 p = gl_FragCoord.xy - position;
+ // Distances to the four sides, and which of each pair is nearer.
+ float dl = p.x;
+ float dr = size.x - p.x;
+ float dt = p.y;
+ float db = size.y - p.y;
+ bool left = dl <= dr;
+ bool top = dt <= db;
+ float dv = left ? dl : dr;
+ float dh = top ? dt : db;
+
+ // The valleys sit R in from every corner; nothing reaches deeper than
+ // that, so anything past it is interior.
+ float R = 0.25 * min(size.x, size.y);
+ if (dist > 0.0 || dist < -(R + 2.0)) {
+ discard;
+ }
+ float depth = -dist; // 0 at the silhouette, growing inward
+
// A client popover owns this rect; the ring yields to it wholesale. The
// rect arrives top-left-origin (y down), the same space as p.
if (exclusion.z > 0.0 && exclusion.w > 0.0
@@ -102,83 +131,51 @@ void main() {
discard;
}
- // Which side owns this fragment: whichever edge it sits nearer. The
- // comparison is on distance to the edge, so the split runs along the
- // diagonals and every corner is shared consistently by its two sides.
- float dl = p.x;
- float dr = size.x - p.x;
- float dt = p.y;
- float db = size.y - p.y;
- bool vertical = min(dl, dr) < min(dt, db);
-
- // `along` is the coordinate down the owning side, `len` its full length.
- float along = vertical ? p.y : p.x;
- float len = vertical ? size.y : size.x;
- // Distance from the nearer end of that side.
- float u = min(along, len - along);
-
- // The corner run, clamped against ITS OWN side (see the zone comment
- // below); the band profile and the zone cut share it so the thickness
- // minimum lands exactly on the seam.
- float cl = min(corner_len, 0.45 * len);
-
- // The band: band_min at the seams, swelling away from them both ways —
- // an edge bar to `band` at the side's midpoint, a corner arm back toward
- // the apex, where the pad's union takes over. v is 0 at a seam and 1 at
- // a piece's peak; swell_curve pulls the gain early (below 1) or late.
- float half_side = max(0.5 * len, 1e-3);
- float v;
- if (u <= cl) {
- v = 1.0 - u / max(cl, 1e-3);
+ float A = max(band - band_min, 0.0);
+ // The inner superellipse's radius, and the exponent that puts its
+ // diagonal point — R - Rp * 2^(-1/n) in from each side — at `band`.
+ // Clamped to a circle at the low end (a window too small for the
+ // solve gets a taller corner rather than a concave one).
+ float Rp = max(R - band_min, 1e-3);
+ float ratio = clamp(1.0 - A / Rp, 0.05, 0.9999); // 2^(-1/n)
+ float n = clamp(-LN2 / log(ratio), 2.0, 12.0);
+
+ // Signed distance to the interior, positive in the ring: in a corner
+ // square, the superellipse (its implicit function scaled by its
+ // gradient, exact where it matters — at the edge); elsewhere, how far
+ // short of the nearer inner edges the fragment falls.
+ float f;
+ if (dv < R && dh < R) {
+ vec2 c = max(vec2(R - dv, R - dh) / Rp, 0.0);
+ vec2 cn = pow(c, vec2(n));
+ float F = cn.x + cn.y - 1.0;
+ vec2 grad = n * pow(c, vec2(n - 1.0)) / Rp;
+ f = F / max(length(grad), 1e-4);
} else {
- float run = max(half_side - cl - gap, 1e-3);
- v = clamp((u - cl - gap) / run, 0.0, 1.0);
+ float t_h = band_min + A * side_hill(p.x, size.x, R);
+ float t_v = band_min + A * side_hill(p.y, size.y, R);
+ f = max(t_h - dh, t_v - dv);
+ }
+ if (f <= 0.0) {
+ discard;
}
- float s = pow(smoothstep(0.0, 1.0, v), max(swell_curve, 0.01));
- float thickness = mix(band_min, band, s);
- float f_ring = thickness - depth;
- // The corner bulge: a disc centred on the corner ARC's centre, so it sits
- // flush behind the silhouette and pokes inward by its radius. Positive
- // inside, like f_ring.
- bool left = dl <= dr;
- bool top = dt <= db;
- vec2 arc_c = vec2(left ? corner_radius : size.x - corner_radius,
- top ? corner_radius : size.y - corner_radius);
- float f_disc = bulge - length(p - arc_c);
-
- // Smooth union of band and bulge: the pad flows into the moulding with a
- // fillet instead of a notch. Blend radius scales with the pad.
- float k = max(0.35 * bulge, 1.0);
- float h = clamp(0.5 + 0.5 * (f_disc - f_ring) / k, 0.0, 1.0);
- float f = mix(f_ring, f_disc, h) + k * h * (1.0 - h);
-
- // 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 per side because two corner zones on one side
- // must never meet — past that, the side's midpoint would resize
- // diagonally.
+ // Zones: the side the fragment is nearer to owns it, so the split runs
+ // along the diagonals; along that side, everything within R of an end
+ // — the corner hill, valley to valley — is the corner zone.
+ bool vertical = dv < dh;
+ float from_end = vertical ? dh : dv;
float zone;
- if (u <= cl || f_disc > 0.0) {
+ if (from_end < R) {
zone = top ? (left ? ZONE_TL : ZONE_TR) : (left ? ZONE_BL : ZONE_BR);
- if (u > cl && u <= cl + gap && f_disc <= 0.0) {
- discard;
- }
- } else if (u <= cl + gap) {
- discard;
} else if (vertical) {
- zone = (dl <= dr) ? ZONE_LEFT : ZONE_RIGHT;
+ zone = left ? ZONE_LEFT : ZONE_RIGHT;
} else {
- zone = (dt <= db) ? ZONE_TOP : ZONE_BOTTOM;
- }
-
- if (f <= 0.0) {
- discard;
+ zone = top ? ZONE_TOP : ZONE_BOTTOM;
}
// Feather both flanks by a pixel: the silhouette side against the
- // window's own rounded edge, the inner side along the unioned boundary.
+ // window's own rounded edge, the inner side along the wave.
float aa = clamp(depth, 0.0, 1.0) * clamp(f, 0.0, 1.0);
vec4 base = (hovered >= 0.0 && abs(hovered - zone) < 0.5) ? hover_color : v_color;
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 85d7a97..b6eaf1e 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -3046,44 +3046,20 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
}
}
- // The corner pads reach further inward than the band (their radius plus
- // the corner arc), and what is drawn must grab: a point within a pad's
- // radius of its corner resizes on that corner's two edges.
- let pad = ((*(*window).server).wm.layout.border_corner_bulge as f64)
- .min(content_w.min(content_h) * 0.3);
- if pad > 0.0 {
- let cx = if rx < content_w / 2.0 { 0.0 } else { content_w };
- let cy = if ry < content_h / 2.0 { 0.0 } else { content_h };
- let (dx, dy) = (rx - cx, ry - cy);
- if (dx * dx + dy * dy).sqrt() < pad {
- return BorderZone::Resize(crate::window::Edges {
- top: cy == 0.0,
- bottom: cy > 0.0,
- left: cx == 0.0,
- right: cx > 0.0,
- });
- }
- }
-
let (near_l, near_r) = (rx < bw, rx >= content_w - bw);
let (near_t, near_b) = (ry < bw, ry >= content_h - bw);
if !(near_l || near_r || near_t || near_b) {
return BorderZone::None;
}
- // 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 cl_x = corner_len.min(content_w * 0.45);
- let cl_y = corner_len.min(content_h * 0.45);
+ // Corner zones are the squares within R of each content corner, R a
+ // quarter of the SHORTER side: the ring's inner edge is a wave with a
+ // hill on every corner and in the middle of every side, the valleys
+ // between them sit R in from each corner, and the valley is where the
+ // shader cuts one zone from the next. The same rule here, so what
+ // lights up is what grabs.
+ let cl_x = content_w.min(content_h) * 0.25;
+ let cl_y = cl_x;
let corner_l = rx < cl_x;
let corner_r = rx >= content_w - cl_x;
let corner_t = ry < cl_y;
diff --git a/src/server/window.rs b/src/server/window.rs
index 04add1a..7adb1a4 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3872,6 +3872,9 @@ impl Window {
// the window's content radius (the widened backplate radius the
// corner clip uses) rather than that plus a band.
let r_in = bg_radius;
+ // corner_len and gap are retired by the wave profile (the
+ // valleys place the seams now, a quarter along each side) and
+ // ignored by the shader; still passed so the node API holds.
let cl = border_corner_len(bw as f64, layout.border_corner_length, r_in as f64) as i32;
let g = layout.border_segment_gap;
@@ -3886,17 +3889,12 @@ impl Window {
let px = |v: i32| (v as f64 * sc) as i32;
ffi::wlr_scene_frame_set_size(self.border.frame, px(cw), px(ch));
ffi::wlr_scene_frame_set_corner_radius(self.border.frame, px(r_in));
- // band_min is the thin corner thickness the swell rises from;
- // half the band keeps the corner pieces clearly lighter than the
- // middle of a side without letting them vanish at small sizes.
+ // band is the hill height, band_min (taper × band) the valley
+ // floor; bulge is the fillet radius that domes each corner hill.
ffi::wlr_scene_frame_set_shape(
self.border.frame,
band_screen as f32,
(band_screen as f32 * layout.border_taper.clamp(0.0, 1.0)).max(2.0),
- // 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,