srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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;
}