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(>k_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(>k_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: >k::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}