Skip to main content

inset_embedder/
menus.rs

1//! Menus the system draws, as values: the menu bar (macOS's), a context menu at a point in a view,
2//! a tray icon's menus, and the Dock icon's menu. Not a dart:ui file: Flutter's macOS embedder
3//! takes the menu bar over the `flutter/menu` channel as a list of maps, which `inset-widgets`'
4//! `PlatformMenuBar` builds this value in place of.
5//!
6//! A menu is a value rebuilt from the app's state and handed to the host whole, each time it
7//! changes, as Flutter's `Menu.setMenus` hands it: an item's check or enabled state follows the
8//! app's state at each rebuild, and the host replaces the system's menu with the new value. One
9//! value serves every place a menu shows. A choice comes back by the id the app gave the item:
10//! the menu bar's through [`EmbedderClient::menu_selected`](crate::EmbedderClient::menu_selected),
11//! the Dock menu's through
12//! [`EmbedderClient::dock_menu_selected`](crate::EmbedderClient::dock_menu_selected), a tray
13//! icon's attached menu's as [`TrayIconEvent::MenuSelected`], and a menu the app opens itself
14//! through the future it is answered.
15//!
16//! Each place is the system's own: the platform answers `None` for a place its system does not
17//! provide, and the app or the framework draws its own there; a host never stands in for a
18//! system. A [`ProvidedItem`] is an item the system itself provides, as AppKit's standard menu
19//! items are; an app's own menu bar replaces the host's standard one whole, so a provided item is
20//! the default, never a lock.
21
22use std::fmt;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::Arc;
26
27use crate::{KeyData, Offset, ViewId};
28
29/// The id the app gives a menu's entry, which a choice comes back by.
30pub type MenuId = u32;
31
32/// One entry of a menu.
33#[derive(Clone, Debug, PartialEq)]
34pub enum MenuEntry {
35    /// A menu of its own, opened from this entry: a menu bar's top-level menus are these.
36    Submenu {
37        id: MenuId,
38        label: String,
39        enabled: bool,
40        entries: Vec<MenuEntry>,
41    },
42    /// An item of the app's.
43    Item { id: MenuId, item: MenuItem },
44    /// An item the system provides, which acts as the system's own does.
45    Provided {
46        id: MenuId,
47        item: ProvidedItem,
48        enabled: bool,
49    },
50    /// A line between groups of entries.
51    Separator,
52}
53
54/// An item of the app's: what it shows, and whether it can be chosen.
55#[derive(Clone, Debug, PartialEq)]
56pub struct MenuItem {
57    pub label: String,
58    pub enabled: bool,
59    pub check: MenuItemCheck,
60    /// The keys that choose it, which the menu shows beside its label.
61    pub shortcut: Option<MenuShortcut>,
62    pub icon: Option<IconImage>,
63}
64
65impl MenuItem {
66    /// An item labelled `label` that can be chosen, with no check, shortcut or icon.
67    pub fn new(label: impl Into<String>) -> MenuItem {
68        MenuItem {
69            label: label.into(),
70            enabled: true,
71            check: MenuItemCheck::Plain,
72            shortcut: None,
73            icon: None,
74        }
75    }
76
77    pub fn enabled(mut self, enabled: bool) -> MenuItem {
78        self.enabled = enabled;
79        self
80    }
81
82    pub fn check(mut self, check: MenuItemCheck) -> MenuItem {
83        self.check = check;
84        self
85    }
86
87    pub fn shortcut(mut self, shortcut: MenuShortcut) -> MenuItem {
88        self.shortcut = Some(shortcut);
89        self
90    }
91
92    pub fn icon(mut self, icon: IconImage) -> MenuItem {
93        self.icon = Some(icon);
94        self
95    }
96}
97
98/// Whether an item shows a check, and which.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum MenuItemCheck {
101    /// No check: an action.
102    Plain,
103    /// A setting the item toggles.
104    Checkbox(CheckState),
105    /// One of a group of settings, of which one is on: `true` for the one.
106    Radio(bool),
107}
108
109/// A checkbox's state.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum CheckState {
112    Off,
113    On,
114    /// Some of what it stands for is on, some off.
115    Mixed,
116}
117
118/// The keys that choose an item: a key with modifiers held, as Flutter's `ShortcutSerialization`
119/// carries a trigger and its modifiers.
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub struct MenuShortcut {
122    /// The key, by Flutter's `LogicalKeyboardKey.keyId`: a character's Unicode value for a key that
123    /// types one.
124    pub key: u64,
125    pub modifiers: ShortcutModifiers,
126}
127
128/// The modifier keys a shortcut holds.
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
130pub struct ShortcutModifiers {
131    /// Command on Apple's systems, the Windows key elsewhere.
132    pub meta: bool,
133    pub control: bool,
134    pub alt: bool,
135    pub shift: bool,
136}
137
138/// An image a menu item or a tray icon shows: RGBA pixels, row by row.
139#[derive(Clone, PartialEq)]
140pub struct IconImage {
141    pub width: u32,
142    pub height: u32,
143    /// Four bytes a pixel, not premultiplied.
144    pub rgba: Arc<[u8]>,
145    /// Drawn by its alpha alone, in the colour the system gives the place, as macOS draws a
146    /// template image in its menu bar; a system without templates shows the colours.
147    pub template: bool,
148}
149
150impl fmt::Debug for IconImage {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("IconImage")
153            .field("width", &self.width)
154            .field("height", &self.height)
155            .field("template", &self.template)
156            .finish_non_exhaustive()
157    }
158}
159
160/// An item the system itself provides, which acts as the system's own does.
161#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
162pub enum ProvidedItem {
163    // The app's.
164    About,
165    /// The system's services, as a submenu.
166    Services,
167    Hide,
168    HideOthers,
169    ShowAll,
170    /// Quits the app, through the same request the system's own quit makes.
171    Quit,
172    // The focused window's.
173    Minimize,
174    Zoom,
175    ToggleFullScreen,
176    BringAllToFront,
177    // The focused view's edit commands, sent to it as events: `clipboard_event` for cut, copy and
178    // paste, `edit_command` for the others.
179    Undo,
180    Redo,
181    Cut,
182    Copy,
183    Paste,
184    SelectAll,
185    Delete,
186}
187
188/// An edit command of a provided item, sent to the focused view: Flutter's text editing intents
189/// for undo, redo, select all and delete.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum EditCommand {
192    Undo,
193    Redo,
194    SelectAll,
195    /// Deletes what is selected, as the delete key does.
196    Delete,
197}
198
199/// The app's menu bar: macOS's. Where the system has none, the platform answers `None` and the app
200/// draws its own.
201pub trait MenuBar {
202    /// Replaces the app's menu bar whole: its top-level menus, each a [`MenuEntry::Submenu`]. A
203    /// choice reaches the app as `EmbedderClient::menu_selected(id)`.
204    fn set_menus(&self, menus: &[MenuEntry]);
205
206    /// Offers the menu bar a key-down the app kept back when it came and has since left
207    /// unhandled, as Chrome sends on a key its page did not consume; answers whether an item took
208    /// it, which then reaches the app as `EmbedderClient::menu_selected`. For an app that answers
209    /// a key later than the host must, such as one that waits on another thread: the host's own
210    /// order, the framework first and the menu bar after, needs no call.
211    ///
212    /// `key` is the key as the host reported it (`EmbedderClient::key_data`), which the
213    /// framework's key event gives back. The host finds the system's own event by it, which
214    /// carries the keyboard layout and the modifiers, and keeps it only a short while: once that
215    /// event is gone, or the key was offered already, the answer is `false`.
216    fn perform_key_equivalent(&self, key: &KeyData) -> bool;
217}
218
219/// A menu the system opens at a point in a view, answered with the item chosen.
220pub trait PopupMenus {
221    /// Shows `menu` at `at`, in logical pixels from the top-left corner of `view`; answers the id
222    /// of the item chosen once it closes, `None` when it closed with none chosen, or the view is
223    /// gone.
224    ///
225    /// The host opens the menu after this call returns and runs the menu's own loop outside every
226    /// call into the app, so the app's frames, timers and events go on while it is open, as they
227    /// do in Flutter, whose platform calls answer later.
228    fn show(&self, view: ViewId, at: Offset, menu: &[MenuEntry]) -> PopupMenuFuture;
229}
230
231/// A menu on screen: the id of the item chosen, once it closes. Flutter's `Future<int?>` from a
232/// platform call.
233pub type PopupMenuFuture = Pin<Box<dyn Future<Output = Option<MenuId>>>>;
234
235/// The menu of the app's Dock icon: macOS's.
236pub trait DockMenu {
237    /// Replaces the menu whole; the system shows it above its own items. A choice reaches the app
238    /// as `EmbedderClient::dock_menu_selected(id)`.
239    fn set_menu(&self, menu: &[MenuEntry]);
240}
241
242/// The app's icons in the system's tray: macOS's status items, Windows' notification area,
243/// Linux's StatusNotifierItems.
244pub trait TrayIcons {
245    /// Adds an icon showing `image`, answered once the icon exists, as the system made it: the
246    /// icon is then what the app asks which menus it offers, and it is removed when dropped. A
247    /// host that makes the icon at once answers at once; one that makes it elsewhere, on another
248    /// thread or in another process, answers when it is there. What happens to the icon reaches
249    /// the app as `EmbedderClient::tray_icon_event`, by the icon's id.
250    fn add(&self, image: &IconImage, tooltip: Option<&str>) -> TrayIconFuture;
251}
252
253/// The icon a [`TrayIcons::add`] made, or why the system did not show it.
254pub type TrayIconFuture = Pin<Box<dyn Future<Output = Result<Box<dyn TrayIcon>, TrayIconError>>>>;
255
256/// Why a tray icon did not appear.
257#[derive(Clone, Debug, PartialEq, Eq)]
258pub enum TrayIconError {
259    /// The system did not show it: its tray refused the icon, or the host could not reach the
260    /// tray, as when a Linux desktop's bus refuses the item.
261    Failed(String),
262}
263
264impl fmt::Display for TrayIconError {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match self {
267            TrayIconError::Failed(reason) => write!(f, "the tray icon did not appear: {reason}"),
268        }
269    }
270}
271
272impl std::error::Error for TrayIconError {}
273
274/// The id of a tray icon, which its events come by.
275pub type TrayIconId = u64;
276
277/// An icon in the system's tray, removed when dropped. What it offers is what its system allows;
278/// its conventions, such as a menu opened on a right click, are the app's or the framework's.
279pub trait TrayIcon {
280    fn id(&self) -> TrayIconId;
281
282    fn set_image(&self, image: &IconImage);
283
284    fn set_tooltip(&self, tooltip: Option<&str>);
285
286    /// A menu the system opens by itself, when it chooses: macOS on a click, Linux when the
287    /// desktop decides. `None` where the system opens none, as on Windows.
288    fn attached_menu(&self) -> Option<&dyn AttachedMenu>;
289
290    /// A menu the app opens at the icon, now: macOS and Windows. `None` where the system lets an
291    /// app open none, as on Linux.
292    fn menu_at_icon(&self) -> Option<&dyn MenuAtIcon>;
293}
294
295/// A tray icon's menu the system opens by itself.
296pub trait AttachedMenu {
297    /// Replaces the menu whole; `[]` takes it away. A choice reaches the app as
298    /// [`TrayIconEvent::MenuSelected`].
299    fn set_menu(&self, menu: &[MenuEntry]);
300}
301
302/// A tray icon's menu the app opens.
303pub trait MenuAtIcon {
304    /// Shows `menu` at the icon; answers the id of the item chosen once it closes.
305    fn show(&self, menu: &[MenuEntry]) -> PopupMenuFuture;
306}
307
308/// What happened to a tray icon, as its system reports it: each system reports some of these.
309/// Positions are in the coordinates of [`HostWindow::frame`](crate::HostWindow::frame) on macOS
310/// and Windows; a Linux desktop gives its own, which on Wayland are often zero.
311#[derive(Clone, Copy, Debug, PartialEq)]
312pub enum TrayIconEvent {
313    /// The icon was clicked, or chosen from the keyboard.
314    Activated {
315        position: Offset,
316    },
317    /// The user asked for the icon's menu: Windows' right click, or its keyboard's menu key.
318    ContextMenuRequested {
319        position: Offset,
320    },
321    /// The icon was clicked with the middle button, where the desktop says so.
322    SecondaryActivated {
323        position: Offset,
324    },
325    DoubleClicked,
326    /// The wheel turned over the icon, by its steps.
327    Scrolled {
328        delta: f64,
329    },
330    /// An item of the icon's attached menu was chosen.
331    MenuSelected(MenuId),
332}