context_menu.rs

  1use gpui::{
  2    elements::*, geometry::vector::Vector2F, Action, Entity, RenderContext, View, ViewContext,
  3};
  4use settings::Settings;
  5use std::{marker::PhantomData, sync::Arc};
  6
  7pub enum ContextMenuItem {
  8    Item {
  9        label: String,
 10        action: Box<dyn Action>,
 11    },
 12    Separator,
 13}
 14
 15pub struct ContextMenu<T> {
 16    position: Vector2F,
 17    items: Arc<[ContextMenuItem]>,
 18    state: UniformListState,
 19    selected_index: Option<usize>,
 20    widest_item_index: Option<usize>,
 21    visible: bool,
 22    _phantom: PhantomData<T>,
 23}
 24
 25impl<T: 'static> Entity for ContextMenu<T> {
 26    type Event = ();
 27}
 28
 29impl<T: 'static> View for ContextMenu<T> {
 30    fn ui_name() -> &'static str {
 31        "ContextMenu"
 32    }
 33
 34    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 35        if !self.visible {
 36            return Empty::new().boxed();
 37        }
 38
 39        let theme = &cx.global::<Settings>().theme;
 40        let menu_style = &theme.project_panel.context_menu;
 41        let separator_style = menu_style.separator;
 42        let item_style = menu_style.item.clone();
 43        let items = self.items.clone();
 44        let selected_ix = self.selected_index;
 45        Overlay::new(
 46            UniformList::new(
 47                self.state.clone(),
 48                self.items.len(),
 49                move |range, elements, cx| {
 50                    let start = range.start;
 51                    elements.extend(items[range].iter().enumerate().map(|(ix, item)| {
 52                        let item_ix = start + ix;
 53                        match item {
 54                            ContextMenuItem::Item { label, action } => {
 55                                let action = action.boxed_clone();
 56                                MouseEventHandler::new::<T, _, _>(item_ix, cx, |state, _| {
 57                                    let style =
 58                                        item_style.style_for(state, Some(item_ix) == selected_ix);
 59                                    Flex::row()
 60                                        .with_child(
 61                                            Label::new(label.to_string(), style.label.clone())
 62                                                .boxed(),
 63                                        )
 64                                        .boxed()
 65                                })
 66                                .on_click(move |_, _, cx| {
 67                                    cx.dispatch_any_action(action.boxed_clone())
 68                                })
 69                                .boxed()
 70                            }
 71                            ContextMenuItem::Separator => {
 72                                Empty::new().contained().with_style(separator_style).boxed()
 73                            }
 74                        }
 75                    }))
 76                },
 77            )
 78            .with_width_from_item(self.widest_item_index)
 79            .boxed(),
 80        )
 81        .with_abs_position(self.position)
 82        .contained()
 83        .with_style(menu_style.container)
 84        .boxed()
 85    }
 86
 87    fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
 88        self.visible = false;
 89        cx.notify();
 90    }
 91}
 92
 93impl<T: 'static> ContextMenu<T> {
 94    pub fn new() -> Self {
 95        Self {
 96            position: Default::default(),
 97            items: Arc::from([]),
 98            state: Default::default(),
 99            selected_index: Default::default(),
100            widest_item_index: Default::default(),
101            visible: false,
102            _phantom: PhantomData,
103        }
104    }
105
106    pub fn show(
107        &mut self,
108        position: Vector2F,
109        items: impl IntoIterator<Item = ContextMenuItem>,
110        cx: &mut ViewContext<Self>,
111    ) {
112        self.items = items.into_iter().collect();
113        self.widest_item_index = self
114            .items
115            .iter()
116            .enumerate()
117            .max_by_key(|(_, item)| match item {
118                ContextMenuItem::Item { label, .. } => label.chars().count(),
119                ContextMenuItem::Separator => 0,
120            })
121            .map(|(ix, _)| ix);
122        self.position = position;
123        self.visible = true;
124        cx.focus_self();
125        cx.notify();
126    }
127}