From 94fed226dfb17622c8a3db618b7c5719ea72de07 Mon Sep 17 00:00:00 2001 From: Jason Baldwin <33934381+bitshiftza@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:39:28 +0200 Subject: [PATCH] MENU-BAR: a scrolled menu draws no scrollbar over its rows (#691) A menu taller than the room under the menu bar scrolls inside an egui ScrollArea whose floating scrollbar is drawn over the rows' right edge, where the shortcut text ends. At 1.0x the 2-point handle grazes the last glyph; at 1.75x the bar is in its wide hovered state with a semi-opaque background and hides the last character of every shortcut, which reads as clipping. The scroll content, its clip and the rows are the level's width in both modes (instrumented), so nothing else needs to move: the scroll area hides its bar. The up/down strips, the wheel and dragging were already the designed ways to scroll a menu, like native menus; the native macOS menu bar does not go through this code. Test: tests_menubar::a_scrolled_menu_draws_no_scrollbar_over_its_shortcuts (a 700 pt and a 1400 pt window: no tall narrow rectangle along the level's right edge, equal widths), via a new HeadlessView::painted_shapes; fails on main with the bar's track [512, 59]-[514, 660] and handle. --- crates/ui-egui/src/headless.rs | 13 +++++++++++ crates/ui-egui/src/menu_level.rs | 4 ++++ crates/ui-egui/src/tests_menubar.rs | 36 +++++++++++++++++++++++++++++ docs/parity.md | 2 +- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index e2884935a..ab1c17757 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -175,6 +175,19 @@ impl HeadlessView { } /// Every piece of text the last frame painted (labels, tooltips, values), in paint order. + /// Every shape of the last frame, with `Shape::Vec` flattened. + pub fn painted_shapes(&self) -> Vec { + fn flat(shape: &egui::epaint::Shape, out: &mut Vec) { + match shape { + egui::epaint::Shape::Vec(shapes) => shapes.iter().for_each(|shape| flat(shape, out)), + other => out.push(other.clone()), + } + } + let mut out = Vec::new(); + self.shapes.iter().for_each(|s| flat(&s.shape, &mut out)); + out + } + pub fn painted_text(&self) -> Vec { fn texts(shape: &egui::epaint::Shape, out: &mut Vec) { match shape { diff --git a/crates/ui-egui/src/menu_level.rs b/crates/ui-egui/src/menu_level.rs index df9bdf09b..379a2dd0b 100644 --- a/crates/ui-egui/src/menu_level.rs +++ b/crates/ui-egui/src/menu_level.rs @@ -86,10 +86,14 @@ pub fn level(ui: &mut Ui, depth: usize, bar_bottom: Option, rows: impl FnOn // The popup's `Ui` is only as tall as the popup was last frame (egui's default area size on // the first), and a scroll area never grows past that. Ask for the room the window has; // auto-shrink then fits the area to its rows, so a menu scrolls only when they don't fit. + // No scrollbar: the ▲ / ▼ strips, the wheel and dragging scroll a menu, like native menus. + // egui's floating bar is drawn over the rows' right edge, where the shortcut text ends, and in + // its wide (hovered) state its background hides the last character (issue #691). let out = egui::ScrollArea::vertical() .id_salt(("menu-level", depth)) .max_height(height) .min_scrolled_height(height) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) .scroll_source(egui::containers::scroll_area::ScrollSource::ALL) .show(ui, rows); let down = over.then(|| ui.allocate_exact_size(vec2(0.0, ARROW), Sense::hover()).0); diff --git a/crates/ui-egui/src/tests_menubar.rs b/crates/ui-egui/src/tests_menubar.rs index d0e905a50..8fbc13608 100644 --- a/crates/ui-egui/src/tests_menubar.rs +++ b/crates/ui-egui/src/tests_menubar.rs @@ -145,3 +145,39 @@ fn click_outside_and_escape_close_the_open_menu() { h.settle(SETTLE); assert_eq!(open_menu(&h), None, "Escape"); } + +// ---- a scrolled menu draws no scrollbar over its rows (issue #691) + +/// egui's floating scrollbar sat on the rows' right edge, where the shortcut text ends, and in +/// its wide (hovered) state its background hid the last character. With the pointer resting on +/// the menu, no tall narrow rectangle (a bar's handle or track) is painted along the level's +/// right edge, in a menu that scrolls as in one that fits, and the level is as wide in both. +#[test] +fn a_scrolled_menu_draws_no_scrollbar_over_its_shortcuts() { + let mut widths = Vec::new(); + for (size, scrolls) in [([1600.0, 700.0], true), ([1600.0, 1400.0], false)] { + let what = format!("{} pt window", size[1]); + let mut h = app(size); + go(&mut h, "ui.clickWidget", "menu:View"); + go(&mut h, "ui.hoverWidget", "menu-level:1"); + let level = widget(&h, "menu-level:1").unwrap_or_else(|| panic!("{what}: no View menu")); + let rows = widgets_with(&h, "menusub:"); + assert!(!rows.is_empty(), "{what}: no submenu rows registered"); + assert_eq!(rows.iter().any(|(_, r)| !level.contains_rect(*r)), scrolls, "{what}: scrolls = {scrolls}, level {level:?}"); + widths.push(level.width()); + // a row's hover highlight spans the level; a separator is one point tall; the ▸ of a + // submenu row and the text are not rectangles: only a scrollbar is tall and narrow there + let bars: Vec = h + .view + .painted_shapes() + .iter() + .filter_map(|s| match s { + egui::epaint::Shape::Rect(r) => Some(r.rect), + _ => None, + }) + .filter(|r| r.width() <= 14.0 && r.height() > 30.0 && r.left() >= level.right() - 16.0 && r.right() <= level.right() + 0.5) + .collect(); + assert!(bars.is_empty(), "{what}: a scrollbar is drawn along the menu's right edge: {bars:?}"); + } + assert!((widths[0] - widths[1]).abs() < 0.5, "a scrolled menu is as wide as one that fits: {widths:?}"); +} diff --git a/docs/parity.md b/docs/parity.md index ad82e3d25..b83534f43 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -544,7 +544,7 @@ visible menu bar yet: the menu model is only exposed through the control channel | Id | Feature | Tier | Status | Evidence | Missing / notes | |---|---|---|---|---|---| -| MENU-BAR | Menu bar rendering | P1 | ✅ | `crates/ui-egui/src/menubar.rs`, `apps/lightcraft/src/native_menu.rs` | native macOS menu bar (muda) with live labels/enabled/checked; in-window menus on web/Windows/Linux (with one menu open, the pointer on another title switches to it, as in native menu bars); ⌫ and X stay egui-handled (contextual), so they show no key in the native menu | +| MENU-BAR | Menu bar rendering | P1 | ✅ | `crates/ui-egui/src/menubar.rs`, `apps/lightcraft/src/native_menu.rs` | native macOS menu bar (muda) with live labels/enabled/checked; in-window menus on web/Windows/Linux (with one menu open, the pointer on another title switches to it, as in native menu bars); ⌫ and X stay egui-handled (contextual), so they show no key in the native menu; a scrolled menu draws no scrollbar over its rows: the ▲ / ▼ strips, the wheel and dragging scroll it (issue #691: egui's floating bar hid the shortcut text's last character; `crates/ui-egui/src/tests_menubar.rs`) | | MENU-APP-ABOUT | About | P2 | ✅ | `cmd:app.about` | | | MENU-APP-SETTINGS | Settings… | P0 | ✅ | `cmd:app.settings` | app menu on macOS, Edit menu elsewhere | | MENU-APP-UPDATES | Check for updates | P2 | ⬜ | | |