diff options
Diffstat (limited to 'crates/wayland')
| -rw-r--r-- | crates/wayland/src/decoration.rs | 110 | ||||
| -rw-r--r-- | crates/wayland/src/decoration/tests.rs | 68 | ||||
| -rw-r--r-- | crates/wayland/src/input/pointer.rs | 17 | ||||
| -rw-r--r-- | crates/wayland/src/state/desktop_icons.rs | 18 | ||||
| -rw-r--r-- | crates/wayland/src/state/menu.rs | 63 |
5 files changed, 199 insertions, 77 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs index aa12a54..d24c035 100644 --- a/crates/wayland/src/decoration.rs +++ b/crates/wayland/src/decoration.rs @@ -87,20 +87,61 @@ pub(crate) const CORNER_RADIUS: u32 = 12; /// Still no submenus/icons - real gaps beyond this pass' own scope, not /// attempted blind. /// -/// A row whose label is *entirely* the box-drawing character `─` -/// (`\u{2500}`, one or more, no other content) renders as a real thin -/// divider line instead of text glyphs - a label that *mixes* `─` with -/// real text (`"─── Move to Workspace ───"`, a deliberate section-header -/// convention `core::ContextMenu` already uses) is untouched and still -/// renders as text, since that dual purpose (divider *and* caption) is -/// the actual design, not a plain separator. A pure divider is drawn as -/// pixels rather than characters because Unicode box-drawing glyphs -/// render inconsistently thin/dotted across fonts at small sizes - a -/// real 1px anti-aliased line, inset from both edges and blended low- -/// opacity against the panel (matching the AGS reference dropdown's own -/// `separator.menu-sep`: `color-mix(in srgb, var(--fg) 12%, transparent)`), -/// reads as an intentional divider rather than a run of stray dashes. -pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], bg: (u8, u8, u8), fg: (u8, u8, u8), highlight_bg: (u8, u8, u8), border: (u8, u8, u8)) -> Vec<u8> { +/// A row's shape, alongside its own label/height - `render_context_menu` +/// used to detect a divider by checking whether a label was made entirely +/// of the box-drawing character `─`, and had no representation for a +/// section-header row at all (`"─── Move to Workspace ───"` rendered, and +/// behaved, as an ordinary clickable-looking item that merely did +/// nothing). Reported live as looking wrong on both counts - a real, +/// distinct row kind is what `srdwm_core::context_menu::MenuAction`'s own +/// `Separator`/`Header` variants exist for; this mirrors that split here +/// without this crate needing to depend on that enum itself. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum MenuRowKind { + Item, + /// A thin divider line, no label. + Separator, + /// A non-interactive section caption - dimmer, smaller text, never + /// highlighted. + Header, +} + +/// Rounded floating panel with a per-row rounded hover highlight, matching +/// the reference this project's own AGS panel already settled on for its +/// global-menu dropdown (`widget/Bar/components/GlobalMenu/style.scss`'s +/// `popover box.menu-list`): flat rows with no border/outline at rest, a +/// soft tinted fill (not a frame) on the highlighted one, inset padding so +/// rows don't touch the panel's own edge, real gaps between rows. Reported +/// live as looking "squished, no spacing/padding/margining, not at all +/// polished" - the previous version drew edge-to-edge square rows with a +/// single hard 1px border around the whole menu, exactly what that +/// complaint (raised about the AGS dropdown, fixed there first) describes. +/// Still no submenus/icons - real gaps beyond this pass' own scope, not +/// attempted blind. +/// +/// `rows` carries each row's own height alongside its label/kind - a +/// [`MenuRowKind::Separator`]/[`MenuRowKind::Header`] row is much shorter +/// than a real item (see `srdwm_core::context_menu`'s own `SEPARATOR_ +/// HEIGHT`/`HEADER_HEIGHT`), reported live as looking wrong when every +/// row, divider included, took the same full item height: a hairline +/// sitting in the middle of a mostly-empty 32px slot. The caller is what +/// actually knows each row's real height (`ContextMenu::row_height_for`); +/// this function just draws whatever it's told, at the offsets implied by +/// summing those heights in order - the same summing `ContextMenu::row_ +/// at`/`row_y` do, so a click and a pixel can never disagree about where a +/// row actually is. +/// +/// A [`MenuRowKind::Separator`] is drawn as a real 1px anti-aliased line, +/// inset from both edges and blended low-opacity against the panel +/// (matching the AGS reference dropdown's own `separator.menu-sep`: +/// `color-mix(in srgb, var(--fg) 12%, transparent)`) - pixels, not +/// Unicode box-drawing characters, which render inconsistently thin/ +/// dotted across fonts at small sizes. A [`MenuRowKind::Header`] draws its +/// label dimmed (the same mix ratio a separator's own line uses) and +/// never highlighted, regardless of what the caller passes for that row's +/// `highlighted` field - section captions aren't clickable, so nothing +/// should ever visually suggest they are. +pub fn render_context_menu(width: u32, rows: &[(&str, bool, u32, MenuRowKind)], bg: (u8, u8, u8), fg: (u8, u8, u8), highlight_bg: (u8, u8, u8), border: (u8, u8, u8)) -> Vec<u8> { let _ = border; // No outline anywhere now - see this function's own doc comment. Kept as a parameter so callers/themes don't need updating for a look this function no longer draws. const PANEL_RADIUS: f32 = 10.0; const ROW_INSET: i32 = 4; @@ -114,17 +155,13 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], // accent to tint toward", not "the literal pixel colour". const HIGHLIGHT_MIX: f32 = 0.22; // Matches the reference's own `separator.menu-sep` background -- - // barely-there, a hairline rather than a visible bar. - const SEPARATOR_MIX: f32 = 0.12; + // barely-there, a hairline rather than a visible bar. Reused as the + // header caption's own dim ratio - both exist to read as "present, + // but deliberately not the main content" against the same panel. + const DIM_MIX: f32 = 0.12; - let (width, row_height) = (width.max(1) as usize, row_height.max(1) as usize); - // Exactly `row_height * items.len()`, same as before this pass -- - // `ContextMenu`/`DesktopMenu`'s own `height()` and `row_at()` (which - // this function has no access to and mustn't get out of sync with) - // assume row `i` starts at `i * row_height` with no extra top/bottom - // inset, so all of this rework happens *inside* that unchanged canvas - // rather than by growing it. - let height = (row_height * items.len().max(1)).max(1); + let width = width.max(1) as usize; + let height = rows.iter().map(|(_, _, h, _)| *h).sum::<u32>().max(1) as usize; let mut buf = vec![0u8; width * height * 4]; // The panel itself: one flat rounded-rect fill on an otherwise fully @@ -133,15 +170,19 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], fill_rounded_rect(&mut buf, width, height, 0, 0, width as i32, height as i32, PANEL_RADIUS, bg, bg); let highlight_fill = mix_rgb(bg, highlight_bg, HIGHLIGHT_MIX); - let separator_color = mix_rgb(bg, fg, SEPARATOR_MIX); + let dim_color = mix_rgb(bg, fg, DIM_MIX); let font = find_system_font(); - for (i, (label, highlighted)) in items.iter().enumerate() { - let row_top = (i * row_height) as i32; - if !label.is_empty() && label.chars().all(|c| c == '\u{2500}') { - let y = row_top + row_height as i32 / 2; - fill_rounded_rect_over(&mut buf, width, height, ROW_INSET * 2, y, width as i32 - ROW_INSET * 2, y + 1, 0.0, separator_color); + let mut row_top: i32 = 0; + for (label, highlighted, row_height, kind) in rows.iter().copied() { + let row_height = row_height as i32; + if kind == MenuRowKind::Separator { + let y = row_top + row_height / 2; + fill_rounded_rect_over(&mut buf, width, height, ROW_INSET * 2, y, width as i32 - ROW_INSET * 2, y + 1, 0.0, dim_color); + row_top += row_height; continue; } + let highlighted = highlighted && kind != MenuRowKind::Header; + let label_color = if kind == MenuRowKind::Header { dim_color } else { fg }; // The background text actually sits on, for `blit_glyph`'s own // blend-toward-a-known-solid-colour contract - the row's own // highlight fill (already baked into `buf` by this point, above) @@ -153,9 +194,9 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], // comment) - used correctly, it would leave a visible dark // fringe around every character's anti-aliased edge instead of a // clean blend into the row's real colour. - let row_bg = if *highlighted { highlight_fill } else { bg }; - if *highlighted { - fill_rounded_rect_over(&mut buf, width, height, ROW_INSET, row_top, width as i32 - ROW_INSET, row_top + row_height as i32, ROW_RADIUS, highlight_fill); + let row_bg = if highlighted { highlight_fill } else { bg }; + if highlighted { + fill_rounded_rect_over(&mut buf, width, height, ROW_INSET, row_top, width as i32 - ROW_INSET, row_top + row_height, ROW_RADIUS, highlight_fill); } if let Some(font) = &font { let baseline = row_top as f32 + row_height as f32 * 0.72; @@ -168,7 +209,7 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], 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, row_bg, fg); + blit_glyph(&mut buf, width, height, glyph_x.round() as i32, glyph_y.round() as i32, &metrics, &coverage, row_bg, label_color); } pen_x += metrics.advance_width; if pen_x as usize >= width { @@ -176,6 +217,7 @@ pub fn render_context_menu(width: u32, row_height: u32, items: &[(&str, bool)], } } } + row_top += row_height; } buf } 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] diff --git a/crates/wayland/src/input/pointer.rs b/crates/wayland/src/input/pointer.rs index 2ecc15c..c6ed5ba 100644 --- a/crates/wayland/src/input/pointer.rs +++ b/crates/wayland/src/input/pointer.rs @@ -514,14 +514,15 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic if let Some(menu) = state.context_menu.take() { if let Some(row) = menu.row_at(pos.x as i32, pos.y as i32) { let (_, action) = menu.items[row]; - // A separator row occupies real space (`row_at` resolves a - // click on it same as any other) but isn't a real action -- - // same "click does nothing, menu stays open" convention any - // native menu's own divider follows, rather than either - // running a no-op action or dismissing the whole menu on - // what was very possibly a slightly-off click at a real - // item just above/below it. - if matches!(action, crate::context_menu::MenuAction::Separator) { + // A separator or section-header row occupies real space + // (`row_at` resolves a click on it same as any other) but + // isn't a real action - same "click does nothing, menu + // stays open" convention any native menu's own divider/ + // caption follows, rather than either running a no-op + // action or dismissing the whole menu on what was very + // possibly a slightly-off click at a real item just + // above/below it. + if !action.is_interactive() { state.context_menu = Some(menu); return; } diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs index 8ed22a5..9d2dd91 100644 --- a/crates/wayland/src/state/desktop_icons.rs +++ b/crates/wayland/src/state/desktop_icons.rs @@ -593,8 +593,22 @@ impl CompState { fn build_desktop_menu_buffer(&mut self, menu: DesktopMenu) { let theme = self.wm.borrow().theme; - let items: Vec<(&str, bool)> = menu.items.iter().map(|&(label, _)| (label, false)).collect(); - let data = decoration::render_context_menu(menu.width, menu.row_height, &items, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color); + // Not redesigned the way the titlebar menu was (`srdwm_core:: + // context_menu`'s own module doc comment) - this menu's rows are + // still all one uniform height, `Separator` included, matching + // its behaviour before `render_context_menu` grew a per-row + // height/kind. Every row here is real content or `DesktopMenuAction + // ::Separator`, never a non-interactive caption, so there is no + // `MenuRowKind::Header` case to map to. + let rows: Vec<(&str, bool, u32, decoration::MenuRowKind)> = menu + .items + .iter() + .map(|(label, action)| { + let kind = if matches!(action, crate::desktop_menu::DesktopMenuAction::Separator) { decoration::MenuRowKind::Separator } else { decoration::MenuRowKind::Item }; + (*label, false, menu.row_height, kind) + }) + .collect(); + let data = decoration::render_context_menu(menu.width, &rows, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color); let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (menu.width as i32, menu.height()), 1, Transform::Normal, None); self.desktop_menu_buffer = Some(buffer); self.desktop_menu = Some(menu); diff --git a/crates/wayland/src/state/menu.rs b/crates/wayland/src/state/menu.rs index b8cf094..b0b0a6e 100644 --- a/crates/wayland/src/state/menu.rs +++ b/crates/wayland/src/state/menu.rs @@ -14,8 +14,20 @@ impl CompState { return; }; let theme = self.wm.borrow().theme; - let items: Vec<(&str, bool)> = menu.items.iter().map(|&(label, _)| (label, false)).collect(); - let data = decoration::render_context_menu(menu.width, menu.row_height, &items, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color); + let rows: Vec<(&str, bool, u32, decoration::MenuRowKind)> = menu + .items + .iter() + .enumerate() + .map(|(i, &(label, action))| { + let kind = match action { + crate::context_menu::MenuAction::Separator => decoration::MenuRowKind::Separator, + crate::context_menu::MenuAction::Header => decoration::MenuRowKind::Header, + _ => decoration::MenuRowKind::Item, + }; + (label, false, menu.row_height_for(i), kind) + }) + .collect(); + let data = decoration::render_context_menu(menu.width, &rows, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color); let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (menu.width as i32, menu.height()), 1, Transform::Normal, None); self.context_menu_buffer = Some(buffer); self.context_menu = Some(menu); @@ -60,17 +72,54 @@ impl CompState { MenuAction::MoveToWorkspace(workspace) => { self.wm.borrow_mut().move_window_to_workspace(window, workspace); } + MenuAction::CycleButtonStyle => { + let mut wm = self.wm.borrow_mut(); + wm.theme.traffic_light_buttons = !wm.theme.traffic_light_buttons; + drop(wm); + self.redraw_every_decoration(); + } + MenuAction::CycleButtonSide => { + let mut wm = self.wm.borrow_mut(); + wm.theme.buttons_left = !wm.theme.buttons_left; + drop(wm); + self.redraw_every_decoration(); + } MenuAction::Close => { if let Some(w) = self.id_to_window.get(&window) { crate::input::close_dwindow(w); } } // Never actually reached: the click-dispatch site - // (`input/pointer.rs`) intercepts `Separator` before calling - // this function at all. Handled here too so this match stays - // exhaustive without a catch-all that would silently swallow a - // real future variant added without updating this function. - MenuAction::Separator => {} + // (`input/pointer.rs`) intercepts `Separator`/`Header` before + // calling this function at all. Handled here too so this + // match stays exhaustive without a catch-all that would + // silently swallow a real future variant added without + // updating this function. + MenuAction::Separator | MenuAction::Header => {} + } + } + + /// Rebuilds every open window's titlebar/border bitmap against + /// whatever `wm.theme` currently holds - what `CycleButtonStyle`/ + /// `CycleButtonSide` need to actually show their effect immediately. + /// + /// `srd set button_style`/`button_side` (`crates/platform/src/ipc/ + /// dispatch.rs`) change the exact same `ThemeConfig` fields but are + /// deliberately scoped to "only affects windows created (or + /// redecorated) after this call" - that crate is backend-agnostic + /// and has no way to reach into a Wayland-specific redraw. A titlebar + /// menu action has no such excuse: it's *this* backend's own code, + /// already holding `&mut self`, and a "customize" action that doesn't + /// visibly change the very titlebar you clicked would be exactly the + /// kind of "doesn't make sense" this menu was reported for in the + /// first place. `redraw_decoration_buffer` already no-ops on any + /// window whose decoration signature didn't actually change, so + /// calling it for every window here costs nothing for the (common) + /// case where most of them don't use server-side decoration at all. + fn redraw_every_decoration(&mut self) { + let ids: Vec<WindowId> = self.wm.borrow().windows().map(|w| w.id).collect(); + for id in ids { + self.redraw_decoration_buffer(id); } } |