From 9fb4f996fe76dc5eeee861e463e62fdb6f7de816 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Fri, 23 May 2025 02:31:00 +0200 Subject: Fix the border ring's inner cut using the wrong circle for undecorated windows carve_inner_corner_pixel (an earlier ring fix) cut the border strip's own disk using an inner circle at the *same* centre as the outer cut, radius - border_width - correct when a titlebar sits underneath (it shares that exact circle, by construction), but wrong whenever client content sits underneath instead: content's own rounded-corner mask (rounded_corners_pixman.rs's apply_corner_mask) is centred radius from *its own* buffer's edges, and that buffer starts border_width rows/columns inside the border strip's - a same-radius circle offset (border_width, border_width) diagonally from the border's own outer one, not a smaller concentric one. Reusing the titlebar-style ring left a real, if very small, gap along part of the seam and a thin sliver of double coverage along the rest - reported live, at extreme zoom against a solid-colour wallpaper (chosen specifically to make a sub-pixel gap easy to spot against, unlike the usual desktop image): "very tiny gaps... corner radius does not match." round_top_corners/round_bottom_corners's own `inner_radius: Option` parameter is now `inner: Option`, an explicit (center_row, center_col, radius) rather than an implicit "same centre, smaller radius" - InnerRing's own doc comment has the full geometry for both cases. render_border_top gained a `decorated` parameter to pick the right one (titlebar-aligned when true, content-aligned - centre shifted by border_width on both axes, radius unchanged - when false); render_border_bottom always uses the content-aligned ring, since this compositor never draws a bottom titlebar. apply_corner_mask (rounded_corners_pixman.rs) made pub(crate) so the new regression test can build a real masked content buffer via the actual production function, not a reimplementation of its math. border_top_and_content_mask_have_no_gap_along_the_corner_diagonal_when_undecorated checks coverage along the diagonal ray between the two circles' centres - the direction they're actually offset along, and so the worst case for a gap opening up between them - against the real render_border_top/apply_corner_mask output, not a hand-rederived formula. --- crates/wayland/src/decoration/tests.rs | 88 ++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) (limited to 'crates/wayland/src/decoration/tests.rs') diff --git a/crates/wayland/src/decoration/tests.rs b/crates/wayland/src/decoration/tests.rs index 4507a05..c8eb9b2 100644 --- a/crates/wayland/src/decoration/tests.rs +++ b/crates/wayland/src/decoration/tests.rs @@ -468,7 +468,7 @@ fn border_top_rounds_its_own_top_corners_to_match_the_titlebar() { // change now depends on. let color = (0x40, 0x50, 0x60); let (width, thickness) = (60, 2); - let buf = render_border_top(width, thickness, color, CORNER_RADIUS); + let buf = render_border_top(width, thickness, color, CORNER_RADIUS, true); let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3]; assert_eq!(alpha_at(0, 0), 0, "top-left corner pixel should be clipped"); assert_eq!(alpha_at(width as usize - 1, 0), 0, "top-right corner pixel should be clipped"); @@ -500,7 +500,7 @@ fn border_top_and_titlebar_corners_meet_without_a_seam() { // immediately below it. let color = (0x40, 0x50, 0x60); let (width, thickness, radius) = (60, 4, 6); - let border = render_border_top(width, thickness, color, radius); + let border = render_border_top(width, thickness, color, radius, true); let titlebar = render_titlebar(width, 24, "", color, (0xff, 0xff, 0xff), true, radius, thickness, true, None, false, false, false, None, true, false); let border_alpha_at = |x: usize| border[((thickness as usize - 1) * width as usize + x) * 4 + 3]; let titlebar_alpha_at = |xt: usize| titlebar[xt * 4 + 3]; @@ -546,6 +546,86 @@ fn border_top_and_titlebar_corners_meet_without_a_seam() { } } +#[test] +fn border_top_and_content_mask_have_no_gap_along_the_corner_diagonal_when_undecorated() { + // The undecorated counterpart to `border_top_and_titlebar_corners_ + // meet_without_a_seam`: an undecorated (CSD) window has no titlebar + // band under the border's own "extra" rows - client content sits + // there directly, masked by `rounded_corners_pixman::apply_corner_ + // mask` at its own independently-centred circle, not the titlebar's + // shared one. `InnerRing`'s own doc comment has the full geometry: + // content's mask circle is offset `(thickness, thickness)` from the + // border's own outer circle - two same-radius circles with + // *different* centres, not one circle at two different radii, so + // "which single row/column is the seam" (the titlebar test's own + // approach) doesn't carry over cleanly here. What has to hold + // instead: at every point *along the diagonal ray* from the true + // corner outward (the direction the two circles' centres are + // actually offset along, so the worst case for a gap between them), + // at least one of the two curves covers it. Verified by hand first + // (see this fix's own commit) that the two circles' coverage + // actually overlaps continuously along this ray for a real + // radius/thickness pair; this checks that promise against the real + // rendering functions, not a reimplementation of their math. + let color = (0x40, 0x50, 0x60); + let (width, thickness, radius) = (60u32, 4u32, 6u32); + let border = render_border_top(width, thickness, color, radius, false); + let border_h = (thickness as usize).max(radius as usize); + // A real content buffer, masked the same way `masked_content_buffer` + // masks a window's actual composited surface tree - opaque + // premultiplied white everywhere before masking, so alpha alone + // tells the whole story. + let (cw, ch) = (width - 2 * thickness, radius * 4); + let mut content = vec![0u8; (cw * ch * 4) as usize]; + for px in content.chunks_exact_mut(4) { + px.copy_from_slice(&[0xff, 0xff, 0xff, 0xff]); + } + crate::rounded_corners_pixman::apply_corner_mask(&mut content, cw as i32, ch as i32, cw as i32 * 4, radius as f32, crate::rounded_corners::RoundedCorners::ALL); + // Both sampled in the *same* global coordinate frame (border's own + // origin is the true corner, `(0, 0)`) - `None` past either + // buffer's own edge, same as that position genuinely contributing no + // coverage of its own there. + let border_alpha_at = |row: i32, col: i32| -> u8 { + if row < 0 || col < 0 || row as usize >= border_h || col as usize >= width as usize { + return 0; + } + border[(row as usize * width as usize + col as usize) * 4 + 3] + }; + let content_alpha_at = |row: i32, col: i32| -> u8 { + let (r, c) = (row - thickness as i32, col - thickness as i32); + if r < 0 || c < 0 || r as usize >= ch as usize || c as usize >= cw as usize { + return 0; + } + content[(r as usize * cw as usize + c as usize) * 4 + 3] + }; + // Every half-pixel step along the 45-degree diagonal from just past + // the true corner tip (where both curves are correctly, deliberately + // transparent - that IS the rounding) out to comfortably past where + // content's own circle takes over full coverage. + let mut steps = 0; + let mut min_combined = 255u8; + let mut min_at = 0.0f32; + let mut s = radius as f32 - 1.0; + while s <= (radius + thickness + radius) as f32 - 2.0 { + let (row, col) = ((s / std::f32::consts::SQRT_2).round() as i32, (s / std::f32::consts::SQRT_2).round() as i32); + let combined = border_alpha_at(row, col).max(content_alpha_at(row, col)); + if combined < min_combined { + min_combined = combined; + min_at = s; + } + steps += 1; + s += 0.5; + } + assert!(steps > 10, "sanity: the sampled range should cover more than a couple of points"); + assert!(min_combined > 160, "diagonal distance {min_at} from the true corner: neither border (alpha={}) nor content (alpha={}) covers this point - a real gap", { + let (row, col) = ((min_at / std::f32::consts::SQRT_2).round() as i32, (min_at / std::f32::consts::SQRT_2).round() as i32); + border_alpha_at(row, col) + }, { + let (row, col) = ((min_at / std::f32::consts::SQRT_2).round() as i32, (min_at / std::f32::consts::SQRT_2).round() as i32); + content_alpha_at(row, col) + }); +} + #[test] fn border_top_visible_rows_decorated_shows_the_whole_taller_buffer() { let (row0, rows, shift) = border_top_visible_rows(true, 4, 11); @@ -605,7 +685,7 @@ fn border_top_extra_rows_are_transparent_outside_the_corners() { // live-confirmed bug, the top-strip half of the pair). let color = (0x40, 0x50, 0x60); let (width, thickness, radius) = (60, 2, 6); - let buf = render_border_top(width, thickness, color, radius); + let buf = render_border_top(width, thickness, color, radius, true); let height = (thickness as usize).max(radius as usize); assert_eq!(buf.len(), width as usize * height * 4, "buffer must actually be the taller max(thickness, radius) height"); let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3]; @@ -651,7 +731,7 @@ fn border_top_curve_actually_closes_within_the_side_strips_own_width() { // edges can give at their closest approach. let color = (0x40, 0x50, 0x60); let (width, thickness, radius) = (60u32, 3u32, 6u32); - let buf = render_border_top(width, thickness, color, radius); + let buf = render_border_top(width, thickness, color, radius, true); let height = (thickness as usize).max(radius as usize); let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3]; let alpha = alpha_at(thickness as usize - 1, height - 1); -- cgit v1.2.3