1use crate::{Action, App, Platform, SharedString};
2
3/// A menu of the application, either a main menu or a submenu
4pub struct Menu {
5 /// The name of the menu
6 pub name: SharedString,
7
8 /// The items in the menu
9 pub items: Vec<MenuItem>,
10
11 /// Whether this menu is disabled
12 pub disabled: bool,
13}
14
15impl Menu {
16 /// Create a new Menu with the given name
17 pub fn new(name: impl Into<SharedString>) -> Self {
18 Self {
19 name: name.into(),
20 items: vec![],
21 disabled: false,
22 }
23 }
24
25 /// Set items to be in this menu
26 pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
27 self.items = items.into_iter().collect();
28 self
29 }
30
31 /// Set whether this menu is disabled
32 pub fn disabled(mut self, disabled: bool) -> Self {
33 self.disabled = disabled;
34 self
35 }
36
37 /// Create an OwnedMenu from this Menu
38 pub fn owned(self) -> OwnedMenu {
39 OwnedMenu {
40 name: self.name.to_string().into(),
41 items: self.items.into_iter().map(|item| item.owned()).collect(),
42 disabled: self.disabled,
43 }
44 }
45}
46
47/// OS menus are menus that are recognized by the operating system
48/// This allows the operating system to provide specialized items for
49/// these menus
50pub struct OsMenu {
51 /// The name of the menu
52 pub name: SharedString,
53
54 /// The type of menu
55 pub menu_type: SystemMenuType,
56}
57
58impl OsMenu {
59 /// Create an OwnedOsMenu from this OsMenu
60 pub fn owned(self) -> OwnedOsMenu {
61 OwnedOsMenu {
62 name: self.name.to_string().into(),
63 menu_type: self.menu_type,
64 }
65 }
66}
67
68/// The type of system menu
69#[derive(Copy, Clone, Eq, PartialEq)]
70pub enum SystemMenuType {
71 /// The 'Services' menu in the Application menu on macOS
72 Services,
73}
74
75/// The different kinds of items that can be in a menu
76pub enum MenuItem {
77 /// A separator between items
78 Separator,
79
80 /// A submenu
81 Submenu(Menu),
82
83 /// A menu, managed by the system (for example, the Services menu on macOS)
84 SystemMenu(OsMenu),
85
86 /// An action that can be performed
87 Action {
88 /// The name of this menu item
89 name: SharedString,
90
91 /// The action to perform when this menu item is selected
92 action: Box<dyn Action>,
93
94 /// The OS Action that corresponds to this action, if any
95 /// See [`OsAction`] for more information
96 os_action: Option<OsAction>,
97
98 /// Whether this action is checked
99 checked: bool,
100
101 /// Whether this action is disabled
102 disabled: bool,
103 },
104}
105
106impl MenuItem {
107 /// Creates a new menu item that is a separator
108 pub fn separator() -> Self {
109 Self::Separator
110 }
111
112 /// Creates a new menu item that is a submenu
113 pub fn submenu(menu: Menu) -> Self {
114 Self::Submenu(menu)
115 }
116
117 /// Creates a new submenu that is populated by the OS
118 pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
119 Self::SystemMenu(OsMenu {
120 name: name.into(),
121 menu_type,
122 })
123 }
124
125 /// Creates a new menu item that invokes an action
126 pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
127 Self::Action {
128 name: name.into(),
129 action: Box::new(action),
130 os_action: None,
131 checked: false,
132 disabled: false,
133 }
134 }
135
136 /// Creates a new menu item that invokes an action and has an OS action
137 pub fn os_action(
138 name: impl Into<SharedString>,
139 action: impl Action,
140 os_action: OsAction,
141 ) -> Self {
142 Self::Action {
143 name: name.into(),
144 action: Box::new(action),
145 os_action: Some(os_action),
146 checked: false,
147 disabled: false,
148 }
149 }
150
151 /// Create an OwnedMenuItem from this MenuItem
152 pub fn owned(self) -> OwnedMenuItem {
153 match self {
154 MenuItem::Separator => OwnedMenuItem::Separator,
155 MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
156 MenuItem::Action {
157 name,
158 action,
159 os_action,
160 checked,
161 disabled,
162 } => OwnedMenuItem::Action {
163 name: name.into(),
164 action,
165 os_action,
166 checked,
167 disabled,
168 },
169 MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
170 }
171 }
172
173 /// Set whether this menu item is checked
174 ///
175 /// Only for [`MenuItem::Action`], otherwise, will be ignored
176 pub fn checked(mut self, checked: bool) -> Self {
177 match &mut self {
178 MenuItem::Action { checked: old, .. } => {
179 *old = checked;
180 }
181 _ => {}
182 }
183 self
184 }
185
186 /// Returns whether this menu item is checked
187 ///
188 /// Only for [`MenuItem::Action`], otherwise, returns false
189 #[inline]
190 pub fn is_checked(&self) -> bool {
191 match self {
192 MenuItem::Action { checked, .. } => *checked,
193 _ => false,
194 }
195 }
196
197 /// Set whether this menu item is disabled
198 pub fn disabled(mut self, disabled: bool) -> Self {
199 match &mut self {
200 MenuItem::Action { disabled: old, .. } => {
201 *old = disabled;
202 }
203 MenuItem::Submenu(submenu) => {
204 submenu.disabled = disabled;
205 }
206 _ => {}
207 }
208 self
209 }
210
211 /// Returns whether this menu item is disabled
212 ///
213 /// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false
214 #[inline]
215 pub fn is_disabled(&self) -> bool {
216 match self {
217 MenuItem::Action { disabled, .. } => *disabled,
218 MenuItem::Submenu(submenu) => submenu.disabled,
219 _ => false,
220 }
221 }
222}
223
224/// OS menus are menus that are recognized by the operating system
225/// This allows the operating system to provide specialized items for
226/// these menus
227#[derive(Clone)]
228pub struct OwnedOsMenu {
229 /// The name of the menu
230 pub name: SharedString,
231
232 /// The type of menu
233 pub menu_type: SystemMenuType,
234}
235
236/// A menu of the application, either a main menu or a submenu
237#[derive(Clone)]
238pub struct OwnedMenu {
239 /// The name of the menu
240 pub name: SharedString,
241
242 /// The items in the menu
243 pub items: Vec<OwnedMenuItem>,
244
245 /// Whether this menu is disabled
246 pub disabled: bool,
247}
248
249/// The different kinds of items that can be in a menu
250pub enum OwnedMenuItem {
251 /// A separator between items
252 Separator,
253
254 /// A submenu
255 Submenu(OwnedMenu),
256
257 /// A menu, managed by the system (for example, the Services menu on macOS)
258 SystemMenu(OwnedOsMenu),
259
260 /// An action that can be performed
261 Action {
262 /// The name of this menu item
263 name: String,
264
265 /// The action to perform when this menu item is selected
266 action: Box<dyn Action>,
267
268 /// The OS Action that corresponds to this action, if any
269 /// See [`OsAction`] for more information
270 os_action: Option<OsAction>,
271
272 /// Whether this action is checked
273 checked: bool,
274
275 /// Whether this action is disabled
276 disabled: bool,
277 },
278}
279
280impl Clone for OwnedMenuItem {
281 fn clone(&self) -> Self {
282 match self {
283 OwnedMenuItem::Separator => OwnedMenuItem::Separator,
284 OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
285 OwnedMenuItem::Action {
286 name,
287 action,
288 os_action,
289 checked,
290 disabled,
291 } => OwnedMenuItem::Action {
292 name: name.clone(),
293 action: action.boxed_clone(),
294 os_action: *os_action,
295 checked: *checked,
296 disabled: *disabled,
297 },
298 OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
299 }
300 }
301}
302
303// TODO: As part of the global selections refactor, these should
304// be moved to GPUI-provided actions that make this association
305// without leaking the platform details to GPUI users
306
307/// OS actions are actions that are recognized by the operating system
308/// This allows the operating system to provide specialized behavior for
309/// these actions
310#[derive(Copy, Clone, Eq, PartialEq)]
311pub enum OsAction {
312 /// The 'cut' action
313 Cut,
314
315 /// The 'copy' action
316 Copy,
317
318 /// The 'paste' action
319 Paste,
320
321 /// The 'select all' action
322 SelectAll,
323
324 /// The 'undo' action
325 Undo,
326
327 /// The 'redo' action
328 Redo,
329}
330
331pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
332 platform.on_will_open_app_menu(Box::new({
333 let cx = cx.to_async();
334 move || {
335 if let Some(app) = cx.app.upgrade() {
336 app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
337 }
338 }
339 }));
340
341 platform.on_validate_app_menu_command(Box::new({
342 let cx = cx.to_async();
343 move |action| {
344 cx.app
345 .upgrade()
346 .map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action)))
347 .unwrap_or(false)
348 }
349 }));
350
351 platform.on_app_menu_action(Box::new({
352 let cx = cx.to_async();
353 move |action| {
354 if let Some(app) = cx.app.upgrade() {
355 app.borrow_mut().update(|cx| cx.dispatch_action(action));
356 }
357 }
358 }));
359}
360
361#[cfg(test)]
362mod tests {
363 use crate::Menu;
364
365 #[test]
366 fn test_menu() {
367 let menu = Menu::new("App")
368 .items(vec![
369 crate::MenuItem::action("Action 1", gpui::NoAction),
370 crate::MenuItem::separator(),
371 ])
372 .disabled(true);
373
374 assert_eq!(menu.name.as_ref(), "App");
375 assert_eq!(menu.items.len(), 2);
376 assert!(menu.disabled);
377 }
378
379 #[test]
380 fn test_menu_item_builder() {
381 use super::MenuItem;
382
383 let item = MenuItem::action("Test Action", gpui::NoAction);
384 assert_eq!(
385 match &item {
386 MenuItem::Action { name, .. } => name.as_ref(),
387 _ => unreachable!(),
388 },
389 "Test Action"
390 );
391 assert!(matches!(
392 item,
393 MenuItem::Action {
394 checked: false,
395 disabled: false,
396 ..
397 }
398 ));
399
400 assert!(
401 MenuItem::action("Test Action", gpui::NoAction)
402 .checked(true)
403 .is_checked()
404 );
405 assert!(
406 MenuItem::action("Test Action", gpui::NoAction)
407 .disabled(true)
408 .is_disabled()
409 );
410
411 let submenu = MenuItem::submenu(super::Menu {
412 name: "Submenu".into(),
413 items: vec![],
414 disabled: true,
415 });
416 assert_eq!(
417 match &submenu {
418 MenuItem::Submenu(menu) => menu.name.as_ref(),
419 _ => unreachable!(),
420 },
421 "Submenu"
422 );
423 assert!(!submenu.is_checked());
424 assert!(submenu.is_disabled());
425 }
426}