context_menu.rs

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