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        cx.emit(DismissEvent);
126    }
127
128    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
129        cx.emit(DismissEvent);
130        cx.emit(DismissEvent);
131    }
132
133    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
134        self.selected_index = self.items.iter().position(|item| item.is_selectable());
135        cx.notify();
136    }
137
138    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
139        for (ix, item) in self.items.iter().enumerate().rev() {
140            if item.is_selectable() {
141                self.selected_index = Some(ix);
142                cx.notify();
143                break;
144            }
145        }
146    }
147
148    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
149        if let Some(ix) = self.selected_index {
150            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
151                if item.is_selectable() {
152                    self.selected_index = Some(ix);
153                    cx.notify();
154                    break;
155                }
156            }
157        } else {
158            self.select_first(&Default::default(), cx);
159        }
160    }
161
162    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
163        if let Some(ix) = self.selected_index {
164            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
165                if item.is_selectable() {
166                    self.selected_index = Some(ix);
167                    cx.notify();
168                    break;
169                }
170            }
171        } else {
172            self.select_last(&Default::default(), cx);
173        }
174    }
175
176    pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
177        if let Some(ix) = self.items.iter().position(|item| {
178            if let ContextMenuItem::Entry {
179                action: Some(action),
180                ..
181            } = item
182            {
183                action.partial_eq(&**dispatched)
184            } else {
185                false
186            }
187        }) {
188            self.selected_index = Some(ix);
189            self.delayed = true;
190            cx.notify();
191            let action = dispatched.boxed_clone();
192            cx.spawn(|this, mut cx| async move {
193                cx.background_executor()
194                    .timer(Duration::from_millis(50))
195                    .await;
196                this.update(&mut cx, |this, cx| {
197                    cx.dispatch_action(action);
198                    this.cancel(&Default::default(), cx)
199                })
200            })
201            .detach_and_log_err(cx);
202        } else {
203            cx.propagate()
204        }
205    }
206}
207
208impl ContextMenuItem {
209    fn is_selectable(&self) -> bool {
210        matches!(self, Self::Entry { .. })
211    }
212}
213
214impl Render for ContextMenu {
215    type Element = Div;
216
217    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
218        div().elevation_2(cx).flex().flex_row().child(
219            v_stack()
220                .min_w(px(200.))
221                .track_focus(&self.focus_handle)
222                .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
223                .key_context("menu")
224                .on_action(cx.listener(ContextMenu::select_first))
225                .on_action(cx.listener(ContextMenu::select_last))
226                .on_action(cx.listener(ContextMenu::select_next))
227                .on_action(cx.listener(ContextMenu::select_prev))
228                .on_action(cx.listener(ContextMenu::confirm))
229                .on_action(cx.listener(ContextMenu::cancel))
230                .when(!self.delayed, |mut el| {
231                    for item in self.items.iter() {
232                        if let ContextMenuItem::Entry {
233                            action: Some(action),
234                            ..
235                        } = item
236                        {
237                            el = el.on_boxed_action(
238                                action,
239                                cx.listener(ContextMenu::on_action_dispatch),
240                            );
241                        }
242                    }
243                    el
244                })
245                .flex_none()
246                .child(List::new().children(self.items.iter_mut().enumerate().map(
247                    |(ix, item)| {
248                        match item {
249                            ContextMenuItem::Separator => ListSeparator.into_any_element(),
250                            ContextMenuItem::Header(header) => {
251                                ListSubHeader::new(header.clone()).into_any_element()
252                            }
253                            ContextMenuItem::Entry {
254                                label,
255                                handler,
256                                icon,
257                                action,
258                            } => {
259                                let handler = handler.clone();
260
261                                let label_element = if let Some(icon) = icon {
262                                    h_stack()
263                                        .gap_1()
264                                        .child(Label::new(label.clone()))
265                                        .child(IconElement::new(*icon))
266                                        .into_any_element()
267                                } else {
268                                    Label::new(label.clone()).into_any_element()
269                                };
270
271                                ListItem::new(ix)
272                                    .inset(true)
273                                    .selected(Some(ix) == self.selected_index)
274                                    .on_click(move |_, cx| handler(cx))
275                                    .child(
276                                        h_stack()
277                                            .w_full()
278                                            .justify_between()
279                                            .child(label_element)
280                                            .children(action.as_ref().and_then(|action| {
281                                                KeyBinding::for_action(&**action, cx)
282                                                    .map(|binding| div().ml_1().child(binding))
283                                            })),
284                                    )
285                                    .into_any_element()
286                            }
287                            ContextMenuItem::CustomEntry { entry_render } => ListItem::new(ix)
288                                .inset(true)
289                                .selected(Some(ix) == self.selected_index)
290                                .child(entry_render(cx))
291                                .into_any_element(),
292                        }
293                    },
294                ))),
295        )
296    }
297}