context_menu.rs

  1use std::{any::TypeId, time::Duration};
  2
  3use gpui::{
  4    elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext,
  5    Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View,
  6    ViewContext,
  7};
  8use menu::*;
  9use settings::Settings;
 10
 11pub fn init(cx: &mut MutableAppContext) {
 12    cx.add_action(ContextMenu::select_first);
 13    cx.add_action(ContextMenu::select_last);
 14    cx.add_action(ContextMenu::select_next);
 15    cx.add_action(ContextMenu::select_prev);
 16    cx.add_action(ContextMenu::confirm);
 17    cx.add_action(ContextMenu::cancel);
 18}
 19
 20pub enum ContextMenuItem {
 21    Item {
 22        label: String,
 23        action: Box<dyn Action>,
 24    },
 25    Separator,
 26}
 27
 28impl ContextMenuItem {
 29    pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
 30        Self::Item {
 31            label: label.to_string(),
 32            action: Box::new(action),
 33        }
 34    }
 35
 36    pub fn separator() -> Self {
 37        Self::Separator
 38    }
 39
 40    fn is_separator(&self) -> bool {
 41        matches!(self, Self::Separator)
 42    }
 43
 44    fn action_id(&self) -> Option<TypeId> {
 45        match self {
 46            ContextMenuItem::Item { action, .. } => Some(action.id()),
 47            ContextMenuItem::Separator => None,
 48        }
 49    }
 50}
 51
 52pub struct ContextMenu {
 53    show_count: usize,
 54    position: Vector2F,
 55    items: Vec<ContextMenuItem>,
 56    selected_index: Option<usize>,
 57    visible: bool,
 58    previously_focused_view_id: Option<usize>,
 59    _actions_observation: Subscription,
 60}
 61
 62impl Entity for ContextMenu {
 63    type Event = ();
 64}
 65
 66impl View for ContextMenu {
 67    fn ui_name() -> &'static str {
 68        "ContextMenu"
 69    }
 70
 71    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
 72        let mut cx = Self::default_keymap_context();
 73        cx.set.insert("menu".into());
 74        cx
 75    }
 76
 77    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 78        if !self.visible {
 79            return Empty::new().boxed();
 80        }
 81
 82        // Render the menu once at minimum width.
 83        let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
 84        let expanded_menu = self
 85            .render_menu(cx)
 86            .constrained()
 87            .dynamically(move |constraint, cx| {
 88                SizeConstraint::strict_along(
 89                    Axis::Horizontal,
 90                    collapsed_menu.layout(constraint, cx).x(),
 91                )
 92            })
 93            .boxed();
 94
 95        Overlay::new(expanded_menu)
 96            .hoverable(true)
 97            .align_to_fit(true)
 98            .with_abs_position(self.position)
 99            .boxed()
100    }
101
102    fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
103        self.reset(cx);
104    }
105}
106
107impl ContextMenu {
108    pub fn new(cx: &mut ViewContext<Self>) -> Self {
109        Self {
110            show_count: 0,
111            position: Default::default(),
112            items: Default::default(),
113            selected_index: Default::default(),
114            visible: Default::default(),
115            previously_focused_view_id: Default::default(),
116            _actions_observation: cx.observe_actions(Self::action_dispatched),
117        }
118    }
119
120    fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
121        if let Some(ix) = self
122            .items
123            .iter()
124            .position(|item| item.action_id() == Some(action_id))
125        {
126            self.selected_index = Some(ix);
127            cx.notify();
128            cx.spawn(|this, mut cx| async move {
129                cx.background().timer(Duration::from_millis(100)).await;
130                this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
131            })
132            .detach();
133        }
134    }
135
136    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
137        if let Some(ix) = self.selected_index {
138            if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
139                let window_id = cx.window_id();
140                let view_id = cx.view_id();
141                cx.dispatch_action_at(window_id, view_id, action.as_ref());
142            }
143        }
144    }
145
146    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
147        self.reset(cx);
148        let show_count = self.show_count;
149        cx.defer(move |this, cx| {
150            if cx.handle().is_focused(cx) && this.show_count == show_count {
151                let window_id = cx.window_id();
152                (**cx).focus(window_id, this.previously_focused_view_id.take());
153            }
154        });
155    }
156
157    fn reset(&mut self, cx: &mut ViewContext<Self>) {
158        self.items.clear();
159        self.visible = false;
160        self.selected_index.take();
161        cx.notify();
162    }
163
164    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
165        self.selected_index = self.items.iter().position(|item| !item.is_separator());
166        cx.notify();
167    }
168
169    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
170        for (ix, item) in self.items.iter().enumerate().rev() {
171            if !item.is_separator() {
172                self.selected_index = Some(ix);
173                cx.notify();
174                break;
175            }
176        }
177    }
178
179    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
180        if let Some(ix) = self.selected_index {
181            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
182                if !item.is_separator() {
183                    self.selected_index = Some(ix);
184                    cx.notify();
185                    break;
186                }
187            }
188        } else {
189            self.select_first(&Default::default(), cx);
190        }
191    }
192
193    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
194        if let Some(ix) = self.selected_index {
195            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
196                if !item.is_separator() {
197                    self.selected_index = Some(ix);
198                    cx.notify();
199                    break;
200                }
201            }
202        } else {
203            self.select_last(&Default::default(), cx);
204        }
205    }
206
207    pub fn show(
208        &mut self,
209        position: Vector2F,
210        items: impl IntoIterator<Item = ContextMenuItem>,
211        cx: &mut ViewContext<Self>,
212    ) {
213        let mut items = items.into_iter().peekable();
214        if items.peek().is_some() {
215            self.items = items.collect();
216            self.position = position;
217            self.visible = true;
218            self.show_count += 1;
219            if !cx.is_self_focused() {
220                self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
221            }
222            cx.focus_self();
223        } else {
224            self.visible = false;
225        }
226        cx.notify();
227    }
228
229    fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
230        let style = cx.global::<Settings>().theme.context_menu.clone();
231        Flex::row()
232            .with_child(
233                Flex::column()
234                    .with_children(self.items.iter().enumerate().map(|(ix, item)| {
235                        match item {
236                            ContextMenuItem::Item { label, .. } => {
237                                let style = style
238                                    .item
239                                    .style_for(Default::default(), Some(ix) == self.selected_index);
240                                Label::new(label.to_string(), style.label.clone())
241                                    .contained()
242                                    .with_style(style.container)
243                                    .boxed()
244                            }
245                            ContextMenuItem::Separator => Empty::new()
246                                .collapsed()
247                                .contained()
248                                .with_style(style.separator)
249                                .constrained()
250                                .with_height(1.)
251                                .boxed(),
252                        }
253                    }))
254                    .boxed(),
255            )
256            .with_child(
257                Flex::column()
258                    .with_children(self.items.iter().enumerate().map(|(ix, item)| {
259                        match item {
260                            ContextMenuItem::Item { action, .. } => {
261                                let style = style
262                                    .item
263                                    .style_for(Default::default(), Some(ix) == self.selected_index);
264                                KeystrokeLabel::new(
265                                    action.boxed_clone(),
266                                    style.keystroke.container,
267                                    style.keystroke.text.clone(),
268                                )
269                                .boxed()
270                            }
271                            ContextMenuItem::Separator => Empty::new()
272                                .collapsed()
273                                .constrained()
274                                .with_height(1.)
275                                .contained()
276                                .with_style(style.separator)
277                                .boxed(),
278                        }
279                    }))
280                    .boxed(),
281            )
282            .contained()
283            .with_style(style.container)
284    }
285
286    fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
287        enum Menu {}
288        enum MenuItem {}
289        let style = cx.global::<Settings>().theme.context_menu.clone();
290        MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
291            Flex::column()
292                .with_children(self.items.iter().enumerate().map(|(ix, item)| {
293                    match item {
294                        ContextMenuItem::Item { label, action } => {
295                            let action = action.boxed_clone();
296                            MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
297                                let style =
298                                    style.item.style_for(state, Some(ix) == self.selected_index);
299                                Flex::row()
300                                    .with_child(
301                                        Label::new(label.to_string(), style.label.clone()).boxed(),
302                                    )
303                                    .with_child({
304                                        KeystrokeLabel::new(
305                                            action.boxed_clone(),
306                                            style.keystroke.container,
307                                            style.keystroke.text.clone(),
308                                        )
309                                        .flex_float()
310                                        .boxed()
311                                    })
312                                    .contained()
313                                    .with_style(style.container)
314                                    .boxed()
315                            })
316                            .with_cursor_style(CursorStyle::PointingHand)
317                            .on_click(move |_, _, cx| {
318                                cx.dispatch_any_action(action.boxed_clone());
319                                cx.dispatch_action(Cancel);
320                            })
321                            .boxed()
322                        }
323                        ContextMenuItem::Separator => Empty::new()
324                            .constrained()
325                            .with_height(1.)
326                            .contained()
327                            .with_style(style.separator)
328                            .boxed(),
329                    }
330                }))
331                .contained()
332                .with_style(style.container)
333                .boxed()
334        })
335        .on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
336        .on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
337    }
338}