1use crate::{
2 Action, App, KeyContext, Keymap, Keystroke, OsAction, Platform, PlatformMenu, PlatformMenuItem,
3 PlatformOsMenu, SharedString, SystemMenuType,
4};
5use itertools::Itertools as _;
6use std::sync::OnceLock;
7
8pub struct Menu {
10 pub name: SharedString,
12
13 pub items: Vec<MenuItem>,
15
16 pub disabled: bool,
18}
19
20impl Menu {
21 pub fn new(name: impl Into<SharedString>) -> Self {
23 Self {
24 name: name.into(),
25 items: vec![],
26 disabled: false,
27 }
28 }
29
30 pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
32 self.items = items.into_iter().collect();
33 self
34 }
35
36 pub fn disabled(mut self, disabled: bool) -> Self {
38 self.disabled = disabled;
39 self
40 }
41
42 pub fn owned(self) -> OwnedMenu {
44 OwnedMenu {
45 name: self.name.to_string().into(),
46 items: self.items.into_iter().map(|item| item.owned()).collect(),
47 disabled: self.disabled,
48 }
49 }
50}
51
52pub struct OsMenu {
56 pub name: SharedString,
58
59 pub menu_type: SystemMenuType,
61}
62
63impl OsMenu {
64 pub fn owned(self) -> OwnedOsMenu {
66 OwnedOsMenu {
67 name: self.name.to_string().into(),
68 menu_type: self.menu_type,
69 }
70 }
71}
72
73pub enum MenuItem {
75 Separator,
77
78 Submenu(Menu),
80
81 SystemMenu(OsMenu),
83
84 Action {
86 name: SharedString,
88
89 action: Box<dyn Action>,
91
92 os_action: Option<OsAction>,
95
96 checked: bool,
98
99 disabled: bool,
101 },
102}
103
104impl MenuItem {
105 pub fn separator() -> Self {
107 Self::Separator
108 }
109
110 pub fn submenu(menu: Menu) -> Self {
112 Self::Submenu(menu)
113 }
114
115 pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
117 Self::SystemMenu(OsMenu {
118 name: name.into(),
119 menu_type,
120 })
121 }
122
123 pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
125 Self::Action {
126 name: name.into(),
127 action: Box::new(action),
128 os_action: None,
129 checked: false,
130 disabled: false,
131 }
132 }
133
134 pub fn os_action(
136 name: impl Into<SharedString>,
137 action: impl Action,
138 os_action: OsAction,
139 ) -> Self {
140 Self::Action {
141 name: name.into(),
142 action: Box::new(action),
143 os_action: Some(os_action),
144 checked: false,
145 disabled: false,
146 }
147 }
148
149 pub fn owned(self) -> OwnedMenuItem {
151 match self {
152 MenuItem::Separator => OwnedMenuItem::Separator,
153 MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
154 MenuItem::Action {
155 name,
156 action,
157 os_action,
158 checked,
159 disabled,
160 } => OwnedMenuItem::Action {
161 name: name.into(),
162 action,
163 os_action,
164 checked,
165 disabled,
166 },
167 MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
168 }
169 }
170
171 pub fn checked(mut self, checked: bool) -> Self {
175 match &mut self {
176 MenuItem::Action { checked: old, .. } => {
177 *old = checked;
178 }
179 _ => {}
180 }
181 self
182 }
183
184 #[inline]
188 pub fn is_checked(&self) -> bool {
189 match self {
190 MenuItem::Action { checked, .. } => *checked,
191 _ => false,
192 }
193 }
194
195 pub fn disabled(mut self, disabled: bool) -> Self {
197 match &mut self {
198 MenuItem::Action { disabled: old, .. } => {
199 *old = disabled;
200 }
201 MenuItem::Submenu(submenu) => {
202 submenu.disabled = disabled;
203 }
204 _ => {}
205 }
206 self
207 }
208
209 #[inline]
213 pub fn is_disabled(&self) -> bool {
214 match self {
215 MenuItem::Action { disabled, .. } => *disabled,
216 MenuItem::Submenu(submenu) => submenu.disabled,
217 _ => false,
218 }
219 }
220}
221
222#[derive(Clone)]
226pub struct OwnedOsMenu {
227 pub name: SharedString,
229
230 pub menu_type: SystemMenuType,
232}
233
234#[derive(Clone)]
236pub struct OwnedMenu {
237 pub name: SharedString,
239
240 pub items: Vec<OwnedMenuItem>,
242
243 pub disabled: bool,
245}
246
247pub enum OwnedMenuItem {
249 Separator,
251
252 Submenu(OwnedMenu),
254
255 SystemMenu(OwnedOsMenu),
257
258 Action {
260 name: String,
262
263 action: Box<dyn Action>,
265
266 os_action: Option<OsAction>,
269
270 checked: bool,
272
273 disabled: bool,
275 },
276}
277
278impl Clone for OwnedMenuItem {
279 fn clone(&self) -> Self {
280 match self {
281 OwnedMenuItem::Separator => OwnedMenuItem::Separator,
282 OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
283 OwnedMenuItem::Action {
284 name,
285 action,
286 os_action,
287 checked,
288 disabled,
289 } => OwnedMenuItem::Action {
290 name: name.clone(),
291 action: action.boxed_clone(),
292 os_action: *os_action,
293 checked: *checked,
294 disabled: *disabled,
295 },
296 OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
297 }
298 }
299}
300
301pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
302 platform.on_will_open_app_menu(Box::new({
303 let cx = cx.to_async();
304 move || {
305 if let Some(app) = cx.app.upgrade() {
306 app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
307 }
308 }
309 }));
310
311 platform.on_validate_app_menu_command(Box::new({
312 let cx = cx.to_async();
313 move |command_id| {
314 cx.app
315 .upgrade()
316 .map(|app| {
317 app.borrow_mut()
318 .update(|cx| cx.is_menu_command_available(command_id))
319 })
320 .unwrap_or(false)
321 }
322 }));
323
324 platform.on_app_menu_action(Box::new({
325 let cx = cx.to_async();
326 move |command_id| {
327 if let Some(app) = cx.app.upgrade() {
328 app.borrow_mut()
329 .update(|cx| cx.dispatch_menu_command(command_id));
330 }
331 }
332 }));
333}
334
335pub(crate) fn resolve_menus(
338 menus: &[OwnedMenu],
339 keymap: &Keymap,
340 actions: &mut Vec<Box<dyn Action>>,
341) -> Vec<PlatformMenu> {
342 menus
343 .iter()
344 .map(|menu| resolve_menu(menu, keymap, actions))
345 .collect()
346}
347
348pub(crate) fn resolve_dock_menu(
351 items: &[OwnedMenuItem],
352 keymap: &Keymap,
353 actions: &mut Vec<Box<dyn Action>>,
354) -> Vec<PlatformMenuItem> {
355 items
356 .iter()
357 .map(|item| resolve_item(item, keymap, actions))
358 .collect()
359}
360
361fn resolve_menu(
362 menu: &OwnedMenu,
363 keymap: &Keymap,
364 actions: &mut Vec<Box<dyn Action>>,
365) -> PlatformMenu {
366 PlatformMenu {
367 name: menu.name.clone(),
368 items: menu
369 .items
370 .iter()
371 .map(|item| resolve_item(item, keymap, actions))
372 .collect(),
373 disabled: menu.disabled,
374 }
375}
376
377fn resolve_item(
378 item: &OwnedMenuItem,
379 keymap: &Keymap,
380 actions: &mut Vec<Box<dyn Action>>,
381) -> PlatformMenuItem {
382 match item {
383 OwnedMenuItem::Separator => PlatformMenuItem::Separator,
384 OwnedMenuItem::Submenu(menu) => {
385 PlatformMenuItem::Submenu(resolve_menu(menu, keymap, actions))
386 }
387 OwnedMenuItem::SystemMenu(OwnedOsMenu { name, menu_type }) => {
388 PlatformMenuItem::SystemMenu(PlatformOsMenu {
389 name: name.clone(),
390 menu_type: *menu_type,
391 })
392 }
393 OwnedMenuItem::Action {
394 name,
395 action,
396 os_action,
397 checked,
398 disabled,
399 } => {
400 let keystroke = resolve_keystroke(action.as_ref(), keymap);
401 let command_id = actions.len();
402 actions.push(action.boxed_clone());
403 PlatformMenuItem::Action {
404 name: name.clone(),
405 command_id,
406 keystroke,
407 os_action: *os_action,
408 checked: *checked,
409 disabled: *disabled,
410 }
411 }
412 }
413}
414
415fn resolve_keystroke(action: &dyn Action, keymap: &Keymap) -> Option<Keystroke> {
422 static DEFAULT_CONTEXT: OnceLock<Vec<KeyContext>> = OnceLock::new();
423
424 let keystrokes = keymap
425 .bindings_for_action(action)
426 .find_or_first(|binding| {
427 binding.predicate().is_none_or(|predicate| {
428 predicate.eval(DEFAULT_CONTEXT.get_or_init(|| {
429 let mut workspace_context = KeyContext::new_with_defaults();
430 workspace_context.add("Workspace");
431 let mut pane_context = KeyContext::new_with_defaults();
432 pane_context.add("Pane");
433 let mut editor_context = KeyContext::new_with_defaults();
434 editor_context.add("Editor");
435
436 pane_context.extend(&editor_context);
437 workspace_context.extend(&pane_context);
438 vec![workspace_context]
439 }))
440 })
441 })
442 .map(|binding| binding.keystrokes());
443
444 match keystrokes {
445 Some(keystrokes) if keystrokes.len() == 1 => keystrokes
446 .into_iter()
447 .next()
448 .map(|keystroke| keystroke.inner().clone()),
449 _ => None,
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use crate::Menu;
456
457 #[test]
458 fn test_menu() {
459 let menu = Menu::new("App")
460 .items(vec![
461 crate::MenuItem::action("Action 1", gpui::NoAction),
462 crate::MenuItem::separator(),
463 ])
464 .disabled(true);
465
466 assert_eq!(menu.name.as_ref(), "App");
467 assert_eq!(menu.items.len(), 2);
468 assert!(menu.disabled);
469 }
470
471 #[test]
472 fn test_menu_item_builder() {
473 use super::MenuItem;
474
475 let item = MenuItem::action("Test Action", gpui::NoAction);
476 assert_eq!(
477 match &item {
478 MenuItem::Action { name, .. } => name.as_ref(),
479 _ => unreachable!(),
480 },
481 "Test Action"
482 );
483 assert!(matches!(
484 item,
485 MenuItem::Action {
486 checked: false,
487 disabled: false,
488 ..
489 }
490 ));
491
492 assert!(
493 MenuItem::action("Test Action", gpui::NoAction)
494 .checked(true)
495 .is_checked()
496 );
497 assert!(
498 MenuItem::action("Test Action", gpui::NoAction)
499 .disabled(true)
500 .is_disabled()
501 );
502
503 let submenu = MenuItem::submenu(super::Menu {
504 name: "Submenu".into(),
505 items: vec![],
506 disabled: true,
507 });
508 assert_eq!(
509 match &submenu {
510 MenuItem::Submenu(menu) => menu.name.as_ref(),
511 _ => unreachable!(),
512 },
513 "Submenu"
514 );
515 assert!(!submenu.is_checked());
516 assert!(submenu.is_disabled());
517 }
518}