mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-15 19:57:07 -04:00
Menus: menus taller than the window scroll
At a large UI scale the menu bar menus ran off the bottom of the window, so items such as Edit > Preferences could not be reached (#267). Every menu_button popup (menu bar menus and submenus, workspace and zoom menus, panel menus) now goes through menus::menu_button. It caps the menu's height at the larger of the spaces below and above its button, and the rest scrolls with the mouse wheel. Short menus are unchanged.
This commit is contained in:
1 parent
4e0e3949f1
commit
717ddb6070
6 files changed
+141
-46
No files matched your search
@@ -51,46 +51,56 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.add_space(8.0);
|
||||
let current = app.ui.workspace.clone();
|
||||
let shown = crate::i18n::workspace_name(&app.ui.language, ¤t);
|
||||
ui.menu_button(crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)), |ui| {
|
||||
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
|
||||
for w in
|
||||
["Essentials", "Advanced", "Book", "Digital Publishing", "Interactive for PDF", "Printing and Proofing", "Typography"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.chain(customs.iter().cloned())
|
||||
{
|
||||
if ui.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w))).clicked()
|
||||
crate::menus::menu_button(
|
||||
ui,
|
||||
crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)),
|
||||
|ui| {
|
||||
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
|
||||
for w in
|
||||
["Essentials", "Advanced", "Book", "Digital Publishing", "Interactive for PDF", "Printing and Proofing", "Typography"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.chain(customs.iter().cloned())
|
||||
{
|
||||
let _ = app.run("window.workspace", json!({"name": w}));
|
||||
if ui
|
||||
.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w)))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.workspace", json!({"name": w}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.button(crate::rtl::widget(
|
||||
ui,
|
||||
format!(
|
||||
"{} {}",
|
||||
crate::i18n::tr(&app.ui.language, "Reset"),
|
||||
crate::i18n::workspace_name(&app.ui.language, ¤t)
|
||||
),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.resetWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.button(crate::rtl::widget(
|
||||
ui,
|
||||
format!("{} {}", crate::i18n::tr(&app.ui.language, "Reset"), crate::i18n::workspace_name(&app.ui.language, ¤t)),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("window.resetWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
|
||||
let _ = app.run("window.newWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if !customs.is_empty() {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
|
||||
for w in &customs {
|
||||
if ui.button(w).clicked() {
|
||||
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
|
||||
ui.close();
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
|
||||
let _ = app.run("window.newWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if !customs.is_empty() {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
|
||||
for w in &customs {
|
||||
if ui.button(w).clicked() {
|
||||
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() {
|
||||
app.status("Export a PDF, IDML or package to share — no cloud account needed.");
|
||||
@@ -511,7 +521,7 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
return;
|
||||
}
|
||||
let z = app.view().map(|v| v.zoom).unwrap_or(1.0);
|
||||
ui.menu_button(egui::RichText::new(format!("{:.2}% ▾", z * 100.0)).font(small.clone()), |ui| {
|
||||
crate::menus::menu_button(ui, egui::RichText::new(format!("{:.2}% ▾", z * 100.0)).font(small.clone()), |ui| {
|
||||
for p in [0.05, 0.125, 0.25, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0] {
|
||||
if ui.button(format!("{}%", p * 100.0)).clicked() {
|
||||
let _ = app.run("view.zoom", json!({"zoom": p}));
|
||||
|
||||
@@ -1743,6 +1743,42 @@ pub fn activate(app: &mut DesignApp, id: &str, params: &Value) {
|
||||
let _ = app.run(id, p);
|
||||
}
|
||||
|
||||
/// [`egui::Ui::menu_button`] for every DesignCraft menu: a menu taller than the window scrolls
|
||||
/// with the mouse wheel inside the larger of the spaces below and above its button, as native
|
||||
/// menus do. Shorter menus are unchanged.
|
||||
pub fn menu_button<'a, R>(
|
||||
ui: &mut egui::Ui,
|
||||
atoms: impl egui::IntoAtoms<'a>,
|
||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> egui::InnerResponse<Option<R>> {
|
||||
// The button's rect from the last frame: a menu only opens on a button that was shown.
|
||||
let button = ui.ctx().read_response(ui.next_auto_id()).map(|r| r.rect);
|
||||
let max_height = menu_max_height(ui, button);
|
||||
ui.menu_button(atoms, |ui| {
|
||||
let mut scroll = egui::ScrollArea::vertical().max_height(max_height).auto_shrink([true, true]);
|
||||
if ui.is_sizing_pass() {
|
||||
// A menu that just opened starts at its top.
|
||||
scroll = scroll.vertical_scroll_offset(0.0);
|
||||
}
|
||||
scroll.show(ui, add_contents).inner
|
||||
})
|
||||
}
|
||||
|
||||
/// The tallest a menu's contents may be. It depends only on the button and the window (never on
|
||||
/// where the popup ended up), so the popup's placement cannot oscillate between frames.
|
||||
fn menu_max_height(ui: &egui::Ui, button: Option<egui::Rect>) -> f32 {
|
||||
let screen = ui.ctx().content_rect();
|
||||
let space = match button {
|
||||
// Submenus open beside their item, going down from its top or up from its bottom.
|
||||
Some(r) if egui::containers::menu::is_in_menu(ui) => (screen.bottom() - r.top()).max(r.bottom() - screen.top()),
|
||||
Some(r) => (screen.bottom() - r.bottom()).max(r.top() - screen.top()),
|
||||
None => screen.height(),
|
||||
};
|
||||
// The popup's frame and a small gap to the window edge.
|
||||
let chrome = egui::Frame::menu(ui.style()).total_margin().sum().y + 8.0;
|
||||
(space - chrome).max(ui.spacing().interact_size.y)
|
||||
}
|
||||
|
||||
/// The menu bar contents (inside the app bar; macOS uses the native menu instead).
|
||||
pub fn menu_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let lang = app.ui.language.clone();
|
||||
@@ -1751,7 +1787,7 @@ pub fn menu_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
menus.reverse();
|
||||
}
|
||||
for (menu, entries) in menus {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&lang, menu)), |ui| {
|
||||
menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&lang, menu)), |ui| {
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
ui.set_max_width(320.0);
|
||||
}
|
||||
@@ -1785,7 +1821,7 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
|
||||
Item::Sub(name, children) => {
|
||||
let sub = format!("{path}/{name}");
|
||||
let shown = crate::i18n::tr(&app.ui.language, name).to_owned();
|
||||
ui.menu_button(crate::rtl::widget(ui, shown), |ui| {
|
||||
menu_button(ui, crate::rtl::widget(ui, shown), |ui| {
|
||||
hidden += menu_items(app, ui, children, &sub);
|
||||
});
|
||||
}
|
||||
@@ -2501,4 +2537,53 @@ mod tests {
|
||||
assert!(app.ui.hidden_menu_items.is_empty());
|
||||
assert!(items.len() > 100, "every menu's items are listed: {}", items.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_taller_than_the_window_scrolls() {
|
||||
// Issue #267: at a large UI scale the menus ran off the bottom of the window.
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
|
||||
let last = std::cell::Cell::new(None::<egui::Rect>);
|
||||
let button = std::cell::Cell::new(None::<egui::Response>);
|
||||
let frame = |t: f64, events: Vec<egui::Event>| {
|
||||
let input = egui::RawInput { screen_rect: Some(screen), time: Some(t), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
let r = menu_button(ui, "Long", |ui| {
|
||||
for i in 0..60 {
|
||||
let r = ui.button(format!("Item {i}"));
|
||||
if i == 59 {
|
||||
last.set(Some(r.rect));
|
||||
}
|
||||
}
|
||||
});
|
||||
button.set(Some(r.response));
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(0.0, vec![]);
|
||||
let b = button.take().unwrap();
|
||||
let p = b.rect.center();
|
||||
let click = |pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(0.1, vec![egui::Event::PointerMoved(p), click(true)]);
|
||||
frame(0.2, vec![click(false)]);
|
||||
frame(0.3, vec![]);
|
||||
frame(0.4, vec![]);
|
||||
let popup = ctx.memory(|m| m.area_rect(egui::Popup::default_response_id(&b))).unwrap();
|
||||
assert!(popup.top() >= b.rect.bottom() - 1.0, "the menu opens below its button: {popup:?}");
|
||||
assert!(popup.bottom() <= screen.bottom(), "the menu fits in the window: {popup:?}");
|
||||
assert!(last.get().unwrap().top() > screen.bottom(), "the last item starts out of view");
|
||||
let inside = egui::pos2(popup.center().x, popup.bottom() - 20.0);
|
||||
let wheel = egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(0.0, -5000.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
frame(0.5, vec![egui::Event::PointerMoved(inside), wheel]);
|
||||
for i in 0..30 {
|
||||
frame(0.6 + f64::from(i) * 0.1, vec![]);
|
||||
}
|
||||
let l = last.get().unwrap();
|
||||
assert!(popup.contains_rect(l), "the wheel scrolls the last item into view: {l:?} in {popup:?}");
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[None]"))).clicked() {
|
||||
let _ = app.run("layout.pages.applyParent", json!({"pages": [abs], "parent": null}));
|
||||
ui.close();
|
||||
|
||||
@@ -1330,7 +1330,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
let features: Vec<String> =
|
||||
c["otfFeatures"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
|
||||
ui.menu_button("OpenType", |ui| open_type_menu(app, ui, &features));
|
||||
crate::menus::menu_button(ui, "OpenType", |ui| open_type_menu(app, ui, &features));
|
||||
});
|
||||
// Underline / Strikethrough Options.
|
||||
for (key, title) in [("underline", "Underline Options"), ("strikethrough", "Strikethrough Options")] {
|
||||
@@ -1385,7 +1385,7 @@ pub fn open_type_menu(app: &mut DesignApp, ui: &mut egui::Ui, features: &[String
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Stylistic Sets")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Stylistic Sets")), |ui| {
|
||||
let mask = otf::stylistic_sets(features);
|
||||
for n in 1..=20u32 {
|
||||
let mut on = mask & (1 << (n - 1)) != 0;
|
||||
|
||||
@@ -125,7 +125,7 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
|
||||
ui.close();
|
||||
}
|
||||
if !n.starts_with('[') {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Group")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Group")), |ui| {
|
||||
if n.contains('/') && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[No Group]"))).clicked() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": ""}));
|
||||
ui.close();
|
||||
|
||||
@@ -73,8 +73,8 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if icons::button(ui, "plus", 20.0, false, crate::i18n::tr(&app.ui.language, "New Swatch")).clicked() {
|
||||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
ui.menu_button("☰", |ui| {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
crate::menus::menu_button(ui, "☰", |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
let Ok(l) = app.session.execute("ink.list", &json!({})) else { return };
|
||||
let mut all = l["allToProcess"].as_bool().unwrap_or(false);
|
||||
if ui.checkbox(&mut all, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "All Spots to Process"))).changed() {
|
||||
@@ -162,7 +162,7 @@ fn swatch_row(
|
||||
ui.close();
|
||||
}
|
||||
if !sw.locked {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Color Group")), |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Color Group")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "(Top Level)"))).clicked() {
|
||||
let _ = app.run("swatch.moveToGroup", json!({"swatches": [sw.name], "group": null}));
|
||||
ui.close();
|
||||
|
||||
Reference in new issue
Block a user