srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-08-23 22:23:00 +0200
committersrdusr <[email protected]>2024-08-23 22:23:00 +0200
commit281378656bdf9520e5f89d31864b9b2f822d4e3e (patch)
treebb858d6c79ae8ca2c423cd366c10ded90fa8a97e /crates/wayland
parent78da614d67c09e197dffc1cebb1c5ce2357c7128 (diff)
downloadsrdwm-281378656bdf9520e5f89d31864b9b2f822d4e3e.tar.gz
srdwm-281378656bdf9520e5f89d31864b9b2f822d4e3e.zip
Round the bottom border strip's corners to match the top
MISSING.md listed the border frame's bottom/left/right strips as staying square while the top one (and the titlebar above it) rounds -- "unrelated to client content rounding... not attempted." The left/ right strips genuinely can't participate (border_strips' geometry has them span only the height between the top and bottom strips, no corner to round), but the bottom strip is exactly the same shape as the top one and had no reason left to stay square. decoration::render_border_bottom mirrors render_border_top exactly (round_bottom_corners mirrors round_top_corners), cached the same way in a new border_bottom_decorations map, and drawn in both render loops via the same all-or-nothing occlusion check the top strip already uses - pulled out of the left/right strips' per-fragment occlusion splitting into its own dedicated bitmap path, matching top's existing trade-off (cropping a rounded bitmap's source rect per fragment is real extra work for a strip this thin) rather than inventing a new one. One real bug caught before it shipped: round_bottom_corners' corner- centre math (height - r - 1) panics on unsigned underflow whenever the radius clamp lands on the strip's own full height (a real, common case - a 2px-thick test strip hits it immediately). Fixed by computing the centre as a signed offset instead, mirroring how the existing dx/dy distance math already avoids the same class of issue.
Diffstat (limited to 'crates/wayland')
-rw-r--r--crates/wayland/src/decoration.rs75
-rw-r--r--crates/wayland/src/state/lifecycle.rs10
-rw-r--r--crates/wayland/src/state/mod.rs3
-rw-r--r--crates/wayland/src/udev/platform.rs1
-rw-r--r--crates/wayland/src/udev/render.rs29
-rw-r--r--crates/wayland/src/winit/connect.rs1
-rw-r--r--crates/wayland/src/winit/render.rs36
7 files changed, 132 insertions, 23 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs
index 2888d69..42d3231 100644
--- a/crates/wayland/src/decoration.rs
+++ b/crates/wayland/src/decoration.rs
@@ -276,9 +276,13 @@ fn edge_distance(pos: u32, margin: u32, extent: u32) -> u32 {
/// too, at a radius `width` pixels larger than the titlebar's (so the cut
/// continues outward from the titlebar's own, rather than starting over),
/// is what makes a bordered window's corner read as one continuous curve
-/// instead of a rounded titlebar sitting inside a square frame. The other
-/// three strips (bottom/left/right) don't participate in any visible
-/// corner and stay plain solid fills - see their render call sites.
+/// instead of a rounded titlebar sitting inside a square frame.
+///
+/// [`render_border_bottom`] gives the bottom strip the matching treatment
+/// for its own two corners. The left/right strips don't participate in any
+/// visible corner at all (`border_strips`' geometry has them span only the
+/// height *between* the top and bottom strips) and stay plain solid fills
+/// - see their render call sites.
pub fn render_border_top(width: u32, thickness: u32, color: (u8, u8, u8)) -> Vec<u8> {
let (width, thickness) = (width.max(1) as usize, thickness.max(1) as usize);
let bg = rgb_to_bgra(color, 255);
@@ -290,6 +294,29 @@ pub fn render_border_top(width: u32, thickness: u32, color: (u8, u8, u8)) -> Vec
buf
}
+/// [`render_border_top`]'s mirror for the bottom strip - same construction,
+/// its own two corners (bottom-left/bottom-right) cut instead. Reported
+/// live, alongside the top-corner work: a bordered window's bottom two
+/// corners still read as square next to the now-rounded top ones, the same
+/// "inconsistently square" complaint that motivated rounding the top strip
+/// in the first place.
+///
+/// Handled as one all-or-nothing bitmap rather than folded into the
+/// left/right strips' per-fragment occlusion splitting (`visible_border_
+/// fragments`) - the same trade-off `render_border_top`'s own call site
+/// already makes and for the same reason: cropping a rounded bitmap's
+/// source rect per fragment is real extra work for a strip this thin.
+pub fn render_border_bottom(width: u32, thickness: u32, color: (u8, u8, u8)) -> Vec<u8> {
+ let (width, thickness) = (width.max(1) as usize, thickness.max(1) as usize);
+ let bg = rgb_to_bgra(color, 255);
+ let mut buf = vec![0u8; width * thickness * 4];
+ for px in buf.chunks_exact_mut(4) {
+ px.copy_from_slice(&bg);
+ }
+ round_bottom_corners(&mut buf, width, thickness, CORNER_RADIUS + thickness as u32);
+ buf
+}
+
/// Renders a `width x height` BGRA8 buffer: filled with `background`, with
/// `title` drawn left-aligned in `foreground` (best-effort glyph layout --
/// no text shaping/kerning, adequate for the ASCII-heavy titles window
@@ -403,6 +430,37 @@ fn round_top_corners(buf: &mut [u8], width: usize, height: usize, radius: u32) {
}
}
+/// [`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.
+fn round_bottom_corners(buf: &mut [u8], width: usize, height: usize, radius: u32) {
+ let r = (radius as usize).min(width / 2).min(height);
+ if r == 0 {
+ return;
+ }
+ // `cy` as a signed offset, not a `usize` - `height - r` can be exactly
+ // `0` (a strip whose radius clamp landed on its own full height, same
+ // as `round_top_corners` allows for `r == height`), which would
+ // underflow a plain `usize` subtraction one step further below.
+ let is_outside_corner = |x: usize, y: usize, cx: usize, cy: i64| -> bool {
+ let (dx, dy) = (x as i64 - cx as i64, y as i64 - cy);
+ (dx * dx + dy * dy) as u64 > (r * r) as u64
+ };
+ let cy = height as i64 - r as i64 - 1;
+ for y in (height - r)..height {
+ for x in 0..r {
+ if is_outside_corner(x, y, r, cy) {
+ buf[(y * width + x) * 4 + 3] = 0;
+ }
+ }
+ for x in (width - r)..width {
+ if is_outside_corner(x, y, width - r - 1, cy) {
+ buf[(y * width + x) * 4 + 3] = 0;
+ }
+ }
+ }
+}
+
/// Sets one pixel to `color` if it falls inside the buffer - every icon
/// drawn below goes through this so none of them need their own bounds
/// checks.
@@ -687,6 +745,17 @@ mod tests {
}
#[test]
+ fn border_bottom_rounds_its_own_bottom_corners() {
+ let color = (0x40, 0x50, 0x60);
+ let (width, thickness) = (60, 2);
+ let buf = render_border_bottom(width, thickness, color);
+ let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3];
+ assert_eq!(alpha_at(0, thickness as usize - 1), 0, "bottom-left corner pixel should be clipped");
+ assert_eq!(alpha_at(width as usize - 1, thickness as usize - 1), 0, "bottom-right corner pixel should be clipped");
+ assert_eq!(alpha_at(width as usize / 2, 0), 255, "centre of the strip must stay opaque");
+ }
+
+ #[test]
fn context_menu_is_one_row_tall_per_item() {
let items = [("Minimize", false), ("Maximize", false), ("Always on Top", false), ("Close", false)];
let buf = render_context_menu(160, 28, &items, (0x2e, 0x34, 0x40), (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10));
diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs
index f7ae40a..0d301dc 100644
--- a/crates/wayland/src/state/lifecycle.rs
+++ b/crates/wayland/src/state/lifecycle.rs
@@ -109,8 +109,17 @@ impl CompState {
} else {
self.border_top_decorations.remove(&id);
}
+ if strips[1].width > 0 && strips[1].height > 0 {
+ let data = decoration::render_border_bottom(strips[1].width, w.border_width, color);
+ let buffer =
+ MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (strips[1].width as i32, w.border_width as i32), 1, Transform::Normal, None);
+ self.border_bottom_decorations.insert(id, buffer);
+ } else {
+ self.border_bottom_decorations.remove(&id);
+ }
} else {
self.border_top_decorations.remove(&id);
+ self.border_bottom_decorations.remove(&id);
}
// No shadow for a maximized/fullscreen window: it already reaches
// (or, for fullscreen, exceeds) the monitor's own edge, so there is
@@ -138,6 +147,7 @@ impl CompState {
}
self.decorations.remove(&id);
self.border_top_decorations.remove(&id);
+ self.border_bottom_decorations.remove(&id);
self.shadow_buffers.remove(&id);
self.border_side_buffers.remove(&id);
self.last_synced_size.remove(&id);
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index ff7ef74..834cf6a 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -237,6 +237,9 @@ pub(crate) struct CompState {
/// `elements::border_side_render_element`'s doc comment for the damage-
/// tracking reason a per-frame rebuild was wrong in the first place.
pub(crate) border_top_decorations: HashMap<WindowId, MemoryRenderBuffer>,
+ /// [`Self::border_top_decorations`]'s mirror for the bottom strip's own
+ /// two corners - same cache, same trigger points, same reasoning.
+ pub(crate) border_bottom_decorations: HashMap<WindowId, MemoryRenderBuffer>,
/// A window's drop-shadow bitmap (`decoration::shadow_bitmap`), cached
/// the same way and at the same trigger points as `border_top_decorations`
/// - rebuilt only on creation or a real size change, not per frame, for
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index 8043e20..f562da2 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -178,6 +178,7 @@ impl UdevPlatform {
dead_layer_surfaces: HashSet::new(),
decorations: HashMap::new(),
border_top_decorations: HashMap::new(),
+ border_bottom_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
rounded_corners_program: None,
content_epoch: HashMap::new(),
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 65c21dd..caf0e0e 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -236,12 +236,13 @@ impl CompState {
if w.border_width > 0 {
let color = crate::state::effective_border_color(w.border_color, focused == Some(id));
let strips = decoration::border_strips(geom, w.border_width);
- // Strip 0 (top) rounded to match the titlebar under
- // it - see `render_border_top`'s doc comment - so
- // it's a cached bitmap (rebuilt only in
+ // Strips 0/1 (top/bottom) rounded on their own two
+ // corners - see `render_border_top`/
+ // `render_border_bottom`'s doc comments - so both
+ // are cached bitmaps (rebuilt only in
// `redraw_decoration_buffer`, same as the titlebar
// itself), not rasterized fresh here every frame.
- // Not fragment-clipped like the other three strips
+ // Not fragment-clipped like the left/right strips
// below - cropping a bitmap's source rect per
// fragment is real extra work for a strip that's
// only `border_width` pixels tall to begin with, so
@@ -257,9 +258,21 @@ impl CompState {
}
}
}
- // The other three strips are persistent
- // `SolidColorBuffer`s updated in place, not rebuilt
- // with a fresh `Id` every frame - see
+ // Same all-or-nothing bitmap treatment as the top
+ // strip, for its own two corners - see
+ // `decoration::render_border_bottom`'s doc comment.
+ if strips[1].width > 0 && strips[1].height > 0 && !strips[1].subtract_all(&occluders).is_empty() {
+ if let Some(buffer) = self.border_bottom_decorations.get(&id) {
+ let pos = ((strips[1].x - origin.x) as f64, (strips[1].y - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Err(e) => log::warn!("udev: failed to import bottom border buffer: {e}"),
+ }
+ }
+ }
+ // The remaining two strips (left/right) are
+ // persistent `SolidColorBuffer`s updated in place,
+ // not rebuilt with a fresh `Id` every frame - see
// `elements::border_side_render_element`'s doc
// comment for why that distinction is load-bearing
// for damage tracking, not cosmetic. Each strip is
@@ -269,7 +282,7 @@ impl CompState {
// here.
let pool = self.border_side_buffers.entry(id).or_default();
let mut buf_index = 0;
- for strip in &strips[1..] {
+ for strip in &strips[2..] {
if strip.width == 0 || strip.height == 0 {
continue;
}
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
index 61f6dd1..d771d1f 100644
--- a/crates/wayland/src/winit/connect.rs
+++ b/crates/wayland/src/winit/connect.rs
@@ -149,6 +149,7 @@ impl WaylandPlatform {
dead_layer_surfaces: HashSet::new(),
decorations: HashMap::new(),
border_top_decorations: HashMap::new(),
+ border_bottom_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
rounded_corners_program,
content_epoch: HashMap::new(),
diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs
index 902aed4..3ab3354 100644
--- a/crates/wayland/src/winit/render.rs
+++ b/crates/wayland/src/winit/render.rs
@@ -162,17 +162,18 @@ impl WaylandPlatform {
if w.border_width > 0 {
let color = crate::state::effective_border_color(w.border_color, focused == Some(id));
let strips = decoration::border_strips(geom, w.border_width);
- // Strip 0 (top) is rounded to match the titlebar underneath
- // it - see `render_border_top`'s doc comment - so it's a
- // cached bitmap (rebuilt only in `redraw_decoration_buffer`,
- // same as the titlebar itself), not rasterized fresh here
- // every frame; the other three don't touch a rounded corner
- // and stay persistent solid-colour buffers instead - see
- // `elements::border_side_render_element`'s doc comment for
- // why a per-frame rebuild of either was a real, continuous
- // cost, not a cosmetic one. Not fragment-clipped like the
- // other three below - see the matching comment in
- // `udev/render.rs` for why the top strip only gets the cheaper
+ // Strips 0/1 (top/bottom) are rounded on their own two
+ // corners - see `render_border_top`/`render_border_bottom`'s
+ // doc comments - so both are cached bitmaps (rebuilt only in
+ // `redraw_decoration_buffer`, same as the titlebar itself),
+ // not rasterized fresh here every frame; the remaining two
+ // (left/right) never touch a corner and stay persistent
+ // solid-colour buffers instead - see `elements::
+ // border_side_render_element`'s doc comment for why a
+ // per-frame rebuild of either was a real, continuous cost,
+ // not a cosmetic one. Not fragment-clipped like the left/
+ // right strips below - see the matching comment in
+ // `udev/render.rs` for why top/bottom only get the cheaper
// all-or-nothing occlusion check.
if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() {
if let Some(buffer) = self.state.border_top_decorations.get(&id) {
@@ -182,9 +183,20 @@ impl WaylandPlatform {
}
}
}
+ // Same all-or-nothing bitmap treatment as the top strip,
+ // for its own two corners - see `decoration::
+ // render_border_bottom`'s doc comment.
+ if strips[1].width > 0 && strips[1].height > 0 && !strips[1].subtract_all(&occluders).is_empty() {
+ if let Some(buffer) = self.state.border_bottom_decorations.get(&id) {
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[1].x as f64, strips[1].y as f64), buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))),
+ Err(e) => log::warn!("failed to import bottom border buffer for window {id}: {e}"),
+ }
+ }
+ }
let pool = self.state.border_side_buffers.entry(id).or_default();
let mut buf_index = 0;
- for strip in &strips[1..] {
+ for strip in &strips[2..] {
if strip.width == 0 || strip.height == 0 {
continue;
}