diff options
| author | srdusr <[email protected]> | 2025-09-28 21:25:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-09-28 21:25:00 +0200 |
| commit | ffdae252782d047a6b2d76c9bb4efeb92c391db3 (patch) | |
| tree | 11ddf590e643e55cdb1e907c0f236dd077236e03 /crates/wayland/src/decoration | |
| parent | 58a368df5f7a5d579335d1cb68213baacba5bc63 (diff) | |
| download | srdwm-ffdae252782d047a6b2d76c9bb4efeb92c391db3.tar.gz srdwm-ffdae252782d047a6b2d76c9bb4efeb92c391db3.zip | |
Context/desktop menu polish: real hover tint, real separator line, Select All
Reported live: "looks weird and unpolished... need a lot more items".
Compared directly against the exact AGS reference this project's own
menu rebuild already targets rather than guessing:
- Highlighted rows used a flat, fully-saturated fill instead of the
reference's subtle 22%-accent-into-background wash. New decoration::
color::mix_rgb (channel-wise linear blend, generalizing brighten/
darken's fixed-target blends to an arbitrary second colour/ratio) lets
render_context_menu reproduce that same ratio.
- Every separator row was a label string of Unicode box-drawing
characters rendered as text glyphs, which render inconsistently at
small sizes - a label that's entirely U+2500 now draws a real 1px
hairline instead; a label that mixes it with real text ("--- Move to
Workspace ---", a deliberate section-header convention) still renders
as text, unchanged.
- "Select All" added to the bare-desktop menu, the one action every
mainstream desktop's own menu offers that this one lacked.
New tests needed real care: the panel's own rounded-corner distance
field softens alpha within its radius of any canvas edge, not just the
visible corners, so a naive full-row pixel scan against bg picked that
up as a false positive on the first attempt - fixed by scanning only
rows/columns confirmed (via a throwaway debug dump) to sit inside the
panel's genuinely flat interior.
Full workspace build/test/clippy clean, built and installed. Real
submenus and per-row icons remain real, separate scope - this
project's floating-menu UI has no nested-panel concept yet.
Diffstat (limited to 'crates/wayland/src/decoration')
| -rw-r--r-- | crates/wayland/src/decoration/color.rs | 14 | ||||
| -rw-r--r-- | crates/wayland/src/decoration/tests.rs | 72 |
2 files changed, 85 insertions, 1 deletions
diff --git a/crates/wayland/src/decoration/color.rs b/crates/wayland/src/decoration/color.rs index 7ea9d80..5262cec 100644 --- a/crates/wayland/src/decoration/color.rs +++ b/crates/wayland/src/decoration/color.rs @@ -32,3 +32,17 @@ pub(crate) fn darken(color: (u8, u8, u8)) -> (u8, u8, u8) { let mix = |c: u8| (c as f32 * (1.0 - AMOUNT)).round() as u8; (mix(color.0), mix(color.1), mix(color.2)) } + +/// Linear channel-wise blend of `a` toward `b` by `t` (`0.0` is pure `a`, +/// `1.0` is pure `b`) - unlike `brighten`/`darken`, which blend toward a +/// fixed white/black, this blends toward an arbitrary second colour, at +/// an arbitrary caller-chosen ratio. `render_context_menu`'s own row +/// highlight uses this to reproduce the AGS reference dropdown's own +/// subtle wash (`color-mix(in srgb, var(--primary-bg) 22%, var(--widget- +/// bg))`) instead of a flat, fully-saturated fill - the same reference +/// this project's own menu rebuild already targets elsewhere. +pub(crate) fn mix_rgb(a: (u8, u8, u8), b: (u8, u8, u8), t: f32) -> (u8, u8, u8) { + let t = t.clamp(0.0, 1.0); + let mix = |ac: u8, bc: u8| (ac as f32 + (bc as f32 - ac as f32) * t).round() as u8; + (mix(a.0, b.0), mix(a.1, b.1), mix(a.2, b.2)) +} diff --git a/crates/wayland/src/decoration/tests.rs b/crates/wayland/src/decoration/tests.rs index 5cbc12f..b14cbca 100644 --- a/crates/wayland/src/decoration/tests.rs +++ b/crates/wayland/src/decoration/tests.rs @@ -809,7 +809,15 @@ fn context_menu_highlighted_row_has_a_different_background_than_the_rest() { [buf[i + 2], buf[i + 1], buf[i]] // BGRA -> RGB }; assert_eq!(px_at(100, 5), [bg.0, bg.1, bg.2], "row 0 (not highlighted) should use bg"); - assert_eq!(px_at(100, 33), [highlight.0, highlight.1, highlight.2], "row 1 (highlighted) should use highlight_bg"); + // Not `highlight` at full strength: the AGS reference dropdown's own + // hover fill is a 22%-mixed wash of the accent over the panel + // background (`color-mix(in srgb, var(--primary-bg) 22%, var( + // --widget-bg))`), not a flat, fully-saturated fill - see `render_ + // context_menu`'s own doc comment. `mix_rgb(bg, highlight, 0.22)`, + // computed by hand here rather than imported, so this test would + // actually catch the ratio drifting. + let expected = (53, 59, 73); // (46,52,64) blended 22% toward (76,86,106) + assert_eq!(px_at(100, 33), [expected.0, expected.1, expected.2], "row 1 (highlighted) should use a subtle tinted wash toward highlight_bg, not a flat fill of it"); } #[test] @@ -836,6 +844,68 @@ 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 + // 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). + // + // Three items, separator in the middle: `fill_rounded_rect`'s own + // distance field softens alpha within `PANEL_RADIUS` of *any* of the + // panel's four edges, not just the visible corner curves (a rounded + // rect's SDF clamps the comparison point toward whichever edge is + // nearest along each axis independently, so a point that's flat-on + // to one edge but still within `radius` of another edge still gets + // partial coverage). Sandwiching the separator row between two + // others, and scanning only rows/columns comfortably past `PANEL_ + // RADIUS` in from every canvas edge, keeps this test inside the + // panel's genuinely flat, fully-opaque interior - confirmed by + // measurement (a debug dump of raw pixel deltas), not assumed -- + // 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)); + let px_at = |x: usize, y: usize| -> [u8; 3] { + let i = (y * width + x) * 4; + [buf[i + 2], buf[i + 1], buf[i]] + }; + const PANEL_RADIUS: usize = 10; + 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(); + 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. + 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)); + 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}"); +} + +#[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)); |