context_menu.rs

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