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}