app_menu.rs

  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}