Skip to main content

gpui_cn/menu/
state.rs

1//! The open menu: its rows, where it opened, the levels of submenus open
2//! in it, and the row the keyboard is on in each.
3
4use std::{
5    cell::{Cell, RefCell},
6    collections::HashMap,
7    rc::Rc,
8};
9
10use gpui_kit::{
11    App, Bounds, Context, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, ListState,
12    Pixels, Point, SharedString, Subscription, Window,
13    base::{Align, DeferredPopover, GlobalState, Placement},
14};
15
16use super::entry::{MenuEntry, PendingActivation};
17use crate::ActiveTheme as _;
18
19/// What a menu opens from.
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub enum MenuAnchor {
22    /// Under (or, without room, over) a trigger's bounds in window
23    /// coordinates, lined up with the given edge of it.
24    Trigger(Bounds<Pixels>, Align),
25    /// At a point in window coordinates, such as the pointer of a right
26    /// click. The menu's top-left corner sits on it, moved into the window
27    /// when it would not fit.
28    Point(Point<Pixels>),
29}
30
31/// What a [`MenuState`] reports.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum MenuEvent {
34    /// The menu opened.
35    Opened,
36    /// The menu closed, chosen from or dismissed.
37    Closed,
38    /// An item was chosen. The payload is its key.
39    Activated(SharedString),
40}
41
42/// One panel of an open menu: the root, or a submenu beside a row of the
43/// level before it.
44pub(crate) struct Level {
45    /// The row of the level before that opened this one; none for the
46    /// root.
47    parent_row: Option<usize>,
48    /// Where that row was, across the width of its panel, which this
49    /// level opens beside.
50    anchor: Bounds<Pixels>,
51    highlighted: Option<usize>,
52    list: ListState,
53    /// Where the panel was last laid out, so a press on it is not taken
54    /// for a press outside the menu.
55    bounds: Rc<Cell<Bounds<Pixels>>>,
56    /// Where each submenu row was last laid out, which its submenu opens
57    /// beside.
58    submenu_rows: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
59}
60
61impl Level {
62    fn new(
63        parent_row: Option<usize>,
64        anchor: Bounds<Pixels>,
65        rows: usize,
66        overdraw: Pixels,
67    ) -> Self {
68        Self {
69            parent_row,
70            anchor,
71            highlighted: None,
72            // The list lays out a menu's height of rows on its first frame,
73            // so every row that shows measures before the panel widens.
74            list: ListState::new(rows, ListAlignment::Top, overdraw),
75            bounds: Rc::new(Cell::new(Bounds::default())),
76            submenu_rows: Rc::new(RefCell::new(HashMap::new())),
77        }
78    }
79
80    pub(crate) fn list(&self) -> &ListState {
81        &self.list
82    }
83
84    pub(crate) fn anchor(&self) -> Bounds<Pixels> {
85        self.anchor
86    }
87
88    pub(crate) fn bounds(&self) -> &Rc<Cell<Bounds<Pixels>>> {
89        &self.bounds
90    }
91
92    pub(crate) fn submenu_rows(&self) -> &Rc<RefCell<HashMap<usize, Bounds<Pixels>>>> {
93        &self.submenu_rows
94    }
95
96    pub(crate) fn highlighted(&self) -> Option<usize> {
97        self.highlighted
98    }
99}
100
101/// A menu that is open: nothing here exists while it is closed, so a
102/// closed menu cannot keep a highlight or a stale level.
103struct OpenMenu {
104    entries: Vec<MenuEntry>,
105    anchor: MenuAnchor,
106    /// The side of the trigger a trigger menu opened on, decided once
107    /// from the room its full height needs.
108    placement: Placement,
109    /// The root, then each open submenu. Never empty.
110    levels: Vec<Level>,
111    restore_focus: Option<FocusHandle>,
112    action_context: Option<FocusHandle>,
113    /// Held while open, so the rest of the application knows a popup is
114    /// up, and released with the state if it is dropped open.
115    _deferred: DeferredPopover,
116    _focus_watch: Subscription,
117}
118
119/// The open menu of a [`DropdownMenu`](super::DropdownMenu), a
120/// [`ContextMenu`](super::ContextMenu), or a text field.
121///
122/// Owned by the view that shows the menu, which observes it so a change
123/// renders. The rows are given when the menu opens, so what they show
124/// (whether Paste has something to paste) is read at that moment.
125///
126/// While open the menu holds focus. It closes on an outside press, on
127/// Escape at its root, when focus leaves it, and when an item is chosen,
128/// and then gives focus back to the element that had it before.
129pub struct MenuState {
130    open: Option<OpenMenu>,
131    focus: FocusHandle,
132    /// Where a dropdown's trigger was last laid out, which it opens from.
133    trigger_bounds: Rc<Cell<Bounds<Pixels>>>,
134    step_handler: Option<StepHandler>,
135}
136
137/// What Left or Right does when there is no submenu to act on.
138pub(crate) type StepHandler = Rc<dyn Fn(isize, &mut Window, &mut App)>;
139
140impl EventEmitter<MenuEvent> for MenuState {}
141
142/// The menu's own focus handle, which holds the keyboard while it is open.
143impl Focusable for MenuState {
144    fn focus_handle(&self, _: &App) -> FocusHandle {
145        self.focus.clone()
146    }
147}
148
149impl MenuState {
150    /// A closed menu.
151    pub fn new(cx: &mut Context<Self>) -> Self {
152        Self {
153            open: None,
154            focus: cx.focus_handle(),
155            trigger_bounds: Rc::new(Cell::new(Bounds::default())),
156            step_handler: None,
157        }
158    }
159
160    /// Whether the menu is open.
161    pub fn is_open(&self) -> bool {
162        self.open.is_some()
163    }
164
165    /// The key of the row the keyboard or the pointer is on, in the
166    /// innermost level that has one.
167    pub fn highlighted(&self) -> Option<SharedString> {
168        let open = self.open.as_ref()?;
169        let depth = open.active_depth();
170        let row = open.levels[depth].highlighted?;
171        open.entries_at(depth)?.get(row)?.key().cloned()
172    }
173
174    /// Opens the menu with `entries` at `anchor` and moves focus into it.
175    /// An action an item dispatches goes to the element that had focus
176    /// before, as its key binding would, and its shortcut is read there;
177    /// with nothing focused it goes up from the element the menu is drawn
178    /// in. A menu that is open already opens again in the new place.
179    pub fn open(
180        &mut self,
181        entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
182        anchor: MenuAnchor,
183        window: &mut Window,
184        cx: &mut Context<Self>,
185    ) {
186        let context = self.restore_target(window, cx);
187        if context.is_none() {
188            // GPUI reads key bindings from the frame drawn last, which the
189            // menu is not in yet; one more frame shows its shortcuts.
190            let this = cx.weak_entity();
191            window.on_next_frame(move |_, cx| {
192                this.update(cx, |_, cx| cx.notify()).ok();
193            });
194        }
195        self.open_with_context(entries, anchor, context, window, cx);
196    }
197
198    /// Replaces the rows of the open menu, as an answer that arrives
199    /// after it opened. Submenus close; the keyboard stays on the row
200    /// with the same key when there is one. A closed menu ignores it.
201    pub fn set_entries(
202        &mut self,
203        entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
204        cx: &mut Context<Self>,
205    ) {
206        let Some(open) = &mut self.open else {
207            return;
208        };
209        let highlighted = open.levels[0]
210            .highlighted
211            .and_then(|row| open.entries.get(row))
212            .and_then(|entry| entry.key().cloned());
213        open.entries = normalize(entries.into_iter().map(Into::into).collect());
214        open.levels.truncate(1);
215        let root = &mut open.levels[0];
216        root.list.reset(open.entries.len());
217        root.highlighted = highlighted.and_then(|key| {
218            open.entries
219                .iter()
220                .position(|entry| entry.key() == Some(&key) && entry.is_selectable())
221        });
222        if let Some(row) = root.highlighted {
223            root.list.scroll_to_reveal_item(row);
224        }
225        cx.notify();
226    }
227
228    /// Opens the menu as [`open`](Self::open) does, with the actions its
229    /// items dispatch going to `action_context`, and their shortcuts read
230    /// from it.
231    pub fn open_for(
232        &mut self,
233        entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
234        anchor: MenuAnchor,
235        action_context: &FocusHandle,
236        window: &mut Window,
237        cx: &mut Context<Self>,
238    ) {
239        self.open_with_context(entries, anchor, Some(action_context.clone()), window, cx);
240    }
241
242    /// Closes the whole menu and gives focus back.
243    pub fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
244        let Some(open) = self.open.take() else {
245            return;
246        };
247        if let Some(focus) = open.restore_focus {
248            focus.focus(window, cx);
249        }
250        cx.emit(MenuEvent::Closed);
251        cx.notify();
252    }
253
254    /// The element to give focus back to: the one focused now, unless the
255    /// menu is open and has it, when the target stays the one it had.
256    fn restore_target(&self, window: &Window, cx: &App) -> Option<FocusHandle> {
257        match &self.open {
258            Some(open) => open.restore_focus.clone(),
259            None => window.focused(cx),
260        }
261    }
262
263    fn open_with_context(
264        &mut self,
265        entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
266        anchor: MenuAnchor,
267        action_context: Option<FocusHandle>,
268        window: &mut Window,
269        cx: &mut Context<Self>,
270    ) {
271        let restore_focus = self.restore_target(window, cx);
272        let entries = normalize(entries.into_iter().map(Into::into).collect());
273        let (max_height, gap) = {
274            let metrics = &cx.theme().metrics;
275            (metrics.menu_max_height, metrics.menu_gap)
276        };
277        let placement = match anchor {
278            MenuAnchor::Trigger(bounds, _) => {
279                super::placement(bounds, window.viewport_size().height, max_height, gap)
280            }
281            MenuAnchor::Point(_) => Placement::Bottom,
282        };
283        let root = Level::new(None, Bounds::default(), entries.len(), max_height);
284        let focus_watch = cx.on_focus_out(&self.focus, window, |this, _, window, cx| {
285            this.close(window, cx);
286        });
287        let was_open = self.open.is_some();
288        self.open = Some(OpenMenu {
289            entries,
290            anchor,
291            placement,
292            levels: vec![root],
293            restore_focus,
294            action_context,
295            _deferred: GlobalState::register_deferred_popover(cx),
296            _focus_watch: focus_watch,
297        });
298        self.focus.focus(window, cx);
299        if !was_open {
300            cx.emit(MenuEvent::Opened);
301        }
302        cx.notify();
303    }
304
305    pub(crate) fn trigger_bounds(&self) -> &Rc<Cell<Bounds<Pixels>>> {
306        &self.trigger_bounds
307    }
308
309    pub(crate) fn anchor(&self) -> Option<(MenuAnchor, Placement)> {
310        let open = self.open.as_ref()?;
311        Some((open.anchor, open.placement))
312    }
313
314    pub(crate) fn levels(&self) -> &[Level] {
315        self.open.as_ref().map_or(&[], |open| &open.levels)
316    }
317
318    /// The rows of the level at `depth`.
319    pub(crate) fn entries_at(&self, depth: usize) -> Option<&[MenuEntry]> {
320        self.open.as_ref()?.entries_at(depth)
321    }
322
323    /// The row of the level at `depth` whose submenu is open beside it.
324    pub(crate) fn open_child_row(&self, depth: usize) -> Option<usize> {
325        self.open.as_ref()?.levels.get(depth + 1)?.parent_row
326    }
327
328    /// Where the items' actions go: the action context, else the menu
329    /// itself, whose path runs up through the element it belongs to.
330    pub(crate) fn action_context(&self) -> &FocusHandle {
331        self.open
332            .as_ref()
333            .and_then(|open| open.action_context.as_ref())
334            .unwrap_or(&self.focus)
335    }
336
337    /// Whether a press at `position` lands on one of the menu's panels.
338    pub(crate) fn contains(&self, position: Point<Pixels>) -> bool {
339        self.levels()
340            .iter()
341            .any(|level| level.bounds.get().contains(&position))
342    }
343
344    /// The pointer is on `row` of the level at `depth`: it takes the
345    /// highlight, deeper levels close, and a submenu row opens its menu.
346    pub(crate) fn hover_row(&mut self, depth: usize, row: usize, cx: &mut Context<Self>) {
347        let Some(open) = &mut self.open else {
348            return;
349        };
350        if !open.is_selectable(depth, row) {
351            return;
352        }
353        let unchanged = open.levels.get(depth).and_then(|level| level.highlighted) == Some(row)
354            && (open.levels.len() == depth + 1 || open.levels[depth + 1].parent_row == Some(row));
355        if unchanged {
356            return;
357        }
358        open.levels.truncate(depth + 1);
359        open.levels[depth].highlighted = Some(row);
360        open.open_submenu(depth, row, cx);
361        cx.notify();
362    }
363
364    /// Moves the highlight one selectable row up or down in the active
365    /// level, wrapping at the ends, and closes what was open beside it.
366    pub(crate) fn move_highlight(&mut self, delta: isize, cx: &mut Context<Self>) {
367        let Some(open) = &mut self.open else {
368            return;
369        };
370        let depth = open.active_depth();
371        let count = open.entries_at(depth).map_or(0, <[_]>::len);
372        if count == 0 {
373            return;
374        }
375        let start = open.levels[depth]
376            .highlighted
377            .map(|row| row as isize)
378            .unwrap_or(if delta > 0 { -1 } else { count as isize });
379        let mut row = start;
380        for _ in 0..count {
381            row = (row + delta).rem_euclid(count as isize);
382            if open.is_selectable(depth, row as usize) {
383                open.highlight(depth, row as usize);
384                cx.notify();
385                return;
386            }
387        }
388    }
389
390    /// Highlights the first (or last) selectable row of the active level.
391    pub(crate) fn highlight_edge(&mut self, last: bool, cx: &mut Context<Self>) {
392        let Some(open) = &mut self.open else {
393            return;
394        };
395        let depth = open.active_depth();
396        let count = open.entries_at(depth).map_or(0, <[_]>::len);
397        let mut rows: Box<dyn Iterator<Item = usize>> = if last {
398            Box::new((0..count).rev())
399        } else {
400            Box::new(0..count)
401        };
402        if let Some(row) = rows.find(|row| open.is_selectable(depth, *row)) {
403            open.highlight(depth, row);
404            cx.notify();
405        }
406    }
407
408    /// Opens the highlighted submenu of the active level and puts the
409    /// keyboard on its first row.
410    pub(crate) fn enter_submenu(&mut self, cx: &mut Context<Self>) -> bool {
411        let Some(open) = &mut self.open else {
412            return false;
413        };
414        let depth = open.active_depth();
415        let Some(row) = open.levels[depth].highlighted else {
416            return false;
417        };
418        let is_submenu = open
419            .entries_at(depth)
420            .and_then(|entries| entries.get(row))
421            .is_some_and(|entry| entry.submenu().is_some() && entry.is_selectable());
422        if !is_submenu {
423            return false;
424        }
425        open.levels.truncate(depth + 1);
426        open.open_submenu(depth, row, cx);
427        let child = depth + 1;
428        if let Some(first) = (0..open.entries_at(child).map_or(0, <[_]>::len))
429            .find(|r| open.is_selectable(child, *r))
430        {
431            open.highlight(child, first);
432        }
433        cx.notify();
434        true
435    }
436
437    /// Closes the innermost level the keyboard is in, keeping the root;
438    /// false at the root.
439    pub(crate) fn leave_submenu(&mut self, cx: &mut Context<Self>) -> bool {
440        let Some(open) = &mut self.open else {
441            return false;
442        };
443        let depth = open.active_depth();
444        if depth == 0 {
445            return false;
446        }
447        open.levels.truncate(depth);
448        cx.notify();
449        true
450    }
451
452    /// Hands Left or Right that has no submenu to act on to the owner
453    /// that set a step handler, such as a menu bar.
454    pub(crate) fn set_step_handler(&mut self, handler: StepHandler) {
455        self.step_handler = Some(handler);
456    }
457
458    pub(crate) fn step(this: &Entity<Self>, delta: isize, window: &mut Window, cx: &mut App) {
459        let handler = this.read(cx).step_handler.clone();
460        if let Some(handler) = handler {
461            handler(delta, window, cx);
462        }
463    }
464
465    /// Escape: closes the innermost open level, or the whole menu at its
466    /// root.
467    pub(crate) fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
468        match &mut self.open {
469            Some(open) if open.levels.len() > 1 => {
470                open.levels.pop();
471                cx.notify();
472            }
473            _ => self.close(window, cx),
474        }
475    }
476
477    /// Chooses `row` of the level at `depth`: a submenu row opens its
478    /// menu; an item closes the menu, reports its key, and hands back
479    /// what it does, to run once the state is released.
480    pub(crate) fn choose(
481        &mut self,
482        depth: usize,
483        row: usize,
484        window: &mut Window,
485        cx: &mut Context<Self>,
486    ) -> Option<PendingActivation> {
487        let open = self.open.as_mut()?;
488        if !open.is_selectable(depth, row) {
489            return None;
490        }
491        let entry = open.entries_at(depth)?.get(row)?.clone();
492        match entry {
493            MenuEntry::Submenu(_) => {
494                if open
495                    .levels
496                    .get(depth + 1)
497                    .and_then(|level| level.parent_row)
498                    != Some(row)
499                {
500                    open.levels.truncate(depth + 1);
501                    open.levels[depth].highlighted = Some(row);
502                    open.open_submenu(depth, row, cx);
503                    cx.notify();
504                }
505                None
506            }
507            MenuEntry::Item(item) => {
508                let pending = item.pending(
509                    open.action_context
510                        .clone()
511                        .unwrap_or_else(|| self.focus.clone()),
512                );
513                cx.emit(MenuEvent::Activated(item.key().clone()));
514                self.close(window, cx);
515                Some(pending)
516            }
517            MenuEntry::Separator | MenuEntry::Label(_) => None,
518        }
519    }
520
521    /// Enter or Space: chooses the highlighted row of the active level; a
522    /// submenu row opens with the keyboard on its first row.
523    pub(crate) fn confirm(
524        &mut self,
525        window: &mut Window,
526        cx: &mut Context<Self>,
527    ) -> Option<PendingActivation> {
528        if self.enter_submenu(cx) {
529            return None;
530        }
531        let open = self.open.as_ref()?;
532        let depth = open.active_depth();
533        let row = open.levels[depth].highlighted?;
534        self.choose(depth, row, window, cx)
535    }
536
537    /// Chooses a row as the pointer or the keyboard does, then runs what
538    /// the item does once the state is released, so a command that opens
539    /// the menu again, or reads it, can.
540    pub(crate) fn activate(
541        this: &Entity<Self>,
542        choose: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> Option<PendingActivation>,
543        window: &mut Window,
544        cx: &mut App,
545    ) {
546        let pending = this.update(cx, |state, cx| choose(state, window, cx));
547        if let Some(pending) = pending {
548            pending.run(window, cx);
549        }
550    }
551}
552
553impl OpenMenu {
554    /// The rows of the level at `depth`, following each open submenu row.
555    fn entries_at(&self, depth: usize) -> Option<&[MenuEntry]> {
556        let mut entries = self.entries.as_slice();
557        for level in self.levels.get(1..=depth)? {
558            entries = entries.get(level.parent_row?)?.submenu()?.children();
559        }
560        Some(entries)
561    }
562
563    /// The level the keyboard is in: the innermost one with a highlight. A
564    /// submenu the pointer opened has none until the keyboard enters it.
565    fn active_depth(&self) -> usize {
566        self.levels
567            .iter()
568            .rposition(|level| level.highlighted.is_some())
569            .unwrap_or(0)
570    }
571
572    fn is_selectable(&self, depth: usize, row: usize) -> bool {
573        self.entries_at(depth)
574            .and_then(|entries| entries.get(row))
575            .is_some_and(MenuEntry::is_selectable)
576    }
577
578    /// Puts the keyboard on `row` of the level at `depth`, closes what was
579    /// open beside it, and scrolls the row into view.
580    fn highlight(&mut self, depth: usize, row: usize) {
581        self.levels.truncate(depth + 1);
582        let level = &mut self.levels[depth];
583        level.highlighted = Some(row);
584        level.list.scroll_to_reveal_item(row);
585    }
586
587    /// Opens the submenu at `row` of the level at `depth`, when the row is
588    /// one, beside where the row was last laid out.
589    fn open_submenu(&mut self, depth: usize, row: usize, cx: &App) {
590        let Some(count) = self
591            .entries_at(depth)
592            .and_then(|entries| entries.get(row))
593            .and_then(MenuEntry::submenu)
594            .map(|submenu| submenu.children().len())
595        else {
596            return;
597        };
598        let level = &self.levels[depth];
599        let row_bounds = level
600            .submenu_rows
601            .borrow()
602            .get(&row)
603            .copied()
604            .unwrap_or_default();
605        let panel = level.bounds.get();
606        let anchor = Bounds::from_corners(
607            gpui_kit::point(panel.left(), row_bounds.top()),
608            gpui_kit::point(panel.right(), row_bounds.bottom()),
609        );
610        let overdraw = cx.theme().metrics.menu_max_height;
611        self.levels
612            .push(Level::new(Some(row), anchor, count, overdraw));
613    }
614}
615
616/// The rows as a menu draws them: no separator at either end, none
617/// doubled, and none around a group with nothing to choose, such as a
618/// lone label. Submenus are tidied the same way.
619pub(crate) fn normalize(entries: Vec<MenuEntry>) -> Vec<MenuEntry> {
620    let mut groups: Vec<Vec<MenuEntry>> = vec![Vec::new()];
621    for entry in entries {
622        match entry {
623            MenuEntry::Separator => groups.push(Vec::new()),
624            MenuEntry::Submenu(submenu) => groups
625                .last_mut()
626                .expect("one group at least")
627                .push(MenuEntry::Submenu(submenu.normalized())),
628            entry => groups.last_mut().expect("one group at least").push(entry),
629        }
630    }
631    let mut rows = Vec::new();
632    for group in groups.into_iter().filter(|group| {
633        group
634            .iter()
635            .any(|entry| matches!(entry, MenuEntry::Item(_) | MenuEntry::Submenu(_)))
636    }) {
637        if !rows.is_empty() {
638            rows.push(MenuEntry::Separator);
639        }
640        rows.extend(group);
641    }
642    rows
643}