Skip to main content

kui_core/runtime/
menubar_api.rs

1//! `Core`'s application menu bar: the declaration, the one open menu the
2//! drawn bar keeps, and the two doors a chosen item comes back through.
3//!
4//! The declaration is data and sticky, the way the window title is: a frame
5//! that declares none leaves the last one in force and a frame that
6//! declares an empty bar takes it away, so the driver diffs
7//! [`Core::menu_bar_revision`] rather than a tree.
8//!
9//! Where the platform owns a bar the driver hands the declaration over and
10//! reports back through [`Core::activate_menu_bar_item`]. Where it does not,
11//! [`crate::widgets::menu_bar`] draws it: its titles and its rows are
12//! ordinary nodes with ordinary click payloads, and this module *takes
13//! those events back* by key — recorded while building, never guessed at
14//! from a payload — exactly as `menu_api` does for the open context menu's
15//! rows. What reaches the app is one `{kind:"menu", role, item}`, the same
16//! event both menus have always posted.
17
18use crate::input::UiEvent;
19use crate::key::Key;
20use crate::menu::{MenuBar, MenuItem};
21use crate::runtime::Core;
22use crate::tree::OriginId;
23
24impl Core {
25    /// Declares the application menu for this frame.
26    ///
27    /// Sticky, and diffed: declaring the same bar again costs one
28    /// comparison and changes nothing, a different one bumps
29    /// [`Self::menu_bar_revision`] for the driver to notice, and an empty
30    /// [`MenuBar`] is how an app takes the bar away. A frame that says
31    /// nothing leaves the last declaration standing — which is what lets a
32    /// palette window declare no menu and leave the document window's bar
33    /// alone.
34    ///
35    /// Every item's accelerator is normalized on the way in: a portable
36    /// `"mod+s"` becomes the platform's own spelling (`"⌘S"`, `"Ctrl+S"`),
37    /// so the drawn bar and the platform's read the same and the platform's
38    /// can bind the key. A spelling kui cannot parse is left exactly as
39    /// written and drawn as written — an app's own shortcut is the app's.
40    pub fn declare_menu_bar(&mut self, bar: MenuBar) {
41        let bar = normalize(bar);
42        if self.menu_bar.as_ref() == Some(&bar) {
43            return;
44        }
45        // An open menu describes a bar that may no longer exist. Identity
46        // is the label at that index: an item list that changed under an
47        // open menu is ordinary (a row enabling, a setting checking), but a
48        // *different menu* at that index leaves the open index naming
49        // something the user never opened — and the row keys are derived
50        // from the index, so the next click would perform the new menu's
51        // item at that position.
52        let open_moved = self.menu_bar_open.is_some_and(|i| {
53            fn label(b: &MenuBar, i: usize) -> Option<&str> {
54                b.menus.get(i).map(|m| m.label.as_str())
55            }
56            self.menu_bar.as_ref().and_then(|old| label(old, i)) != label(&bar, i)
57        });
58        if open_moved {
59            self.menu_bar_open = None;
60            self.menu_bar_sub = super::menu_api::Submenus::default();
61        }
62        self.menu_bar = Some(bar);
63        self.menu_bar_origin = self.origin();
64        self.menu_bar_rev += 1;
65    }
66
67    /// The declaration in force, if any frame has made one.
68    pub fn menu_bar(&self) -> Option<&MenuBar> {
69        self.menu_bar.as_ref()
70    }
71
72    /// Bumped whenever the declaration changes. A driver keeps the number
73    /// it last applied and touches the platform only when they differ —
74    /// the menu-bar analogue of the title diff, and the reason re-declaring
75    /// an unchanged bar every frame costs nothing.
76    pub fn menu_bar_revision(&self) -> u64 {
77        self.menu_bar_rev
78    }
79
80    /// Tells the core that the platform owns the menu bar — macOS's, which
81    /// is not in any window. The drawn bar then
82    /// draws nothing, and the driver is the one that hands the declaration
83    /// over and reports what was chosen.
84    ///
85    /// Off by default, like [`Self::set_native_menus`]: a host that says
86    /// nothing draws its own bar, which is what every headless test and
87    /// every binding without a runner sees.
88    pub fn set_native_menu_bar(&mut self, on: bool) {
89        self.native_menu_bar = on;
90    }
91
92    /// Whether the platform said the menu bar is its.
93    pub fn native_menu_bar(&self) -> bool {
94        self.native_menu_bar
95    }
96
97    /// Which menu of the drawn bar is open, if any. Retained by the core
98    /// because it is the one thing the widget cannot derive from the frame
99    /// — and always `None` while the platform draws
100    /// the bar, since then the open menu is the platform's.
101    pub fn menu_bar_open(&self) -> Option<usize> {
102        self.menu_bar_open
103    }
104
105    /// The submenus open in the drawn bar's open menu, as
106    /// [`Self::menu_submenus`] reads the context menu's (backlog F128).
107    pub fn menu_bar_submenus(&self) -> &[usize] {
108        &self.menu_bar_sub.open
109    }
110
111    /// Opens one of the drawn bar's menus, closes it (`None`), and is what
112    /// a press on a title goes through. Public because a keymap is as good
113    /// a reason to open the File menu as a click is; out of range closes.
114    pub fn set_menu_bar_open(&mut self, menu: Option<usize>) {
115        let menu = menu.filter(|i| {
116            self.menu_bar
117                .as_ref()
118                .and_then(|b| b.menus.get(*i))
119                .is_some_and(|m| m.enabled && !m.items.is_empty())
120        });
121        if menu == self.menu_bar_open {
122            return;
123        }
124        // Another menu, or none: whatever submenus were open were the old
125        // one's (backlog F128).
126        self.menu_bar_sub = super::menu_api::Submenus::default();
127        // Which editor the menu is about, remembered as the menu opens for
128        // the reason `open_menu` remembers it: the rows are focusable, so
129        // by the time Copy runs the field the user was in has lost focus.
130        //
131        // Only when there *is* one, and never `None` over an answer already
132        // recorded: a press on a title has moved focus to the title before
133        // this runs (`Core::note_menu_bar_editor` is where the field was
134        // caught), and hovering across to another title runs it again with
135        // focus inside the open menu. Either would otherwise erase the
136        // field the menu is about and leave Cut with nothing to cut.
137        if menu.is_some()
138            && let Some(editor) = self.edit.focused()
139        {
140            self.menu_editor = Some(editor);
141        }
142        self.menu_bar_open = menu;
143    }
144
145    /// The platform's menu bar reports that an item was chosen: the same
146    /// path a press on the drawn bar's row takes. The core performs the
147    /// standard roles it can, queues what the host must do
148    /// (`take_menu_actions`) and returns the events the app hears.
149    ///
150    /// An index past the end does nothing, which is what a host reporting a
151    /// row this build does not have should do.
152    pub fn activate_menu_bar_item(&mut self, menu: usize, item: usize) -> Vec<UiEvent> {
153        self.activate_menu_bar_path(menu, &[item])
154    }
155
156    /// [`Self::activate_menu_bar_item`] for a row inside a submenu of menu
157    /// `menu`, by its path ([`crate::menu::MenuBar::item_at`]) — what a
158    /// platform bar whose menus nest reports (backlog F128). A row that
159    /// opens a submenu, a dead row or one under a dead row or menu, or a
160    /// path that names none, does nothing.
161    pub fn activate_menu_bar_path(&mut self, menu: usize, path: &[usize]) -> Vec<UiEvent> {
162        let mut out = Vec::new();
163        let Some(item) = self
164            .menu_bar
165            .as_ref()
166            .and_then(|b| b.menus.get(menu))
167            .filter(|m| m.enabled && MenuItem::choosable_at(&m.items, path))
168            .and_then(|m| MenuItem::at_path(&m.items, path))
169            .cloned()
170        else {
171            return out;
172        };
173        // Nothing displaced focus — the platform's menu bar is not in this
174        // window and takes none — so the editor is simply whichever one has
175        // it now.
176        self.menu_editor = self.edit.focused();
177        let (target, origin) = (self.menu_bar_target(), self.menu_bar_origin);
178        self.perform_menu_item(&item, target, origin, &mut out);
179        out
180    }
181
182    /// Where a menu-bar event lands: the bar's own root when one is drawn,
183    /// and the frame root when the platform draws it and there is no node.
184    fn menu_bar_target(&self) -> Key {
185        self.menu_bar_root.unwrap_or(Key::ROOT)
186    }
187
188    /// The drawn bar reports its root this frame (`widgets::menu_bar`):
189    /// where a menu-bar event lands (`menu_bar_target`). Its titles and
190    /// rows are known by their origin, not by key.
191    pub(crate) fn set_menu_bar_root(&mut self, root: Key) {
192        self.menu_bar_root = Some(root);
193    }
194
195    /// Filters the events one input produced: anything belonging to the
196    /// drawn bar is consumed and acted on — a title opens or closes its
197    /// menu, a row performs its item, a dismissal closes what is open — and
198    /// what the app hears instead is the item's own payload.
199    ///
200    /// Runs beside `consume_menu_events` on the way out of `handle_input`,
201    /// so a bar the app never declared can leak nothing and an app that
202    /// declared one never sees its plumbing.
203    pub(crate) fn consume_menu_bar_events(&mut self, out: &mut Vec<UiEvent>) {
204        if self.menu_bar_root.is_none() {
205            return;
206        }
207        // A `dismiss` is the bar's own: Escape, or a press outside it while
208        // a menu is open (the bar is the modal scope then, so its titles
209        // stay live and the app below does not).
210        let taken = Self::take_surface_events(out, OriginId::MENU_BAR);
211        let (title, row, dismissed) = (taken.title, taken.row_path(), taken.dismissed);
212        if let Some(i) = title {
213            // A press on the open menu's own title closes it, which is
214            // what every menu bar does and what makes the title a toggle.
215            let next = (self.menu_bar_open != Some(i)).then_some(i);
216            self.set_menu_bar_open(next);
217        } else if let Some(path) = row {
218            let Some(menu) = self.menu_bar_open else {
219                return;
220            };
221            let item = self
222                .menu_bar
223                .as_ref()
224                .and_then(|b| b.item_at(menu, &path))
225                .cloned();
226            // A row with a submenu opens it, as in the context menu.
227            if item.as_ref().is_some_and(MenuItem::has_submenu) {
228                let keyboard = self.focus_visible;
229                self.open_submenu(super::MenuSurface::Bar, path, keyboard);
230                return;
231            }
232            self.set_menu_bar_open(None);
233            if let Some(item) = item {
234                let (target, origin) = (self.menu_bar_target(), self.menu_bar_origin);
235                self.perform_menu_item(&item, target, origin, out);
236            }
237        } else if dismissed {
238            self.set_menu_bar_open(None);
239        }
240    }
241
242    /// A press has landed on a node of the drawn bar: remember the editor
243    /// it is about *now*, because the press is what moves focus onto the
244    /// title and the menu does not open until the release.
245    ///
246    /// The truth at press time, `None` included — a menu opened with no
247    /// field focused is about no field, and must not inherit the one an
248    /// earlier menu was about.
249    pub(crate) fn note_menu_bar_editor(&mut self) {
250        self.menu_editor = self.edit.focused();
251    }
252}
253
254/// Rewrites every accelerator kui can parse into the platform's own
255/// spelling, and leaves the rest exactly as declared. Runs once per
256/// changed declaration and not per frame, which is why `declare_menu_bar`
257/// compares after normalizing rather than before.
258fn normalize(mut bar: MenuBar) -> MenuBar {
259    for menu in &mut bar.menus {
260        MenuItem::normalize_accels(&mut menu.items);
261    }
262    bar
263}