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