muda_win/
lib.rs

1#![allow(clippy::uninlined_format_args)]
2
3//! muda-win is a Menu Utilities library for Desktop Applications on Windows.
4//!
5//!
6//! # Notes:
7//!
8//! - Accelerators don't work unless the win32 message loop calls
9//!   [`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html).
10//!   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
11//!
12//! # Example
13//!
14//! Create the menu and add your items
15//!
16//! ```no_run
17//! # use muda_win::{Menu, Submenu, MenuItem, accelerator::{Code, Modifiers, Accelerator}, PredefinedMenuItem};
18//! let menu = Menu::new();
19//! let menu_item2 = MenuItem::new("Menu item #2", false, None);
20//! let submenu = Submenu::with_items(
21//!     "Submenu Outer",
22//!     true,
23//!     &[
24//!         &MenuItem::new(
25//!             "Menu item #1",
26//!             true,
27//!             Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)),
28//!         ),
29//!         &PredefinedMenuItem::separator(),
30//!         &menu_item2,
31//!         &MenuItem::new("Menu item #3", true, None),
32//!         &PredefinedMenuItem::separator(),
33//!         &Submenu::with_items(
34//!             "Submenu Inner",
35//!             true,
36//!             &[
37//!                 &MenuItem::new("Submenu item #1", true, None),
38//!                 &PredefinedMenuItem::separator(),
39//!                 &menu_item2,
40//!             ],
41//!         ).unwrap(),
42//!     ],
43//! );
44//! ```
45//!
46//! Then add your root menu to a Window on Windows and Linux
47//! or use it as your global app menu on macOS
48//!
49//! ```no_run
50//! # let menu = muda_win::Menu::new();
51//! # let window_hwnd = 0;
52//! // --snip--
53//! unsafe { menu.init_for_hwnd(window_hwnd) };
54//! ```
55//!
56//! # Context menus (Popup menus)
57//!
58//! You can also use a [`Menu`] or a [`Submenu`] show a context menu.
59//!
60//! ```no_run
61//! use muda_win::ContextMenu;
62//! # let menu = muda_win::Menu::new();
63//! # let window_hwnd = 0;
64//! // --snip--
65//! let position = muda_win::dpi::PhysicalPosition { x: 100., y: 120. };
66//! unsafe { menu.show_context_menu_for_hwnd(window_hwnd, Some(position.into())) };
67//! ```
68//! # Processing menu events
69//!
70//! You can use [`MenuEvent::receiver`] to get a reference to the [`MenuEventReceiver`]
71//! which you can use to listen to events when a menu item is activated
72//! ```no_run
73//! # use muda_win::MenuEvent;
74//! #
75//! # let save_item: muda_win::MenuItem = unsafe { std::mem::zeroed() };
76//! if let Ok(event) = MenuEvent::receiver().try_recv() {
77//!     match event.id {
78//!         id if id == save_item.id() => {
79//!             println!("Save menu item activated");
80//!         },
81//!         _ => {}
82//!     }
83//! }
84//! ```
85//!
86//! ### Note for [winit] or [tao] users:
87//!
88//! You should use [`MenuEvent::set_event_handler`] and forward
89//! the menu events to the event loop by using [`EventLoopProxy`]
90//! so that the event loop is awakened on each menu event.
91//!
92//! ```no_run
93//! # use winit::event_loop::EventLoop;
94//! enum UserEvent {
95//!   MenuEvent(muda_win::MenuEvent)
96//! }
97//!
98//! let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
99//!
100//! let proxy = event_loop.create_proxy();
101//! muda_win::MenuEvent::set_event_handler(Some(move |event| {
102//!     proxy.send_event(UserEvent::MenuEvent(event));
103//! }));
104//! ```
105//!
106//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
107//! [winit]: https://docs.rs/winit
108//! [tao]: https://docs.rs/tao
109
110use crossbeam_channel::{unbounded, Receiver, Sender};
111use std::sync::{LazyLock, OnceLock};
112
113pub mod about_metadata;
114pub mod accelerator;
115mod builders;
116mod error;
117mod icon;
118mod items;
119mod menu;
120mod menu_id;
121mod platform_impl;
122mod util;
123
124pub use about_metadata::AboutMetadata;
125pub use builders::*;
126pub use dpi;
127pub use error::*;
128pub use icon::{BadIcon, Icon, NativeIcon};
129pub use items::*;
130pub use menu::*;
131pub use menu_id::MenuId;
132
133/// An enumeration of all available menu types, useful to match against
134/// the items returned from [`Menu::items`] or [`Submenu::items`]
135#[derive(Clone)]
136pub enum MenuItemKind {
137    MenuItem(MenuItem),
138    Submenu(Submenu),
139    Predefined(PredefinedMenuItem),
140    Check(CheckMenuItem),
141    Icon(IconMenuItem),
142}
143
144impl MenuItemKind {
145    /// Returns a unique identifier associated with this menu item.
146    pub fn id(&self) -> &MenuId {
147        match self {
148            MenuItemKind::MenuItem(i) => i.id(),
149            MenuItemKind::Submenu(i) => i.id(),
150            MenuItemKind::Predefined(i) => i.id(),
151            MenuItemKind::Check(i) => i.id(),
152            MenuItemKind::Icon(i) => i.id(),
153        }
154    }
155
156    /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't.
157    pub fn as_menuitem(&self) -> Option<&MenuItem> {
158        match self {
159            MenuItemKind::MenuItem(i) => Some(i),
160            _ => None,
161        }
162    }
163
164    /// Casts this item to a [`MenuItem`], and panics if it wasn't.
165    pub fn as_menuitem_unchecked(&self) -> &MenuItem {
166        match self {
167            MenuItemKind::MenuItem(i) => i,
168            _ => panic!("Not a MenuItem"),
169        }
170    }
171
172    /// Casts this item to a [`Submenu`], and returns `None` if it wasn't.
173    pub fn as_submenu(&self) -> Option<&Submenu> {
174        match self {
175            MenuItemKind::Submenu(i) => Some(i),
176            _ => None,
177        }
178    }
179
180    /// Casts this item to a [`Submenu`], and panics if it wasn't.
181    pub fn as_submenu_unchecked(&self) -> &Submenu {
182        match self {
183            MenuItemKind::Submenu(i) => i,
184            _ => panic!("Not a Submenu"),
185        }
186    }
187
188    /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't.
189    pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem> {
190        match self {
191            MenuItemKind::Predefined(i) => Some(i),
192            _ => None,
193        }
194    }
195
196    /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't.
197    pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem {
198        match self {
199            MenuItemKind::Predefined(i) => i,
200            _ => panic!("Not a PredefinedMenuItem"),
201        }
202    }
203
204    /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't.
205    pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem> {
206        match self {
207            MenuItemKind::Check(i) => Some(i),
208            _ => None,
209        }
210    }
211
212    /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't.
213    pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem {
214        match self {
215            MenuItemKind::Check(i) => i,
216            _ => panic!("Not a CheckMenuItem"),
217        }
218    }
219
220    /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't.
221    pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem> {
222        match self {
223            MenuItemKind::Icon(i) => Some(i),
224            _ => None,
225        }
226    }
227
228    /// Casts this item to a [`IconMenuItem`], and panics if it wasn't.
229    pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem {
230        match self {
231            MenuItemKind::Icon(i) => i,
232            _ => panic!("Not an IconMenuItem"),
233        }
234    }
235
236    /// Convert this item into its menu ID.
237    pub fn into_id(self) -> MenuId {
238        match self {
239            MenuItemKind::MenuItem(i) => i.into_id(),
240            MenuItemKind::Submenu(i) => i.into_id(),
241            MenuItemKind::Predefined(i) => i.into_id(),
242            MenuItemKind::Check(i) => i.into_id(),
243            MenuItemKind::Icon(i) => i.into_id(),
244        }
245    }
246}
247
248/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`]
249pub trait IsMenuItem: sealed::IsMenuItemBase {
250    /// Returns a [`MenuItemKind`] associated with this item.
251    fn kind(&self) -> MenuItemKind;
252    /// Returns a unique identifier associated with this menu item.
253    fn id(&self) -> &MenuId;
254    /// Convert this menu item into its menu ID.
255    fn into_id(self) -> MenuId;
256}
257
258mod sealed {
259    pub trait IsMenuItemBase {}
260}
261
262#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264pub(crate) enum MenuItemType {
265    MenuItem,
266    Submenu,
267    Predefined,
268    Check,
269    Icon,
270}
271
272impl Default for MenuItemType {
273    fn default() -> Self {
274        Self::MenuItem
275    }
276}
277
278/// A helper trait with methods to help creating a context menu.
279pub trait ContextMenu {
280    /// Get the popup [`HMENU`] for this menu.
281    ///
282    /// The returned [`HMENU`] is valid as long as the `ContextMenu` is.
283    ///
284    /// [`HMENU`]: windows_sys::Win32::UI::WindowsAndMessaging::HMENU
285    fn hpopupmenu(&self) -> isize;
286
287    /// Shows this menu as a context menu inside a win32 window.
288    ///
289    /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
290    ///
291    /// Returns `true` if menu tracking ended because an item was selected, and `false` if menu tracking was cancelled for any reason.
292    ///
293    /// # Safety
294    ///
295    /// The `hwnd` must be a valid window HWND.
296    unsafe fn show_context_menu_for_hwnd(
297        &self,
298        hwnd: isize,
299        position: Option<dpi::Position>,
300    ) -> bool;
301
302    /// Attach the menu subclass handler to the given hwnd
303    /// so you can recieve events from that window using [MenuEvent::receiver]
304    ///
305    /// This can be used along with [`ContextMenu::hpopupmenu`] when implementing a tray icon menu.
306    ///
307    /// # Safety
308    ///
309    /// The `hwnd` must be a valid window HWND.
310    unsafe fn attach_menu_subclass_for_hwnd(&self, hwnd: isize);
311
312    /// Remove the menu subclass handler from the given hwnd
313    ///
314    /// The view must be a pointer to a valid `NSView`.
315    ///
316    /// # Safety
317    ///
318    /// The `hwnd` must be a valid window HWND.
319    unsafe fn detach_menu_subclass_from_hwnd(&self, hwnd: isize);
320}
321
322/// Describes a menu event emitted when a menu item is activated
323#[derive(Debug, Clone)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub struct MenuEvent {
326    /// Id of the menu item which triggered this event
327    pub id: MenuId,
328}
329
330/// A reciever that could be used to listen to menu events.
331pub type MenuEventReceiver = Receiver<MenuEvent>;
332pub type MenuEventHandler = Box<dyn Fn(MenuEvent) + Send + Sync + 'static>;
333
334static MENU_CHANNEL: LazyLock<(Sender<MenuEvent>, MenuEventReceiver)> = LazyLock::new(unbounded);
335static MENU_EVENT_HANDLER: OnceLock<Option<MenuEventHandler>> = OnceLock::new();
336
337impl MenuEvent {
338    /// Returns the id of the menu item which triggered this event
339    pub fn id(&self) -> &MenuId {
340        &self.id
341    }
342
343    /// Gets a reference to the event channel's [`MenuEventReceiver`]
344    /// which can be used to listen for menu events.
345    ///
346    /// ## Note
347    ///
348    /// This will not receive any events if [`MenuEvent::set_event_handler`] has been called with a `Some` value.
349    pub fn receiver<'a>() -> &'a MenuEventReceiver {
350        &MENU_CHANNEL.1
351    }
352
353    /// Set a handler to be called for new events. Useful for implementing custom event sender.
354    ///
355    /// ## Note
356    ///
357    /// Calling this function with a `Some` value,
358    /// will not send new events to the channel associated with [`MenuEvent::receiver`]
359    pub fn set_event_handler<F: Fn(MenuEvent) + Send + Sync + 'static>(
360        f: Option<F>,
361    ) -> Option<Option<MenuEventHandler>> {
362        if let Some(f) = f {
363            // Wrap the closure in a Box to store on the heap
364            let boxed_handler = Box::new(f);
365            let _ = MENU_EVENT_HANDLER.set(Some(boxed_handler));
366        } else {
367            let _ = MENU_EVENT_HANDLER.set(None);
368        }
369
370        // Dereference and return the Box
371        match MENU_EVENT_HANDLER.get() {
372            Some(Some(handler)) => Some(Some(Box::new(handler))),
373            _ => None,
374        }
375    }
376
377    pub(crate) fn send(event: MenuEvent) {
378        if let Some(handler) = MENU_EVENT_HANDLER.get_or_init(|| None) {
379            handler(event);
380        } else {
381            let _ = MENU_CHANNEL.0.send(event);
382        }
383    }
384}