muda/
lib.rs

1// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5#![allow(clippy::uninlined_format_args)]
6
7//! muda is a Menu Utilities library for Desktop Applications.
8//!
9//! # Platforms supported:
10//!
11//! - Windows
12//! - macOS
13//! - Linux (gtk Only)
14//!
15//! # Platform-specific notes:
16//!
17//! - On macOS, menus can only be used from the main thread, and most
18//!   functionality will panic if you try to use it from any other thread.
19//!
20//! - On Windows, accelerators don't work unless the win32 message loop calls
21//!   [`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html).
22//!   See [`Menu::init_for_hwnd`](https://docs.rs/muda/latest/x86_64-pc-windows-msvc/muda/struct.Menu.html#method.init_for_hwnd) for more details
23//!
24//! # Dependencies (Linux Only)
25//!
26//! `gtk` is used for menus and `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work. Be sure to install following packages before building:
27//!
28//! #### Arch Linux / Manjaro:
29//!
30//! ```sh
31//! pacman -S gtk3 xdotool
32//! ```
33//!
34//! #### Debian / Ubuntu:
35//!
36//! ```sh
37//! sudo apt install libgtk-3-dev libxdo-dev
38//! ```
39//!
40//! # Example
41//!
42//! Create the menu and add your items
43//!
44//! ```no_run
45//! # use muda::{Menu, Submenu, MenuItem, accelerator::{Code, Modifiers, Accelerator}, PredefinedMenuItem};
46//! let menu = Menu::new();
47//! let menu_item2 = MenuItem::new("Menu item #2", false, None);
48//! let submenu = Submenu::with_items(
49//!     "Submenu Outer",
50//!     true,
51//!     &[
52//!         &MenuItem::new(
53//!             "Menu item #1",
54//!             true,
55//!             Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)),
56//!         ),
57//!         &PredefinedMenuItem::separator(),
58//!         &menu_item2,
59//!         &MenuItem::new("Menu item #3", true, None),
60//!         &PredefinedMenuItem::separator(),
61//!         &Submenu::with_items(
62//!             "Submenu Inner",
63//!             true,
64//!             &[
65//!                 &MenuItem::new("Submenu item #1", true, None),
66//!                 &PredefinedMenuItem::separator(),
67//!                 &menu_item2,
68//!             ],
69//!         ).unwrap(),
70//!     ],
71//! );
72//! ```
73//!
74//! Then add your root menu to a Window on Windows and Linux
75//! or use it as your global app menu on macOS
76//!
77//! ```no_run
78//! # let menu = muda::Menu::new();
79//! # let window_hwnd = 0;
80//! # #[cfg(target_os = "linux")]
81//! # let gtk_window = gtk::Window::builder().build();
82//! # #[cfg(target_os = "linux")]
83//! # let vertical_gtk_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
84//! // --snip--
85//! #[cfg(target_os = "windows")]
86//! unsafe { menu.init_for_hwnd(window_hwnd) };
87//! #[cfg(target_os = "linux")]
88//! menu.init_for_gtk_window(&gtk_window, Some(&vertical_gtk_box));
89//! #[cfg(target_os = "macos")]
90//! menu.init_for_nsapp();
91//! ```
92//!
93//! # Context menus (Popup menus)
94//!
95//! You can also use a [`Menu`] or a [`Submenu`] show a context menu.
96//!
97//! ```no_run
98//! use muda::ContextMenu;
99//! # let menu = muda::Menu::new();
100//! # let window_hwnd = 0;
101//! # #[cfg(target_os = "linux")]
102//! # let gtk_window = gtk::Window::builder().build();
103//! # #[cfg(target_os = "macos")]
104//! # let nsview = std::ptr::null();
105//! // --snip--
106//! let position = muda::dpi::PhysicalPosition { x: 100., y: 120. };
107//! #[cfg(target_os = "windows")]
108//! unsafe { menu.show_context_menu_for_hwnd(window_hwnd, Some(position.into())) };
109//! #[cfg(target_os = "linux")]
110//! menu.show_context_menu_for_gtk_window(&gtk_window, Some(position.into()));
111//! #[cfg(target_os = "macos")]
112//! unsafe { menu.show_context_menu_for_nsview(nsview, Some(position.into())) };
113//! ```
114//! # Processing menu events
115//!
116//! You can use [`MenuEvent::receiver`] to get a reference to the [`MenuEventReceiver`]
117//! which you can use to listen to events when a menu item is activated
118//! ```no_run
119//! # use muda::MenuEvent;
120//! #
121//! # let save_item: muda::MenuItem = unsafe { std::mem::zeroed() };
122//! if let Ok(event) = MenuEvent::receiver().try_recv() {
123//!     match event.id {
124//!         id if id == save_item.id() => {
125//!             println!("Save menu item activated");
126//!         },
127//!         _ => {}
128//!     }
129//! }
130//! ```
131//!
132//! ### Note for [winit] or [tao] users:
133//!
134//! You should use [`MenuEvent::set_event_handler`] and forward
135//! the menu events to the event loop by using [`EventLoopProxy`]
136//! so that the event loop is awakened on each menu event.
137//!
138//! ```no_run
139//! # use tao::event_loop::EventLoopBuilder;
140//! enum UserEvent {
141//!   MenuEvent(muda::MenuEvent)
142//! }
143//!
144//! let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
145//!
146//! let proxy = event_loop.create_proxy();
147//! muda::MenuEvent::set_event_handler(Some(move |event| {
148//!     proxy.send_event(UserEvent::MenuEvent(event));
149//! }));
150//! ```
151//!
152//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
153//! [winit]: https://docs.rs/winit
154//! [tao]: https://docs.rs/tao
155
156use crossbeam_channel::{unbounded, Receiver, Sender};
157use once_cell::sync::{Lazy, OnceCell};
158
159pub mod about_metadata;
160pub mod accelerator;
161mod builders;
162mod error;
163mod icon;
164mod items;
165mod menu;
166mod menu_id;
167mod platform_impl;
168mod util;
169
170pub use about_metadata::AboutMetadata;
171pub use builders::*;
172pub use dpi;
173pub use error::*;
174pub use icon::{BadIcon, Icon, NativeIcon};
175pub use items::*;
176pub use menu::*;
177pub use menu_id::MenuId;
178
179/// An enumeration of all available menu types, useful to match against
180/// the items returned from [`Menu::items`] or [`Submenu::items`]
181#[derive(Clone)]
182pub enum MenuItemKind {
183    MenuItem(MenuItem),
184    Submenu(Submenu),
185    Predefined(PredefinedMenuItem),
186    Check(CheckMenuItem),
187    Icon(IconMenuItem),
188}
189
190impl MenuItemKind {
191    /// Returns a unique identifier associated with this menu item.
192    pub fn id(&self) -> &MenuId {
193        match self {
194            MenuItemKind::MenuItem(i) => i.id(),
195            MenuItemKind::Submenu(i) => i.id(),
196            MenuItemKind::Predefined(i) => i.id(),
197            MenuItemKind::Check(i) => i.id(),
198            MenuItemKind::Icon(i) => i.id(),
199        }
200    }
201
202    /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't.
203    pub fn as_menuitem(&self) -> Option<&MenuItem> {
204        match self {
205            MenuItemKind::MenuItem(i) => Some(i),
206            _ => None,
207        }
208    }
209
210    /// Casts this item to a [`MenuItem`], and panics if it wasn't.
211    pub fn as_menuitem_unchecked(&self) -> &MenuItem {
212        match self {
213            MenuItemKind::MenuItem(i) => i,
214            _ => panic!("Not a MenuItem"),
215        }
216    }
217
218    /// Casts this item to a [`Submenu`], and returns `None` if it wasn't.
219    pub fn as_submenu(&self) -> Option<&Submenu> {
220        match self {
221            MenuItemKind::Submenu(i) => Some(i),
222            _ => None,
223        }
224    }
225
226    /// Casts this item to a [`Submenu`], and panics if it wasn't.
227    pub fn as_submenu_unchecked(&self) -> &Submenu {
228        match self {
229            MenuItemKind::Submenu(i) => i,
230            _ => panic!("Not a Submenu"),
231        }
232    }
233
234    /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't.
235    pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem> {
236        match self {
237            MenuItemKind::Predefined(i) => Some(i),
238            _ => None,
239        }
240    }
241
242    /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't.
243    pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem {
244        match self {
245            MenuItemKind::Predefined(i) => i,
246            _ => panic!("Not a PredefinedMenuItem"),
247        }
248    }
249
250    /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't.
251    pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem> {
252        match self {
253            MenuItemKind::Check(i) => Some(i),
254            _ => None,
255        }
256    }
257
258    /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't.
259    pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem {
260        match self {
261            MenuItemKind::Check(i) => i,
262            _ => panic!("Not a CheckMenuItem"),
263        }
264    }
265
266    /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't.
267    pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem> {
268        match self {
269            MenuItemKind::Icon(i) => Some(i),
270            _ => None,
271        }
272    }
273
274    /// Casts this item to a [`IconMenuItem`], and panics if it wasn't.
275    pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem {
276        match self {
277            MenuItemKind::Icon(i) => i,
278            _ => panic!("Not an IconMenuItem"),
279        }
280    }
281
282    /// Convert this item into its menu ID.
283    pub fn into_id(self) -> MenuId {
284        match self {
285            MenuItemKind::MenuItem(i) => i.into_id(),
286            MenuItemKind::Submenu(i) => i.into_id(),
287            MenuItemKind::Predefined(i) => i.into_id(),
288            MenuItemKind::Check(i) => i.into_id(),
289            MenuItemKind::Icon(i) => i.into_id(),
290        }
291    }
292}
293
294/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`]
295pub trait IsMenuItem: sealed::IsMenuItemBase {
296    /// Returns a [`MenuItemKind`] associated with this item.
297    fn kind(&self) -> MenuItemKind;
298    /// Returns a unique identifier associated with this menu item.
299    fn id(&self) -> &MenuId;
300    /// Convert this menu item into its menu ID.
301    fn into_id(self) -> MenuId;
302}
303
304mod sealed {
305    pub trait IsMenuItemBase {}
306}
307
308#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
310pub(crate) enum MenuItemType {
311    MenuItem,
312    Submenu,
313    Predefined,
314    Check,
315    Icon,
316}
317
318impl Default for MenuItemType {
319    fn default() -> Self {
320        Self::MenuItem
321    }
322}
323
324/// A helper trait with methods to help creating a context menu.
325pub trait ContextMenu {
326    /// Get the popup [`HMENU`] for this menu.
327    ///
328    /// The returned [`HMENU`] is valid as long as the `ContextMenu` is.
329    ///
330    /// [`HMENU`]: windows_sys::Win32::UI::WindowsAndMessaging::HMENU
331    #[cfg(target_os = "windows")]
332    fn hpopupmenu(&self) -> isize;
333
334    /// Shows this menu as a context menu inside a win32 window.
335    ///
336    /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
337    ///
338    /// Returns `true` if menu tracking ended because an item was selected, and `false` if menu tracking was cancelled for any reason.
339    ///
340    /// # Safety
341    ///
342    /// The `hwnd` must be a valid window HWND.
343    #[cfg(target_os = "windows")]
344    unsafe fn show_context_menu_for_hwnd(
345        &self,
346        hwnd: isize,
347        position: Option<dpi::Position>,
348    ) -> bool;
349
350    /// Attach the menu subclass handler to the given hwnd
351    /// so you can recieve events from that window using [MenuEvent::receiver]
352    ///
353    /// This can be used along with [`ContextMenu::hpopupmenu`] when implementing a tray icon menu.
354    ///
355    /// # Safety
356    ///
357    /// The `hwnd` must be a valid window HWND.
358    #[cfg(target_os = "windows")]
359    unsafe fn attach_menu_subclass_for_hwnd(&self, hwnd: isize);
360
361    /// Remove the menu subclass handler from the given hwnd
362    ///
363    /// The view must be a pointer to a valid `NSView`.
364    ///
365    /// # Safety
366    ///
367    /// The `hwnd` must be a valid window HWND.
368    #[cfg(target_os = "windows")]
369    unsafe fn detach_menu_subclass_from_hwnd(&self, hwnd: isize);
370
371    /// Shows this menu as a context menu inside a [`gtk::Window`]
372    ///
373    /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
374    ///
375    /// Returns `true` if menu tracking ended because an item was selected or clicked outside the menu to dismiss it.
376    ///
377    /// Returns `false` if menu tracking was cancelled for any reason.
378    #[cfg(all(target_os = "linux", feature = "gtk"))]
379    fn show_context_menu_for_gtk_window(
380        &self,
381        w: &gtk::Window,
382        position: Option<dpi::Position>,
383    ) -> bool;
384
385    /// Get the underlying gtk menu reserved for context menus.
386    ///
387    /// The returned [`gtk::Menu`] is valid as long as the `ContextMenu` is.
388    #[cfg(all(target_os = "linux", feature = "gtk"))]
389    fn gtk_context_menu(&self) -> gtk::Menu;
390
391    /// Shows this menu as a context menu for the specified `NSView`.
392    ///
393    /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
394    ///
395    /// Returns `true` if menu tracking ended because an item was selected, and `false` if menu tracking was cancelled for any reason.
396    ///
397    /// # Safety
398    ///
399    /// The view must be a pointer to a valid `NSView`.
400    #[cfg(target_os = "macos")]
401    unsafe fn show_context_menu_for_nsview(
402        &self,
403        view: *const std::ffi::c_void,
404        position: Option<dpi::Position>,
405    ) -> bool;
406
407    /// Get the underlying NSMenu reserved for context menus.
408    ///
409    /// The returned pointer is valid for as long as the `ContextMenu` is. If
410    /// you need it to be alive for longer, retain it.
411    #[cfg(target_os = "macos")]
412    fn ns_menu(&self) -> *mut std::ffi::c_void;
413
414    /// Cast this context menu to a [`Menu`], and returns `None` if it wasn't.
415    fn as_menu(&self) -> Option<&Menu> {
416        None
417    }
418
419    /// Casts this context menu to a [`Menu`], and panics if it wasn't.
420    fn as_menu_unchecked(&self) -> &Menu {
421        self.as_menu().expect("Not a Menu")
422    }
423
424    /// Cast this context menu to a [`Submenu`], and returns `None` if it wasn't.
425    fn as_submenu(&self) -> Option<&Submenu> {
426        None
427    }
428
429    /// Casts this context menu to a [`Submenu`], and panics if it wasn't.
430    fn as_submenu_unchecked(&self) -> &Menu {
431        self.as_menu().expect("Not a Submenu")
432    }
433}
434
435/// Describes a menu event emitted when a menu item is activated
436#[derive(Debug, Clone)]
437#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
438pub struct MenuEvent {
439    /// Id of the menu item which triggered this event
440    pub id: MenuId,
441}
442
443/// A reciever that could be used to listen to menu events.
444pub type MenuEventReceiver = Receiver<MenuEvent>;
445type MenuEventHandler = Box<dyn Fn(MenuEvent) + Send + Sync + 'static>;
446
447static MENU_CHANNEL: Lazy<(Sender<MenuEvent>, MenuEventReceiver)> = Lazy::new(unbounded);
448static MENU_EVENT_HANDLER: OnceCell<Option<MenuEventHandler>> = OnceCell::new();
449
450impl MenuEvent {
451    /// Returns the id of the menu item which triggered this event
452    pub fn id(&self) -> &MenuId {
453        &self.id
454    }
455
456    /// Gets a reference to the event channel's [`MenuEventReceiver`]
457    /// which can be used to listen for menu events.
458    ///
459    /// ## Note
460    ///
461    /// This will not receive any events if [`MenuEvent::set_event_handler`] has been called with a `Some` value.
462    pub fn receiver<'a>() -> &'a MenuEventReceiver {
463        &MENU_CHANNEL.1
464    }
465
466    /// Set a handler to be called for new events. Useful for implementing custom event sender.
467    ///
468    /// ## Note
469    ///
470    /// Calling this function with a `Some` value,
471    /// will not send new events to the channel associated with [`MenuEvent::receiver`]
472    pub fn set_event_handler<F: Fn(MenuEvent) + Send + Sync + 'static>(f: Option<F>) {
473        if let Some(f) = f {
474            let _ = MENU_EVENT_HANDLER.set(Some(Box::new(f)));
475        } else {
476            let _ = MENU_EVENT_HANDLER.set(None);
477        }
478    }
479
480    pub(crate) fn send(event: MenuEvent) {
481        if let Some(handler) = MENU_EVENT_HANDLER.get_or_init(|| None) {
482            handler(event);
483        } else {
484            let _ = MENU_CHANNEL.0.send(event);
485        }
486    }
487}