1use std::{any::TypeId, time::Duration};
2
3use gpui::{
4 elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext,
5 Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View,
6 ViewContext,
7};
8use menu::*;
9use settings::Settings;
10
11pub fn init(cx: &mut MutableAppContext) {
12 cx.add_action(ContextMenu::select_first);
13 cx.add_action(ContextMenu::select_last);
14 cx.add_action(ContextMenu::select_next);
15 cx.add_action(ContextMenu::select_prev);
16 cx.add_action(ContextMenu::confirm);
17 cx.add_action(ContextMenu::cancel);
18}
19
20pub enum ContextMenuItem {
21 Item {
22 label: String,
23 action: Box<dyn Action>,
24 },
25 Separator,
26}
27
28impl ContextMenuItem {
29 pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
30 Self::Item {
31 label: label.to_string(),
32 action: Box::new(action),
33 }
34 }
35
36 pub fn separator() -> Self {
37 Self::Separator
38 }
39
40 fn is_separator(&self) -> bool {
41 matches!(self, Self::Separator)
42 }
43
44 fn action_id(&self) -> Option<TypeId> {
45 match self {
46 ContextMenuItem::Item { action, .. } => Some(action.id()),
47 ContextMenuItem::Separator => None,
48 }
49 }
50}
51
52pub struct ContextMenu {
53 show_count: usize,
54 position: Vector2F,
55 items: Vec<ContextMenuItem>,
56 selected_index: Option<usize>,
57 visible: bool,
58 previously_focused_view_id: Option<usize>,
59 _actions_observation: Subscription,
60}
61
62impl Entity for ContextMenu {
63 type Event = ();
64}
65
66impl View for ContextMenu {
67 fn ui_name() -> &'static str {
68 "ContextMenu"
69 }
70
71 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
72 let mut cx = Self::default_keymap_context();
73 cx.set.insert("menu".into());
74 cx
75 }
76
77 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
78 if !self.visible {
79 return Empty::new().boxed();
80 }
81
82 // Render the menu once at minimum width.
83 let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
84 let expanded_menu = self
85 .render_menu(cx)
86 .constrained()
87 .dynamically(move |constraint, cx| {
88 SizeConstraint::strict_along(
89 Axis::Horizontal,
90 collapsed_menu.layout(constraint, cx).x(),
91 )
92 })
93 .boxed();
94
95 Overlay::new(expanded_menu)
96 .hoverable(true)
97 .fit_mode(OverlayFitMode::SnapToWindow)
98 .with_abs_position(self.position)
99 .boxed()
100 }
101
102 fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
103 self.reset(cx);
104 }
105}
106
107impl ContextMenu {
108 pub fn new(cx: &mut ViewContext<Self>) -> Self {
109 Self {
110 show_count: 0,
111 position: Default::default(),
112 items: Default::default(),
113 selected_index: Default::default(),
114 visible: Default::default(),
115 previously_focused_view_id: Default::default(),
116 _actions_observation: cx.observe_actions(Self::action_dispatched),
117 }
118 }
119
120 fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
121 if let Some(ix) = self
122 .items
123 .iter()
124 .position(|item| item.action_id() == Some(action_id))
125 {
126 self.selected_index = Some(ix);
127 cx.notify();
128 cx.spawn(|this, mut cx| async move {
129 cx.background().timer(Duration::from_millis(100)).await;
130 this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
131 })
132 .detach();
133 }
134 }
135
136 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
137 if let Some(ix) = self.selected_index {
138 if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
139 let window_id = cx.window_id();
140 let view_id = cx.view_id();
141 cx.dispatch_action_at(window_id, view_id, action.as_ref());
142 }
143 }
144 }
145
146 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
147 self.reset(cx);
148 let show_count = self.show_count;
149 cx.defer(move |this, cx| {
150 if cx.handle().is_focused(cx) && this.show_count == show_count {
151 let window_id = cx.window_id();
152 (**cx).focus(window_id, this.previously_focused_view_id.take());
153 }
154 });
155 }
156
157 fn reset(&mut self, cx: &mut ViewContext<Self>) {
158 self.items.clear();
159 self.visible = false;
160 self.selected_index.take();
161 cx.notify();
162 }
163
164 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
165 self.selected_index = self.items.iter().position(|item| !item.is_separator());
166 cx.notify();
167 }
168
169 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
170 for (ix, item) in self.items.iter().enumerate().rev() {
171 if !item.is_separator() {
172 self.selected_index = Some(ix);
173 cx.notify();
174 break;
175 }
176 }
177 }
178
179 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
180 if let Some(ix) = self.selected_index {
181 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
182 if !item.is_separator() {
183 self.selected_index = Some(ix);
184 cx.notify();
185 break;
186 }
187 }
188 } else {
189 self.select_first(&Default::default(), cx);
190 }
191 }
192
193 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
194 if let Some(ix) = self.selected_index {
195 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
196 if !item.is_separator() {
197 self.selected_index = Some(ix);
198 cx.notify();
199 break;
200 }
201 }
202 } else {
203 self.select_last(&Default::default(), cx);
204 }
205 }
206
207 pub fn show(
208 &mut self,
209 position: Vector2F,
210 items: impl IntoIterator<Item = ContextMenuItem>,
211 cx: &mut ViewContext<Self>,
212 ) {
213 let mut items = items.into_iter().peekable();
214 if items.peek().is_some() {
215 self.items = items.collect();
216 self.position = position;
217 self.visible = true;
218 self.show_count += 1;
219 if !cx.is_self_focused() {
220 self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
221 }
222 cx.focus_self();
223 } else {
224 self.visible = false;
225 }
226 cx.notify();
227 }
228
229 fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
230 let style = cx.global::<Settings>().theme.context_menu.clone();
231 Flex::row()
232 .with_child(
233 Flex::column()
234 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
235 match item {
236 ContextMenuItem::Item { label, .. } => {
237 let style = style
238 .item
239 .style_for(Default::default(), Some(ix) == self.selected_index);
240 Label::new(label.to_string(), style.label.clone())
241 .contained()
242 .with_style(style.container)
243 .boxed()
244 }
245 ContextMenuItem::Separator => Empty::new()
246 .collapsed()
247 .contained()
248 .with_style(style.separator)
249 .constrained()
250 .with_height(1.)
251 .boxed(),
252 }
253 }))
254 .boxed(),
255 )
256 .with_child(
257 Flex::column()
258 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
259 match item {
260 ContextMenuItem::Item { action, .. } => {
261 let style = style
262 .item
263 .style_for(Default::default(), Some(ix) == self.selected_index);
264 KeystrokeLabel::new(
265 action.boxed_clone(),
266 style.keystroke.container,
267 style.keystroke.text.clone(),
268 )
269 .boxed()
270 }
271 ContextMenuItem::Separator => Empty::new()
272 .collapsed()
273 .constrained()
274 .with_height(1.)
275 .contained()
276 .with_style(style.separator)
277 .boxed(),
278 }
279 }))
280 .boxed(),
281 )
282 .contained()
283 .with_style(style.container)
284 }
285
286 fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
287 enum Menu {}
288 enum MenuItem {}
289 let style = cx.global::<Settings>().theme.context_menu.clone();
290 MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
291 Flex::column()
292 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
293 match item {
294 ContextMenuItem::Item { label, action } => {
295 let action = action.boxed_clone();
296 MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
297 let style =
298 style.item.style_for(state, Some(ix) == self.selected_index);
299 Flex::row()
300 .with_child(
301 Label::new(label.to_string(), style.label.clone()).boxed(),
302 )
303 .with_child({
304 KeystrokeLabel::new(
305 action.boxed_clone(),
306 style.keystroke.container,
307 style.keystroke.text.clone(),
308 )
309 .flex_float()
310 .boxed()
311 })
312 .contained()
313 .with_style(style.container)
314 .boxed()
315 })
316 .with_cursor_style(CursorStyle::PointingHand)
317 .on_click(move |_, _, cx| {
318 cx.dispatch_any_action(action.boxed_clone());
319 cx.dispatch_action(Cancel);
320 })
321 .boxed()
322 }
323 ContextMenuItem::Separator => Empty::new()
324 .constrained()
325 .with_height(1.)
326 .contained()
327 .with_style(style.separator)
328 .boxed(),
329 }
330 }))
331 .contained()
332 .with_style(style.container)
333 .boxed()
334 })
335 .on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
336 .on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
337 }
338}