Skip to main content

gpui_cn/menu/
entry.rs

1//! The rows of a menu: items, submenus, group labels, and separators.
2
3use std::{collections::HashSet, rc::Rc};
4
5use gpui_kit::{
6    Action, AnyElement, App, FocusHandle, IntoElement, OwnedMenuItem, SharedString, Window,
7    base::Disableable,
8};
9
10use crate::Icon;
11
12type SelectHandler = Rc<dyn Fn(&mut Window, &mut App)>;
13pub(crate) type RowRenderer = Rc<dyn Fn(MenuRowState, &mut Window, &mut App) -> AnyElement>;
14
15/// What an item does when it is chosen.
16enum Activation {
17    /// Nothing beyond the state's `Activated` event.
18    Event,
19    /// Dispatches an action to the action context, and shows the
20    /// action's shortcut.
21    Action(Box<dyn Action>),
22    /// Calls the application.
23    Handler(SelectHandler),
24    /// Opens a URL in the system's browser or mail client.
25    Link(SharedString),
26}
27
28impl Clone for Activation {
29    fn clone(&self) -> Self {
30        match self {
31            Self::Event => Self::Event,
32            Self::Action(action) => Self::Action(action.boxed_clone()),
33            Self::Handler(handler) => Self::Handler(handler.clone()),
34            Self::Link(url) => Self::Link(url.clone()),
35        }
36    }
37}
38
39/// An item's activation, taken out of the menu as it closes, so it runs
40/// once the menu's state is no longer borrowed.
41pub(crate) struct PendingActivation {
42    activation: Activation,
43    context: FocusHandle,
44}
45
46impl PendingActivation {
47    /// Runs the item: an action goes to the action context.
48    pub(crate) fn run(self, window: &mut Window, cx: &mut App) {
49        match self.activation {
50            Activation::Event => {}
51            Activation::Action(action) => self.context.dispatch_action(action.as_ref(), window, cx),
52            Activation::Handler(handler) => handler(window, cx),
53            Activation::Link(url) => cx.open_url(&url),
54        }
55    }
56}
57
58/// What an item shows in the leading slot, before its label.
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub(crate) enum Indicator {
61    /// The icon, if any.
62    #[default]
63    None,
64    /// A toggle: a check while on.
65    Check(bool),
66    /// One of a group the application keeps one of on: a dot while on.
67    Radio(bool),
68}
69
70/// The state of a row a custom renderer draws; see [`MenuItem::render`].
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72#[non_exhaustive]
73pub struct MenuRowState {
74    pub(crate) highlighted: bool,
75    pub(crate) disabled: bool,
76    pub(crate) checked: bool,
77}
78
79impl MenuRowState {
80    /// Whether the keyboard or the pointer is on the row.
81    pub fn is_highlighted(&self) -> bool {
82        self.highlighted
83    }
84
85    /// Whether the row's item is disabled.
86    pub fn is_disabled(&self) -> bool {
87        self.disabled
88    }
89
90    /// Whether the row's toggle or radio is on.
91    pub fn is_checked(&self) -> bool {
92        self.checked
93    }
94}
95
96/// A command in a menu.
97///
98/// The key names the row's element id and the
99/// [`MenuEvent::Activated`](super::MenuEvent::Activated) payload, so it
100/// comes from the domain and is unique in the menu, submenus included.
101/// An item does one thing when chosen: it dispatches an
102/// [`action`](Self::action), whose shortcut the row shows, calls
103/// [`on_select`](Self::on_select), or opens a [`link`](Self::link). An
104/// item with none of them only reports its key.
105#[derive(Clone)]
106#[non_exhaustive]
107pub struct MenuItem {
108    key: SharedString,
109    label: SharedString,
110    description: Option<SharedString>,
111    icon: Option<Icon>,
112    disabled: bool,
113    indicator: Indicator,
114    destructive: bool,
115    external_link_icon: bool,
116    action_context: Option<FocusHandle>,
117    render: Option<RowRenderer>,
118    activation: Activation,
119}
120
121impl MenuItem {
122    /// An item with a stable key and the label it shows.
123    pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
124        Self {
125            key: key.into(),
126            label: label.into(),
127            description: None,
128            icon: None,
129            disabled: false,
130            indicator: Indicator::None,
131            destructive: false,
132            external_link_icon: true,
133            action_context: None,
134            render: None,
135            activation: Activation::Event,
136        }
137    }
138
139    /// An icon before the label.
140    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
141        self.icon = Some(icon.into());
142        self
143    }
144
145    /// A line under the label, in the menu's description color.
146    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
147        self.description = Some(description.into());
148        self
149    }
150
151    /// Makes the item a toggle: a check shows in the leading slot while
152    /// `checked` is true, in place of the icon.
153    pub fn checked(mut self, checked: bool) -> Self {
154        self.indicator = Indicator::Check(checked);
155        self
156    }
157
158    /// Makes the item one of a group of radio items: a dot shows in the
159    /// leading slot while `selected` is true. The application keeps which
160    /// one of the group is on.
161    pub fn radio(mut self, selected: bool) -> Self {
162        self.indicator = Indicator::Radio(selected);
163        self
164    }
165
166    /// Paints the item in the destructive color, for a command that
167    /// deletes or cannot be undone.
168    pub fn destructive(mut self) -> Self {
169        self.destructive = true;
170        self
171    }
172
173    /// Dispatches `action` when the item is chosen, to the element the
174    /// menu acts on, and shows the action's shortcut.
175    pub fn action(mut self, action: impl Action) -> Self {
176        self.activation = Activation::Action(Box::new(action));
177        self
178    }
179
180    /// The element this item's action goes to, and whose key bindings its
181    /// shortcut shows, in place of the menu's.
182    pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
183        self.action_context = Some(focus_handle.clone());
184        self
185    }
186
187    /// Calls `handler` when the item is chosen, once the menu has closed.
188    pub fn on_select(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
189        self.activation = Activation::Handler(Rc::new(handler));
190        self
191    }
192
193    /// Opens `url` in the system's browser or mail client when chosen. The
194    /// row shows an external-link icon after the label.
195    pub fn link(mut self, url: impl Into<SharedString>) -> Self {
196        self.activation = Activation::Link(url.into());
197        self
198    }
199
200    /// Whether a link item shows its external-link icon. On by default.
201    pub fn external_link_icon(mut self, visible: bool) -> Self {
202        self.external_link_icon = visible;
203        self
204    }
205
206    /// Draws the inside of the row: `render` gets the row's state and
207    /// returns what goes between the row's padding, in place of the
208    /// indicator, icon, label, description, and shortcut. The row itself
209    /// (its height, highlight, hover, and click) stays the menu's.
210    pub fn render<E: IntoElement>(
211        mut self,
212        render: impl Fn(MenuRowState, &mut Window, &mut App) -> E + 'static,
213    ) -> Self {
214        self.render = Some(Rc::new(move |row, window, cx| {
215            render(row, window, cx).into_any_element()
216        }));
217        self
218    }
219
220    /// The item's key.
221    pub fn key(&self) -> &SharedString {
222        &self.key
223    }
224
225    /// The text the item shows.
226    pub fn label(&self) -> &SharedString {
227        &self.label
228    }
229
230    /// The line under the label.
231    pub fn description_text(&self) -> Option<&SharedString> {
232        self.description.as_ref()
233    }
234
235    /// Whether the item ignores input.
236    pub fn is_disabled(&self) -> bool {
237        self.disabled
238    }
239
240    /// Whether the item is a toggle or a radio item that is on.
241    pub fn is_checked(&self) -> bool {
242        matches!(
243            self.indicator,
244            Indicator::Check(true) | Indicator::Radio(true)
245        )
246    }
247
248    /// Whether the item is painted as destructive.
249    pub fn is_destructive(&self) -> bool {
250        self.destructive
251    }
252
253    pub(crate) fn indicator(&self) -> Indicator {
254        self.indicator
255    }
256
257    pub(crate) fn leading_icon(&self) -> Option<&Icon> {
258        self.icon.as_ref()
259    }
260
261    pub(crate) fn renderer(&self) -> Option<&RowRenderer> {
262        self.render.as_ref()
263    }
264
265    /// Whether the row shows the external-link icon.
266    pub(crate) fn shows_link_icon(&self) -> bool {
267        self.external_link_icon && matches!(self.activation, Activation::Link(_))
268    }
269
270    /// The action the item dispatches, if it dispatches one.
271    pub(crate) fn dispatched_action(&self) -> Option<&dyn Action> {
272        match &self.activation {
273            Activation::Action(action) => Some(action.as_ref()),
274            _ => None,
275        }
276    }
277
278    /// The item's own action context, if it has one.
279    pub(crate) fn own_action_context(&self) -> Option<&FocusHandle> {
280        self.action_context.as_ref()
281    }
282
283    /// What choosing the item runs: its action goes to the item's own
284    /// action context, else to the menu's `context`.
285    pub(crate) fn pending(&self, context: FocusHandle) -> PendingActivation {
286        PendingActivation {
287            activation: self.activation.clone(),
288            context: self.action_context.clone().unwrap_or(context),
289        }
290    }
291}
292
293impl Disableable for MenuItem {
294    fn disabled(mut self, disabled: bool) -> Self {
295        self.disabled = disabled;
296        self
297    }
298}
299
300/// A row that opens a further menu beside it.
301#[derive(Clone)]
302#[non_exhaustive]
303pub struct MenuSubmenu {
304    key: SharedString,
305    label: SharedString,
306    icon: Option<Icon>,
307    disabled: bool,
308    entries: Vec<MenuEntry>,
309}
310
311impl MenuSubmenu {
312    /// A submenu with a stable key and the label its row shows.
313    pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
314        Self {
315            key: key.into(),
316            label: label.into(),
317            icon: None,
318            disabled: false,
319            entries: Vec::new(),
320        }
321    }
322
323    /// An icon before the label.
324    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
325        self.icon = Some(icon.into());
326        self
327    }
328
329    /// The rows of the menu it opens.
330    pub fn entries(mut self, entries: impl IntoIterator<Item = impl Into<MenuEntry>>) -> Self {
331        self.entries.extend(entries.into_iter().map(Into::into));
332        self
333    }
334
335    /// The submenu's key.
336    pub fn key(&self) -> &SharedString {
337        &self.key
338    }
339
340    /// The text the row shows.
341    pub fn label(&self) -> &SharedString {
342        &self.label
343    }
344
345    /// Whether the row ignores input.
346    pub fn is_disabled(&self) -> bool {
347        self.disabled
348    }
349
350    pub(crate) fn leading_icon(&self) -> Option<&Icon> {
351        self.icon.as_ref()
352    }
353
354    pub(crate) fn children(&self) -> &[MenuEntry] {
355        &self.entries
356    }
357
358    /// The submenu with its rows tidied as a menu draws them.
359    pub(crate) fn normalized(mut self) -> Self {
360        self.entries = super::state::normalize(std::mem::take(&mut self.entries));
361        self
362    }
363}
364
365impl Disableable for MenuSubmenu {
366    fn disabled(mut self, disabled: bool) -> Self {
367        self.disabled = disabled;
368        self
369    }
370}
371
372/// One row of a menu.
373#[derive(Clone)]
374pub enum MenuEntry {
375    /// A command.
376    Item(MenuItem),
377    /// A hairline between two groups.
378    Separator,
379    /// A muted heading over a group.
380    Label(SharedString),
381    /// A row that opens a further menu.
382    Submenu(MenuSubmenu),
383}
384
385impl MenuEntry {
386    /// A group heading.
387    pub fn label(text: impl Into<SharedString>) -> Self {
388        Self::Label(text.into())
389    }
390
391    /// The row's key, for an item or a submenu.
392    pub fn key(&self) -> Option<&SharedString> {
393        match self {
394            Self::Item(item) => Some(item.key()),
395            Self::Submenu(submenu) => Some(submenu.key()),
396            Self::Separator | Self::Label(_) => None,
397        }
398    }
399
400    /// Whether the keyboard and the pointer can land on the row.
401    pub(crate) fn is_selectable(&self) -> bool {
402        match self {
403            Self::Item(item) => !item.is_disabled(),
404            Self::Submenu(submenu) => !submenu.is_disabled(),
405            Self::Separator | Self::Label(_) => false,
406        }
407    }
408
409    pub(crate) fn submenu(&self) -> Option<&MenuSubmenu> {
410        match self {
411            Self::Submenu(submenu) => Some(submenu),
412            _ => None,
413        }
414    }
415}
416
417impl MenuEntry {
418    /// The row for an item of GPUI's application menu data, the one
419    /// `App::set_menus` takes, so an application defines its menus once
420    /// for the native bar and for a [`MenuBar`](super::MenuBar). An action
421    /// item keys its row by the action's name, or by the name and the
422    /// label when an earlier row of the same menu has that key already;
423    /// a submenu keys its row by its name. A menu the system owns, such
424    /// as Services, has no row.
425    pub fn from_app_menu_item(item: OwnedMenuItem) -> Option<Self> {
426        Self::from_app_item_keyed(item, &mut HashSet::new())
427    }
428
429    /// As [`from_app_menu_item`](Self::from_app_menu_item), keeping the
430    /// keys unique across the rows `used` holds.
431    pub(crate) fn from_app_item_keyed(
432        item: OwnedMenuItem,
433        used: &mut HashSet<SharedString>,
434    ) -> Option<Self> {
435        let mut unique = |key: SharedString, label: &str| {
436            let key = if used.contains(&key) {
437                SharedString::from(format!("{key}/{label}"))
438            } else {
439                key
440            };
441            used.insert(key.clone());
442            key
443        };
444        match item {
445            OwnedMenuItem::Separator => Some(Self::Separator),
446            OwnedMenuItem::Submenu(menu) => {
447                let key = unique(menu.name.clone(), &menu.name);
448                let entries: Vec<MenuEntry> = menu
449                    .items
450                    .into_iter()
451                    .filter_map(|item| Self::from_app_item_keyed(item, used))
452                    .collect();
453                Some(
454                    MenuSubmenu::new(key, menu.name)
455                        .disabled(menu.disabled)
456                        .entries(entries)
457                        .into(),
458                )
459            }
460            OwnedMenuItem::SystemMenu(_) => None,
461            OwnedMenuItem::Action {
462                name,
463                action,
464                checked,
465                disabled,
466                ..
467            } => {
468                let key = unique(action.name().into(), &name);
469                let mut item = MenuItem::new(key, name).disabled(disabled);
470                if checked {
471                    item = item.checked(true);
472                }
473                item.activation = Activation::Action(action);
474                Some(item.into())
475            }
476        }
477    }
478}
479
480impl From<MenuItem> for MenuEntry {
481    fn from(item: MenuItem) -> Self {
482        Self::Item(item)
483    }
484}
485
486impl From<MenuSubmenu> for MenuEntry {
487    fn from(submenu: MenuSubmenu) -> Self {
488        Self::Submenu(submenu)
489    }
490}