diff options
| author | srdusr <[email protected]> | 2026-01-28 14:20:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-01-28 14:20:00 +0200 |
| commit | d16f7dab25bd9508271017bc6dc15024c30cbf71 (patch) | |
| tree | 86162f301d361cea5aced086e644707a7366c261 /crates/wayland/src/decoration | |
| parent | 393f886eb9e275c3b7a9e3ef396092ba71297dfb (diff) | |
| download | srdwm-d16f7dab25bd9508271017bc6dc15024c30cbf71.tar.gz srdwm-d16f7dab25bd9508271017bc6dc15024c30cbf71.zip | |
Redesign the titlebar right-click menu: real separators/headers, live customization
Reported live: "looks very ugly currently and some of it doesn't make
sense." Both were real. Every row, including a bare divider, took one
full TITLEBAR_HEIGHT slot, so a separator was a 1px hairline in the
middle of 32px of empty space; "Move to Workspace" faked a section
caption by embedding box-drawing characters directly in an ordinary
item's label, which rendered - and behaved, until the click-dispatch
site's own special case - exactly like a clickable row that did
nothing. Separately, "Floating" was always offered even though
Window::floating only affects the "tiling" layout: toggling it under
this project's own default "dynamic" layout visibly changes nothing,
reading as a broken control rather than an inapplicable one.
ContextMenu (crates/core/src/context_menu.rs) gained real Separator
(9px) and Header (22px, non-interactive, dimmed) row kinds with their
own small heights, replacing the label-hack outright. Both backends'
rendering now sum each row's own real height instead of assuming one
uniform value, so hit-testing and pixels can't disagree about where a
row is. Floating is omitted entirely outside the tiling layout.
New, in direct response to "allow customizing from there as well": a
Customize section with live Button Style / Button Side toggles. Each
flips the matching ThemeConfig field and immediately redraws every open
window's titlebar - not routed through srd set's own path, which is
scoped to windows created after the call for lack of a redraw hook it
can reach; a menu action that didn't visibly change the titlebar you
clicked would be its own "doesn't make sense" bug.
Full workspace build/test/clippy clean (242 core tests, +8; 152
wayland, net-even after rewriting the old label-hack tests).
Diffstat (limited to 'crates/wayland/src/decoration')
| -rw-r--r-- | crates/wayland/src/decoration/tests.rs | 68 |
1 files changed, 42 insertions, 26 deletions
diff --git a/crates/wayland/src/decoration/tests.rs b/crates/wayland/src/decoration/tests.rs index b14cbca..f2a9ded 100644 --- a/crates/wayland/src/decoration/tests.rs +++ b/crates/wayland/src/decoration/tests.rs @@ -790,18 +790,18 @@ fn border_bottom_extra_rows_are_transparent_outside_the_corners() { } #[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)); +fn context_menu_is_the_sum_of_each_rows_own_height() { + let items = [("Minimize", false, 28, MenuRowKind::Item), ("Maximize", false, 28, MenuRowKind::Item), ("Always on Top", false, 28, MenuRowKind::Item), ("Close", false, 28, MenuRowKind::Item)]; + let buf = render_context_menu(160, &items, (0x2e, 0x34, 0x40), (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10)); assert_eq!(buf.len(), 160 * (28 * 4) * 4); } #[test] fn context_menu_highlighted_row_has_a_different_background_than_the_rest() { - let items = [("Minimize", false), ("Close", true)]; + let items = [("Minimize", false, 28, MenuRowKind::Item), ("Close", true, 28, MenuRowKind::Item)]; let bg = (0x2e, 0x34, 0x40); let highlight = (0x4c, 0x56, 0x6a); - let buf = render_context_menu(160, 28, &items, bg, (0xff, 0xff, 0xff), highlight, (0x10, 0x10, 0x10)); + let buf = render_context_menu(160, &items, bg, (0xff, 0xff, 0xff), highlight, (0x10, 0x10, 0x10)); let width = 160usize; // Sample a background pixel from each row, away from the text/border. let px_at = |x: usize, y: usize| -> [u8; 3] { @@ -830,8 +830,8 @@ fn context_menu_panel_is_opaque_in_the_middle_but_rounded_at_the_corners() { // opaque - the opposite of what this test used to assert - while an // edge's midpoint (away from any corner's curve) and the panel's own // interior stay fully opaque either way. - let items = [("Close", false)]; - let buf = render_context_menu(100, 28, &items, (0, 0, 0), (0xff, 0xff, 0xff), (0, 0, 0), (0x99, 0x99, 0x99)); + let items = [("Close", false, 28, MenuRowKind::Item)]; + let buf = render_context_menu(100, &items, (0, 0, 0), (0xff, 0xff, 0xff), (0, 0, 0), (0x99, 0x99, 0x99)); let alpha_at = |x: usize, y: usize| buf[(y * 100 + x) * 4 + 3]; assert_eq!(alpha_at(0, 0), 0, "the exact corner pixel is now outside the rounded curve, not a hard square"); // 2px in from the flat top/bottom edges, at the midpoint (far enough @@ -844,15 +844,18 @@ fn context_menu_panel_is_opaque_in_the_middle_but_rounded_at_the_corners() { } #[test] -fn a_pure_separator_row_draws_a_narrow_line_not_a_full_text_row() { - // A label of only `\u{2500}` renders as a thin graphical line instead - // of text glyphs - see `render_context_menu`'s own doc comment for - // why (Unicode box-drawing glyphs render inconsistently at small +fn a_separator_row_draws_a_narrow_line_not_a_full_text_row() { + // `MenuRowKind::Separator` renders as a thin graphical line regardless + // of its (empty) label - see `render_context_menu`'s own doc comment + // for why (Unicode box-drawing glyphs render inconsistently at small // sizes; a real line reads as an intentional divider). Scans every // pixel in the row (not one fixed column, which could accidentally // land in a font glyph's own hollow spot) - a hairline should touch // only a couple of the row's own pixel rows, nowhere near a real text - // row's spread (see the sibling test just below). + // row's spread (see the sibling test just below). Given its own much + // smaller `SEPARATOR_HEIGHT`-sized slot (9px here), not the item rows' + // 28px, so this also exercises variable row heights, not just the + // divider's own look. // // Three items, separator in the middle: `fill_rounded_rect`'s own // distance field softens alpha within `PANEL_RADIUS` of *any* of the @@ -868,8 +871,9 @@ fn a_pure_separator_row_draws_a_narrow_line_not_a_full_text_row() { // so only the separator itself can account for a non-`bg` pixel here. let bg = (0x2e, 0x34, 0x40); let width = 160usize; - let items = [("Open", false), ("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}", false), ("Close", false)]; - let buf = render_context_menu(width as u32, 28, &items, bg, (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10)); + const SEPARATOR_HEIGHT: u32 = 9; + let items = [("Open", false, 28, MenuRowKind::Item), ("", false, SEPARATOR_HEIGHT, MenuRowKind::Separator), ("Close", false, 28, MenuRowKind::Item)]; + let buf = render_context_menu(width as u32, &items, bg, (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10)); let px_at = |x: usize, y: usize| -> [u8; 3] { let i = (y * width + x) * 4; [buf[i + 2], buf[i + 1], buf[i]] @@ -878,31 +882,43 @@ fn a_pure_separator_row_draws_a_narrow_line_not_a_full_text_row() { let safe_x = (PANEL_RADIUS + 2)..(width - PANEL_RADIUS - 2); let row_has_any_non_bg = |y: usize| safe_x.clone().any(|x| px_at(x, y) != [bg.0, bg.1, bg.2]); let separator_row_top = 28; - let non_bg_rows = (separator_row_top..separator_row_top + 28).filter(|&y| row_has_any_non_bg(y)).count(); + let non_bg_rows = (separator_row_top..separator_row_top + SEPARATOR_HEIGHT as usize).filter(|&y| row_has_any_non_bg(y)).count(); assert!(non_bg_rows <= 3, "a hairline separator should touch only a couple of the row's pixel rows, got {non_bg_rows}"); assert!(non_bg_rows >= 1, "the separator must actually draw something, not vanish entirely"); } #[test] -fn a_labeled_section_header_separator_still_renders_as_text() { - // `"─── Move to Workspace ───"` deliberately mixes the divider - // character with real text (`core::ContextMenu`'s own section-header - // convention) - it must keep rendering as text, not collapse into a - // plain hairline just because it contains `\u{2500}` characters too. - // Same whole-row scan and same "sandwich away from the panel's own - // rounded-edge antialiasing" shape as the sibling test above. +fn a_header_row_renders_dimmed_text_not_a_divider_and_ignores_highlight() { + // `MenuRowKind::Header` replaced an earlier hack that faked a section + // caption by embedding box-drawing characters directly in an ordinary + // item's label (`"─── Move to Workspace ───"`) - indistinguishable + // from a real, clickable row except for the dashes, and reported live + // as looking exactly like that: a menu item that does nothing. A real + // header row must still render as text (not collapse into a hairline + // the way `Separator` does), and must never show a hover highlight + // even if the caller passes `highlighted: true` for it by mistake -- + // a section caption isn't a target, so nothing should ever suggest it + // is one. let bg = (0x2e, 0x34, 0x40); let width = 200usize; - let items = [("Open", false), ("\u{2500}\u{2500}\u{2500} Move to Workspace \u{2500}\u{2500}\u{2500}", false), ("Close", false)]; - let buf = render_context_menu(width as u32, 28, &items, bg, (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10)); + const HEADER_HEIGHT: u32 = 22; + let items = [("Open", false, 28, MenuRowKind::Item), ("Move to Workspace", true, HEADER_HEIGHT, MenuRowKind::Header), ("Close", false, 28, MenuRowKind::Item)]; + let buf = render_context_menu(width as u32, &items, bg, (0xff, 0xff, 0xff), (0x4c, 0x56, 0x6a), (0x10, 0x10, 0x10)); let px_at = |x: usize, y: usize| -> [u8; 3] { let i = (y * width + x) * 4; [buf[i + 2], buf[i + 1], buf[i]] }; let row_has_any_non_bg = |y: usize| (0..width).any(|x| px_at(x, y) != [bg.0, bg.1, bg.2]); let header_row_top = 28; - let non_bg_rows = (header_row_top..header_row_top + 28).filter(|&y| row_has_any_non_bg(y)).count(); - assert!(non_bg_rows > 3, "a text row (mixed divider+caption) should paint well more than a hairline's worth of rows, got {non_bg_rows}"); + let non_bg_rows = (header_row_top..header_row_top + HEADER_HEIGHT as usize).filter(|&y| row_has_any_non_bg(y)).count(); + assert!(non_bg_rows > 3, "a header's own caption text should paint well more than a hairline's worth of rows, got {non_bg_rows}"); + // No highlight fill anywhere in the header's own row band, even though + // `highlighted: true` was passed for it above - a flat highlight fill + // would show up as a large, uniform block of `highlight_bg`-tinted + // pixels well away from the text glyphs themselves; sampling a point + // near the row's own right edge (past where "Move to Workspace" 's + // text reaches) catches that without depending on exact glyph shapes. + assert_eq!(px_at(width - 10, header_row_top + 10), [bg.0, bg.1, bg.2], "a header row must never show the hover highlight fill"); } #[test] |