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}