diff options
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/decoration.rs | 75 | ||||
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 10 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 3 | ||||
| -rw-r--r-- | crates/wayland/src/udev/platform.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 29 | ||||
| -rw-r--r-- | crates/wayland/src/winit/connect.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 36 |
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; } |