M14.1: long menus scroll inside the window and use all the height they have

Menu bar menus, their submenus, panel menus, the effects menu groups and library
submenus are as tall as their items down to the window's bottom edge, and scroll
only past that (widgets::menu_scroll), instead of running off screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 22:43:48 +07:00
co-authored by Claude Opus 5.5
parent 36a6a8ae49
commit d4c4361b4a
6 changed files with 73 additions and 16 deletions
+1 -1
View File
@@ -150,7 +150,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
| M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 |
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 |
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux and content-sized dialogs and menus done; accessibility, Windows/Linux packaging pending | 18–28 |
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux, content-sized dialogs and menus that scroll when longer than the window done; accessibility, Windows/Linux packaging pending | 18–28 |
| — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance) | ⬜ | 60–90 |
| — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 | 50–80 |
| | **Total to full parity** (one agent; re-derived from the [Parity estimate](#parity-estimate) table) | | **~335–520** |
+43 -5
View File
@@ -13,6 +13,7 @@ use crate::VectorcraftApp;
use crate::io;
use crate::state::{DockTab, ICON_PANELS, next_zoom};
use crate::theme::{self, Brightness, Tokens};
use crate::widgets;
#[derive(Clone, Debug)]
pub enum Item {
@@ -1860,10 +1861,13 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
end
}
/// A top-level menu's popup: as wide as its widest item (label plus shortcut), at least 230 pt.
/// A top-level menu's popup: as wide as its widest item (label plus shortcut), at least 230 pt;
/// it scrolls when it is taller than the window.
fn menu_body(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked: &mut Option<(String, Value)>) {
ui.set_min_width(230.0);
render_items(app, ui, items, clicked);
widgets::menu_scroll(ui, |ui| {
ui.set_min_width(230.0);
render_items(app, ui, items, clicked);
});
}
fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked: &mut Option<(String, Value)>) {
@@ -1878,8 +1882,10 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked
}
Item::Sub(label, children) => {
ui.menu_button(*label, |ui| {
ui.set_min_width(200.0);
render_items(app, ui, children, clicked);
widgets::menu_scroll(ui, |ui| {
ui.set_min_width(200.0);
render_items(app, ui, children, clicked);
});
});
}
Item::Todo(label, sc) => {
@@ -2428,4 +2434,36 @@ mod tests {
let w = ctx.memory(|m| m.area_rect(id)).unwrap().width();
assert!((230.0..340.0).contains(&w), "File menu is {w} pt wide");
}
#[test]
fn long_menus_scroll_inside_a_short_window() {
// The Window menu is taller than a 600 pt window: it stops above the bottom edge and
// scrolls instead of running off screen.
let app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let tree = menu_tree();
let (_, window) = tree.iter().find(|(t, _)| *t == "Window").unwrap();
let id = egui::Id::new("test-long-menu");
let rect = |height: f32| {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, height));
for _ in 0..3 {
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
egui::Area::new(id).fixed_pos(egui::pos2(300.0, 30.0)).show(ui.ctx(), |ui| {
egui::containers::menu::menu_style(ui.style_mut());
ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| menu_body(&app, ui, window, &mut None));
});
});
out.textures_delta.clear();
}
ctx.memory(|m| m.area_rect(id)).unwrap()
};
let short = rect(600.0);
assert!(short.bottom() <= 600.0, "Window menu ends at {} in a 600 pt window", short.bottom());
// With room to spare it shows whole, taller than the short window allowed.
let tall = rect(3000.0);
assert!(tall.height() > short.height() + 100.0, "{} vs {}", tall.height(), short.height());
assert!(tall.bottom() < 3000.0);
}
}
+7 -5
View File
@@ -870,12 +870,14 @@ pub(crate) fn fx_menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
for (g, items) in groups {
ui.menu_button(g, |ui| {
for (id, label) in items {
if ui.button(format!("{label}…")).clicked() {
app.run("effect.dialog", json!({"effect": id})).ok();
ui.close();
crate::widgets::menu_scroll(ui, |ui| {
for (id, label) in items {
if ui.button(format!("{label}…")).clicked() {
app.run("effect.dialog", json!({"effect": id})).ok();
ui.close();
}
}
}
});
});
}
}
+5 -3
View File
@@ -374,9 +374,11 @@ pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&s
}
for s in subs {
ui.menu_button(s, |ui| {
for l in libs.iter().filter(|l| l.submenu == Some(s)) {
item(ui, l);
}
crate::widgets::menu_scroll(ui, |ui| {
for l in libs.iter().filter(|l| l.submenu == Some(s)) {
item(ui, l);
}
});
});
}
chosen
+4 -2
View File
@@ -158,8 +158,10 @@ pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) {
icons::paint(ui, "menu", rect.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text_dim });
let resp = resp.on_hover_text("Panel menu");
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(220.0);
panel_menu_items(app, ui, id);
crate::widgets::menu_scroll(ui, |ui| {
ui.set_min_width(220.0);
panel_menu_items(app, ui, id);
});
});
}
+13
View File
@@ -426,6 +426,19 @@ fn combo<R>(
.inner
}
/// The body of a menu or popup list: as tall as its items up to the bottom of the window, and
/// scrolling only past that, so long menus (Window, Effect, a panel's menu) stay reachable on
/// small windows.
pub fn menu_scroll<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
// Room left below the popup's first item, less the popup frame and a small gap.
const BOTTOM_GAP: f32 = 12.0;
const MIN_HEIGHT: f32 = 120.0;
let room = (ui.ctx().content_rect().bottom() - ui.next_widget_position().y - BOTTOM_GAP).max(MIN_HEIGHT);
// The minimum lets the list grow past a popup's default 400 pt size; it still shrinks to
// its items when they need less.
egui::ScrollArea::vertical().max_height(room).min_scrolled_height(room).show(ui, add_contents).inner
}
/// Whether the blend-mode list draws a separator above `BlendMode::ALL[i]`: where a
/// [`BlendMode::group`] starts.
pub fn blend_separator_before(i: usize) -> bool {