Skip to main content

ui/
context_menu.rs

1//! A menu opened at the pointer — a right press on a text surface, say.
2//!
3//! It holds the keyboard while it is open: the arrows walk its rows, `enter`
4//! runs the live one and `escape` closes it, under [`KEY_CONTEXT`], whose
5//! bindings are in [`crate::menu::bindings`]. Closing by key or by a chosen row
6//! hands the focus back to whatever held it before the menu opened. A press
7//! elsewhere closes it and leaves the focus to that press.
8
9use crate::{
10    menu::{self, Confirm, Cursor, Dismiss, Hit, Item, SelectNext, SelectPrevious},
11    popover::{self, Popup},
12};
13use gpui::{AnyElement, Context, FocusHandle, Pixels, Point, SharedString, Window, prelude::*};
14use std::rc::Rc;
15use theme::Theme;
16
17/// The key context an open menu claims.
18pub const KEY_CONTEXT: &str = "ContextMenu";
19
20/// What choosing a row runs, given the row's path into the items.
21type OnChoose<V> = Rc<dyn Fn(&mut V, &[usize], &mut Window, &mut Context<V>)>;
22
23/// Where an owning view keeps its menu.
24pub type Slot<V> = fn(&mut V) -> &mut ContextMenu;
25
26struct Open {
27    at: Point<Pixels>,
28    cursor: Cursor,
29    items: Vec<Item>,
30    focus: FocusHandle,
31    back: Option<FocusHandle>,
32}
33
34/// A view's context menu: closed, or open with its rows.
35#[derive(Default)]
36pub struct ContextMenu(Popup<Open>);
37
38impl ContextMenu {
39    /// Open `items` at `at`, taking the keyboard.
40    pub fn open<V: 'static>(
41        &mut self,
42        at: Point<Pixels>,
43        items: Vec<Item>,
44        window: &mut Window,
45        cx: &mut Context<V>,
46    ) {
47        let back = window.focused(cx);
48        let focus = cx.focus_handle();
49        window.focus(&focus, cx);
50        self.0.open(Open {
51            at,
52            cursor: Cursor::default(),
53            items,
54            focus,
55            back,
56        });
57        cx.notify();
58    }
59
60    pub fn is_open(&self) -> bool {
61        self.0.is_open()
62    }
63
64    /// The menu, while it is open. `id` prefixes its rows' element ids and
65    /// debug selectors — see [`menu::card`].
66    pub fn render<V: 'static>(
67        &self,
68        id: impl Into<SharedString>,
69        slot: Slot<V>,
70        on_choose: impl Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
71        window: &mut Window,
72        cx: &mut Context<V>,
73    ) -> Option<AnyElement> {
74        let open = self.0.get()?;
75        let id = id.into();
76        let on_choose: OnChoose<V> = Rc::new(on_choose);
77        let theme = Theme::of(cx).clone();
78        let choose = on_choose.clone();
79        let card = menu::card(
80            &theme,
81            id.clone(),
82            &open.items,
83            &open.cursor,
84            window,
85            cx,
86            move |view: &mut V, hit, window, cx| match hit {
87                Hit::Point(path) => {
88                    if let Some(open) = slot(view).0.open_mut()
89                        && open.cursor.point_at(&open.items, &path)
90                    {
91                        cx.notify();
92                    }
93                }
94                Hit::Choose(path) => {
95                    close(view, slot, true, window, cx);
96                    choose(view, &path, window, cx);
97                }
98                Hit::Dismiss => close(view, slot, false, window, cx),
99            },
100        )
101        .track_focus(&open.focus)
102        .key_context(KEY_CONTEXT)
103        .on_action(cx.listener(move |view, _: &SelectNext, _, cx| step(view, slot, 1, cx)))
104        .on_action(cx.listener(move |view, _: &SelectPrevious, _, cx| step(view, slot, -1, cx)))
105        .on_action(cx.listener(move |view, _: &Confirm, window, cx| {
106            let Some(path) = slot(view).0.get().and_then(|open| open.cursor.path()) else {
107                return;
108            };
109            close(view, slot, true, window, cx);
110            on_choose(view, &path, window, cx);
111        }))
112        .on_action(
113            cx.listener(move |view, _: &Dismiss, window, cx| close(view, slot, true, window, cx)),
114        );
115        Some(popover::menu_at(
116            id,
117            open.at,
118            card.into_any_element(),
119            self.0.closing_since(),
120        ))
121    }
122}
123
124fn step<V: 'static>(view: &mut V, slot: Slot<V>, delta: isize, cx: &mut Context<V>) {
125    if let Some(open) = slot(view).0.open_mut() {
126        open.cursor.step(&open.items, delta);
127        cx.notify();
128    }
129}
130
131/// Close the menu, and when `refocus`, give the keyboard back to what held it
132/// before.
133fn close<V: 'static>(
134    view: &mut V,
135    slot: Slot<V>,
136    refocus: bool,
137    window: &mut Window,
138    cx: &mut Context<V>,
139) {
140    let back = slot(view).0.get().and_then(|open| open.back.clone());
141    popover::close_popup(view, cx, move |view| &mut slot(view).0);
142    if refocus && let Some(back) = back {
143        window.focus(&back, cx);
144    }
145}