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