mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-07 12:13:20 +00:00
craft-menu: shrink-to-fit bar, bar-run Quit, Window list opt-out on macOS; parse the minus sign (#354)
* Add craft-menu: Mac menu-bar layout and native-menu driver A platform-free menu model (built directly or from flat rows), the macOS layout (app menu, Window, Help, system-key clash resolution, translatable labels), and NativeMenu, which feeds a backend's key equivalents to egui as key presses, runs Minimize and craft-quit's Quit, and re-syncs only when the app's state changes. Generalised from PhotoCraft's native_menu.rs. * craft-menu: macOS menu bar through muda * craft-menu: clipboard keys, layout-assigned shortcuts, Hide Others and atomic rebuilds * craft-menu: menu key equivalents run while a text field has the keys * craft-menu: commands as CommandRef, item kinds, craft-keymap chords Items and rows run a craft_keymap::CommandRef and take a craft_keymap::Chord shortcut; crates/menu/src/chord.rs is gone. Native ids are the command's key, made unique with #2, #3 when a command is shown twice. Item.checked becomes ItemKind { Plain, Check, Radio } (radio shows as a check item on macOS). The built-in items' commands are Builtin::{Hide, HideOthers, Minimize, Quit}. MenuRole gains Edit, View and Other; tag_submenu finds its menu by role. While text editing, the keys left to the field are the clipboard keys and craft_keymap::text_editing_chords. Adds the Platforms, Document and Gate types (data only) and MenuBar::documents. * craft-menu: Gate::allows, the one rule for what may run from the menus * craft-menu: Windows, Linux and web layouts, documents in Window, keymap adapter layout(bar, Target, options) dispatches to mac_layout, desktop_layout or web_layout. Every layout drops what the target's Platforms leave out and lists MenuBar::documents as radio items last in the Window menu (ItemRole::Document is gone). Windows and Linux put About in Help, Settings in Edit as Preferences and Quit in File (Exit on Windows); the web layout has no Quit and takes the browser's own shortcuts off the items, reporting clashes. tidy drops empty submenus. apply_keymap sets shortcuts and toggle check items from a craft-keymap Keymap; apply_state sets enabled and checked per frame. * craft-menu: the egui-drawn menu bar for Windows, Linux and the web * craft-menu: gate, command hook and one muda handler for the native menu - NativeMenu::set_gate: app items greyed in place while modal or recording a shortcut; take_clicks drops clicks the gate forbids when they are taken. While recording, key equivalents reach egui as plain key presses. - NativeMenu::on_command: a hook called from the menu event handler for each allowed app click; take_clicks still returns every command. - Dispatch, shared between the driver and the backend's handler (Backend::attach replaces Backend::set_text_editing). - Only Cmd or Ctrl chords become key equivalents; other shortcuts are not shown natively (building an attributed title needs unsafe objc2 calls). - mac: one muda event handler per process, routed to the latest install; mac::install_app_menu ports craft-quit's minimal app menu onto it. - craft_quit::install_app_menu is deprecated in favor of craft_menu::mac::install_app_menu. * craft-menu: the bar takes Target, README section and NOTICE credit * craft-menu: bar tests beside the layout and native ones; only a click outside closes the menus * craft-menu: clippy (complex types, Copy clones, elided lifetimes) * craft-menu: keyboard access to the egui bar A tap on Alt alone or F10 focuses the bar, Alt+letter opens a menu and a letter in an open menu runs or opens the row with that mnemonic, as in a Windows menu bar. Mnemonics are assigned per level from the shown labels, underlined only while the keyboard drives the bar, and a label without an ASCII letter shows its English letter after it ("文件(F)") through the optional english hook. Preferred letters come from MenuBarUi::mnemonics. A Mac target has none, and a dialog or the shortcut recorder keeps the keys. Ported from PhotoCraft's menu_keys.rs and menu_keys_tests.rs. * craft-menu: one installer or the other; sort mnemonics before checking uniqueness * craft-menu: egui 0.37 * craft-menu: text fields keep their Edit items; web names by the browser's system; sync per frame; stable check kinds; handled hook clicks * craft-menu: sync reports a clash change on an unchanged bar * craft-menu: shrink-to-fit bar, bar runs Quit and Minimize, mnemonic before an ellipsis documented; craft-keymap: parse the minus sign * craft-keymap: test the arrow glyphs as keys * craft-menu: mac::install_with and MacOptions::window_list, for apps that list their documents themselves * craft-menu: a negative fit limit draws no bar; Window list docs name the Window menu
This commit is contained in:
@@ -126,12 +126,14 @@ const KEY_ALIASES: &[(&str, Key)] = &[
|
||||
("grave", Key::Backtick),
|
||||
("backquote", Key::Backtick),
|
||||
("apostrophe", Key::Quote),
|
||||
// The minus sign menus and docs print (U+2212), not the hyphen-minus the key types.
|
||||
("−", Key::Minus),
|
||||
];
|
||||
|
||||
impl Key {
|
||||
/// Reads a key name: canonical (`K`, `PageUp`, `;`), egui variant names (`ArrowUp`, `Num5`,
|
||||
/// `Semicolon`) and the aliases `esc`, `return`, `del`, `pgup`, `pgdn`, `spacebar`, `ins`,
|
||||
/// and the Mac key glyphs. Case does not matter.
|
||||
/// the Mac key glyphs and the minus sign `−`. Case does not matter.
|
||||
pub fn parse(name: &str) -> Option<Key> {
|
||||
Key::ALL
|
||||
.iter()
|
||||
|
||||
@@ -27,6 +27,22 @@ fn canonical_text_round_trips_for_every_key_and_modifier_set() {
|
||||
assert_eq!(serde_json::to_string(&chord("shift+cmd+k")).unwrap(), "\"Cmd+Shift+K\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plus_and_minus_keys_parse_from_their_glyphs() {
|
||||
assert_eq!(Key::parse("+"), Some(Key::Plus));
|
||||
assert_eq!(Key::parse("-"), Some(Key::Minus));
|
||||
assert_eq!(Key::parse("−"), Some(Key::Minus), "U+2212");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrow_glyphs_parse_as_arrow_keys() {
|
||||
for (glyph, key) in [("←", Key::Left), ("→", Key::Right), ("↑", Key::Up), ("↓", Key::Down)] {
|
||||
assert_eq!(Key::parse(glyph), Some(key), "{glyph}");
|
||||
}
|
||||
assert_eq!(Chord::parse("⌘←").unwrap().to_string(), "Cmd+Left");
|
||||
assert_eq!(Chord::parse("Alt+Shift+→").unwrap().to_string(), "Alt+Shift+Right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases_parse_to_canonical_chords() {
|
||||
let cases = [
|
||||
@@ -50,6 +66,8 @@ fn aliases_parse_to_canonical_chords() {
|
||||
("Cmd+Plus", "Cmd++"),
|
||||
("cmd+Equals", "Cmd+="),
|
||||
("Cmd+-", "Cmd+-"),
|
||||
("Cmd+−", "Cmd+-"),
|
||||
("⌘−", "Cmd+-"),
|
||||
("Cmd + K", "Cmd+K"),
|
||||
("esc", "Escape"),
|
||||
("return", "Enter"),
|
||||
|
||||
+15
-10
@@ -106,9 +106,7 @@ let laid_out = craft_menu::layout(&menu_bar, Target::current(), &LayoutOptions::
|
||||
egui::Panel::top("menu-bar").show(ui, |ui| {
|
||||
let gate = Gate { modal: dialog_open, recording_shortcut, text_editing: ui.ctx().text_edit_focused() };
|
||||
for command in MenuBarUi::new(&laid_out).gate(gate).label(&tr).show(ui) {
|
||||
if !craft_quit::on_menu_event(ui.ctx(), &command.id) {
|
||||
app.run(&command);
|
||||
}
|
||||
app.run(&command);
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -129,9 +127,9 @@ egui::Panel::top("menu-bar").show(ui, |ui| {
|
||||
keyboard. Each menu level assigns them from the labels as shown, so translated: unique within
|
||||
the level, the first unused letter, word starts first; `.mnemonics(&[("View", 'W'),
|
||||
("file.open", 'P')])` names preferred letters by title as shown or by command id. A label
|
||||
without an ASCII letter or digit shows its English label's letter after it, "文件(F)", when
|
||||
`.english(&hook)` returns that English label for the `Labeled::Command` or `Labeled::Title`;
|
||||
without one it has no mnemonic. While the bar has the keyboard the app's shortcuts wait, as
|
||||
without an ASCII letter or digit shows its English label's letter after it, "文件(F)", before a
|
||||
trailing ellipsis ("開く(O)…"), when `.english(&hook)` returns that English label for the
|
||||
`Labeled::Command` or `Labeled::Title`; without one it has no mnemonic. While the bar has the keyboard the app's shortcuts wait, as
|
||||
for an open menu (`craft_menu::bar::is_focused(ctx)`); call
|
||||
`craft_menu::bar::leave_for_shortcut(ctx)` before them so that a command shortcut (Ctrl+S,
|
||||
F7) leaves the bar and runs. While a dialog is open or a shortcut is being recorded (`Gate`),
|
||||
@@ -147,10 +145,13 @@ egui::Panel::top("menu-bar").show(ui, |ui| {
|
||||
choose).
|
||||
- **Narrow windows:** the menus that don't fit move into a trailing `⋯` menu; when not even the
|
||||
first fits, one `Menu` title holds them all. `.label(&tr)` translates `⋯`, `Menu` and the
|
||||
scroll arrows' names, with the same hook as `LayoutOptions::label`.
|
||||
scroll arrows' names, with the same hook as `LayoutOptions::label`. The bar fills its row;
|
||||
`.shrink_to_fit(max_width)` makes it take only the width its titles need, up to `max_width`,
|
||||
so other controls can share the row, and the menus past that overflow.
|
||||
- **Quit:** without an app Quit item, the Windows and Linux layouts add `Builtin::Quit`
|
||||
(`craft_quit::QUIT_MENU_ID`). The bar returns it like any other command; pass it to
|
||||
`craft_quit::on_menu_event`, which asks the window to close so the unsaved-changes check runs.
|
||||
(`craft_quit::QUIT_MENU_ID`). The bar runs it itself through `craft_quit::on_menu_event`,
|
||||
which asks the window to close so the unsaved-changes check runs, and doesn't return it; the
|
||||
same goes for `Builtin::Minimize`.
|
||||
- **Accessibility:** AccessKit sees a menu bar of menu items, the open menus, check and radio
|
||||
items with their state, disabled items, and each item's shortcut and mnemonic (`access_key`).
|
||||
- **Style:** `BarStyle::from_style(ui.style())` by default; `.style(..)` sets title padding and
|
||||
@@ -186,7 +187,11 @@ so the app's unsaved-changes prompt runs). Each menu event wakes egui, so a chos
|
||||
once rather than with the next input event.
|
||||
|
||||
The backend calls AppKit's Window and Help menu hooks for the menus with `MenuRole::Window` and
|
||||
`MenuRole::Help`; it never looks at titles. Services is AppKit's own submenu.
|
||||
`MenuRole::Help`; it never looks at titles. Services is AppKit's own submenu. AppKit's Window
|
||||
menu lists the app's windows after the menu's own items. An app that keeps its documents in one
|
||||
window and lists them in the Window menu itself (`MenuBar::documents`) would see each twice, so it
|
||||
installs with
|
||||
`mac::install_with(ctx, &bar, MacOptions::default().window_list(false))`.
|
||||
|
||||
Only shortcuts with ⌘ or ⌃ become key equivalents. The others (`B`, `⇧B`, `⌥B`, a bare `F5`) are
|
||||
tool keys the app handles itself; as key equivalents AppKit would take them from text fields.
|
||||
|
||||
@@ -18,6 +18,7 @@ use egui::containers::menu::{MenuConfig, MenuState, SubMenu, SubMenuButton};
|
||||
use egui::text::LayoutJob;
|
||||
use egui::{Color32, Id, PopupCloseBehavior, Response, TextFormat, TextStyle, Ui, UiKind, UiStackInfo, WidgetText};
|
||||
|
||||
use crate::model::Builtin;
|
||||
use crate::{Gate, Item, ItemKind, Menu, MenuBar, Node, Target};
|
||||
use keys::Keys;
|
||||
use nav::Nav;
|
||||
@@ -106,6 +107,7 @@ pub struct MenuBarUi<'a> {
|
||||
english: Option<&'a EnglishHook<'a>>,
|
||||
preferred: &'a [(&'a str, char)],
|
||||
id: Id,
|
||||
fit: Option<f32>,
|
||||
}
|
||||
|
||||
impl<'a> MenuBarUi<'a> {
|
||||
@@ -120,6 +122,7 @@ impl<'a> MenuBarUi<'a> {
|
||||
english: None,
|
||||
preferred: &[],
|
||||
id: Id::unique("craft-menu-bar"),
|
||||
fit: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ impl<'a> MenuBarUi<'a> {
|
||||
|
||||
/// The English label of a row or title, for labels the keyboard can't type a letter of
|
||||
/// (Chinese, Japanese, Korean…): such a label shows its English label's mnemonic after it,
|
||||
/// "文件(F)", as Windows menus do. Without this hook (or when it returns `None`) such a label
|
||||
/// "文件(F)", before a trailing ellipsis ("開く(O)…"), as Windows menus do. Without this hook (or when it returns `None`) such a label
|
||||
/// has no mnemonic. Not asked on a Mac target, which has no mnemonics.
|
||||
pub fn english(mut self, english: &'a dyn Fn(Labeled<'_>) -> Option<String>) -> MenuBarUi<'a> {
|
||||
self.english = Some(english);
|
||||
@@ -170,6 +173,14 @@ impl<'a> MenuBarUi<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Take only the width the titles need, at most `max_width` (`f32::INFINITY` for no limit)
|
||||
/// and the room the `Ui` has, so that other controls can share the row; past that the menus
|
||||
/// overflow into "⋯". By default the bar fills the row.
|
||||
pub fn shrink_to_fit(mut self, max_width: f32) -> MenuBarUi<'a> {
|
||||
self.fit = Some(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
/// The bar's id, where its state lives in egui memory: tells two bars in one window apart.
|
||||
pub fn id(mut self, id: Id) -> MenuBarUi<'a> {
|
||||
self.id = id;
|
||||
@@ -178,6 +189,8 @@ impl<'a> MenuBarUi<'a> {
|
||||
|
||||
/// Draw the bar and its open menu; returns the commands chosen this frame (at most one),
|
||||
/// clicked or run with ↩ or Space. Disabled items, and items the gate forbids, never run.
|
||||
/// [`Builtin::Quit`] (asks the window to close, see [`craft_quit::on_menu_event`]) and
|
||||
/// [`Builtin::Minimize`] run here and are left out.
|
||||
pub fn show(self, ui: &mut Ui) -> Vec<CommandRef> {
|
||||
let ctx = ui.ctx().clone();
|
||||
let style = self.style.clone().unwrap_or_else(|| BarStyle::from_style(ui.style()));
|
||||
@@ -207,7 +220,13 @@ impl<'a> MenuBarUi<'a> {
|
||||
press: self.id.with("press"),
|
||||
clicked: None,
|
||||
};
|
||||
ui.scope(|ui| {
|
||||
let room = self.fit.map_or(ui.available_width(), |max| max.min(ui.available_width())).max(0.0);
|
||||
let width = match self.fit {
|
||||
// The half point keeps rounding from pushing the last title into "⋯".
|
||||
Some(_) => (self.title_widths(ui, &draw, style.title_padding.x).iter().sum::<f32>() + 0.5).min(room),
|
||||
None => room,
|
||||
};
|
||||
ui.allocate_ui(egui::vec2(width, ui.available_height()), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ctx.accesskit_node_builder(ui.unique_id(), |n| n.set_role(Role::MenuBar));
|
||||
@@ -282,7 +301,28 @@ impl<'a> MenuBarUi<'a> {
|
||||
if ctx.input(|i| i.pointer.primary_released() || !i.pointer.primary_down()) {
|
||||
ctx.data_mut(|d| d.remove::<bool>(draw.press));
|
||||
}
|
||||
draw.clicked.into_iter().collect()
|
||||
draw.clicked
|
||||
.into_iter()
|
||||
.filter(|command| match Builtin::of(command) {
|
||||
Some(Builtin::Quit) => !craft_quit::on_menu_event(&ctx, Builtin::Quit.id()),
|
||||
Some(Builtin::Minimize) => {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Each title's width as shown, with any "(F)" it takes, and `pad` on either side.
|
||||
fn title_widths(&self, ui: &Ui, draw: &Draw<'_>, pad: f32) -> Vec<f32> {
|
||||
let font = TextStyle::Button.resolve(ui.style());
|
||||
let names: Vec<(&str, Name<'_>)> = self.bar.menus.iter().map(|m| (m.title.as_str(), Name::App(Labeled::Title(&m.title)))).collect();
|
||||
draw.mnemonics(&names)
|
||||
.0
|
||||
.iter()
|
||||
.map(|t| ui.painter().layout_no_wrap(t.to_owned(), font.clone(), Color32::PLACEHOLDER).size().x + 2.0 * pad)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The titles to draw: every menu when they fit in the row; else the menus that fit with
|
||||
@@ -293,9 +333,7 @@ impl<'a> MenuBarUi<'a> {
|
||||
let pad = 2.0 * ui.spacing().button_padding.x;
|
||||
let width = |t: &str| ui.painter().layout_no_wrap(t.to_owned(), font.clone(), Color32::PLACEHOLDER).size().x + pad;
|
||||
let room = ui.available_width();
|
||||
// Measured as shown, with any "(F)" a title takes.
|
||||
let names: Vec<(&str, Name<'_>)> = menus.iter().map(|m| (m.title.as_str(), Name::App(Labeled::Title(&m.title)))).collect();
|
||||
let widths: Vec<f32> = draw.mnemonics(&names).0.iter().map(|t| width(t)).collect();
|
||||
let widths = self.title_widths(ui, draw, ui.spacing().button_padding.x);
|
||||
if widths.iter().sum::<f32>() <= room {
|
||||
return menus.iter().map(Top::Menu).collect();
|
||||
}
|
||||
|
||||
+40
-5
@@ -73,11 +73,40 @@ thread_local! {
|
||||
/// or the deprecated `craft_quit::install_app_menu`, never both: called first, that one leaves
|
||||
/// this menu without events; called after, it replaces this menu bar with its app menu.
|
||||
pub fn install(ctx: &egui::Context, bar: &MenuBar) -> Option<NativeMenu> {
|
||||
install_with(ctx, bar, MacOptions::default())
|
||||
}
|
||||
|
||||
/// How [`install_with`] sets up the native menus.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct MacOptions {
|
||||
/// Make the [`MenuRole::Window`] menu AppKit's Windows menu, which lists the app's windows
|
||||
/// after the menu's own items. On by default. An app that lists its documents itself
|
||||
/// in the Window menu ([`MenuBar::documents`]) and keeps them in one window turns it off: AppKit
|
||||
/// would list that window as well.
|
||||
pub window_list: bool,
|
||||
}
|
||||
|
||||
impl Default for MacOptions {
|
||||
fn default() -> MacOptions {
|
||||
MacOptions { window_list: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl MacOptions {
|
||||
pub fn window_list(mut self, on: bool) -> MacOptions {
|
||||
self.window_list = on;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// [`install`] with `options`.
|
||||
pub fn install_with(ctx: &egui::Context, bar: &MenuBar, options: MacOptions) -> Option<NativeMenu> {
|
||||
MainThreadMarker::new()?;
|
||||
if INSTALLED.swap(true, Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
let mut menu = MacMenu::new(ctx);
|
||||
let mut menu = MacMenu::new(ctx, options);
|
||||
if !menu.build(bar) {
|
||||
INSTALLED.store(false, Ordering::SeqCst);
|
||||
return None;
|
||||
@@ -237,10 +266,11 @@ struct MacMenu {
|
||||
window: Option<WindowMenu>,
|
||||
/// The latest menus, waiting for the open menu to close.
|
||||
pending: Option<MenuBar>,
|
||||
options: MacOptions,
|
||||
}
|
||||
|
||||
impl MacMenu {
|
||||
fn new(ctx: &egui::Context) -> MacMenu {
|
||||
fn new(ctx: &egui::Context, options: MacOptions) -> MacMenu {
|
||||
let (tx, rx) = channel();
|
||||
MacMenu {
|
||||
ctx: ctx.clone(),
|
||||
@@ -252,6 +282,7 @@ impl MacMenu {
|
||||
structure: 0,
|
||||
window: None,
|
||||
pending: None,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +305,7 @@ impl MacMenu {
|
||||
if m.role == MenuRole::Window {
|
||||
window = Some((sub.clone(), ids, window_key(bar)));
|
||||
}
|
||||
if matches!(m.role, MenuRole::Window | MenuRole::Help) {
|
||||
if m.role == MenuRole::Help || (m.role == MenuRole::Window && self.options.window_list) {
|
||||
special.push((m.role, sub));
|
||||
}
|
||||
}
|
||||
@@ -302,7 +333,9 @@ impl MacMenu {
|
||||
if let Some(old) = &self.menu {
|
||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||
old.init_for_nsapp();
|
||||
if let Some((sub, ..)) = &self.window {
|
||||
if let Some((sub, ..)) = &self.window
|
||||
&& self.options.window_list
|
||||
{
|
||||
sub.set_as_windows_menu_for_nsapp();
|
||||
}
|
||||
}));
|
||||
@@ -334,7 +367,9 @@ impl MacMenu {
|
||||
let _ = menu.insert(&old, at);
|
||||
return Err(e);
|
||||
}
|
||||
sub.set_as_windows_menu_for_nsapp();
|
||||
if self.options.window_list {
|
||||
sub.set_as_windows_menu_for_nsapp();
|
||||
}
|
||||
Ok((sub, ids, window_key(bar)))
|
||||
}));
|
||||
match replaced {
|
||||
|
||||
@@ -65,6 +65,11 @@ struct Bar {
|
||||
pointer: Pos2,
|
||||
tree: Vec<(NodeId, accesskit::Node)>,
|
||||
clicked: Vec<CommandRef>,
|
||||
/// `shrink_to_fit`'s limit; the bar then shares a row with a label, which starts at `after`.
|
||||
fit: Option<f32>,
|
||||
after: Option<f32>,
|
||||
/// The viewport commands the bar sent.
|
||||
sent: Vec<egui::ViewportCommand>,
|
||||
}
|
||||
|
||||
impl Bar {
|
||||
@@ -88,6 +93,9 @@ impl Bar {
|
||||
pointer: pos2(-10.0, -10.0),
|
||||
tree: Vec::new(),
|
||||
clicked: Vec::new(),
|
||||
fit: None,
|
||||
after: None,
|
||||
sent: Vec::new(),
|
||||
};
|
||||
b.run(3);
|
||||
b
|
||||
@@ -102,16 +110,29 @@ impl Bar {
|
||||
events: std::mem::take(&mut self.events),
|
||||
..Default::default()
|
||||
};
|
||||
let (bar, gate, target, english, preferred) = (&self.bar, self.gate, self.target, self.english, &self.preferred);
|
||||
let (bar, gate, target, english, preferred, fit) = (&self.bar, self.gate, self.target, self.english, &self.preferred, self.fit);
|
||||
let mut out = Vec::new();
|
||||
let mut after = None;
|
||||
let mut full = self.ctx.run_ui(raw, |ui| {
|
||||
let mut ui_bar = MenuBarUi::new(bar).gate(gate).target(target).mnemonics(preferred);
|
||||
if let Some(english) = &english {
|
||||
ui_bar = ui_bar.english(english);
|
||||
}
|
||||
out = ui_bar.show(ui);
|
||||
match fit {
|
||||
Some(max) => {
|
||||
ui.horizontal(|ui| {
|
||||
out = ui_bar.shrink_to_fit(max).show(ui);
|
||||
after = Some(ui.label("after").rect.left());
|
||||
});
|
||||
}
|
||||
None => out = ui_bar.show(ui),
|
||||
}
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
self.after = after;
|
||||
if let Some(root) = full.viewport_output.remove(&egui::ViewportId::ROOT) {
|
||||
self.sent.extend(root.commands);
|
||||
}
|
||||
if let Some(update) = full.platform_output.accesskit_update {
|
||||
self.tree = update.nodes;
|
||||
}
|
||||
@@ -273,7 +294,8 @@ fn disabled_and_gate_forbidden_items_return_nothing() {
|
||||
b.click("New");
|
||||
assert!(b.take().is_empty(), "and they don't run");
|
||||
b.click("Quit");
|
||||
assert_eq!(b.take(), vec![Builtin::Quit.command()], "the built-in Quit still runs");
|
||||
assert!(b.take().is_empty(), "the bar runs the built-in Quit itself");
|
||||
assert!(b.sent.contains(&egui::ViewportCommand::Close), "and it still runs: the window is asked to close");
|
||||
|
||||
b.gate = Gate { text_editing: true, ..Gate::default() };
|
||||
b.click("File");
|
||||
@@ -446,6 +468,26 @@ fn the_web_shows_the_browsers_system_names_and_the_mac_its_glyphs() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `shrink_to_fit` leaves the rest of the row to other controls; past its limit the menus
|
||||
/// overflow into "⋯".
|
||||
#[test]
|
||||
fn a_bar_shrunk_to_fit_shares_its_row() {
|
||||
let mut b = wide();
|
||||
b.fit = Some(f32::INFINITY);
|
||||
b.run(2);
|
||||
let after = b.after.expect("the label after the bar");
|
||||
assert!(after < b.size.x / 2.0, "the bar takes only its titles' width: {after}");
|
||||
assert!(b.title("Help").is_some() && b.title("⋯").is_none());
|
||||
b.fit = Some(100.0);
|
||||
b.run(2);
|
||||
assert!(b.title("⋯").is_some(), "the menus past 100 points overflow");
|
||||
// The bar's 100 points and the row's item spacing (8).
|
||||
assert!(b.after.is_some_and(|x| x <= 109.0), "{:?}", b.after);
|
||||
// A limit an app computed from a full row can go negative.
|
||||
b.fit = Some(-20.0);
|
||||
b.run(2);
|
||||
}
|
||||
|
||||
/// A menu far taller than the window.
|
||||
fn tall() -> Bar {
|
||||
let rows = (0..60).map(|i| item(&format!("cmd.{i}"), &format!("Item {i}")).into()).collect();
|
||||
|
||||
Reference in New Issue
Block a user