gpui/platform/
app_menu.rs

1use crate::{Action, App, Platform, SharedString};
2use util::ResultExt;
3
4/// A menu of the application, either a main menu or a submenu
5pub struct Menu {
6    /// The name of the menu
7    pub name: SharedString,
8
9    /// The items in the menu
10    pub items: Vec<MenuItem>,
11}
12
13impl Menu {
14    /// Create an OwnedMenu from this Menu
15    pub fn owned(self) -> OwnedMenu {
16        OwnedMenu {
17            name: self.name.to_string().into(),
18            items: self.items.into_iter().map(|item| item.owned()).collect(),
19        }
20    }
21}
22
23/// OS menus are menus that are recognized by the operating system
24/// This allows the operating system to provide specialized items for
25/// these menus
26pub struct OsMenu {
27    /// The name of the menu
28    pub name: SharedString,
29
30    /// The type of menu
31    pub menu_type: SystemMenuType,
32}
33
34impl OsMenu {
35    /// Create an OwnedOsMenu from this OsMenu
36    pub fn owned(self) -> OwnedOsMenu {
37        OwnedOsMenu {
38            name: self.name.to_string().into(),
39            menu_type: self.menu_type,
40        }
41    }
42}
43
44/// The type of system menu
45#[derive(Copy, Clone, Eq, PartialEq)]
46pub enum SystemMenuType {
47    /// The 'Services' menu in the Application menu on macOS
48    Services,
49}
50
51/// The different kinds of items that can be in a menu
52pub enum MenuItem {
53    /// A separator between items
54    Separator,
55
56    /// A submenu
57    Submenu(Menu),
58
59    /// A menu, managed by the system (for example, the Services menu on macOS)
60    SystemMenu(OsMenu),
61
62    /// An action that can be performed
63    Action {
64        /// The name of this menu item
65        name: SharedString,
66
67        /// The action to perform when this menu item is selected
68        action: Box<dyn Action>,
69
70        /// The OS Action that corresponds to this action, if any
71        /// See [`OsAction`] for more information
72        os_action: Option<OsAction>,
73
74        /// Whether this action is checked
75        checked: bool,
76    },
77}
78
79impl MenuItem {
80    /// Creates a new menu item that is a separator
81    pub fn separator() -> Self {
82        Self::Separator
83    }
84
85    /// Creates a new menu item that is a submenu
86    pub fn submenu(menu: Menu) -> Self {
87        Self::Submenu(menu)
88    }
89
90    /// Creates a new submenu that is populated by the OS
91    pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
92        Self::SystemMenu(OsMenu {
93            name: name.into(),
94            menu_type,
95        })
96    }
97
98    /// Creates a new menu item that invokes an action
99    pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
100        Self::Action {
101            name: name.into(),
102            action: Box::new(action),
103            os_action: None,
104            checked: false,
105        }
106    }
107
108    /// Creates a new menu item that invokes an action and has an OS action
109    pub fn os_action(
110        name: impl Into<SharedString>,
111        action: impl Action,
112        os_action: OsAction,
113    ) -> Self {
114        Self::Action {
115            name: name.into(),
116            action: Box::new(action),
117            os_action: Some(os_action),
118            checked: false,
119        }
120    }
121
122    /// Create an OwnedMenuItem from this MenuItem
123    pub fn owned(self) -> OwnedMenuItem {
124        match self {
125            MenuItem::Separator => OwnedMenuItem::Separator,
126            MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
127            MenuItem::Action {
128                name,
129                action,
130                os_action,
131                checked,
132            } => OwnedMenuItem::Action {
133                name: name.into(),
134                action,
135                os_action,
136                checked,
137            },
138            MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
139        }
140    }
141
142    /// Set whether this menu item is checked
143    ///
144    /// Only for [`MenuItem::Action`], otherwise, will be ignored
145    pub fn checked(mut self, checked: bool) -> Self {
146        match self {
147            MenuItem::Action {
148                action,
149                os_action,
150                name,
151                ..
152            } => MenuItem::Action {
153                name,
154                action,
155                os_action,
156                checked,
157            },
158            _ => self,
159        }
160    }
161}
162
163/// OS menus are menus that are recognized by the operating system
164/// This allows the operating system to provide specialized items for
165/// these menus
166#[derive(Clone)]
167pub struct OwnedOsMenu {
168    /// The name of the menu
169    pub name: SharedString,
170
171    /// The type of menu
172    pub menu_type: SystemMenuType,
173}
174
175/// A menu of the application, either a main menu or a submenu
176#[derive(Clone)]
177pub struct OwnedMenu {
178    /// The name of the menu
179    pub name: SharedString,
180
181    /// The items in the menu
182    pub items: Vec<OwnedMenuItem>,
183}
184
185/// The different kinds of items that can be in a menu
186pub enum OwnedMenuItem {
187    /// A separator between items
188    Separator,
189
190    /// A submenu
191    Submenu(OwnedMenu),
192
193    /// A menu, managed by the system (for example, the Services menu on macOS)
194    SystemMenu(OwnedOsMenu),
195
196    /// An action that can be performed
197    Action {
198        /// The name of this menu item
199        name: String,
200
201        /// The action to perform when this menu item is selected
202        action: Box<dyn Action>,
203
204        /// The OS Action that corresponds to this action, if any
205        /// See [`OsAction`] for more information
206        os_action: Option<OsAction>,
207
208        /// Whether this action is checked
209        checked: bool,
210    },
211}
212
213impl Clone for OwnedMenuItem {
214    fn clone(&self) -> Self {
215        match self {
216            OwnedMenuItem::Separator => OwnedMenuItem::Separator,
217            OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
218            OwnedMenuItem::Action {
219                name,
220                action,
221                os_action,
222                checked,
223            } => OwnedMenuItem::Action {
224                name: name.clone(),
225                action: action.boxed_clone(),
226                os_action: *os_action,
227                checked: *checked,
228            },
229            OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
230        }
231    }
232}
233
234// TODO: As part of the global selections refactor, these should
235// be moved to GPUI-provided actions that make this association
236// without leaking the platform details to GPUI users
237
238/// OS actions are actions that are recognized by the operating system
239/// This allows the operating system to provide specialized behavior for
240/// these actions
241#[derive(Copy, Clone, Eq, PartialEq)]
242pub enum OsAction {
243    /// The 'cut' action
244    Cut,
245
246    /// The 'copy' action
247    Copy,
248
249    /// The 'paste' action
250    Paste,
251
252    /// The 'select all' action
253    SelectAll,
254
255    /// The 'undo' action
256    Undo,
257
258    /// The 'redo' action
259    Redo,
260}
261
262pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
263    platform.on_will_open_app_menu(Box::new({
264        let cx = cx.to_async();
265        move || {
266            cx.update(|cx| cx.clear_pending_keystrokes()).ok();
267        }
268    }));
269
270    platform.on_validate_app_menu_command(Box::new({
271        let cx = cx.to_async();
272        move |action| {
273            cx.update(|cx| cx.is_action_available(action))
274                .unwrap_or(false)
275        }
276    }));
277
278    platform.on_app_menu_action(Box::new({
279        let cx = cx.to_async();
280        move |action| {
281            cx.update(|cx| cx.dispatch_action(action)).log_err();
282        }
283    }));
284}