context_menu.rs

  1use gpui::{
  2    elements::*, geometry::vector::Vector2F, Action, Axis, Entity, RenderContext, SizeConstraint,
  3    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
 15pub struct ContextMenu {
 16    position: Vector2F,
 17    items: Vec<ContextMenuItem>,
 18    widest_item_index: usize,
 19    selected_index: Option<usize>,
 20    visible: bool,
 21}
 22
 23impl Entity for ContextMenu {
 24    type Event = ();
 25}
 26
 27impl View for ContextMenu {
 28    fn ui_name() -> &'static str {
 29        "ContextMenu"
 30    }
 31
 32    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 33        enum Tag {}
 34
 35        if !self.visible {
 36            return Empty::new().boxed();
 37        }
 38
 39        let style = cx.global::<Settings>().theme.context_menu.clone();
 40
 41        let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style);
 42
 43        Overlay::new(
 44            Flex::column()
 45                .with_children(
 46                    (0..self.items.len()).map(|ix| self.render_menu_item::<Tag>(ix, cx, &style)),
 47                )
 48                .constrained()
 49                .dynamically(move |constraint, cx| {
 50                    SizeConstraint::strict_along(
 51                        Axis::Horizontal,
 52                        widest_item.layout(constraint, cx).x(),
 53                    )
 54                })
 55                .contained()
 56                .with_style(style.container)
 57                .boxed(),
 58        )
 59        .with_abs_position(self.position)
 60        .boxed()
 61    }
 62
 63    fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
 64        self.visible = false;
 65        cx.notify();
 66    }
 67}
 68
 69impl ContextMenu {
 70    pub fn new() -> Self {
 71        Self {
 72            position: Default::default(),
 73            items: Default::default(),
 74            selected_index: Default::default(),
 75            widest_item_index: Default::default(),
 76            visible: false,
 77        }
 78    }
 79
 80    pub fn show(
 81        &mut self,
 82        position: Vector2F,
 83        items: impl IntoIterator<Item = ContextMenuItem>,
 84        cx: &mut ViewContext<Self>,
 85    ) {
 86        let mut items = items.into_iter().peekable();
 87        assert!(items.peek().is_some(), "must have at least one item");
 88        self.items = items.collect();
 89        self.widest_item_index = self
 90            .items
 91            .iter()
 92            .enumerate()
 93            .max_by_key(|(_, item)| match item {
 94                ContextMenuItem::Item { label, .. } => label.chars().count(),
 95                ContextMenuItem::Separator => 0,
 96            })
 97            .unwrap()
 98            .0;
 99        self.position = position;
100        self.visible = true;
101        cx.focus_self();
102        cx.notify();
103    }
104
105    fn render_menu_item<T: 'static>(
106        &self,
107        ix: usize,
108        cx: &mut RenderContext<ContextMenu>,
109        style: &theme::ContextMenu,
110    ) -> ElementBox {
111        match &self.items[ix] {
112            ContextMenuItem::Item { label, action } => {
113                let action = action.boxed_clone();
114                MouseEventHandler::new::<T, _, _>(ix, cx, |state, _| {
115                    let style = style.item.style_for(state, Some(ix) == self.selected_index);
116                    Flex::row()
117                        .with_child(Label::new(label.to_string(), style.label.clone()).boxed())
118                        .boxed()
119                })
120                .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
121                .boxed()
122            }
123            ContextMenuItem::Separator => Empty::new()
124                .contained()
125                .with_style(style.separator)
126                .constrained()
127                .with_height(1.)
128                .flex(1., false)
129                .boxed(),
130        }
131    }
132}