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            if !cx.is_self_focused() {
173                self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
174            }
175            cx.focus_self();
176        } else {
177            self.visible = false;
178        }
179        cx.notify();
180    }
181
182    fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
183        let style = cx.global::<Settings>().theme.context_menu.clone();
184        Flex::row()
185            .with_child(
186                Flex::column()
187                    .with_children(self.items.iter().enumerate().map(|(ix, item)| {
188                        match item {
189                            ContextMenuItem::Item { label, .. } => {
190                                let style = style
191                                    .item
192                                    .style_for(Default::default(), Some(ix) == self.selected_index);
193                                Label::new(label.to_string(), style.label.clone())
194                                    .contained()
195                                    .with_style(style.container)
196                                    .boxed()
197                            }
198                            ContextMenuItem::Separator => Empty::new()
199                                .collapsed()
200                                .contained()
201                                .with_style(style.separator)
202                                .constrained()
203                                .with_height(1.)
204                                .boxed(),
205                        }
206                    }))
207                    .boxed(),
208            )
209            .with_child(
210                Flex::column()
211                    .with_children(self.items.iter().enumerate().map(|(ix, item)| {
212                        match item {
213                            ContextMenuItem::Item { action, .. } => {
214                                let style = style
215                                    .item
216                                    .style_for(Default::default(), Some(ix) == self.selected_index);
217                                KeystrokeLabel::new(
218                                    action.boxed_clone(),
219                                    style.keystroke.container,
220                                    style.keystroke.text.clone(),
221                                )
222                                .boxed()
223                            }
224                            ContextMenuItem::Separator => Empty::new()
225                                .collapsed()
226                                .constrained()
227                                .with_height(1.)
228                                .contained()
229                                .with_style(style.separator)
230                                .boxed(),
231                        }
232                    }))
233                    .boxed(),
234            )
235            .contained()
236            .with_style(style.container)
237    }
238
239    fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
240        enum Tag {}
241        let style = cx.global::<Settings>().theme.context_menu.clone();
242        Flex::column()
243            .with_children(self.items.iter().enumerate().map(|(ix, item)| {
244                match item {
245                    ContextMenuItem::Item { label, action } => {
246                        let action = action.boxed_clone();
247                        MouseEventHandler::new::<Tag, _, _>(ix, cx, |state, _| {
248                            let style =
249                                style.item.style_for(state, Some(ix) == self.selected_index);
250                            Flex::row()
251                                .with_child(
252                                    Label::new(label.to_string(), style.label.clone()).boxed(),
253                                )
254                                .with_child({
255                                    KeystrokeLabel::new(
256                                        action.boxed_clone(),
257                                        style.keystroke.container,
258                                        style.keystroke.text.clone(),
259                                    )
260                                    .flex_float()
261                                    .boxed()
262                                })
263                                .contained()
264                                .with_style(style.container)
265                                .boxed()
266                        })
267                        .with_cursor_style(CursorStyle::PointingHand)
268                        .on_click(move |_, _, cx| {
269                            cx.dispatch_any_action(action.boxed_clone());
270                            cx.dispatch_action(Cancel);
271                        })
272                        .boxed()
273                    }
274                    ContextMenuItem::Separator => Empty::new()
275                        .constrained()
276                        .with_height(1.)
277                        .contained()
278                        .with_style(style.separator)
279                        .boxed(),
280                }
281            }))
282            .contained()
283            .with_style(style.container)
284    }
285}