context_menu.rs

  1use crate::{
  2    h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
  3    ListSeparator, ListSubHeader,
  4};
  5use gpui::{
  6    px, Action, AnyElement, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
  7    FocusableView, IntoElement, Render, Subscription, View, VisualContext,
  8};
  9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
 10use std::{rc::Rc, time::Duration};
 11
 12enum ContextMenuItem {
 13    Separator,
 14    Header(SharedString),
 15    Entry {
 16        label: SharedString,
 17        icon: Option<Icon>,
 18        handler: Rc<dyn Fn(&mut WindowContext)>,
 19        action: Option<Box<dyn Action>>,
 20    },
 21    CustomEntry {
 22        entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
 23    },
 24}
 25
 26pub struct ContextMenu {
 27    items: Vec<ContextMenuItem>,
 28    focus_handle: FocusHandle,
 29    selected_index: Option<usize>,
 30    delayed: bool,
 31    _on_blur_subscription: Subscription,
 32}
 33
 34impl FocusableView for ContextMenu {
 35    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
 36        self.focus_handle.clone()
 37    }
 38}
 39
 40impl EventEmitter<DismissEvent> for ContextMenu {}
 41
 42impl ContextMenu {
 43    pub fn build(
 44        cx: &mut WindowContext,
 45        f: impl FnOnce(Self, &mut WindowContext) -> Self,
 46    ) -> View<Self> {
 47        cx.build_view(|cx| {
 48            let focus_handle = cx.focus_handle();
 49            let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| {
 50                this.cancel(&menu::Cancel, cx)
 51            });
 52            f(
 53                Self {
 54                    items: Default::default(),
 55                    focus_handle,
 56                    selected_index: None,
 57                    delayed: false,
 58                    _on_blur_subscription,
 59                },
 60                cx,
 61            )
 62        })
 63    }
 64
 65    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
 66        self.items.push(ContextMenuItem::Header(title.into()));
 67        self
 68    }
 69
 70    pub fn separator(mut self) -> Self {
 71        self.items.push(ContextMenuItem::Separator);
 72        self
 73    }
 74
 75    pub fn entry(
 76        mut self,
 77        label: impl Into<SharedString>,
 78        handler: impl Fn(&mut WindowContext) + 'static,
 79    ) -> Self {
 80        self.items.push(ContextMenuItem::Entry {
 81            label: label.into(),
 82            handler: Rc::new(handler),
 83            icon: None,
 84            action: None,
 85        });
 86        self
 87    }
 88
 89    pub fn custom_entry(
 90        mut self,
 91        entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static,
 92    ) -> Self {
 93        self.items.push(ContextMenuItem::CustomEntry {
 94            entry_render: Box::new(entry_render),
 95        });
 96        self
 97    }
 98
 99    pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
100        self.items.push(ContextMenuItem::Entry {
101            label: label.into(),
102            action: Some(action.boxed_clone()),
103            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
104            icon: None,
105        });
106        self
107    }
108
109    pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
110        self.items.push(ContextMenuItem::Entry {
111            label: label.into(),
112            action: Some(action.boxed_clone()),
113            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
114            icon: Some(Icon::Link),
115        });
116        self
117    }
118
119    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
120        if let Some(ContextMenuItem::Entry { handler, .. }) =
121            self.selected_index.and_then(|ix| self.items.get(ix))
122        {
123            (handler)(cx)
124        }
125
126        cx.emit(DismissEvent);
127    }
128
129    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
130        cx.emit(DismissEvent);
131        cx.emit(DismissEvent);
132    }
133
134    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
135        self.selected_index = self.items.iter().position(|item| item.is_selectable());
136        cx.notify();
137    }
138
139    pub fn select_last(&mut self) -> Option<usize> {
140        for (ix, item) in self.items.iter().enumerate().rev() {
141            if item.is_selectable() {
142                self.selected_index = Some(ix);
143                return Some(ix);
144            }
145        }
146        None
147    }
148
149    fn handle_select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
150        if self.select_last().is_some() {
151            cx.notify();
152        }
153    }
154
155    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
156        if let Some(ix) = self.selected_index {
157            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
158                if item.is_selectable() {
159                    self.selected_index = Some(ix);
160                    cx.notify();
161                    break;
162                }
163            }
164        } else {
165            self.select_first(&Default::default(), cx);
166        }
167    }
168
169    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
170        if let Some(ix) = self.selected_index {
171            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
172                if item.is_selectable() {
173                    self.selected_index = Some(ix);
174                    cx.notify();
175                    break;
176                }
177            }
178        } else {
179            self.handle_select_last(&Default::default(), cx);
180        }
181    }
182
183    pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
184        if let Some(ix) = self.items.iter().position(|item| {
185            if let ContextMenuItem::Entry {
186                action: Some(action),
187                ..
188            } = item
189            {
190                action.partial_eq(&**dispatched)
191            } else {
192                false
193            }
194        }) {
195            self.selected_index = Some(ix);
196            self.delayed = true;
197            cx.notify();
198            let action = dispatched.boxed_clone();
199            cx.spawn(|this, mut cx| async move {
200                cx.background_executor()
201                    .timer(Duration::from_millis(50))
202                    .await;
203                this.update(&mut cx, |this, cx| {
204                    cx.dispatch_action(action);
205                    this.cancel(&menu::Cancel, cx)
206                })
207            })
208            .detach_and_log_err(cx);
209        } else {
210            cx.propagate()
211        }
212    }
213}
214
215impl ContextMenuItem {
216    fn is_selectable(&self) -> bool {
217        matches!(self, Self::Entry { .. } | Self::CustomEntry { .. })
218    }
219}
220
221impl Render for ContextMenu {
222    type Element = Div;
223
224    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
225        div().elevation_2(cx).flex().flex_row().child(
226            v_stack()
227                .min_w(px(200.))
228                .track_focus(&self.focus_handle)
229                .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
230                .key_context("menu")
231                .on_action(cx.listener(ContextMenu::select_first))
232                .on_action(cx.listener(ContextMenu::handle_select_last))
233                .on_action(cx.listener(ContextMenu::select_next))
234                .on_action(cx.listener(ContextMenu::select_prev))
235                .on_action(cx.listener(ContextMenu::confirm))
236                .on_action(cx.listener(ContextMenu::cancel))
237                .when(!self.delayed, |mut el| {
238                    for item in self.items.iter() {
239                        if let ContextMenuItem::Entry {
240                            action: Some(action),
241                            ..
242                        } = item
243                        {
244                            el = el.on_boxed_action(
245                                action,
246                                cx.listener(ContextMenu::on_action_dispatch),
247                            );
248                        }
249                    }
250                    el
251                })
252                .flex_none()
253                .child(List::new().children(self.items.iter_mut().enumerate().map(
254                    |(ix, item)| {
255                        match item {
256                            ContextMenuItem::Separator => ListSeparator.into_any_element(),
257                            ContextMenuItem::Header(header) => {
258                                ListSubHeader::new(header.clone()).into_any_element()
259                            }
260                            ContextMenuItem::Entry {
261                                label,
262                                handler,
263                                icon,
264                                action,
265                            } => {
266                                let handler = handler.clone();
267
268                                let label_element = if let Some(icon) = icon {
269                                    h_stack()
270                                        .gap_1()
271                                        .child(Label::new(label.clone()))
272                                        .child(IconElement::new(*icon))
273                                        .into_any_element()
274                                } else {
275                                    Label::new(label.clone()).into_any_element()
276                                };
277
278                                ListItem::new(ix)
279                                    .inset(true)
280                                    .selected(Some(ix) == self.selected_index)
281                                    .on_click(move |_, cx| handler(cx))
282                                    .child(
283                                        h_stack()
284                                            .w_full()
285                                            .justify_between()
286                                            .child(label_element)
287                                            .children(action.as_ref().and_then(|action| {
288                                                KeyBinding::for_action(&**action, cx)
289                                                    .map(|binding| div().ml_1().child(binding))
290                                            })),
291                                    )
292                                    .into_any_element()
293                            }
294                            ContextMenuItem::CustomEntry { entry_render } => ListItem::new(ix)
295                                .inset(true)
296                                .selected(Some(ix) == self.selected_index)
297                                .child(entry_render(cx))
298                                .into_any_element(),
299                        }
300                    },
301                ))),
302        )
303    }
304}