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