1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
//! Rounding a rasterized titlebar/border bitmap's own top or bottom
//! corners to a quarter-circle, by fading the pixels outside it to fully
//! transparent - the CPU-bitmap equivalent of `rounded_corners.rs`'s GLES
//! fragment shader, for the software-only udev/Pixman render path. Shared
//! by `titlebar.rs` and `border.rs`, which both cut corners out of their
//! own otherwise-independent buffers and need the two curves to agree
//! exactly where they meet.
/// Clips the top-left and top-right corners of a titlebar buffer to a
/// quarter-circle by making the pixels outside it fully transparent, so
/// whatever's behind (the desktop, on every top-level window) shows through
/// instead of a hard square corner.
///
/// Only the *top* corners: the titlebar's bottom edge meets the window's
/// content, which this compositor has no way to clip (content is rendered
/// entirely by the client) - rounding that seam too would need a
/// compositor-wide clip mask over arbitrary client buffers, a much larger
/// change than this cosmetic pass. Real desktops mostly round this the same
/// way: only the outermost corners of a window, not every internal seam.
///
/// Hard cutoff rather than an anti-aliased edge, matching this codebase's
/// existing pixel-art aesthetic elsewhere (the cursor bitmaps) rather than
/// mixing rendering styles for one corner treatment.
///
/// Zeroes all four BGRA bytes for a cut pixel, not just alpha: this buffer
/// is `Fourcc::Argb8888`, which both Wayland/`wl_shm` and Pixman treat as
/// premultiplied - 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 the opaque titlebar-background
/// RGB behind while zeroing only alpha produced a byte pattern Pixman's own
/// `OVER` compositing (`result = src + dst * (1 - src_alpha)`) does not
/// actually treat as "nothing here": with `src_alpha = 0` the formula still
/// adds the stale, un-premultiplied `src` RGB straight through, so the
/// "cut" pixel came out opaque and the corner still read as square --
/// confirmed live, pixel-by-pixel, no visible transparency anywhere in a
/// window's real top corner despite this function running and a nonzero
/// radius. `rounded_corners_pixman.rs`'s `apply_corner_mask` - the
/// equivalent mask for client *content* - already gets this right (scales
/// all four bytes together); this was the one corner-rounding path in the
/// codebase that didn't match it.
/// `center_row` is which row of *this* buffer's own local coordinates the
/// corner circle's centre sits on - not always `radius` itself. A plain
/// titlebar with nothing above it passes `radius as i32` (the ordinary
/// case: the circle's top tip is this buffer's own row 0, same as this
/// function always assumed before `center_row` existed). A border-top
/// strip sitting `thickness` rows *above* the titlebar it visually
/// continues into needs the *same* radius and the *same* circle - not a
/// same-centre-different-radius circle of its own, which is what passing
/// `radius + thickness` here used to do (see the doc comment on
/// `render_border_top`'s call site for why that was tried first). Two
/// concentric circles of different radii do not meet smoothly at any
/// boundary between them: at the exact seam, one buffer's mask is
/// computed against one radius and the other buffer's mask is computed
/// one pixel later against a different radius, producing a visible jump
/// rather than a continuous curve - confirmed live, screenshotted at
/// actual render resolution, not just reasoned about: the titlebar-to-
/// border seam showed a hard stepped notch, not a curve. Since a border
/// strip's own row 0 already sits at the *true* top of the combined
/// shape, it passes `radius as i32` too (unshifted) - it's the titlebar,
/// 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) {
let r = (radius as usize).min(width / 2);
if r == 0 {
return;
}
let rf = r as f32;
let cy = center_row as f32;
// 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;
// rows above `center_row - r` or at/below `center_row` are either
// already past the transparent tip or already fully inside the
// shape, and calling `blend_corner_pixel` there would either be a
// wasted no-op (large `dist`, `mask >= 1`) or - critically, for a
// *tall* buffer whose straight edge extends far past the corner --
// wrongly compute a huge `dist` from being far below the centre and
// clip an ordinary straight-edge pixel to transparent. The original
// unshifted version of this function avoided that the same way, by
// simply never iterating past row `r`; this is that same bound,
// 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);
for y in y_lo..y_hi {
for x in 0..r {
blend_corner_pixel(buf, width, x, y, rf, cy, rf);
}
for x in (width - r)..width {
blend_corner_pixel(buf, width, x, y, (width - r - 1) as f32, cy, rf);
}
}
}
/// Multiplies the pixel at `(x, y)` by a smoothed 0..1 mask based on its
/// distance from `(cx, cy)` versus `radius` - `1` (unchanged) well inside
/// the circle, `0` (fully transparent) well outside it, blended over a ~2px
/// band at the boundary. Same anti-aliasing technique `rounded_corners.rs`'s
/// GLES fragment shader already uses for content rounding
/// (`smoothstep(radius - 1.0, radius + 1.0, dist)`), applied here to a CPU
/// bitmap pixel by pixel instead of a per-fragment shader.
///
/// The previous version of both callers did a hard binary cut instead --
/// fully opaque or fully transparent, nothing between - which read as a
/// jagged single-pixel "break" in the border line rather than a curve,
/// especially in a border strip only a couple of rows tall (the common
/// case: `border_width` is usually 2-3px) where there's no room for the
/// eye to average a staircase into something that looks round. Reported
/// live as "line breaks" right where a window's border met its curved
/// corner.
///
/// `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 - same reasoning
/// `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) {
let (dx, dy) = (x as f32 - cx, y as f32 - 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));
if mask >= 1.0 {
return;
}
let idx = (y * width + x) * 4;
if mask <= 0.0 {
buf[idx..idx + 4].fill(0);
return;
}
for c in &mut buf[idx..idx + 4] {
*c = (*c as f32 * mask).round() as u8;
}
}
/// [`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) {
let r = (radius as usize).min(width / 2);
if r == 0 {
return;
}
// See `round_top_corners`' matching comment: `r` (the real corner
// radius) must stay unclamped by `height`, or a strip thinner than the
// radius cuts its own separate, too-tight arc instead of continuing the
// titlebar's. `rows` is just how many of that circle's rows this
// buffer actually has room for.
let rows = r.min(height);
let rf = r as f32;
// As a float, not the signed-`i64`-offset trick the hard-cut version
// needed to avoid a `usize` underflow - `blend_corner_pixel` already
// takes float centres, so `height - r - 1` going negative when `r >
// height` (the strip-thinner-than-radius case above) is just a
// negative `f32`, no special-casing required.
let cy = height as f32 - rf - 1.0;
for y in (height - rows)..height {
for x in 0..r {
blend_corner_pixel(buf, width, x, y, rf, cy, rf);
}
for x in (width - r)..width {
blend_corner_pixel(buf, width, x, y, (width - r - 1) as f32, cy, rf);
}
}
}
|