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.
This commit is contained in:
Jason Baldwin 2026-10-10 11:39:28 +02:00
parent 5436951299
commit 94fed226df
4 changed files with 54 additions and 1 deletions

View File

@ -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<egui::epaint::Shape> {
fn flat(shape: &egui::epaint::Shape, out: &mut Vec<egui::epaint::Shape>) {
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<String> {
fn texts(shape: &egui::epaint::Shape, out: &mut Vec<String>) {
match shape {

View File

@ -86,10 +86,14 @@ pub fn level(ui: &mut Ui, depth: usize, bar_bottom: Option<f32>, 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);

View File

@ -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<egui::Rect> = 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:?}");
}

View File

@ -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 | ⬜ | | |