diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 3bdae5a93..7e30d3ca9 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-checklist.md b/docs/parity-checklist.md index b359c324c..d1985c858 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -481,7 +481,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 | ⬜ | | |