Merge pull request #23 from storytold/pr/ui-dialog-menu-sizing

UI: dialogs and menus size to their content
This commit is contained in:
BFlat 2026-10-05 00:58:01 +07:00 committed by GitHub
commit 25c8f32fdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 8 deletions

View File

@ -195,7 +195,8 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
});
let heading = (spec.heading)(&d);
egui::Window::new(heading.as_str())
.id(egui::Id::new("dialog"))
// One window per kind, so a dialog never inherits another dialog's size.
.id(egui::Id::new(("dialog", d.kind.as_str())))
.order(egui::Order::Foreground)
.collapsible(false)
.resizable(false)
@ -211,7 +212,10 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
ui.add_space(12.0);
cancel = (spec.body)(app, ui, &mut d);
ui.add_space(16.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// The button row is as tall as the buttons: a right-to-left layout would otherwise take
// all the height left in the window, so the window could never shrink to its content.
let row = egui::vec2(ui.available_width(), ui.spacing().interact_size.y);
ui.allocate_ui_with_layout(row, egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some(label) = spec.ok
&& widgets::primary_button(ui, label).clicked()
{

View File

@ -15,6 +15,7 @@ fn app() -> VectorcraftApp {
fn frame(app: &mut VectorcraftApp, input: egui::RawInput) {
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let mut out = ctx.run_ui(input, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
}
@ -176,3 +177,25 @@ fn effect_dialog_applies_to_its_appearance_item() {
assert_eq!(n.appearance.items[1].effects().len(), 1, "the effect goes to the stroke");
assert!(n.appearance.effects.is_empty() && n.appearance.items[0].effects().is_empty());
}
#[test]
fn dialogs_are_as_tall_as_their_content() {
// The save prompt, shown after a taller dialog, is as tall as its text and buttons: no empty
// band around the buttons, nothing inherited from the other dialog.
let mut app = app();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let height = |app: &mut VectorcraftApp, kind: &str, fields: serde_json::Value| {
open(app, kind, fields);
for _ in 0..30 {
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
}
ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind)))).unwrap().height()
};
let tall = height(&mut app, "command", json!({"__command": "object.move", "a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8}));
let prompt = height(&mut app, crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"}));
assert!(prompt < 180.0 && prompt < tall, "save prompt {prompt} pt tall (the dialog before it: {tall} pt)");
}

View File

@ -1338,10 +1338,7 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
} else {
egui::RichText::new(*title).size(13.0).color(t.text)
};
ui.menu_button(text, |ui| {
ui.set_min_width(230.0);
render_items(app, ui, items, &mut clicked);
});
ui.menu_button(text, |ui| menu_body(app, ui, items, &mut clicked));
}
});
if let Some((id, p)) = clicked {
@ -1349,6 +1346,12 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
}
}
/// A top-level menu's popup: as wide as its widest item (label plus shortcut), at least 230 pt.
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);
}
fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked: &mut Option<(String, Value)>) {
let t = Tokens::get(ui.ctx());
for it in items {
@ -1367,7 +1370,7 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked
}
Item::Todo(label, sc) => {
ui.add_enabled_ui(false, |ui| {
ui.add(egui::Button::new(*label).shortcut_text(pretty_shortcut(sc)).min_size(egui::vec2(ui.available_width(), 0.0)));
ui.add(egui::Button::new(*label).shortcut_text(pretty_shortcut(sc)));
})
.response
.on_disabled_hover_text("Coming soon — tracked in the parity plan");
@ -1386,7 +1389,7 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked
Some(false) => format!(" {label}"),
None => label,
};
let r = ui.add_enabled(en, egui::Button::new(text).shortcut_text(sc).min_size(egui::vec2(ui.available_width(), 0.0)));
let r = ui.add_enabled(en, egui::Button::new(text).shortcut_text(sc));
if r.clicked() {
*clicked = Some(click_target(label_of(it), id, p));
ui.close();
@ -1847,4 +1850,28 @@ mod tests {
menu_entries(&app).into_iter().filter(|e| e.command.as_deref() == Some("effect.expandAppearance")).map(|e| e.path).collect();
assert_eq!(paths, [vec!["Object".to_string()]]);
}
#[test]
fn menus_are_as_wide_as_their_items() {
// Items size the popup to the widest label and shortcut instead of the widest a menu may be.
let app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let tree = menu_tree();
let (_, file) = tree.iter().find(|(t, _)| *t == "File").unwrap();
let id = egui::Id::new("test-menu");
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).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, file, &mut None));
});
});
out.textures_delta.clear();
}
let w = ctx.memory(|m| m.area_rect(id)).unwrap().width();
assert!((230.0..340.0).contains(&w), "File menu is {w} pt wide");
}
}