Skip to main content

ui/
context_menu.rs

1//! A view's menu: a right press on a text surface, a `···` on a row, a `+` in
2//! a header.
3//!
4//! It holds the keyboard while it is open: the arrows walk its rows and its
5//! submenus, `enter` runs the live row or opens its submenu, and `escape` backs
6//! out of a submenu and then closes it, under [`KEY_CONTEXT`], whose bindings
7//! are in [`crate::menu::bindings`] and installed by [`crate::menu::init`].
8//! Closing by key or by a chosen row
9//! hands the focus back to whatever held it before the menu opened. A press
10//! elsewhere closes it and leaves the focus to that press.
11//!
12//! One `ContextMenu` serves every menu a view has: `K` names which one is open,
13//! so opening one closes the rest.
14
15use crate::{
16    menu::{
17        self, Confirm, Cursor, Dismiss, Hit, Item, SelectChild, SelectNext, SelectParent,
18        SelectPrevious,
19    },
20    popover::{self, Popup},
21};
22use gpui::{
23    AnyElement, Context, Div, FocusHandle, Pixels, Point, SharedString, StatefulInteractiveElement,
24    Window, prelude::*,
25};
26use std::rc::Rc;
27use theme::Theme;
28
29/// The key context an open menu claims.
30pub const KEY_CONTEXT: &str = "ContextMenu";
31
32/// What choosing a row runs, given the row's path into the items.
33type OnChoose<V> = Rc<dyn Fn(&mut V, &[usize], &mut Window, &mut Context<V>)>;
34
35/// Where an owning view keeps its menu.
36pub type Slot<V, K = ()> = fn(&mut V) -> &mut ContextMenu<V, K>;
37
38/// Where an open menu stands.
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub enum Place {
41    /// At a window point: where a right press landed.
42    At(Point<Pixels>),
43    /// Down from the trigger's bottom edge, left edges aligned.
44    Below,
45    /// Down from the trigger's bottom edge, right edges aligned.
46    BelowEnd,
47    /// Up from the trigger's top edge, left edges aligned.
48    Above,
49    /// Up from the trigger's top edge, right edges aligned.
50    AboveEnd,
51}
52
53struct Open<V, K> {
54    key: K,
55    place: Place,
56    cursor: Cursor,
57    items: Vec<Item>,
58    on_choose: OnChoose<V>,
59    focus: FocusHandle,
60    back: Option<FocusHandle>,
61}
62
63/// A view's menu: closed, or open with its rows and what choosing one does.
64pub struct ContextMenu<V: 'static, K: 'static = ()>(Popup<Open<V, K>>);
65
66impl<V: 'static, K: 'static> Default for ContextMenu<V, K> {
67    fn default() -> Self {
68        Self(Popup::default())
69    }
70}
71
72impl<V: 'static, K: PartialEq + 'static> ContextMenu<V, K> {
73    /// Open `items` as the menu `key`, standing at `place` and taking the
74    /// keyboard. A menu already open is replaced.
75    pub fn open(
76        &mut self,
77        key: K,
78        place: Place,
79        items: Vec<Item>,
80        on_choose: impl Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
81        window: &mut Window,
82        cx: &mut Context<V>,
83    ) {
84        // A menu replacing another hands the focus back to what held it before
85        // the first.
86        let back = match self.0.as_open() {
87            Some(open) => open.back.clone(),
88            None => window.focused(cx),
89        };
90        let focus = cx.focus_handle();
91        window.focus(&focus, cx);
92        self.0.open(Open {
93            key,
94            place,
95            cursor: Cursor::default(),
96            items,
97            on_choose: Rc::new(on_choose),
98            focus,
99            back,
100        });
101        cx.notify();
102    }
103
104    /// Open the menu `key`, or close it when it is the one up or the press
105    /// that led here found it up — see [`Self::note_press`]. `build` makes the
106    /// rows and what choosing one does, and runs only when the menu opens.
107    pub fn toggle<F>(
108        view: &mut V,
109        slot: Slot<V, K>,
110        key: K,
111        place: Place,
112        build: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> (Vec<Item>, F),
113        window: &mut Window,
114        cx: &mut Context<V>,
115    ) where
116        F: Fn(&mut V, &[usize], &mut Window, &mut Context<V>) + 'static,
117    {
118        let menu = slot(view);
119        if menu.0.take_press_was_open() || menu.key() == Some(&key) {
120            Self::close_back(view, slot, window, cx);
121            return;
122        }
123        let (items, on_choose) = build(view, window, cx);
124        slot(view).open(key, place, items, on_choose, window, cx);
125    }
126
127    /// Note, from a trigger's press, whether the menu `key` is on screen. The
128    /// open card dismisses on that same press, so by click time the menu reads
129    /// as closed, and [`Self::toggle`] would open it straight back without
130    /// this.
131    pub fn note_press(&mut self, key: &K) {
132        self.0.note_trigger_press_matching(|open| open.key == *key);
133    }
134
135    /// `el`, noting its presses for the menu `key` — see [`Self::note_press`].
136    /// Every trigger that calls [`Self::toggle`] needs it.
137    pub fn trigger<E: StatefulInteractiveElement>(
138        el: E,
139        slot: Slot<V, K>,
140        key: K,
141        cx: &Context<V>,
142    ) -> E {
143        // Capture, for the reason `popover::trigger_press_matching` gives.
144        el.capture_any_mouse_down(cx.listener(move |view, _: &gpui::MouseDownEvent, _, _| {
145            slot(view).note_press(&key);
146        }))
147    }
148
149    /// Which menu is open. `None` while none is, or the last one is on its way
150    /// out.
151    pub fn key(&self) -> Option<&K> {
152        self.0.as_open().map(|open| &open.key)
153    }
154
155    pub fn is_open(&self) -> bool {
156        self.0.is_open()
157    }
158
159    /// Whether the menu `key` is on screen, open or on its way out.
160    pub fn showing(&self, key: &K) -> bool {
161        self.0.get().is_some_and(|open| open.key == *key)
162    }
163
164    /// Light the row at `path`, as the pointer would. Whether that moved the
165    /// cursor.
166    pub fn point_at(&mut self, path: &[usize]) -> bool {
167        self.0
168            .open_mut()
169            .is_some_and(|open| open.cursor.point_at(&open.items, path))
170    }
171
172    /// Open the submenu under the live row. Whether there was one.
173    fn descend(&mut self) -> bool {
174        self.0
175            .open_mut()
176            .is_some_and(|open| open.cursor.descend(&open.items))
177    }
178
179    /// Close the innermost open submenu. Whether one was open.
180    fn ascend(&mut self) -> bool {
181        self.0.open_mut().is_some_and(|open| open.cursor.ascend())
182    }
183
184    /// Light no row.
185    pub fn clear_cursor(&mut self) {
186        if let Some(open) = self.0.open_mut() {
187            open.cursor.clear();
188        }
189    }
190
191    /// Close the menu, leaving the focus where it is.
192    pub fn close(view: &mut V, slot: Slot<V, K>, cx: &mut Context<V>) {
193        popover::close_popup(view, cx, move |view| &mut slot(view).0);
194    }
195
196    /// Close the menu and give the keyboard back to what held it before.
197    fn close_back(view: &mut V, slot: Slot<V, K>, window: &mut Window, cx: &mut Context<V>) {
198        let back = slot(view).0.as_open().and_then(|open| open.back.clone());
199        Self::close(view, slot, cx);
200        if let Some(back) = back {
201            window.focus(&back, cx);
202        }
203    }
204
205    /// The menu's card while it is on screen, not yet placed: for a caller
206    /// that sizes it before [`Self::place`]. `id` prefixes its rows' element
207    /// ids and debug selectors — see [`menu::card`].
208    pub fn card(
209        &self,
210        id: impl Into<SharedString>,
211        slot: Slot<V, K>,
212        window: &mut Window,
213        cx: &mut Context<V>,
214    ) -> Option<Div> {
215        let open = self.0.get()?;
216        let theme = Theme::of(cx).clone();
217        let choose = open.on_choose.clone();
218        let card = menu::card(
219            &theme,
220            id,
221            &open.items,
222            &open.cursor,
223            self.0.closing_since(),
224            window,
225            cx,
226            move |view: &mut V, hit, window, cx| match hit {
227                Hit::Point(path) => {
228                    if slot(view).point_at(&path) {
229                        cx.notify();
230                    }
231                }
232                Hit::Choose(path) => {
233                    Self::close_back(view, slot, window, cx);
234                    choose(view, &path, window, cx);
235                }
236                Hit::Dismiss => Self::close(view, slot, cx),
237            },
238        )
239        .track_focus(&open.focus)
240        .key_context(KEY_CONTEXT)
241        .on_action(cx.listener(move |view, _: &SelectNext, _, cx| step(view, slot, 1, cx)))
242        .on_action(cx.listener(move |view, _: &SelectPrevious, _, cx| step(view, slot, -1, cx)))
243        .on_action(cx.listener(move |view, _: &SelectChild, _, cx| {
244            if slot(view).descend() {
245                cx.notify();
246            }
247        }))
248        .on_action(cx.listener(move |view, _: &SelectParent, _, cx| {
249            if slot(view).ascend() {
250                cx.notify();
251            }
252        }))
253        .on_action(cx.listener(move |view, _: &Confirm, window, cx| {
254            if slot(view).descend() {
255                cx.notify();
256                return;
257            }
258            let Some((path, choose)) = slot(view)
259                .0
260                .as_open()
261                .and_then(|open| Some((open.cursor.path()?, open.on_choose.clone())))
262            else {
263                return;
264            };
265            Self::close_back(view, slot, window, cx);
266            choose(view, &path, window, cx);
267        }))
268        .on_action(cx.listener(
269            move |view, _: &Dismiss, window, cx| match slot(view).ascend() {
270                true => cx.notify(),
271                false => Self::close_back(view, slot, window, cx),
272            },
273        ));
274        Some(card)
275    }
276
277    /// Stand `card` where the menu opened. For every [`Place`] but
278    /// [`Place::At`], draw it as a child of the trigger.
279    pub fn place(&self, id: impl Into<SharedString>, card: impl IntoElement) -> AnyElement {
280        let card = card.into_any_element();
281        let closing = self.0.closing_since();
282        match self.0.get().map(|open| open.place) {
283            Some(Place::At(at)) => popover::menu_at(id, at, card, closing),
284            Some(Place::BelowEnd) => popover::anchored_menu_below_end(id, card, closing),
285            Some(Place::Above) => popover::anchored_menu_above(id, card, closing),
286            Some(Place::AboveEnd) => popover::anchored_menu_above_end(id, card, closing),
287            Some(Place::Below) | None => popover::anchored_menu_below(id, card, closing),
288        }
289    }
290
291    /// The menu, placed, while it is on screen.
292    pub fn render(
293        &self,
294        id: impl Into<SharedString>,
295        slot: Slot<V, K>,
296        window: &mut Window,
297        cx: &mut Context<V>,
298    ) -> Option<AnyElement> {
299        let id = id.into();
300        let card = self.card(id.clone(), slot, window, cx)?;
301        Some(self.place(id, card))
302    }
303
304    /// [`Self::render`] for the menu `key` alone: what a trigger hangs beside
305    /// itself.
306    pub fn render_for(
307        &self,
308        key: &K,
309        id: impl Into<SharedString>,
310        slot: Slot<V, K>,
311        window: &mut Window,
312        cx: &mut Context<V>,
313    ) -> Option<AnyElement> {
314        if !self.showing(key) {
315            return None;
316        }
317        self.render(id, slot, window, cx)
318    }
319}
320
321fn step<V: 'static, K: PartialEq + 'static>(
322    view: &mut V,
323    slot: Slot<V, K>,
324    delta: isize,
325    cx: &mut Context<V>,
326) {
327    if let Some(open) = slot(view).0.open_mut() {
328        open.cursor.step(&open.items, delta);
329        cx.notify();
330    }
331}