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