1use crate::{Action, App, Platform, SharedString};
2
3pub struct Menu {
5 pub name: SharedString,
7
8 pub items: Vec<MenuItem>,
10
11 pub disabled: bool,
13}
14
15impl Menu {
16 pub fn new(name: impl Into<SharedString>) -> Self {
18 Self {
19 name: name.into(),
20 items: vec![],
21 disabled: false,
22 }
23 }
24
25 pub fn items(mut self, items: impl IntoIterator<Item = MenuItem>) -> Self {
27 self.items = items.into_iter().collect();
28 self
29 }
30
31 pub fn disabled(mut self, disabled: bool) -> Self {
33 self.disabled = disabled;
34 self
35 }
36
37 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
47pub struct OsMenu {
51 pub name: SharedString,
53
54 pub menu_type: SystemMenuType,
56}
57
58impl OsMenu {
59 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#[derive(Copy, Clone, Eq, PartialEq)]
70pub enum SystemMenuType {
71 Services,
73}
74
75pub enum MenuItem {
77 Separator,
79
80 Submenu(Menu),
82
83 SystemMenu(OsMenu),
85
86 Action {
88 name: SharedString,
90
91 action: Box<dyn Action>,
93
94 os_action: Option<OsAction>,
97
98 checked: bool,
100
101 disabled: bool,
103 },
104}
105
106impl MenuItem {
107 pub fn separator() -> Self {
109 Self::Separator
110 }
111
112 pub fn submenu(menu: Menu) -> Self {
114 Self::Submenu(menu)
115 }
116
117 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 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 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 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 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 #[inline]
190 pub fn is_checked(&self) -> bool {
191 match self {
192 MenuItem::Action { checked, .. } => *checked,
193 _ => false,
194 }
195 }
196
197 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 #[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#[derive(Clone)]
228pub struct OwnedOsMenu {
229 pub name: SharedString,
231
232 pub menu_type: SystemMenuType,
234}
235
236#[derive(Clone)]
238pub struct OwnedMenu {
239 pub name: SharedString,
241
242 pub items: Vec<OwnedMenuItem>,
244
245 pub disabled: bool,
247}
248
249pub enum OwnedMenuItem {
251 Separator,
253
254 Submenu(OwnedMenu),
256
257 SystemMenu(OwnedOsMenu),
259
260 Action {
262 name: String,
264
265 action: Box<dyn Action>,
267
268 os_action: Option<OsAction>,
271
272 checked: bool,
274
275 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#[derive(Copy, Clone, Eq, PartialEq)]
311pub enum OsAction {
312 Cut,
314
315 Copy,
317
318 Paste,
320
321 SelectAll,
323
324 Undo,
326
327 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}