1use gpui::{
2 anyhow,
3 elements::*,
4 geometry::vector::Vector2F,
5 impl_internal_actions,
6 keymap_matcher::KeymapContext,
7 platform::{CursorStyle, MouseButton},
8 Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
9 View, ViewContext,
10};
11use menu::*;
12use settings::Settings;
13use std::{any::TypeId, borrow::Cow, time::Duration};
14
15#[derive(Copy, Clone, PartialEq)]
16struct Clicked;
17
18impl_internal_actions!(context_menu, [Clicked]);
19
20pub fn init(cx: &mut AppContext) {
21 cx.add_action(ContextMenu::select_first);
22 cx.add_action(ContextMenu::select_last);
23 cx.add_action(ContextMenu::select_next);
24 cx.add_action(ContextMenu::select_prev);
25 cx.add_action(ContextMenu::clicked);
26 cx.add_action(ContextMenu::confirm);
27 cx.add_action(ContextMenu::cancel);
28}
29
30pub type StaticItem = Box<dyn Fn(&mut AppContext) -> AnyElement<ContextMenu>>;
31
32type ContextMenuItemBuilder =
33 Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>>;
34
35pub enum ContextMenuItemLabel {
36 String(Cow<'static, str>),
37 Element(ContextMenuItemBuilder),
38}
39
40pub enum ContextMenuItem {
41 Item {
42 label: ContextMenuItemLabel,
43 action: Box<dyn Action>,
44 },
45 Static(StaticItem),
46 Separator,
47}
48
49impl ContextMenuItem {
50 pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
51 Self::Item {
52 label: ContextMenuItemLabel::Element(label),
53 action: Box::new(action),
54 }
55 }
56
57 pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
58 Self::Item {
59 label: ContextMenuItemLabel::String(label.into()),
60 action: Box::new(action),
61 }
62 }
63
64 pub fn separator() -> Self {
65 Self::Separator
66 }
67
68 fn is_action(&self) -> bool {
69 matches!(self, Self::Item { .. })
70 }
71
72 fn action_id(&self) -> Option<TypeId> {
73 match self {
74 ContextMenuItem::Item { action, .. } => Some(action.id()),
75 ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
76 }
77 }
78}
79
80pub struct ContextMenu {
81 show_count: usize,
82 anchor_position: Vector2F,
83 anchor_corner: AnchorCorner,
84 position_mode: OverlayPositionMode,
85 items: Vec<ContextMenuItem>,
86 selected_index: Option<usize>,
87 visible: bool,
88 previously_focused_view_id: Option<usize>,
89 clicked: bool,
90 parent_view_id: usize,
91 _actions_observation: Subscription,
92}
93
94impl Entity for ContextMenu {
95 type Event = ();
96}
97
98impl View for ContextMenu {
99 fn ui_name() -> &'static str {
100 "ContextMenu"
101 }
102
103 fn keymap_context(&self, _: &AppContext) -> KeymapContext {
104 let mut cx = Self::default_keymap_context();
105 cx.add_identifier("menu");
106 cx
107 }
108
109 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
110 if !self.visible {
111 return Empty::new().into_any();
112 }
113
114 // Render the menu once at minimum width.
115 let mut collapsed_menu = self.render_menu_for_measurement(cx);
116 let expanded_menu =
117 self.render_menu(cx)
118 .constrained()
119 .dynamically(move |constraint, view, cx| {
120 SizeConstraint::strict_along(
121 Axis::Horizontal,
122 collapsed_menu.layout(constraint, view, cx).0.x(),
123 )
124 });
125
126 Overlay::new(expanded_menu)
127 .with_hoverable(true)
128 .with_fit_mode(OverlayFitMode::SnapToWindow)
129 .with_anchor_position(self.anchor_position)
130 .with_anchor_corner(self.anchor_corner)
131 .with_position_mode(self.position_mode)
132 .into_any()
133 }
134
135 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
136 self.reset(cx);
137 }
138}
139
140impl ContextMenu {
141 pub fn new(cx: &mut ViewContext<Self>) -> Self {
142 let parent_view_id = cx.parent().unwrap();
143
144 Self {
145 show_count: 0,
146 anchor_position: Default::default(),
147 anchor_corner: AnchorCorner::TopLeft,
148 position_mode: OverlayPositionMode::Window,
149 items: Default::default(),
150 selected_index: Default::default(),
151 visible: Default::default(),
152 previously_focused_view_id: Default::default(),
153 clicked: false,
154 parent_view_id,
155 _actions_observation: cx.observe_actions(Self::action_dispatched),
156 }
157 }
158
159 pub fn visible(&self) -> bool {
160 self.visible
161 }
162
163 fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
164 if let Some(ix) = self
165 .items
166 .iter()
167 .position(|item| item.action_id() == Some(action_id))
168 {
169 if self.clicked {
170 self.cancel(&Default::default(), cx);
171 } else {
172 self.selected_index = Some(ix);
173 cx.notify();
174 cx.spawn(|this, mut cx| async move {
175 cx.background().timer(Duration::from_millis(50)).await;
176 this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
177 anyhow::Ok(())
178 })
179 .detach_and_log_err(cx);
180 }
181 }
182 }
183
184 fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
185 self.clicked = true;
186 }
187
188 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
189 if let Some(ix) = self.selected_index {
190 if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
191 cx.dispatch_any_action(action.boxed_clone());
192 self.reset(cx);
193 }
194 }
195 }
196
197 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
198 self.reset(cx);
199 let show_count = self.show_count;
200 cx.defer(move |this, cx| {
201 if cx.handle().is_focused(cx) && this.show_count == show_count {
202 let window_id = cx.window_id();
203 (**cx).focus(window_id, this.previously_focused_view_id.take());
204 }
205 });
206 }
207
208 fn reset(&mut self, cx: &mut ViewContext<Self>) {
209 self.items.clear();
210 self.visible = false;
211 self.selected_index.take();
212 self.clicked = false;
213 cx.notify();
214 }
215
216 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
217 self.selected_index = self.items.iter().position(|item| item.is_action());
218 cx.notify();
219 }
220
221 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
222 for (ix, item) in self.items.iter().enumerate().rev() {
223 if item.is_action() {
224 self.selected_index = Some(ix);
225 cx.notify();
226 break;
227 }
228 }
229 }
230
231 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
232 if let Some(ix) = self.selected_index {
233 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
234 if item.is_action() {
235 self.selected_index = Some(ix);
236 cx.notify();
237 break;
238 }
239 }
240 } else {
241 self.select_first(&Default::default(), cx);
242 }
243 }
244
245 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
246 if let Some(ix) = self.selected_index {
247 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
248 if item.is_action() {
249 self.selected_index = Some(ix);
250 cx.notify();
251 break;
252 }
253 }
254 } else {
255 self.select_last(&Default::default(), cx);
256 }
257 }
258
259 pub fn show(
260 &mut self,
261 anchor_position: Vector2F,
262 anchor_corner: AnchorCorner,
263 items: Vec<ContextMenuItem>,
264 cx: &mut ViewContext<Self>,
265 ) {
266 let mut items = items.into_iter().peekable();
267 if items.peek().is_some() {
268 self.items = items.collect();
269 self.anchor_position = anchor_position;
270 self.anchor_corner = anchor_corner;
271 self.visible = true;
272 self.show_count += 1;
273 if !cx.is_self_focused() {
274 self.previously_focused_view_id = cx.focused_view_id();
275 }
276 cx.focus_self();
277 } else {
278 self.visible = false;
279 }
280 cx.notify();
281 }
282
283 pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
284 self.position_mode = mode;
285 }
286
287 fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
288 let style = cx.global::<Settings>().theme.context_menu.clone();
289 Flex::row()
290 .with_child(
291 Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
292 match item {
293 ContextMenuItem::Item { label, .. } => {
294 let style = style.item.style_for(
295 &mut Default::default(),
296 Some(ix) == self.selected_index,
297 );
298
299 match label {
300 ContextMenuItemLabel::String(label) => {
301 Label::new(label.to_string(), style.label.clone())
302 .contained()
303 .with_style(style.container)
304 .into_any()
305 }
306 ContextMenuItemLabel::Element(element) => {
307 element(&mut Default::default(), style)
308 }
309 }
310 }
311
312 ContextMenuItem::Static(f) => f(cx),
313
314 ContextMenuItem::Separator => Empty::new()
315 .collapsed()
316 .contained()
317 .with_style(style.separator)
318 .constrained()
319 .with_height(1.)
320 .into_any(),
321 }
322 })),
323 )
324 .with_child(
325 Flex::column()
326 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
327 match item {
328 ContextMenuItem::Item { action, .. } => {
329 let style = style.item.style_for(
330 &mut Default::default(),
331 Some(ix) == self.selected_index,
332 );
333
334 KeystrokeLabel::new(
335 self.parent_view_id,
336 action.boxed_clone(),
337 style.keystroke.container,
338 style.keystroke.text.clone(),
339 )
340 .into_any()
341 }
342
343 ContextMenuItem::Static(_) => Empty::new().into_any(),
344
345 ContextMenuItem::Separator => Empty::new()
346 .collapsed()
347 .constrained()
348 .with_height(1.)
349 .contained()
350 .with_style(style.separator)
351 .into_any(),
352 }
353 }))
354 .contained()
355 .with_margin_left(style.keystroke_margin),
356 )
357 .contained()
358 .with_style(style.container)
359 }
360
361 fn render_menu(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
362 enum Menu {}
363 enum MenuItem {}
364
365 let style = cx.global::<Settings>().theme.context_menu.clone();
366
367 MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
368 Flex::column()
369 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
370 match item {
371 ContextMenuItem::Item { label, action } => {
372 let action = action.boxed_clone();
373 let view_id = self.parent_view_id;
374 MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
375 let style =
376 style.item.style_for(state, Some(ix) == self.selected_index);
377
378 Flex::row()
379 .with_child(match label {
380 ContextMenuItemLabel::String(label) => {
381 Label::new(label.clone(), style.label.clone())
382 .contained()
383 .into_any()
384 }
385 ContextMenuItemLabel::Element(element) => {
386 element(state, style)
387 }
388 })
389 .with_child({
390 KeystrokeLabel::new(
391 view_id,
392 action.boxed_clone(),
393 style.keystroke.container,
394 style.keystroke.text.clone(),
395 )
396 .flex_float()
397 })
398 .contained()
399 .with_style(style.container)
400 })
401 .with_cursor_style(CursorStyle::PointingHand)
402 .on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
403 .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
404 .on_click(MouseButton::Left, move |_, _, cx| {
405 let window_id = cx.window_id();
406 cx.dispatch_action(Clicked);
407 cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
408 })
409 .on_drag(MouseButton::Left, |_, _, _| {})
410 .into_any()
411 }
412
413 ContextMenuItem::Static(f) => f(cx),
414
415 ContextMenuItem::Separator => Empty::new()
416 .constrained()
417 .with_height(1.)
418 .contained()
419 .with_style(style.separator)
420 .into_any(),
421 }
422 }))
423 .contained()
424 .with_style(style.container)
425 })
426 .on_down_out(MouseButton::Left, |_, _, cx| cx.dispatch_action(Cancel))
427 .on_down_out(MouseButton::Right, |_, _, cx| cx.dispatch_action(Cancel))
428 }
429}