context_menu.rs

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