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