MENU-BAR: pointing at another menu title switches the open menu in the in-window bar
This commit is contained in:
parent
bba4172f83
commit
caa102aabd
|
|
@ -752,10 +752,29 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32
|
|||
if total <= max_width {
|
||||
let saved = ui.spacing().item_spacing.x;
|
||||
ui.spacing_mut().item_spacing.x = TITLE_GAP;
|
||||
for (title, items) in &bar {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(crate::i18n::tr(title)).font(font.clone()).color(t.text_label)).frame(false));
|
||||
crate::widgets::register(ui.ctx(), format!("menu:{title}"), r.rect);
|
||||
egui::Popup::menu(&r).show(|ui| crate::menu_level::level(ui, 1, bar_bottom, |ui| nodes_ui(ui, items, mac, &mut clicked, 1, bar_bottom)));
|
||||
// All titles first, then their popups, so a title the pointer moves onto can take over
|
||||
// from the open menu before either is drawn (no frame with two menus or none).
|
||||
let titles: Vec<egui::Response> = bar
|
||||
.iter()
|
||||
.map(|(title, _)| {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(crate::i18n::tr(title)).font(font.clone()).color(t.text_label)).frame(false));
|
||||
crate::widgets::register(ui.ctx(), format!("menu:{title}"), r.rect);
|
||||
r
|
||||
})
|
||||
.collect();
|
||||
let confirmed = switch_open_title(ui.ctx(), &titles);
|
||||
for ((_, items), r) in bar.iter().zip(&titles) {
|
||||
// (opening and closing by click is done in `switch_open_title`)
|
||||
// (the click confirming a menu a hover opened must not close it again)
|
||||
let close = if confirmed == Some(egui::Popup::default_response_id(r)) {
|
||||
egui::PopupCloseBehavior::IgnoreClicks
|
||||
} else {
|
||||
egui::PopupCloseBehavior::CloseOnClick
|
||||
};
|
||||
egui::Popup::menu(r)
|
||||
.close_behavior(close)
|
||||
.open_memory(None)
|
||||
.show(|ui| crate::menu_level::level(ui, 1, bar_bottom, |ui| nodes_ui(ui, items, mac, &mut clicked, 1, bar_bottom)));
|
||||
}
|
||||
ui.spacing_mut().item_spacing.x = saved;
|
||||
} else {
|
||||
|
|
@ -781,6 +800,47 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32
|
|||
ui.cursor().left() - start
|
||||
}
|
||||
|
||||
/// Native menu-bar behaviour for the top-level titles: while one menu is open, resting the pointer
|
||||
/// on another title closes it and opens that one, and a click on another title opens it in that
|
||||
/// same click (instead of only closing the open menu as a click outside). Without an open menu
|
||||
/// hovering does nothing; clicking a title toggles its menu. egui keeps one popup open at a time.
|
||||
/// Returns the menu whose title was just clicked right after a hover opened it.
|
||||
fn switch_open_title(ctx: &egui::Context, titles: &[egui::Response]) -> Option<egui::Id> {
|
||||
let popup = egui::Popup::default_response_id;
|
||||
let key = egui::Id::new("lc-menubar-hover-opened");
|
||||
let open = titles.iter().position(|r| egui::Popup::is_id_open(ctx, popup(r)));
|
||||
// the menu a hover opened, until it closes or is clicked
|
||||
let mut confirmed = None;
|
||||
let mut hover_opened: Option<egui::Id> = ctx.data(|d| d.get_temp(key)).filter(|id| open.is_some_and(|o| popup(&titles[o]) == *id));
|
||||
for (i, r) in titles.iter().enumerate() {
|
||||
if r.clicked() {
|
||||
// the click that follows a hover switch confirms the menu the hover opened
|
||||
if hover_opened == Some(popup(r)) {
|
||||
confirmed = Some(popup(r));
|
||||
} else {
|
||||
egui::Popup::toggle_id(ctx, popup(r));
|
||||
}
|
||||
hover_opened = None;
|
||||
} else if r.hovered() && open.is_some_and(|o| o != i) {
|
||||
egui::Popup::open_id(ctx, popup(r));
|
||||
hover_opened = Some(popup(r));
|
||||
// a pointer move alone doesn't schedule the frame that draws the new menu
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
// (leaving the titles ends it: a later click on the open title closes the menu)
|
||||
let hover_opened = hover_opened.filter(|_| titles.iter().any(|r| r.hovered()));
|
||||
ctx.data_mut(|d| match hover_opened {
|
||||
Some(id) => {
|
||||
d.insert_temp(key, id);
|
||||
}
|
||||
None => {
|
||||
d.remove::<egui::Id>(key);
|
||||
}
|
||||
});
|
||||
confirmed
|
||||
}
|
||||
|
||||
/// A submenu row showing `text` at `depth`, whose rows `children` draws one level deeper,
|
||||
/// bounded like every level. The row is registered as `menusub:<label>` (the untranslated
|
||||
/// label), and while its submenu is open it is the anchor the next frame's room is measured from.
|
||||
|
|
|
|||
|
|
@ -84,3 +84,64 @@ fn a_tall_menu_scrolls_instead_of_growing_past_the_window() {
|
|||
assert!(!rows.is_empty(), "no submenu rows registered");
|
||||
assert!(rows.iter().any(|(_, r)| !level.contains_rect(*r)), "every row fits a 420 pt window: {rows:?}");
|
||||
}
|
||||
|
||||
// ---- top-level menus switch on hover, like native menu bars
|
||||
|
||||
/// The title whose menu is open (the open level hangs from its left edge), or none.
|
||||
fn open_menu(h: &Headless) -> Option<String> {
|
||||
let level = widget(h, "menu-level:1")?;
|
||||
widgets_with(h, "menu:").into_iter().filter(|(id, _)| id != "menu:all").find(|(_, r)| (r.left() - level.left()).abs() < 12.0).map(|(id, _)| id)
|
||||
}
|
||||
|
||||
fn go(h: &mut Headless, method: &str, id: &str) {
|
||||
let r = h.request(method, json!({"id": id}), T);
|
||||
assert_eq!(r["ok"], true, "{method} {id}: {r}");
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hovering_another_title_switches_the_open_menu() {
|
||||
let mut h = app([1280.0, 703.0]);
|
||||
// hovering with no menu open does nothing
|
||||
go(&mut h, "ui.hoverWidget", "menu:Edit");
|
||||
assert_eq!(open_menu(&h), None);
|
||||
go(&mut h, "ui.clickWidget", "menu:File");
|
||||
assert_eq!(open_menu(&h).as_deref(), Some("menu:File"));
|
||||
for title in ["Edit", "View", "Photo", "Window", "Help", "File"] {
|
||||
go(&mut h, "ui.hoverWidget", &format!("menu:{title}"));
|
||||
assert_eq!(open_menu(&h), Some(format!("menu:{title}")), "after hovering {title}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_another_title_opens_it_in_one_click() {
|
||||
let mut h = app([1280.0, 703.0]);
|
||||
go(&mut h, "ui.clickWidget", "menu:Edit");
|
||||
assert_eq!(open_menu(&h).as_deref(), Some("menu:Edit"));
|
||||
// move away from the titles first so only the click can be what switches
|
||||
let r = h.request("ui.move", json!({"x": 640.0, "y": 400.0}), T);
|
||||
assert_eq!(r["ok"], true);
|
||||
h.settle(SETTLE);
|
||||
go(&mut h, "ui.clickWidget", "menu:View");
|
||||
assert_eq!(open_menu(&h).as_deref(), Some("menu:View"));
|
||||
// clicking the open title closes it
|
||||
go(&mut h, "ui.clickWidget", "menu:View");
|
||||
assert_eq!(open_menu(&h), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn click_outside_and_escape_close_the_open_menu() {
|
||||
let mut h = app([1280.0, 703.0]);
|
||||
go(&mut h, "ui.clickWidget", "menu:File");
|
||||
assert!(open_menu(&h).is_some());
|
||||
let r = h.request("ui.click", json!({"x": 640.0, "y": 600.0}), T);
|
||||
assert_eq!(r["ok"], true);
|
||||
h.settle(SETTLE);
|
||||
assert_eq!(open_menu(&h), None, "click outside");
|
||||
go(&mut h, "ui.clickWidget", "menu:Photo");
|
||||
assert!(open_menu(&h).is_some());
|
||||
let r = h.request("ui.key", json!({"key": "Escape"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
assert_eq!(open_menu(&h), None, "Escape");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -531,7 +531,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; ⌫ 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 |
|
||||
| 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 | ⬜ | | |
|
||||
|
|
|
|||
Loading…
Reference in New Issue