Skip to main content

kui_core/runtime/
menu_api.rs

1//! `Core`'s menu surface: opening one, drawing the stock one into the
2//! frame, and consuming the events its own rows produce.
3//!
4//! The menu the core opens is drawn by [`crate::widgets::context_menu`] —
5//! the same public widget an app calls — through the moment `Ui::finish`
6//! already reserves for content the view did not build (the `"root"`
7//! fill). So there is no overlay layer here, no window of the core's own,
8//! and nothing the conformance corpus cannot see: the menu is ordinary
9//! nodes in an ordinary frame.
10//!
11//! Its rows post ordinary click events too, which is why this module
12//! *takes them back*. A row the core built is found by key — recorded
13//! while building, never guessed at from a payload — and the events those
14//! keys produce never reach the app; what reaches the app is the item's
15//! own payload, on the node the menu was opened over.
16
17use crate::geom::Vec2;
18/// The longest selection a definition panel is offered for. A word, a
19/// term, a short phrase — past this it is a passage, and a dictionary has
20/// nothing to say about a passage.
21pub const LOOKUP_MAX: usize = 100;
22
23use crate::input::UiEvent;
24use crate::key::Key;
25use crate::menu::{Menu, MenuAction, MenuItem, MenuRole};
26use crate::runtime::Core;
27use crate::tree::OriginId;
28use crate::ui::Ui;
29use crate::value::Value;
30use crate::window::WindowId;
31
32impl Core {
33    /// The menu this window has open, if any.
34    pub fn menu(&self) -> Option<&Menu> {
35        self.menu.as_ref()
36    }
37
38    /// Tells the core that this host shows menus itself — an `NSMenu`, a
39    /// `TrackPopupMenu`, whatever the platform has. The core then keeps the
40    /// open menu as state and **does not
41    /// draw it**: the host reads `menu()`, shows it, and reports back with
42    /// [`Self::activate_menu_item`] or [`Self::close_menu`].
43    ///
44    /// Declared once by the driver, not per menu, because it is a fact
45    /// about the host and not about any one menu. Off by default: a host
46    /// that says nothing gets the drawn menu, which is every binding's
47    /// starting point and the only thing a headless test can see.
48    pub fn set_native_menus(&mut self, on: bool) {
49        self.native_menus = on;
50    }
51
52    /// Tells the core that this host can show the platform's definition
53    /// panel — macOS's Look Up. The standard Look Up row is then offered
54    /// where it means something, and a force click over text asks for
55    /// one; without it the core neither offers nor asks, because an item
56    /// that does nothing is worse than an item that is not there.
57    pub fn set_lookup_available(&mut self, on: bool) {
58        self.lookup_available = on;
59    }
60
61    /// Whether the host said it can show a definition panel.
62    pub fn lookup_available(&self) -> bool {
63        self.lookup_available
64    }
65
66    /// Whether the host said it shows menus itself.
67    pub fn native_menus(&self) -> bool {
68        self.native_menus
69    }
70
71    /// The host's menu reports that item `i` was chosen: the same path a
72    /// press on the drawn menu's row takes — the core performs what it
73    /// can, queues what the host must do, and returns the events the app
74    /// hears (one `menu` event on the node the menu was about).
75    ///
76    /// `None` when nothing was taken: no menu is open, or row `i` cannot
77    /// be chosen — a disabled row, a separator — in which case the menu
78    /// stays open and nothing is posted, since a native menu never reports
79    /// such a row and the drawn one has no click on it, so a door that
80    /// names one (`activateMenuItem(1)` over a select whose second option
81    /// is disabled) should be answered the way the pointer would be,
82    /// rather than handing the app a choice it disabled.
83    /// An index past the end closes the menu and posts nothing
84    /// (`Some` and empty), which is what a host reporting a row this build
85    /// does not know should do.
86    pub fn activate_menu_item(&mut self, i: usize) -> Option<Vec<UiEvent>> {
87        self.activate_menu_path(&[i])
88    }
89
90    /// [`Self::activate_menu_item`] for a row inside a submenu, by its
91    /// path: `[2, 0]` is the first row of the third row's submenu
92    /// ([`MenuItem::at_path`]) — what a host whose own menu nests them
93    /// (an `NSMenu`'s submenus) reports (backlog F128). A row that opens a
94    /// submenu is refused like a disabled one: the platform opens it and
95    /// never reports it chosen. A path that names no row closes the menu
96    /// and posts nothing, as an index past the end does.
97    pub fn activate_menu_path(&mut self, path: &[usize]) -> Option<Vec<UiEvent>> {
98        let menu = self.menu.as_ref()?;
99        let mut out = Vec::new();
100        match MenuItem::at_path(&menu.items, path) {
101            Some(_) if !MenuItem::choosable_at(&menu.items, path) => return None,
102            Some(_) => self.choose_menu_path(path, &mut out),
103            None => {
104                self.close_menu();
105            }
106        }
107        // The same way out an input's events take: a row of the devtools'
108        // own select is the panel's whichever menu showed it.
109        self.outbound(&mut out);
110        Some(out)
111    }
112
113    /// What the selection would be looked *up* as, or `None` when it is
114    /// not something to look up.
115    ///
116    /// A definition panel answers a word or a short phrase. Handed a
117    /// paragraph it draws the whole thing back over the page as one
118    /// enormous highlighted strip and then says "No Results Found" — seen
119    /// in the field, 2026-09-09 — so a selection that spans lines, or runs
120    /// past 100 characters, is neither offered nor asked about.
121    /// A force click always passes: it selects one word.
122    pub fn lookup_text(&self) -> Option<String> {
123        let text = self.copy_selection().filter(|t| !t.trim().is_empty())?;
124        let one_line = !text.contains('\n');
125        (one_line && text.chars().count() <= LOOKUP_MAX).then_some(text)
126    }
127
128    /// A definition-panel request for whatever is selected: the text, and
129    /// the baseline origin of its first line to anchor the panel at.
130    pub(crate) fn lookup_action(&self) -> Option<MenuAction> {
131        if !self.lookup_available {
132            return None;
133        }
134        let text = self.lookup_text()?;
135        let at = self.selection_anchor()?;
136        Some(MenuAction::LookUp { text, at })
137    }
138
139    /// Opens one. A second replaces the first: a window has one menu, the
140    /// way it has one selection and one focus.
141    ///
142    /// Nothing is drawn here. The next frame draws it, because the menu is
143    /// state and the frame is a function of state — which is also what
144    /// makes an app that never pumps another frame after a right-click a
145    /// bug the app can see rather than a menu that appears out of turn.
146    pub fn open_menu(&mut self, mut menu: Menu) {
147        // `at` arrives in the host's coordinates (ADR 0024); the menu is
148        // kept in the window's, which the drawn one and the native one
149        // both place by.
150        menu.at = menu.at.plus(self.dt_shift());
151        self.open_menu_raw(menu);
152    }
153
154    /// `open_menu` for a point already in window coordinates — the
155    /// right-click's own.
156    pub(crate) fn open_menu_raw(&mut self, mut menu: Menu) {
157        // Whose menu it is, unless the caller said: the node it is about.
158        // An extension that opens a menu over its own node hears the rows
159        // come back, the way it hears every other event it declared (ADR
160        // 0014 decision 6).
161        //
162        // The node and not `self.origin()`, which is the *builder's*: it
163        // is right during a view and stale afterwards — whatever ran last
164        // in the frame that finished, an extension in any app that loads
165        // one — so a host opening a menu from its own event handler would
166        // have the rows answered to somebody else.
167        if menu.origin == crate::tree::OriginId::HOST {
168            menu.origin = self
169                .tree
170                .keys
171                .iter()
172                .position(|k| *k == menu.target)
173                .map_or_else(|| self.origin(), |i| self.tree.origins[i]);
174        }
175        // Which editor the menu is about, remembered now rather than
176        // looked up when a row is chosen: the menu's rows are focusable
177        // (they have to be — the arrow keys are the composite's), so by
178        // the time Copy runs, focus has moved off the field the user
179        // right-clicked. The window's *selection* survives the press
180        // (a press under `OriginId::MENU` is spared), but an editor's
181        // lives with its focus.
182        self.menu_editor = self.edit.focused();
183        self.menu_focus = self.focus;
184        // The accelerators in the platform's spelling, as the bar's are
185        // (`declare_menu_bar`): a portable `"mod+shift+n"` is drawn `⇧⌘N`
186        // or `Ctrl+Shift+N`, and a host that shows the menu itself reads
187        // the same string back (backlog F127).
188        MenuItem::normalize_accels(&mut menu.items);
189        self.menu = Some(menu);
190        // A new menu opens with none of its submenus open.
191        self.menu_sub = Submenus::default();
192    }
193
194    /// A select field built this frame (`widgets::select`): its key and
195    /// the rows its menu will have. Read back by
196    /// [`Self::consume_select_events`] when the field is clicked.
197    pub(crate) fn declare_select(&mut self, key: Key, items: Vec<MenuItem>) {
198        self.selects.push((key, items));
199    }
200
201    /// A click on a select field is not the app's: taken back here and
202    /// answered with the field's menu, opened under its bottom-left
203    /// corner — the core's own menu, so what follows (the rows, a choice,
204    /// a dismissal) is `consume_menu_events`' as for any other. The
205    /// field's node is the target, so the choice is posted on it.
206    pub(crate) fn consume_select_events(&mut self, out: &mut Vec<UiEvent>) {
207        if self.selects.is_empty() {
208            return;
209        }
210        let mut clicked: Option<Key> = None;
211        out.retain(|ev| {
212            let is = ev.payload.get_bool("select") == Some(true)
213                && self.selects.iter().any(|(k, _)| *k == ev.key);
214            if is {
215                clicked = Some(ev.key);
216            }
217            !is
218        });
219        let Some(key) = clicked else {
220            return;
221        };
222        let Some((_, items)) = self.selects.iter().find(|(k, _)| *k == key) else {
223            return;
224        };
225        let items = items.clone();
226        // Under the field, in window coordinates: the last frame laid the
227        // field out, which is the frame the click was made against.
228        let at = self
229            .tree
230            .keys
231            .iter()
232            .position(|k| *k == key)
233            .map(|i| {
234                let (p, s) = (self.tree.pos[i], self.tree.size[i]);
235                Vec2::new(p.x, p.y + s.h)
236            })
237            .unwrap_or_default();
238        self.open_menu_raw(Menu::new(key, at, items));
239    }
240
241    /// Closes it, and every submenu open in it. Returns whether one was
242    /// open.
243    pub fn close_menu(&mut self) -> bool {
244        self.menu_sub = Submenus::default();
245        self.menu.take().is_some()
246    }
247
248    /// The submenus open in the drawn context menu: the row opened at each
249    /// level, outermost first — `[2]` is the third row's submenu, `[2, 0]`
250    /// that and the submenu of its first row. Empty when none is, and
251    /// always while the host shows menus itself (backlog F128).
252    pub fn menu_submenus(&self) -> &[usize] {
253        &self.menu_sub.open
254    }
255
256    /// What choosing an item left for the host: clipboard work, which is
257    /// the host's in this library. Drained like the window and audio
258    /// commands, and empty on every frame of an app whose menus are all
259    /// its own.
260    pub fn take_menu_actions(&mut self) -> Vec<MenuAction> {
261        std::mem::take(&mut self.menu_actions)
262    }
263
264    /// Draws the open menu into the frame being built, if there is one.
265    /// Called by `Ui::finish` after the extensions have filled their
266    /// slots, so the menu is the last thing declared and therefore the
267    /// frame's modal scope and its topmost float.
268    pub(crate) fn build_menu(ui: &mut Ui<'_>) {
269        if ui.core().native_menus {
270            // The host is showing it. Nothing is drawn, so there are no
271            // rows to click and `consume_menu_events` finds nothing to
272            // take back.
273            return;
274        }
275        let Some(menu) = ui.core().menu.clone() else {
276            return;
277        };
278        // The widget floats against the host's viewport, whose origin is
279        // the dock's edge under a left dock (ADR 0024): the window point
280        // becomes a host one. Its nodes are opened under `OriginId::MENU`,
281        // which is how `consume_menu_events` knows them.
282        let at = menu.at.minus(ui.core().dt_shift());
283        crate::widgets::context_menu(ui, at, &menu.items);
284    }
285
286    /// The stock items for a right-click on `region`, or none when there
287    /// is nothing standard to offer there. An editor gets the four every
288    /// platform's field has; a selection scope gets the two that mean
289    /// anything without a text model behind them — nothing owns the text
290    /// under a label, so it cannot be cut into or pasted over.
291    ///
292    /// Every item is always present and only its *enabling* moves: a menu
293    /// whose rows shuffle depending on what happens to be possible is one
294    /// nobody can use without reading it every time.
295    pub(crate) fn default_menu_items(
296        &self,
297        editor: Option<Key>,
298        scope: Option<Key>,
299    ) -> Vec<MenuItem> {
300        if let Some(key) = editor {
301            let has = self.edit.copy_selection(key).is_some_and(|t| !t.is_empty());
302            return vec![
303                MenuItem::role(MenuRole::Cut).enabled(has),
304                MenuItem::role(MenuRole::Copy).enabled(has),
305                // The core cannot see a clipboard, so it cannot know
306                // whether there is anything to paste; the host that can
307                // will say so when it renders this natively (step 3).
308                MenuItem::role(MenuRole::Paste),
309                MenuItem::separator(),
310                MenuItem::role(MenuRole::SelectAll),
311            ];
312        }
313        if scope.is_some() {
314            // `copy_selection`, not `selection_text`: a `cells` grid is a
315            // scope too, and its selection is in cells rather than in
316            // runs — reading only the text one left Copy dimmed over a
317            // terminal with half its screen selected.
318            let has = self.copy_selection().is_some_and(|t| !t.is_empty());
319            let mut items = vec![
320                MenuItem::role(MenuRole::Copy).enabled(has),
321                MenuItem::role(MenuRole::SelectAll),
322            ];
323            // Only where the host can show one, and only with something
324            // to look up: the row is the platform's, and a dead one would
325            // be a promise this library cannot keep.
326            if self.lookup_available {
327                // Enabled only for something a dictionary can answer —
328                // dimmed for a passage, and dimmed rather than missing, so
329                // the rows a reader reaches for stay where they were.
330                let can = self.lookup_text().is_some();
331                items.insert(0, MenuItem::role(MenuRole::LookUp).enabled(has && can));
332                items.insert(1, MenuItem::separator());
333            }
334            return items;
335        }
336        Vec::new()
337    }
338
339    /// A secondary press the app did not claim: opens the stock menu when
340    /// the press landed on something with standard items, and does nothing
341    /// at all otherwise — a right-click on a plain box has never opened a
342    /// menu and does not start now.
343    ///
344    /// `claimed` is whether the node under the pointer declared
345    /// `onContextMenu`. That declaration wins: the app asked to own the
346    /// menu there, and one of the two has to win by declaration rather
347    /// than by luck.
348    pub(crate) fn auto_menu(&mut self, at: Vec2, claimed: bool) {
349        if claimed {
350            return;
351        }
352        let Some(region) = self.interaction.hit_at(at) else {
353            return;
354        };
355        let (key, origin) = (region.key, region.origin);
356        let editor = region.edit_origin.map(|_| key);
357        let scope = region.select_scope;
358        let items = self.default_menu_items(editor, scope);
359        if items.is_empty() {
360            return;
361        }
362        let target = editor.or(scope).unwrap_or(key);
363        self.open_menu_raw(Menu::new(target, at, items).origin(origin));
364        // The press told us which editor this is about, which is better
365        // than what held focus: a right-click moves no focus (it must
366        // leave a selection alone), so the field under the pointer is not
367        // necessarily the focused one.
368        self.menu_editor = editor;
369    }
370
371    /// Filters the events one input produced: anything belonging to the
372    /// core's own menu is consumed and acted on, and what the app hears
373    /// instead is the item's payload on the node the menu was about.
374    ///
375    /// Runs on the way out of `handle_input`, so a menu row cannot leak a
376    /// click into an app that never declared one.
377    pub(crate) fn consume_menu_events(&mut self, out: &mut Vec<UiEvent>) {
378        if self.menu.is_none() {
379            return;
380        }
381        let taken = Self::take_surface_events(out, OriginId::MENU);
382        if let Some(path) = taken.row_path() {
383            // A row that opens a submenu opens it — the click, Enter, a
384            // reader's press — and stays open; any other row is chosen.
385            let opens = self
386                .menu
387                .as_ref()
388                .and_then(|m| MenuItem::at_path(&m.items, &path))
389                .is_some_and(MenuItem::has_submenu);
390            if opens {
391                let keyboard = self.focus_visible;
392                self.open_submenu(MenuSurface::Context, path, keyboard);
393            } else {
394                self.choose_menu_path(&path, out);
395            }
396        } else if taken.dismissed {
397            self.close_menu();
398        }
399    }
400
401    /// Takes every event of one of the core's own surfaces out of `out`
402    /// and reads what they said: a `dismiss` on the surface's modal root,
403    /// a click on the `row`th item (its payload is `{row}`, the message
404    /// `widgets::menu_row_tag` gave it, whether the pointer or Enter
405    /// clicked it), a click on the `title`th menu of the bar (`{title}`).
406    /// The one filter both menus consume through, so what a surface's
407    /// nodes post is read in one place. Hover, focus and the rest of a
408    /// surface's own events are taken with them: none of it is the app's.
409    pub(crate) fn take_surface_events(out: &mut Vec<UiEvent>, origin: OriginId) -> Taken {
410        let mut taken = Taken::default();
411        let index = |v: &Value, name: &str| v.get(name).and_then(Value::as_int).map(|i| i as usize);
412        out.retain(|ev| {
413            if ev.origin != origin {
414                return true;
415            }
416            if ev.kind() == Some("dismiss") {
417                taken.dismissed = true;
418            } else if let Some(i) = index(&ev.payload, "row") {
419                taken.row = Some(i);
420                taken.path = row_path_of(&ev.payload);
421            } else if let Some(i) = index(&ev.payload, "title") {
422                taken.title = Some(i);
423            }
424            false
425        });
426        taken
427    }
428
429    /// Performs one item and closes the menu. A standard role the core can
430    /// finish it finishes; the clipboard three become a [`MenuAction`] for
431    /// the host; everything else is the app's, and reaches it as an event
432    /// on the node the menu was opened over.
433    fn choose_menu_path(&mut self, path: &[usize], out: &mut Vec<UiEvent>) {
434        let Some(menu) = self.menu.clone() else {
435            return;
436        };
437        let Some(item) = MenuItem::at_path(&menu.items, path).cloned() else {
438            return;
439        };
440        self.close_menu();
441        self.perform_menu_item(&item, menu.target, menu.origin, out);
442    }
443
444    /// One item, performed and posted, wherever it was chosen from: the
445    /// open context menu's row, a host's native menu, or a menu of the
446    /// application menu bar. The two callers differ only in what they close first
447    /// and what node the event lands on, so everything after that is here
448    /// and cannot drift between them.
449    ///
450    /// `target` is the node the event is posted on and `origin` who hears
451    /// it; the standard roles act on [`Core::menu_editor`], which each
452    /// caller sets when its menu opens — an editor's selection lives with
453    /// its focus, and a menu's rows take that focus.
454    pub(crate) fn perform_menu_item(
455        &mut self,
456        item: &MenuItem,
457        target: Key,
458        origin: crate::tree::OriginId,
459        out: &mut Vec<UiEvent>,
460    ) {
461        // A row that is its chord plays it instead (backlog F151).
462        if let Some(kp) = item.replayed() {
463            self.replay_chord(kp, out);
464            return;
465        }
466        if item.role == MenuRole::Separator {
467            return;
468        }
469        self.perform_role(item.role, target, out);
470        // Every chosen item posts, including the standard ones: an app
471        // that wants to know its editor was cut from does not have to
472        // guess, and one that does not simply ignores the event.
473        let payload = item.id.clone().unwrap_or_else(|| Value::str(item.text()));
474        out.push(UiEvent {
475            origin,
476            window: WindowId::MAIN,
477            key: target,
478            payload: Value::map([
479                ("kind", Value::str("menu")),
480                ("role", Value::str(item.role.name())),
481                ("item", payload),
482            ]),
483            slot: None,
484        });
485    }
486
487    /// What a standard role does, against [`Core::menu_editor`] or the
488    /// window's selection: everything [`Self::perform_menu_item`] does
489    /// but post the `menu` event.
490    fn perform_role(&mut self, role: MenuRole, target: Key, out: &mut Vec<UiEvent>) {
491        match role {
492            MenuRole::Separator => {}
493            MenuRole::SelectAll => {
494                // The one standard item that needs nobody: an editor
495                // selects its own text, a scope selects its runs.
496                match self.menu_editor {
497                    Some(key) => {
498                        self.move_focus(Some(key));
499                        // The editor directly, not back through
500                        // `handle_input`: this runs *inside* one already,
501                        // and the events the nested call returned were
502                        // dropped on the floor.
503                        self.edit_with_fonts(|edit, fs| {
504                            edit.apply_key(
505                                key,
506                                crate::input::EditKey::SelectAll,
507                                crate::input::Mods::default(),
508                                fs,
509                            )
510                        });
511                    }
512                    None => {
513                        let scope = self.selection().map_or(target, |s| s.scope);
514                        self.select_all_in(scope);
515                    }
516                }
517            }
518            MenuRole::Copy => {
519                // `copy_selection` again, for the reason it is used to
520                // enable the row: a `cells` grid's selection is in cells,
521                // and reading only the text one left Copy lit over a
522                // terminal and then copied nothing when it was chosen.
523                let text = match self.menu_editor {
524                    Some(key) => self.edit.copy_selection(key),
525                    None => self.copy_selection().filter(|t| !t.is_empty()),
526                };
527                if let Some(text) = text {
528                    let html = self.selection_html();
529                    self.menu_actions
530                        .push(MenuAction::SetClipboard { text, html });
531                }
532            }
533            MenuRole::Cut => {
534                // Only an editor can be cut from: nothing owns the text
535                // behind a static selection, so there is nothing to take
536                // it out of. The item is simply not offered there.
537                if let Some(key) = self.menu_editor
538                    && let Some(text) = self.cut_editor(key)
539                {
540                    self.move_focus(Some(key));
541                    // No `html`: an editor's text is one style, and what
542                    // was cut is gone anyway.
543                    self.menu_actions
544                        .push(MenuAction::SetClipboard { text, html: None });
545                    // And the edit it is: every other mutation posts one
546                    // (`apply_text`, `apply_key`, a reader's `setValue`),
547                    // so an app mirroring the field hears this one too
548                    // (AR15).
549                    self.push_edit_event(key, "changed", out);
550                }
551            }
552            MenuRole::Paste => self.queue_paste(),
553            MenuRole::LookUp => {
554                if let Some(action) = self.lookup_action() {
555                    self.menu_actions.push(action);
556                }
557            }
558            MenuRole::Custom => {}
559            // A Mac's application performs these where it draws the menu;
560            // anywhere else the event the row posts is the app's to act on
561            // (backlog F152).
562            MenuRole::About
563            | MenuRole::Hide
564            | MenuRole::HideOthers
565            | MenuRole::ShowAll
566            | MenuRole::Quit => {}
567        }
568    }
569}
570
571impl Core {
572    /// A `replay` row chosen from a menu the core draws, or reported by a
573    /// host's: its chord, played where the keyboard was before the menu
574    /// took it, as the keyboard would have sent it — the press to the key
575    /// sink, the editing key or the text it maps to, the release — and
576    /// for the clipboard and undo chords a driver performs itself
577    /// (`Shell::edit_chord`), the same thing done here, against the editor
578    /// or the selection: the core has no driver to ask (backlog F151).
579    pub(crate) fn replay_chord(&mut self, kp: crate::input::KeyPress, out: &mut Vec<UiEvent>) {
580        use crate::input::{EditKey, InputEvent, KeyCode, Mods};
581        // The menu that was chosen from is still the last frame's modal
582        // scope, and everything outside it inert, until the next frame
583        // lays the window out without it. The chord is for the window
584        // under it, so it is routed as that frame will see it: under the
585        // app's own modal if there is one, else under none.
586        if let Some((i, ..)) = self.modal
587            && matches!(self.tree.origins[i], OriginId::MENU | OriginId::MENU_BAR)
588        {
589            self.modal = (0..i)
590                .rev()
591                .find(|&j| {
592                    self.tree.specs[j].events().modal.is_some()
593                        && !self.tree.origins[j].is_core_surface()
594                })
595                .map(|j| (j, self.tree.subtree_end(j), self.tree.keys[j]));
596        }
597        if let Some(k) = self.menu_focus.take()
598            && self.tree.keys.contains(&k)
599        {
600            self.move_focus(Some(k));
601        }
602        out.extend(self.route_input(InputEvent::KeyDown(kp.clone())));
603        let letter = match kp.code {
604            KeyCode::Char(c) if kp.mods.primary() => Some(c.to_ascii_lowercase()),
605            _ => None,
606        };
607        let editor = self.edit.focused();
608        let scope = self.selection().is_some() || self.cell_selection().is_some();
609        let role = match letter {
610            _ if editor.is_none() && !scope => None,
611            Some('c') => Some(MenuRole::Copy),
612            Some('x') => Some(MenuRole::Cut),
613            Some('v') => Some(MenuRole::Paste),
614            Some('a') => Some(MenuRole::SelectAll),
615            _ => None,
616        };
617        let history = match letter {
618            Some('z') if kp.mods.shift => Some(EditKey::Redo),
619            Some('z') => Some(EditKey::Undo),
620            Some('y') => Some(EditKey::Redo),
621            _ => None,
622        };
623        if let Some(role) = role {
624            self.menu_editor = editor;
625            let target = editor.unwrap_or(Key::ROOT);
626            self.perform_role(role, target, out);
627        } else if let (Some(ek), Some(_)) = (history, editor) {
628            out.extend(self.route_input(InputEvent::Key(ek, Mods::default())));
629        } else if let Some(ev) = kp.edit_event() {
630            out.extend(self.route_input(ev));
631        }
632        out.extend(self.route_input(InputEvent::KeyUp(kp.released())));
633    }
634}
635
636/// What one input said to one of the core's surfaces (`Core::take_surface_events`).
637#[derive(Default)]
638pub(crate) struct Taken {
639    pub dismissed: bool,
640    pub row: Option<usize>,
641    /// The rows opened on the way to `row`, for a row of a submenu.
642    pub path: Vec<usize>,
643    pub title: Option<usize>,
644}
645
646impl Taken {
647    /// The clicked row's whole path, `path` then `row`.
648    pub fn row_path(&self) -> Option<Vec<usize>> {
649        let row = self.row?;
650        Some(self.path.iter().copied().chain([row]).collect())
651    }
652}
653
654/// The `path` a submenu row's tag carries (`widgets::menu_row_tag`);
655/// empty for a top-level row, which carries none.
656fn row_path_of(payload: &Value) -> Vec<usize> {
657    match payload.get("path") {
658        Some(Value::List(ps)) => ps
659            .iter()
660            .filter_map(Value::as_int)
661            .map(|p| p as usize)
662            .collect(),
663        _ => Vec::new(),
664    }
665}
666
667// -- Submenus (backlog F128) --------------------------------------------------
668// A row with a submenu opens it beside itself; the rows inside are drawn by
669// the same `menu_panel`, under the same origin, so their clicks come back
670// through `take_surface_events` like any row's, carrying the path that
671// says which submenu they are in. What is open is the core's, per drawn
672// menu: the frame cannot derive which row the pointer last rested on, or
673// that the keyboard closed what the pointer opened.
674
675/// Which of the core's two drawn menus a row belongs to, told by the
676/// origin its nodes were opened under.
677#[derive(Clone, Copy, Debug, PartialEq, Eq)]
678pub(crate) enum MenuSurface {
679    /// The context menu (`open_menu`), and a select's list.
680    Context,
681    /// The drawn menu bar's open menu.
682    Bar,
683}
684
685impl MenuSurface {
686    pub(crate) fn of(origin: OriginId) -> Option<Self> {
687        match origin {
688            OriginId::MENU => Some(MenuSurface::Context),
689            OriginId::MENU_BAR => Some(MenuSurface::Bar),
690            _ => None,
691        }
692    }
693
694    fn origin(self) -> OriginId {
695        match self {
696            MenuSurface::Context => OriginId::MENU,
697            MenuSurface::Bar => OriginId::MENU_BAR,
698        }
699    }
700}
701
702/// The submenus open in one of the core's drawn menus.
703#[derive(Clone, Debug, Default)]
704pub(crate) struct Submenus {
705    /// The row opened at each level, outermost first: `[2, 0]` is the
706    /// third row's submenu and, inside it, its first row's. Empty: none.
707    pub open: Vec<usize>,
708    /// The row the pointer was last seen on, as a path. The pointer opens
709    /// and closes submenus when this *changes*, so a keyboard that moved on
710    /// is not undone by a pointer resting where it was.
711    pub hovered: Option<Vec<usize>>,
712    /// The keyboard opened the innermost submenu: its first row takes
713    /// focus on the frame that draws it (`submenu_drawn`).
714    pub focus_first: bool,
715    /// A switch away from an open submenu the pointer asked for, waiting
716    /// [`SUBMENU_SWITCH_DELAY`] in case it is only passing over the row on
717    /// its way into that submenu (backlog RG150).
718    pub pending: Option<PendingSwitch>,
719    /// Some row of this menu was under the pointer during the frame's
720    /// build: a pass that sees none forgets `hovered`, so coming back to
721    /// a row the keyboard closed opens it again.
722    pub seen: bool,
723    /// The menu was built this frame (`submenu_pass` ran). A frame that
724    /// builds no menu on this surface — an app that stopped drawing the
725    /// bar, or handed it to the platform — has no rows for a switch to
726    /// ripen on, so a pending one is dropped at the frame's end rather
727    /// than owing frames for good (backlog RG154).
728    pub built: bool,
729}
730
731/// A row the pointer moved onto while a submenu beside another was open.
732#[derive(Clone, Debug)]
733pub(crate) struct PendingSwitch {
734    pub row: Vec<usize>,
735    pub opens: bool,
736    /// The frame clock's reading when the pointer arrived.
737    pub since: f64,
738}
739
740/// How long the pointer rests on another row before an open submenu gives
741/// way to it. Travelling diagonally from a row to a lower row of its
742/// submenu crosses the rows below it; without the wait each one closed
743/// the submenu the pointer was heading for.
744pub(crate) const SUBMENU_SWITCH_DELAY: f64 = 0.3;
745
746impl Submenus {
747    /// What the pointer on `row` means for what is open: a row with a
748    /// submenu opens it, closing whatever was open beside it; any other
749    /// row closes the submenus below its own level.
750    fn switch_to(&mut self, row: &[usize], opens: bool) {
751        let level = row.len() - 1;
752        if opens {
753            // Kept when it is already the open one, deeper submenus and all:
754            // the pointer coming back to its row from inside it.
755            if !self.open.starts_with(row) {
756                self.open = row.to_vec();
757            }
758        } else if self.open.len() > level && self.open[..level] == row[..level] {
759            self.open.truncate(level);
760        }
761    }
762}
763
764impl Core {
765    fn submenus(&mut self, s: MenuSurface) -> &mut Submenus {
766        match s {
767            MenuSurface::Context => &mut self.menu_sub,
768            MenuSurface::Bar => &mut self.menu_bar_sub,
769        }
770    }
771
772    /// The rows of the menu `s` has open: the context menu's, or the bar's
773    /// open menu's.
774    fn surface_items(&self, s: MenuSurface) -> Option<&[MenuItem]> {
775        match s {
776            MenuSurface::Context => self.menu.as_ref().map(|m| m.items.as_slice()),
777            MenuSurface::Bar => {
778                let open = self.menu_bar_open?;
779                Some(self.menu_bar.as_ref()?.menus.get(open)?.items.as_slice())
780            }
781        }
782    }
783
784    /// Opens the submenu of the row at `path` — and with it the ones on the
785    /// way, closing any other — the keyboard's way when `keyboard`, which
786    /// moves focus into it once it is drawn.
787    pub(crate) fn open_submenu(&mut self, s: MenuSurface, path: Vec<usize>, keyboard: bool) {
788        let sub = self.submenus(s);
789        sub.open = path;
790        sub.focus_first = keyboard;
791        sub.pending = None;
792    }
793
794    /// The row in the submenu at `path` that is open, if one is: what the
795    /// panel at that level draws its submenu beside.
796    pub(crate) fn submenu_open_at(&mut self, s: MenuSurface, path: &[usize]) -> Option<usize> {
797        let open = &self.submenus(s).open;
798        (open.len() > path.len() && open[..path.len()] == *path).then(|| open[path.len()])
799    }
800
801    /// The pointer is on the row at `row` (its whole path) this frame. On a
802    /// change of row: a row with a submenu opens it, closing whatever was
803    /// open beside it, and any other row closes the submenus below its own
804    /// level — the way every platform's menus follow the pointer.
805    ///
806    /// A row that would close a submenu open beside another — a sibling of
807    /// the row that opened it, or a row further out — waits
808    /// [`SUBMENU_SWITCH_DELAY`] on the frame clock first, and moving into
809    /// that submenu meanwhile cancels it. A driver that sets no clock
810    /// switches at once, as its transitions snap.
811    pub(crate) fn submenu_hovered(&mut self, s: MenuSurface, row: &[usize], opens: bool) {
812        let now = self.anim.time();
813        let sub = self.submenus(s);
814        sub.seen = true;
815        if sub.hovered.as_deref() == Some(row) {
816            // Still resting there: a switch it is waiting on ripens.
817            if let Some(p) = &sub.pending
818                && p.row == row
819                && now.is_none_or(|t| t - p.since >= SUBMENU_SWITCH_DELAY)
820            {
821                let p = sub.pending.take().unwrap();
822                sub.switch_to(&p.row, p.opens);
823            }
824            return;
825        }
826        sub.hovered = Some(row.to_vec());
827        sub.focus_first = false;
828        let level = row.len() - 1;
829        let away = sub.open.len() > level && !sub.open.starts_with(row);
830        match now {
831            Some(since) if away => {
832                sub.pending = Some(PendingSwitch {
833                    row: row.to_vec(),
834                    opens,
835                    since,
836                });
837            }
838            _ => {
839                sub.pending = None;
840                sub.switch_to(row, opens);
841            }
842        }
843    }
844
845    /// The build of the menu on `s` begins (`begin`) or ends: a build in
846    /// which the pointer was on none of its rows forgets the row it was
847    /// last on, and any switch waiting on it.
848    pub(crate) fn submenu_pass(&mut self, s: MenuSurface, begin: bool) {
849        let sub = self.submenus(s);
850        if begin {
851            sub.seen = false;
852            sub.built = true;
853        } else if !sub.seen {
854            sub.hovered = None;
855            sub.pending = None;
856        }
857    }
858
859    /// The frame is over: a switch waiting on a menu the frame did not
860    /// build has no rows to ripen on and is dropped, so the frames it
861    /// owed are not owed by a bar the app stopped drawing (backlog
862    /// RG154). The context menu is built by every `Ui::finish` that has
863    /// one open; the bar only where `widgets::menu_bar` is called.
864    pub(crate) fn submenu_frame_end(&mut self) {
865        for sub in [&mut self.menu_sub, &mut self.menu_bar_sub] {
866            if !sub.built {
867                sub.pending = None;
868            }
869            sub.built = false;
870        }
871    }
872
873    /// Whether a switch is waiting on the clock: the driver owes the
874    /// frames that let it ripen.
875    pub(crate) fn submenu_waiting(&self) -> bool {
876        self.menu_sub.pending.is_some() || self.menu_bar_sub.pending.is_some()
877    }
878
879    /// The submenu at `path` was drawn this frame, its first row that can
880    /// take focus at `first`: where focus lands when the keyboard opened it.
881    pub(crate) fn submenu_drawn(&mut self, s: MenuSurface, path: &[usize], first: Key) {
882        let sub = self.submenus(s);
883        if sub.focus_first && sub.open == path {
884            sub.focus_first = false;
885            self.move_focus(Some(first));
886            self.focus_visible = true;
887        }
888    }
889
890    /// The keyboard inside one of the core's drawn menus, where a submenu
891    /// changes what a key means: the Right arrow on a row with a submenu
892    /// opens it, focus on its first row; the Left arrow inside a submenu
893    /// closes it, focus back on its row; Escape closes the innermost open
894    /// submenu rather than the whole menu. Returns whether the key was
895    /// taken; every other key goes on as before (the arrows walk the rows
896    /// of whichever panel holds focus, `composite_step`).
897    pub(crate) fn submenu_key(&mut self, ek: crate::input::EditKey) -> bool {
898        use crate::input::EditKey;
899        if !matches!(ek, EditKey::Left | EditKey::Right | EditKey::Escape) {
900            return false;
901        }
902        // No menu of the core's open, no submenu to work: an editor's
903        // arrows skip the walk below, and an app drawing `context_menu`
904        // itself — whose hovers still note a submenu — cannot leave one
905        // behind for a later Escape to be spent on.
906        if self.menu.is_none() && self.menu_bar_open.is_none() {
907            return false;
908        }
909        // The focused row, if it is one of the core's: which menu, and
910        // where in it.
911        let focused = self.focus.and_then(|k| {
912            let i = self.tree.keys.iter().position(|key| *key == k)?;
913            let s = MenuSurface::of(self.tree.origins[i])?;
914            let tag = self.tree.specs[i].events().on_click.as_ref()?;
915            let row = tag.get("row").and_then(Value::as_int)? as usize;
916            let mut path = row_path_of(tag);
917            path.push(row);
918            Some((s, path))
919        });
920        match (ek, focused) {
921            (EditKey::Right, Some((s, path))) => {
922                let opens = self
923                    .surface_items(s)
924                    .and_then(|items| MenuItem::at_path(items, &path))
925                    .is_some_and(|item| item.enabled && item.has_submenu());
926                if opens {
927                    self.open_submenu(s, path, true);
928                }
929                opens
930            }
931            // A row at `[a, b, c]` is in the submenu `[a, b]` opened; what
932            // closes is that one, down to `[a]`.
933            (EditKey::Left, Some((s, path))) if path.len() > 1 => {
934                self.close_submenu(s, path.len() - 2);
935                true
936            }
937            (EditKey::Escape, focused) => {
938                // The menu the focus is in, or else whichever has a submenu
939                // open: the pointer may have opened one the keyboard is not in.
940                let s = focused.map(|(s, _)| s).or_else(|| {
941                    [MenuSurface::Context, MenuSurface::Bar]
942                        .into_iter()
943                        .find(|&s| !self.submenus(s).open.is_empty())
944                });
945                let Some(s) = s else { return false };
946                let depth = self.submenus(s).open.len();
947                if depth == 0 {
948                    return false;
949                }
950                self.close_submenu(s, depth - 1);
951                true
952            }
953            _ => false,
954        }
955    }
956
957    /// Closes the submenus of `s` past the first `keep` — the one the row
958    /// at `open[..=keep]` opened and every one inside it — and puts focus
959    /// on that row, which is where the closed one hung.
960    fn close_submenu(&mut self, s: MenuSurface, keep: usize) {
961        let sub = self.submenus(s);
962        if sub.open.len() <= keep {
963            return;
964        }
965        let row = sub.open[..=keep].to_vec();
966        sub.open.truncate(keep);
967        sub.focus_first = false;
968        sub.pending = None;
969        // The row the closed submenu hangs from, found by the tag its click
970        // carries; it is in the last frame's tree, since its submenu was.
971        let (parent, i) = row.split_at(keep);
972        let origin = s.origin();
973        let at = (0..self.tree.len()).find(|&n| {
974            self.tree.origins[n] == origin
975                && self.tree.specs[n]
976                    .events()
977                    .on_click
978                    .as_ref()
979                    .is_some_and(|tag| {
980                        tag.get("row").and_then(Value::as_int) == Some(i[0] as i64)
981                            && row_path_of(tag) == parent
982                    })
983        });
984        if let Some(n) = at {
985            self.land_focus(n);
986        }
987    }
988}