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}