srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/decoration.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-12-24 20:44:00 +0200
committersrdusr <[email protected]>2024-12-24 20:44:00 +0200
commit3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f (patch)
tree71e9bab462b9073444183e2a969ba512cb94559f /crates/wayland/src/decoration.rs
parent331a1a1d3a0d8dfd4c285b755c2e13785c95ee2a (diff)
downloadsrdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.tar.gz
srdwm-3d73ed0f057a555ded67ad58dbe02b36cd4d2d1f.zip
Accumulate wayland-crate additions: capture rendering, focus/Space sync, VT resume, plumbing
Bundles the remaining wayland-crate changes built up here, touching both backends (udev and winit) and the shared input/rendering code: - udev/capture.rs: off-screen Pixman render of an arbitrary (not necessarily on-screen) workspace's window content to a PPM file -- what crates/core's capture-request queue drives, for a workspace switcher's thumbnail previews. wlr-screencopy structurally can't do this (it can only see what an output is presenting), which is why this exists as a separate render path rather than reusing it. - input::focus_window now also raises the window in smithay's own Space, not just core's stacking order - Space is what actually renders on top and what pointer hit-testing reads, so any focus path that skipped this (an IPC "focus" dispatch, concretely) left a window genuinely focused while still rendering, and receiving clicks, underneath whatever was already topmost. Both backends' poll loops now re-sync this after any IPC mutation. - udev/session.rs's VT-switch resume fix (drains a stale pending page flip before reasserting CRTCs) already has its own earlier, cleanly isolated commit - not duplicated here. - Assorted decoration/cursor/rounded-corners/output-management/ screencopy/XWayland changes and their cross-backend wiring. Coarser than the repo's usual one-purpose-per-commit convention, deliberately - see the core-crate sweep commit's own message for why.
Diffstat (limited to 'crates/wayland/src/decoration.rs')
-rw-r--r--crates/wayland/src/decoration.rs203
1 files changed, 167 insertions, 36 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs
index 42d3231..36915e5 100644
--- a/crates/wayland/src/decoration.rs
+++ b/crates/wayland/src/decoration.rs
@@ -11,8 +11,8 @@
use fontdue::{Font, FontSettings};
use std::sync::OnceLock;
-const FONT_PIXELS: f32 = 13.0;
-const TEXT_LEFT_PADDING: f32 = 8.0;
+pub(crate) const FONT_PIXELS: f32 = 13.0;
+pub(crate) const TEXT_LEFT_PADDING: f32 = 8.0;
/// Titlebar buttons are laid out right-aligned in `height`-wide squares --
/// matching `ResizeEdge::hit_test` in `crates/core/src/window.rs`, whose
@@ -26,7 +26,7 @@ const BUTTON_MARGIN: f32 = 0.32;
/// fontconfig query (no new system dependency for something this small) --
/// if none of these resolve, titlebars fall back to solid-color-only, same
/// as before text rendering existed.
-fn find_system_font() -> Option<Font> {
+pub(crate) fn find_system_font() -> Option<Font> {
static FONT: OnceLock<Option<Font>> = OnceLock::new();
FONT.get_or_init(load_any_monospace_font).clone()
}
@@ -89,7 +89,7 @@ fn find_ttf_preferring_mono(dir: &std::path::Path, best: &mut Option<std::path::
}
}
-fn rgb_to_bgra(rgb: (u8, u8, u8), alpha: u8) -> [u8; 4] {
+pub(crate) fn rgb_to_bgra(rgb: (u8, u8, u8), alpha: u8) -> [u8; 4] {
[rgb.2, rgb.1, rgb.0, alpha]
}
@@ -158,6 +158,68 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)],
buf
}
+/// The Snap-Layouts flyout (`crates/wayland/src/snap_flyout.rs`) - a
+/// `columns`-wide grid of labeled cells, one per `SnapZoneKind`. Same
+/// "deliberately plain" bar `render_context_menu` above sets: solid cells,
+/// left-padded text, grid lines and an outer border in one colour - no
+/// live preview thumbnails, no icons.
+pub fn render_snap_flyout(columns: u32, cell_width: u32, cell_height: u32, labels: &[&str], bg: (u8, u8, u8), fg: (u8, u8, u8), border: (u8, u8, u8)) -> Vec<u8> {
+ let (cell_width, cell_height, columns) = (cell_width.max(1) as usize, cell_height.max(1) as usize, columns.max(1) as usize);
+ let rows = labels.len().div_ceil(columns).max(1);
+ let width = cell_width * columns;
+ let height = cell_height * rows;
+ let mut buf = vec![0u8; width * height * 4];
+
+ let bg_px = rgb_to_bgra(bg, 255);
+ for px in buf.chunks_exact_mut(4) {
+ px.copy_from_slice(&bg_px);
+ }
+
+ let font = find_system_font();
+ for (i, label) in labels.iter().enumerate() {
+ let (col, row) = (i % columns, i / columns);
+ let (cell_x, cell_y) = (col * cell_width, row * cell_height);
+ if let Some(font) = &font {
+ let baseline = cell_y as f32 + cell_height as f32 * 0.58;
+ let mut pen_x = cell_x as f32 + TEXT_LEFT_PADDING;
+ for ch in label.chars() {
+ if ch.is_control() {
+ continue;
+ }
+ let (metrics, coverage) = font.rasterize(ch, FONT_PIXELS);
+ if metrics.width > 0 && metrics.height > 0 {
+ let glyph_x = pen_x + metrics.xmin as f32;
+ let glyph_y = baseline - metrics.height as f32 - metrics.ymin as f32;
+ blit_glyph(&mut buf, width, height, glyph_x.round() as i32, glyph_y.round() as i32, &metrics, &coverage, bg, fg);
+ }
+ pen_x += metrics.advance_width;
+ if pen_x as usize >= cell_x + cell_width {
+ break;
+ }
+ }
+ }
+ }
+
+ // Grid lines (including the outer border) drawn last, one colour, so
+ // nothing overdraws them.
+ let border_px = rgb_to_bgra(border, 255);
+ for y in 0..height {
+ for col in 0..=columns {
+ let x = (col * cell_width).min(width - 1);
+ let idx = y * width + x;
+ buf[idx * 4..idx * 4 + 4].copy_from_slice(&border_px);
+ }
+ }
+ for x in 0..width {
+ for row in 0..=rows {
+ let y = (row * cell_height).min(height - 1);
+ let idx = y * width + x;
+ buf[idx * 4..idx * 4 + 4].copy_from_slice(&border_px);
+ }
+ }
+ buf
+}
+
/// The four border strips (top, bottom, left, right) around a window's
/// full rect, `width` thick, drawn *outside* `geometry` - additive to the
/// window's on-screen footprint, the same as a native X11 border, rather
@@ -283,14 +345,14 @@ fn edge_distance(pos: u32, margin: u32, extent: u32) -> u32 {
/// 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> {
+pub fn render_border_top(width: u32, thickness: u32, color: (u8, u8, u8), radius: u32) -> 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_top_corners(&mut buf, width, thickness, CORNER_RADIUS + thickness as u32);
+ round_top_corners(&mut buf, width, thickness, radius + thickness as u32);
buf
}
@@ -306,14 +368,14 @@ pub fn render_border_top(width: u32, thickness: u32, color: (u8, u8, u8)) -> Vec
/// 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> {
+pub fn render_border_bottom(width: u32, thickness: u32, color: (u8, u8, u8), radius: u32) -> 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);
+ round_bottom_corners(&mut buf, width, thickness, radius + thickness as u32);
buf
}
@@ -331,7 +393,7 @@ pub fn render_border_bottom(width: u32, thickness: u32, color: (u8, u8, u8)) ->
/// `true` here same as a borderless one now - reported live as most
/// windows (anything with the default border) looking inconsistently
/// square next to the few borderless ones that were rounded.
-pub fn render_titlebar(width: u32, height: u32, title: &str, background: (u8, u8, u8), foreground: (u8, u8, u8), round_corners: bool) -> Vec<u8> {
+pub fn render_titlebar(width: u32, height: u32, title: &str, background: (u8, u8, u8), foreground: (u8, u8, u8), round_corners: bool, radius: u32) -> Vec<u8> {
let (width, height) = (width.max(1) as usize, height.max(1) as usize);
let bg = rgb_to_bgra(background, 255);
let mut buf = vec![0u8; width * height * 4];
@@ -371,23 +433,20 @@ pub fn render_titlebar(width: u32, height: u32, title: &str, background: (u8, u8
draw_close_icon(&mut buf, width, height, 0, foreground);
}
if round_corners {
- round_top_corners(&mut buf, width, height, CORNER_RADIUS);
+ round_top_corners(&mut buf, width, height, radius);
}
buf
}
-/// How many pixels of each top corner are clipped away by
-/// `round_top_corners`. Small and fixed rather than configurable: this is a
-/// cosmetic nicety, not a feature surface worth a `srd.theme` knob, and a
-/// value this small barely reads as "rounded" if it gets any larger at the
-/// titlebar heights this compositor actually uses.
-///
-/// `pub(crate)`, not private: `rounded_corners.rs` reuses the exact same
-/// value for a decorated window's own content (its bottom two corners,
-/// where the GLES backend can round the client's actual pixels, unlike
-/// the CPU bitmap clip this file does for the titlebar/border), so a
-/// bordered window's curve reads as one continuous radius from titlebar
-/// to content rather than two different ones meeting at a seam.
+/// Default corner radius, in pixels, applied to a window at creation
+/// (`Window::corner_radius`/`ThemeConfig::default_corner_radius`) - kept
+/// here only as this file's own test fixture default now that the real
+/// radius is a live, per-window value (`theme.decorations.border.radius`
+/// in config, `srd set corner_radius <n>` live, `srd.window.
+/// set_corner_radius(n)`/a rule's `corner_radius` action per-window).
+/// `render_border_top`/`render_border_bottom`/`render_titlebar` all take
+/// the real radius as a parameter now rather than reading this directly.
+#[cfg(test)]
pub(crate) const CORNER_RADIUS: u32 = 6;
/// Clips the top-left and top-right corners of a titlebar buffer to a
@@ -405,6 +464,23 @@ pub(crate) const CORNER_RADIUS: u32 = 6;
/// 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.
fn round_top_corners(buf: &mut [u8], width: usize, height: usize, radius: u32) {
let r = (radius as usize).min(width / 2).min(height);
if r == 0 {
@@ -419,12 +495,12 @@ fn round_top_corners(buf: &mut [u8], width: usize, height: usize, radius: u32) {
for y in 0..r {
for x in 0..r {
if is_outside_corner(x, y, r, r) {
- buf[(y * width + x) * 4 + 3] = 0;
+ buf[(y * width + x) * 4..(y * width + x) * 4 + 4].fill(0);
}
}
for x in (width - r)..width {
if is_outside_corner(x, y, width - r - 1, r) {
- buf[(y * width + x) * 4 + 3] = 0;
+ buf[(y * width + x) * 4..(y * width + x) * 4 + 4].fill(0);
}
}
}
@@ -432,7 +508,8 @@ 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.
+/// from the top. Same premultiplied-alpha fix, same reason - see that
+/// function's own doc comment.
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 {
@@ -450,12 +527,12 @@ fn round_bottom_corners(buf: &mut [u8], width: usize, height: usize, radius: u32
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;
+ buf[(y * width + x) * 4..(y * width + x) * 4 + 4].fill(0);
}
}
for x in (width - r)..width {
if is_outside_corner(x, y, width - r - 1, cy) {
- buf[(y * width + x) * 4 + 3] = 0;
+ buf[(y * width + x) * 4..(y * width + x) * 4 + 4].fill(0);
}
}
}
@@ -529,7 +606,7 @@ fn draw_minimize_icon(buf: &mut [u8], width: usize, height: usize, right_offset:
}
#[allow(clippy::too_many_arguments)]
-fn blit_glyph(
+pub(crate) fn blit_glyph(
buf: &mut [u8],
width: usize,
height: usize,
@@ -621,7 +698,7 @@ mod tests {
#[test]
fn fills_background_when_no_text() {
- let buf = render_titlebar(40, 20, "", (0x2e, 0x34, 0x40), (0xec, 0xef, 0xf4), true);
+ let buf = render_titlebar(40, 20, "", (0x2e, 0x34, 0x40), (0xec, 0xef, 0xf4), true, CORNER_RADIUS);
assert_eq!(buf.len(), 40 * 20 * 4);
// Center, not (0,0): the top-left pixel is inside the rounded
// corner `round_top_corners` clips away, so it's transparent by
@@ -644,7 +721,7 @@ mod tests {
let (width, height) = (300u32, srdwm_core::TITLEBAR_HEIGHT);
let bg = (0x2e, 0x34, 0x40);
let fg = (0xec, 0xef, 0xf4);
- let buf = render_titlebar(width, height, "", bg, fg, true);
+ let buf = render_titlebar(width, height, "", bg, fg, true, CORNER_RADIUS);
let frame = srdwm_core::Rect::new(0, 0, width, height);
let (width, height) = (width as usize, height as usize);
@@ -671,7 +748,7 @@ mod tests {
}
let bg = (0x2e, 0x34, 0x40);
let fg = (0xec, 0xef, 0xf4);
- let buf = render_titlebar(200, 30, "Terminal", bg, fg, true);
+ let buf = render_titlebar(200, 30, "Terminal", bg, fg, true, CORNER_RADIUS);
let bg_bytes = rgb_to_bgra(bg, 255);
let changed = buf.chunks_exact(4).any(|px| px != bg_bytes);
assert!(changed, "expected at least one pixel to differ from the background once text is drawn");
@@ -681,7 +758,7 @@ mod tests {
fn empty_title_leaves_buffer_all_background_outside_the_rounded_corners() {
let bg = (0x10, 0x20, 0x30);
let (width, height) = (50, 24);
- let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), true);
+ let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), true, CORNER_RADIUS);
let bg_bytes = rgb_to_bgra(bg, 255);
for (i, px) in buf.chunks_exact(4).enumerate() {
let (x, y) = (i % width as usize, i / width as usize);
@@ -697,7 +774,7 @@ mod tests {
fn corners_are_clipped_but_the_middle_is_not() {
let bg = (0x10, 0x20, 0x30);
let (width, height) = (50, 24);
- let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), true);
+ let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), true, CORNER_RADIUS);
let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3];
// The very corner pixel is well outside the quarter-circle at any
// sane radius - fully clipped.
@@ -713,10 +790,32 @@ mod tests {
}
#[test]
+ fn clipped_corner_pixels_are_fully_premultiplied_zero_not_just_alpha() {
+ // Regression test: `round_top_corners` used to zero only the alpha
+ // byte of a clipped pixel, leaving the opaque background RGB behind
+ // it untouched. This buffer is `Fourcc::Argb8888`, which both
+ // Wayland/`wl_shm` and Pixman treat as premultiplied - Pixman's own
+ // `OVER` compositing (`result = src + dst * (1 - src_alpha)`) does
+ // not treat `alpha=0, rgb=<something>` as "contributes nothing": it
+ // adds that stale, un-premultiplied `rgb` straight through, so the
+ // "clipped" corner still rendered fully opaque and every window's
+ // top corners read as square regardless of a nonzero radius --
+ // confirmed live, pixel-by-pixel, zero transparency anywhere in a
+ // real window's corner. A genuinely transparent premultiplied pixel
+ // is `(0, 0, 0, 0)` in every channel, not just alpha.
+ let bg = (0x10, 0x20, 0x30);
+ let (width, height) = (50, 24);
+ let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), true, CORNER_RADIUS);
+ let px_at = |x: usize, y: usize| &buf[(y * width as usize + x) * 4..(y * width as usize + x) * 4 + 4];
+ assert_eq!(px_at(0, 0), [0, 0, 0, 0], "top-left corner pixel must be fully zeroed (premultiplied transparent), not just alpha");
+ assert_eq!(px_at(width as usize - 1, 0), [0, 0, 0, 0], "top-right corner pixel must be fully zeroed (premultiplied transparent), not just alpha");
+ }
+
+ #[test]
fn round_corners_false_leaves_the_top_corners_square() {
let bg = (0x10, 0x20, 0x30);
let (width, height) = (50, 24);
- let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), false);
+ let buf = render_titlebar(width, height, "", bg, (0xff, 0xff, 0xff), false, CORNER_RADIUS);
let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3];
assert_eq!(alpha_at(0, 0), 255, "top-left corner should stay square when round_corners is false");
assert_eq!(alpha_at(width as usize - 1, 0), 255, "top-right corner should stay square when round_corners is false");
@@ -733,7 +832,7 @@ mod tests {
// change now depends on.
let color = (0x40, 0x50, 0x60);
let (width, thickness) = (60, 2);
- let buf = render_border_top(width, thickness, color);
+ let buf = render_border_top(width, thickness, color, CORNER_RADIUS);
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");
@@ -748,7 +847,7 @@ mod tests {
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 buf = render_border_bottom(width, thickness, color, CORNER_RADIUS);
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");
@@ -788,4 +887,36 @@ mod tests {
assert_eq!(alpha_at(0, 27), 255);
assert_eq!(alpha_at(99, 27), 255);
}
+
+ #[test]
+ fn snap_flyout_is_sized_for_a_full_grid_of_labels() {
+ let labels = ["Left Half", "Right Half", "Top Left", "Top Right", "Bottom Left", "Bottom Right"];
+ let buf = render_snap_flyout(3, 90, 60, &labels, (0x2e, 0x34, 0x40), (0xff, 0xff, 0xff), (0x10, 0x10, 0x10));
+ // 3 columns x 2 rows (6 labels / 3 columns, rounded up).
+ assert_eq!(buf.len(), (90 * 3) * (60 * 2) * 4);
+ }
+
+ #[test]
+ fn snap_flyout_border_is_opaque_at_every_outer_edge() {
+ let labels = ["A", "B", "C", "D", "E", "F"];
+ let (cell_w, cell_h) = (90, 60);
+ let buf = render_snap_flyout(3, cell_w, cell_h, &labels, (0, 0, 0), (0xff, 0xff, 0xff), (0x99, 0x99, 0x99));
+ let (width, height) = (cell_w * 3, cell_h * 2);
+ let alpha_at = |x: usize, y: usize| buf[(y * width as usize + x) * 4 + 3];
+ assert_eq!(alpha_at(0, 0), 255);
+ assert_eq!(alpha_at(width as usize - 1, 0), 255);
+ assert_eq!(alpha_at(0, height as usize - 1), 255);
+ assert_eq!(alpha_at(width as usize - 1, height as usize - 1), 255);
+ }
+
+ #[test]
+ fn snap_flyout_has_an_internal_grid_line_between_columns() {
+ let labels = ["A", "B", "C", "D", "E", "F"];
+ let (cell_w, cell_h) = (90, 60);
+ let buf = render_snap_flyout(3, cell_w, cell_h, &labels, (0, 0, 0), (0xff, 0xff, 0xff), (0x99, 0x99, 0x99));
+ let width = cell_w * 3;
+ // The boundary between column 0 and column 1, away from the outer border.
+ let idx = (30 * width as usize + cell_w as usize) * 4;
+ assert_eq!(buf[idx + 3], 255, "column boundary must be drawn, not just the outer border");
+ }
}