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, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
  7    IntoElement, Render, View, VisualContext,
  8};
  9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
 10use std::rc::Rc;
 11
 12pub enum ContextMenuItem {
 13    Separator,
 14    Header(SharedString),
 15    Entry {
 16        label: SharedString,
 17        icon: Option<Icon>,
 18        handler: Rc<dyn Fn(&mut WindowContext)>,
 19        key_binding: Option<KeyBinding>,
 20    },
 21}
 22
 23pub struct ContextMenu {
 24    items: Vec<ContextMenuItem>,
 25    focus_handle: FocusHandle,
 26    selected_index: Option<usize>,
 27}
 28
 29impl FocusableView for ContextMenu {
 30    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
 31        self.focus_handle.clone()
 32    }
 33}
 34
 35impl EventEmitter<DismissEvent> for ContextMenu {}
 36
 37impl ContextMenu {
 38    pub fn build(
 39        cx: &mut WindowContext,
 40        f: impl FnOnce(Self, &mut WindowContext) -> Self,
 41    ) -> View<Self> {
 42        // let handle = cx.view().downgrade();
 43        cx.build_view(|cx| {
 44            f(
 45                Self {
 46                    items: Default::default(),
 47                    focus_handle: cx.focus_handle(),
 48                    selected_index: None,
 49                },
 50                cx,
 51            )
 52        })
 53    }
 54
 55    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
 56        self.items.push(ContextMenuItem::Header(title.into()));
 57        self
 58    }
 59
 60    pub fn separator(mut self) -> Self {
 61        self.items.push(ContextMenuItem::Separator);
 62        self
 63    }
 64
 65    pub fn entry(
 66        mut self,
 67        label: impl Into<SharedString>,
 68        on_click: impl Fn(&mut WindowContext) + 'static,
 69    ) -> Self {
 70        self.items.push(ContextMenuItem::Entry {
 71            label: label.into(),
 72            handler: Rc::new(on_click),
 73            key_binding: None,
 74            icon: None,
 75        });
 76        self
 77    }
 78
 79    pub fn action(
 80        mut self,
 81        label: impl Into<SharedString>,
 82        action: Box<dyn Action>,
 83        cx: &mut WindowContext,
 84    ) -> Self {
 85        self.items.push(ContextMenuItem::Entry {
 86            label: label.into(),
 87            key_binding: KeyBinding::for_action(&*action, cx),
 88            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
 89            icon: None,
 90        });
 91        self
 92    }
 93
 94    pub fn link(
 95        mut self,
 96        label: impl Into<SharedString>,
 97        action: Box<dyn Action>,
 98        cx: &mut WindowContext,
 99    ) -> Self {
100        self.items.push(ContextMenuItem::Entry {
101            label: label.into(),
102            key_binding: KeyBinding::for_action(&*action, cx),
103            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
104            icon: Some(Icon::Link),
105        });
106        self
107    }
108
109    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
110        if let Some(ContextMenuItem::Entry { handler, .. }) =
111            self.selected_index.and_then(|ix| self.items.get(ix))
112        {
113            (handler)(cx)
114        }
115        cx.emit(DismissEvent);
116    }
117
118    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
119        cx.emit(DismissEvent);
120    }
121
122    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
123        self.selected_index = self.items.iter().position(|item| item.is_selectable());
124        cx.notify();
125    }
126
127    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
128        for (ix, item) in self.items.iter().enumerate().rev() {
129            if item.is_selectable() {
130                self.selected_index = Some(ix);
131                cx.notify();
132                break;
133            }
134        }
135    }
136
137    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
138        if let Some(ix) = self.selected_index {
139            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
140                if item.is_selectable() {
141                    self.selected_index = Some(ix);
142                    cx.notify();
143                    break;
144                }
145            }
146        } else {
147            self.select_first(&Default::default(), cx);
148        }
149    }
150
151    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
152        if let Some(ix) = self.selected_index {
153            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
154                if item.is_selectable() {
155                    self.selected_index = Some(ix);
156                    cx.notify();
157                    break;
158                }
159            }
160        } else {
161            self.select_last(&Default::default(), cx);
162        }
163    }
164}
165
166impl ContextMenuItem {
167    fn is_selectable(&self) -> bool {
168        matches!(self, Self::Entry { .. })
169    }
170}
171
172impl Render for ContextMenu {
173    type Element = Div;
174
175    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
176        div().elevation_2(cx).flex().flex_row().child(
177            v_stack()
178                .min_w(px(200.))
179                .track_focus(&self.focus_handle)
180                .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
181                .key_context("menu")
182                .on_action(cx.listener(ContextMenu::select_first))
183                .on_action(cx.listener(ContextMenu::select_last))
184                .on_action(cx.listener(ContextMenu::select_next))
185                .on_action(cx.listener(ContextMenu::select_prev))
186                .on_action(cx.listener(ContextMenu::confirm))
187                .on_action(cx.listener(ContextMenu::cancel))
188                .flex_none()
189                .child(
190                    List::new().children(self.items.iter().enumerate().map(
191                        |(ix, item)| match item {
192                            ContextMenuItem::Separator => ListSeparator.into_any_element(),
193                            ContextMenuItem::Header(header) => {
194                                ListSubHeader::new(header.clone()).into_any_element()
195                            }
196                            ContextMenuItem::Entry {
197                                label,
198                                handler,
199                                key_binding,
200                                icon,
201                            } => {
202                                let handler = handler.clone();
203                                let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
204
205                                let label_element = if let Some(icon) = icon {
206                                    h_stack()
207                                        .gap_1()
208                                        .child(Label::new(label.clone()))
209                                        .child(IconElement::new(*icon))
210                                        .into_any_element()
211                                } else {
212                                    Label::new(label.clone()).into_any_element()
213                                };
214
215                                ListItem::new(label.clone())
216                                    .child(
217                                        h_stack()
218                                            .w_full()
219                                            .justify_between()
220                                            .child(label_element)
221                                            .children(
222                                                key_binding
223                                                    .clone()
224                                                    .map(|binding| div().ml_1().child(binding)),
225                                            ),
226                                    )
227                                    .selected(Some(ix) == self.selected_index)
228                                    .on_click(move |event, cx| {
229                                        handler(cx);
230                                        dismiss(event, cx)
231                                    })
232                                    .into_any_element()
233                            }
234                        },
235                    )),
236                ),
237        )
238    }
239}