srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/decoration/corners.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-04-03 22:35:00 +0200
committersrdusr <[email protected]>2025-04-03 22:35:00 +0200
commit254557f94544fe8841a4e9422eee9228e7ed4152 (patch)
tree4a1197daae3571d00e9af5e779107edaca183443 /crates/wayland/src/decoration/corners.rs
parent4d61d76b1b73f56fc14ed977806e4d4ad313fd51 (diff)
downloadsrdwm-254557f94544fe8841a4e9422eee9228e7ed4152.tar.gz
srdwm-254557f94544fe8841a4e9422eee9228e7ed4152.zip
Fix border corner rendered as a solid wedge instead of a ring
round_top_corners/round_bottom_corners only ever cut pixels *outside* the shared corner radius (the rounded outer silhouette). Nothing cut anything *inside* it, so a border strip's own "extra" rows (present whenever corner_radius > border_width) stayed a solid filled disk out to the centre column/row, then hit clip_middle_beyond_thickness's hard, unblended rectangular cut right at the disk's own most opaque point - a clean right-angle step, not a curve. Confirmed live, zoomed: a real square notch bitten into an otherwise smooth arc, reported as "squares on the inside corners of each vertex." Added carve_inner_corner_pixel, the same smoothstep falloff as the existing outer cut but inverted (cuts near the centre instead of far from it), applied at radius - border_width so the corner becomes a genuine ring of ~border_width visible thickness tapering smoothly to transparent, instead of a filled wedge. Only render_border_top/ render_border_bottom pass an inner_radius; the titlebar's own corner and the lock-screen box keep their existing solid-disk behaviour, which is correct for a single flat-coloured panel with nothing of a different colour underneath needing to show through. Also generalizes round_top_corners with an explicit center_col parameter, mirroring the existing center_row shift: the titlebar's own corner circle was never shifted horizontally to match the border strip's (only vertically), leaving a border_width-wide sliver of the titlebar's own misaligned curve poking through at the seam. border_top_and_titlebar_corners_meet_without_a_seam and border_top_curve_actually_closes_within_the_side_strips_own_width updated to match: both now compare the correct corresponding columns (the titlebar's own buffer starts border_width columns inside the border strip's), and the latter no longer demands exact 255 opacity at a point that legitimately sits within the new inner cut's own antialiasing band.
Diffstat (limited to 'crates/wayland/src/decoration/corners.rs')
-rw-r--r--crates/wayland/src/decoration/corners.rs160
1 files changed, 140 insertions, 20 deletions
diff --git a/crates/wayland/src/decoration/corners.rs b/crates/wayland/src/decoration/corners.rs
index 10bad4d..aeac076 100644
--- a/crates/wayland/src/decoration/corners.rs
+++ b/crates/wayland/src/decoration/corners.rs
@@ -60,13 +60,52 @@
/// starting `thickness` rows *into* the circle instead of at its top,
/// that needs to shift, by passing `radius as i32 - border_width as
/// i32` (see `render_titlebar`'s call site).
-pub(crate) fn round_top_corners(buf: &mut [u8], width: usize, height: usize, radius: u32, center_row: i32) {
+///
+/// `center_col` is the exact same idea, horizontally: which *column* of
+/// this buffer's own local coordinates the left corner's circle centre
+/// sits on (the right corner mirrors it, `width - r` outward from the
+/// right edge by the same amount `center_col` is inward from the left).
+/// A border strip's own column 0 is the *true* left edge, so it passes
+/// `radius as i32`, same as its unshifted `center_row`. A titlebar's own
+/// column 0 sits `border_width` columns *inside* that same true edge --
+/// its buffer is only as wide as the content it sits above, not the
+/// wider border strip around it - so it needs the identical `radius as
+/// i32 - border_width as i32` shift horizontally too, or its own circle
+/// centre ends up `border_width` columns to the right of the border
+/// strip's, two different circles again despite `center_row` already
+/// lining up the vertical one. Confirmed live at a real corner, zoomed:
+/// the border's own curve covered most of the shared corner correctly,
+/// but a `border_width`-wide sliver of the titlebar's own (wrongly
+/// centred) curve poked through right where the two should have met
+/// exactly, reading as a small square notch bitten out of an otherwise
+/// smooth arc - reported as "squares on the inside corners of each
+/// vertex/border corner." Every existing caller before this parameter
+/// existed passed the unshifted, no-op case (`radius as i32`, same as
+/// `center_row`'s own default), so this is additive, not a behaviour
+/// change for border's own corner.
+///
+/// `inner_radius`, when `Some`, also carves this corner into a proper ring
+/// - see [`carve_inner_corner_pixel`]'s own doc comment for why that's
+/// needed at all. Only `render_border_top` passes one (`radius -
+/// border_width`, the ring's real visible thickness); every other caller
+/// (a titlebar's own corner, the lock-screen box) passes `None` and keeps
+/// today's solid-disk-past-the-nominal-edge behaviour, which is correct
+/// for a single flat-coloured panel with nothing of a *different* colour
+/// underneath it needing to show through.
+pub(crate) fn round_top_corners(buf: &mut [u8], width: usize, height: usize, radius: u32, center_row: i32, center_col: i32, inner_radius: Option<u32>) {
let r = (radius as usize).min(width / 2);
if r == 0 {
return;
}
let rf = r as f32;
let cy = center_row as f32;
+ // How far `center_col` sits from the unshifted default (`radius`) --
+ // the right corner's own centre needs shifting by the same amount, in
+ // the opposite direction (further *into* the buffer from the right
+ // edge, mirroring how the left corner shifts further *into* it from
+ // the left), since the buffer's own right edge is the mirror image of
+ // its left one, not an independent second true edge.
+ let col_inset = radius as i32 - center_col;
// Only rows that could plausibly need blending at all: below
// `center_row` (this buffer's slice of the circle, whatever portion
// of it falls within `[0, height)`) is where the actual curve lives;
@@ -82,9 +121,18 @@ pub(crate) fn round_top_corners(buf: &mut [u8], width: usize, height: usize, rad
// generalised to an arbitrary `center_row`.
let y_lo = (center_row - r as i32).max(0) as usize;
let y_hi = (center_row.max(0) as usize).min(height);
+ // `> 0`, not just `.is_some()`: a `radius <= border_width` window
+ // (an unusually thick border relative to its corner radius) has no
+ // ring to carve at all - the whole disk out to `radius` already *is*
+ // the intended `border_width`-ish thickness, and an inner radius of
+ // zero or less would carve away the entire corner instead of nothing.
+ let inner_rf = inner_radius.filter(|&r| r > 0).map(|r| r as f32);
for y in y_lo..y_hi {
for x in 0..r {
- blend_corner_pixel(buf, width, x, y, rf, cy, rf);
+ blend_corner_pixel(buf, width, x, y, center_col as f32, cy, rf);
+ if let Some(inner_rf) = inner_rf {
+ carve_inner_corner_pixel(buf, width, x, y, center_col as f32, cy, inner_rf);
+ }
}
for x in (width - r)..width {
// `width - r`, not `width - r - 1` - see `blend_corner_pixel`'s
@@ -93,7 +141,14 @@ pub(crate) fn round_top_corners(buf: &mut [u8], width: usize, height: usize, rad
// right corner's centre column lines up with `rounded_corners_
// pixman.rs`'s `apply_corner_mask` (`px.clamp(radius, wf -
// radius)`, which clamps to exactly `w - r` here) without it.
- blend_corner_pixel(buf, width, x, y, (width - r) as f32, cy, rf);
+ // `+ col_inset`: the same horizontal shift `center_col` applies
+ // to the left corner, mirrored - see this function's own doc
+ // comment on `center_col`/`col_inset`.
+ let cx = (width - r) as f32 + col_inset as f32;
+ blend_corner_pixel(buf, width, x, y, cx, cy, rf);
+ if let Some(inner_rf) = inner_rf {
+ carve_inner_corner_pixel(buf, width, x, y, cx, cy, inner_rf);
+ }
}
}
}
@@ -121,23 +176,43 @@ pub(crate) fn round_top_corners(buf: &mut [u8], width: usize, height: usize, rad
/// `clipped_corner_pixels_are_fully_premultiplied_zero_not_just_alpha`
/// already established for the hard-cut case this replaces.
fn blend_corner_pixel(buf: &mut [u8], width: usize, x: usize, y: usize, cx: f32, cy: f32, radius: f32) {
- // Sampled at the pixel's own *center* (`+ 0.5`), not its raw integer
- // coordinate - matching `rounded_corners_pixman.rs`'s `apply_corner_
- // mask` (`px = x as f32 + 0.5`) and `rounded_corners.rs`'s GLES shader,
- // both of which already use this standard rasterization convention.
- // This function didn't, a half-pixel systematic difference between
- // this border-strip curve and the client-content curve it's supposed
- // to trace exactly the same circle as - confirmed live at extreme
- // zoom: a small but real right-angle step partway along an otherwise
- // smooth arc, right where the two curves are supposed to meet
- // (reported as "squares on the inside corners of each vertex").
- // `round_top_corners`/`round_bottom_corners`'s own right-edge/bottom
- // `cx`/`cy` had a compensating `- 1` baked in against the *old*
- // convention - removed alongside this, see their own doc comments.
+ let keep = corner_keep_mask(x, y, cx, cy, radius);
+ scale_pixel(buf, width, x, y, keep);
+}
+
+/// `1.0` (fully kept) within `radius - 1` of `(cx, cy)`, `0.0` (fully cut)
+/// beyond `radius + 1`, smoothstepped between - the shared falloff both
+/// [`blend_corner_pixel`] (an *outer* cut: keep near the centre, cut far
+/// from it) and [`carve_inner_corner_pixel`] (an *inner* cut: the same
+/// falloff, inverted, so it cuts *near* the centre instead) are built from.
+/// Sampled at the pixel's own *center* (`+ 0.5`), not its raw integer
+/// coordinate - matching `rounded_corners_pixman.rs`'s `apply_corner_mask`
+/// (`px = x as f32 + 0.5`) and `rounded_corners.rs`'s GLES shader, both of
+/// which already use this standard rasterization convention. This function
+/// didn't, once - a half-pixel systematic difference between this
+/// border-strip curve and the client-content curve it's supposed to trace
+/// exactly the same circle as, confirmed live at extreme zoom: a small but
+/// real right-angle step partway along an otherwise smooth arc, right
+/// where the two curves are supposed to meet.
+fn corner_keep_mask(x: usize, y: usize, cx: f32, cy: f32, radius: f32) -> f32 {
let (dx, dy) = (x as f32 + 0.5 - cx, y as f32 + 0.5 - cy);
let dist = (dx * dx + dy * dy).sqrt();
let t = ((dist - (radius - 1.0)) / 2.0).clamp(0.0, 1.0);
- let mask = 1.0 - (t * t * (3.0 - 2.0 * t));
+ 1.0 - (t * t * (3.0 - 2.0 * t))
+}
+
+/// Multiplies every BGRA byte of the pixel at `(x, y)` by `mask` - `buf` is
+/// premultiplied BGRA (`color::rgb_to_bgra`'s own convention), so scaling
+/// all four bytes by the same factor is the correct way to reduce a
+/// pixel's effective alpha (zeroing all four, not just alpha, for a fully
+/// cut pixel - a genuinely transparent premultiplied pixel is `(0, 0, 0,
+/// 0)` in every channel, not just alpha, since the stored colour already
+/// carries the alpha multiplied in; leaving stale opaque RGB behind while
+/// zeroing only alpha produced a byte pattern Pixman's own `OVER`
+/// compositing does not actually treat as "nothing here" - confirmed
+/// live, pixel-by-pixel, no visible transparency despite alpha already
+/// being zero).
+fn scale_pixel(buf: &mut [u8], width: usize, x: usize, y: usize, mask: f32) {
if mask >= 1.0 {
return;
}
@@ -151,11 +226,46 @@ fn blend_corner_pixel(buf: &mut [u8], width: usize, x: usize, y: usize, cx: f32,
}
}
+/// The border strip's own missing half of a proper rounded-corner *ring*:
+/// [`blend_corner_pixel`] already cuts everything *outside* `radius` of the
+/// shared corner centre (the true rounded silhouette), but nothing used to
+/// cut anything *inside* it - so the strip's own "extra" rows (past its
+/// nominal `border_width`, present whenever `corner_radius > border_width`
+/// - see `render_border_top`'s own doc comment) stayed a solid *filled*
+/// quarter-disk out to the centre column/row, then hit `clip_middle_
+/// beyond_thickness`'s hard, unblended rectangular cut at exactly column/
+/// row `radius` - which is essentially the disk's own *most opaque*
+/// point (dead centre, mask ~1.0), not somewhere the curve had already
+/// faded out. The result: a solid wedge of border colour with two straight
+/// inner edges meeting the titlebar/content at a right angle, not a
+/// uniform-width curved ring - confirmed live, zoomed: a clean rectangular
+/// step, not a blend, reported as "squares on the inside corners."
+///
+/// The fix: cut *this* pixel wherever it falls within `border_width` of
+/// the *same* shared centre `blend_corner_pixel` already cut around --
+/// i.e. within `radius - border_width` of it, same smoothstep falloff,
+/// inverted. Combined with the existing outer cut, the strip's corner
+/// becomes a genuine ring of ~`border_width` visible thickness tapering
+/// smoothly to nothing by the point `clip_middle_beyond_thickness`'s own
+/// (already-transparent-by-then) hard cut takes over, instead of jumping
+/// from opaque to transparent in one pixel. Only ever called with `radius
+/// > border_width` (`round_top_corners`/`round_bottom_corners` skip it
+/// otherwise, since there is no ring to speak of - see their own call
+/// sites) - `inner_radius` would otherwise be zero or negative, cutting
+/// the entire disk including the visible outer sliver that's supposed to
+/// remain `border_width` px thick.
+fn carve_inner_corner_pixel(buf: &mut [u8], width: usize, x: usize, y: usize, cx: f32, cy: f32, inner_radius: f32) {
+ let keep = corner_keep_mask(x, y, cx, cy, inner_radius);
+ scale_pixel(buf, width, x, y, 1.0 - keep);
+}
+
/// [`round_top_corners`]'s mirror for the bottom two corners - same
/// construction, corner centres `r` *up* from the bottom instead of down
/// from the top. Same anti-aliasing, same reason - see
-/// [`blend_corner_pixel`]'s own doc comment.
-pub(crate) fn round_bottom_corners(buf: &mut [u8], width: usize, height: usize, radius: u32) {
+/// [`blend_corner_pixel`]'s own doc comment. `inner_radius` is the same
+/// idea as `round_top_corners`' own parameter of the same name - see its
+/// doc comment.
+pub(crate) fn round_bottom_corners(buf: &mut [u8], width: usize, height: usize, radius: u32, inner_radius: Option<u32>) {
let r = (radius as usize).min(width / 2);
if r == 0 {
return;
@@ -178,14 +288,24 @@ pub(crate) fn round_bottom_corners(buf: &mut [u8], width: usize, height: usize,
// pixman.rs`'s own bottom-box centre (`py.clamp(radius, hf - radius)`,
// which clamps to exactly `h - r`) without it.
let cy = height as f32 - rf;
+ // See `round_top_corners`'s matching line for why this is `.filter(|&r|
+ // r > 0)`, not just `.is_some()`.
+ let inner_rf = inner_radius.filter(|&r| r > 0).map(|r| r as f32);
for y in (height - rows)..height {
for x in 0..r {
blend_corner_pixel(buf, width, x, y, rf, cy, rf);
+ if let Some(inner_rf) = inner_rf {
+ carve_inner_corner_pixel(buf, width, x, y, rf, cy, inner_rf);
+ }
}
for x in (width - r)..width {
// See `round_top_corners`'s matching comment for why this is
// `width - r`, not `width - r - 1`.
- blend_corner_pixel(buf, width, x, y, (width - r) as f32, cy, rf);
+ let cx = (width - r) as f32;
+ blend_corner_pixel(buf, width, x, y, cx, cy, rf);
+ if let Some(inner_rf) = inner_rf {
+ carve_inner_corner_pixel(buf, width, x, y, cx, cy, inner_rf);
+ }
}
}
}