context_menu.rs

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