srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/decoration
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/decoration')
-rw-r--r--crates/wayland/src/decoration/color.rs14
-rw-r--r--crates/wayland/src/decoration/tests.rs72
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));