2022-12-09 05:50:24 +11:00
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
2023-02-09 06:00:29 +11:00
#![ allow(clippy::uninlined_format_args) ]
2022-05-09 03:36:39 +10:00
//! muda is a Menu Utilities library for Desktop Applications.
2022-05-07 06:57:58 +10:00
//!
2023-02-09 06:00:29 +11:00
//! # Platforms supported:
//!
//! - Windows
//! - macOS
//! - Linux (gtk Only)
//!
//! # Platform-specific notes:
//!
//! - On Windows, accelerators don't work unless the win32 message loop calls
//! [`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html).
//! See [`Menu::init_for_hwnd`](https://docs.rs/muda/latest/muda/struct.Menu.html#method.init_for_hwnd) for more details
//!
//! # Dependencies (Linux Only)
//!
//! `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:
//!
//! #### Arch Linux / Manjaro:
//!
//! ```sh
//! pacman -S gtk3 xdotool
//! ```
//!
//! #### Debian / Ubuntu:
//!
//! ```sh
//! sudo apt install libgtk-3-dev libxdo-dev
//! ```
//!
2022-11-24 03:29:52 +11:00
//! # Example
2022-05-07 06:57:58 +10:00
//!
2022-11-24 03:29:52 +11:00
//! Create the menu and add your items
2022-05-07 06:57:58 +10:00
//!
//! ```no_run
2022-11-24 03:29:52 +11:00
//! # use muda::{Menu, Submenu, MenuItem, accelerator::{Code, Modifiers, Accelerator}, PredefinedMenuItem};
//! let menu = Menu::new();
//! let menu_item2 = MenuItem::new("Menu item #2", false, None);
//! let submenu = Submenu::with_items(
//! "Submenu Outer",
//! true,
//! &[
//! &MenuItem::new(
//! "Menu item #1",
//! true,
//! Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)),
//! ),
//! &PredefinedMenuItem::separator(),
//! &menu_item2,
//! &MenuItem::new("Menu item #3", true, None),
//! &PredefinedMenuItem::separator(),
//! &Submenu::with_items(
//! "Submenu Inner",
//! true,
//! &[
//! &MenuItem::new("Submenu item #1", true, None),
//! &PredefinedMenuItem::separator(),
//! &menu_item2,
//! ],
2023-07-18 10:44:52 +10:00
//! ).unwrap(),
2022-11-24 03:29:52 +11:00
//! ],
//! );
2022-05-07 06:57:58 +10:00
//! ```
//!
2023-07-28 11:38:38 +10:00
//! Then add your root menu to a Window on Windows and Linux
//! or use it as your global app menu on macOS
2022-05-07 06:57:58 +10:00
//!
//! ```no_run
2022-11-24 03:29:52 +11:00
//! # let menu = muda::Menu::new();
//! # let window_hwnd = 0;
//! # #[cfg(target_os = "linux")]
//! # let gtk_window = gtk::ApplicationWindow::builder().build();
2023-07-26 05:01:35 +10:00
//! # #[cfg(target_os = "linux")]
//! # let vertical_gtk_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
2022-05-07 06:57:58 +10:00
//! // --snip--
//! #[cfg(target_os = "windows")]
2022-11-24 03:29:52 +11:00
//! menu.init_for_hwnd(window_hwnd);
2022-05-07 06:57:58 +10:00
//! #[cfg(target_os = "linux")]
2023-07-26 05:01:35 +10:00
//! menu.init_for_gtk_window(>k_window, Some(&vertical_gtk_box));
2022-05-08 16:32:31 +10:00
//! #[cfg(target_os = "macos")]
//! menu.init_for_nsapp();
2022-05-07 06:57:58 +10:00
//! ```
//!
2022-11-24 03:29:52 +11:00
//! # Context menus (Popup menus)
//!
//! You can also use a [`Menu`] or a [`Submenu`] show a context menu.
//!
//! ```no_run
//! use muda::ContextMenu;
//! # let menu = muda::Menu::new();
//! # let window_hwnd = 0;
//! # #[cfg(target_os = "linux")]
//! # let gtk_window = gtk::ApplicationWindow::builder().build();
//! # #[cfg(target_os = "macos")]
//! # let nsview = 0 as *mut objc::runtime::Object;
//! // --snip--
2023-07-27 21:56:01 +10:00
//! let position = muda::PhysicalPosition { x: 100., y: 120. };
2022-11-24 03:29:52 +11:00
//! #[cfg(target_os = "windows")]
2023-07-27 21:56:01 +10:00
//! menu.show_context_menu_for_hwnd(window_hwnd, Some(position.into()));
2022-11-24 03:29:52 +11:00
//! #[cfg(target_os = "linux")]
2023-07-27 21:56:01 +10:00
//! menu.show_context_menu_for_gtk_window(>k_window, Some(position.into()));
2022-11-24 03:29:52 +11:00
//! #[cfg(target_os = "macos")]
2023-07-27 21:56:01 +10:00
//! menu.show_context_menu_for_nsview(nsview, Some(position.into()));
2022-11-24 03:29:52 +11:00
//! ```
2022-05-07 06:57:58 +10:00
//! # Processing menu events
//!
2023-01-03 13:07:07 +11:00
//! You can use [`MenuEvent::receiver`] to get a reference to the [`MenuEventReceiver`]
2022-05-07 06:57:58 +10:00
//! which you can use to listen to events when a menu item is activated
2022-11-24 03:29:52 +11:00
//! ```no_run
2023-01-03 13:07:07 +11:00
//! # use muda::MenuEvent;
2022-11-24 03:29:52 +11:00
//! #
//! # let save_item: muda::MenuItem = unsafe { std::mem::zeroed() };
2023-01-03 13:07:07 +11:00
//! if let Ok(event) = MenuEvent::receiver().try_recv() {
2022-05-07 06:57:58 +10:00
//! match event.id {
2022-11-24 03:29:52 +11:00
//! id if id == save_item.id() => {
2022-05-07 06:57:58 +10:00
//! println!("Save menu item activated");
//! },
//! _ => {}
//! }
//! }
//! ```
2023-08-05 05:17:58 +10:00
use std ::{ convert ::Infallible , str ::FromStr } ;
2022-05-07 00:25:45 +10:00
use crossbeam_channel ::{ unbounded , Receiver , Sender } ;
2023-01-03 13:07:07 +11:00
use once_cell ::sync ::{ Lazy , OnceCell } ;
2022-05-07 00:25:45 +10:00
2023-07-18 11:59:47 +10:00
mod about_metadata ;
2022-07-20 22:34:09 +10:00
pub mod accelerator ;
2023-08-05 05:17:58 +10:00
mod builders ;
2023-07-27 21:56:01 +10:00
mod dpi ;
2022-11-24 03:29:52 +11:00
mod error ;
2023-08-05 05:17:58 +10:00
mod icon ;
2023-07-18 10:44:52 +10:00
mod items ;
2022-11-24 03:29:52 +11:00
mod menu ;
2022-05-05 21:50:22 +10:00
mod platform_impl ;
2022-11-24 03:29:52 +11:00
mod util ;
#[ cfg(target_os = " macos " ) ]
#[ macro_use ]
extern crate objc ;
2023-07-18 11:59:47 +10:00
pub use about_metadata ::AboutMetadata ;
2023-08-05 05:17:58 +10:00
pub use builders ::* ;
2023-07-27 21:56:01 +10:00
pub use dpi ::* ;
2023-07-18 11:59:47 +10:00
pub use error ::* ;
2023-08-05 05:17:58 +10:00
pub use icon ::{ BadIcon , Icon , NativeIcon } ;
2023-07-18 10:44:52 +10:00
pub use items ::* ;
2022-11-24 03:29:52 +11:00
pub use menu ::Menu ;
2023-08-05 05:17:58 +10:00
/// An unique id that is associated with a menu item.
#[ derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash) ]
2023-08-08 23:39:27 +10:00
pub struct MenuId ( pub String ) ;
2023-08-05 05:17:58 +10:00
impl AsRef < str > for MenuId {
fn as_ref ( & self ) -> & str {
self . 0. as_ref ( )
}
}
impl From < String > for MenuId {
fn from ( value : String ) -> Self {
Self ( value )
}
}
impl From < & str > for MenuId {
fn from ( value : & str ) -> Self {
Self ( value . to_string ( ) )
}
}
impl PartialEq < & str > for MenuId {
fn eq ( & self , other : & & str ) -> bool {
other = = & self . 0
}
}
impl PartialEq < String > for MenuId {
fn eq ( & self , other : & String ) -> bool {
other = = & self . 0
}
}
impl PartialEq < & MenuId > for MenuId {
fn eq ( & self , other : & & MenuId ) -> bool {
other . 0 = = self . 0
}
}
impl FromStr for MenuId {
type Err = Infallible ;
fn from_str ( s : & str ) -> std ::result ::Result < Self , Self ::Err > {
Ok ( Self ( s . to_string ( ) ) )
}
}
2022-11-24 03:29:52 +11:00
2023-07-18 10:44:52 +10:00
/// An enumeration of all available menu types, useful to match against
2023-08-05 05:17:58 +10:00
/// the items returned from [`Menu::items`] or [`Submenu::items`]
2023-07-28 02:58:51 +10:00
#[ derive(Clone) ]
pub enum MenuItemKind {
MenuItem ( MenuItem ) ,
Submenu ( Submenu ) ,
Predefined ( PredefinedMenuItem ) ,
Check ( CheckMenuItem ) ,
Icon ( IconMenuItem ) ,
2022-05-07 00:25:45 +10:00
}
2023-07-28 02:58:51 +10:00
impl MenuItemKind {
/// Casts this item to a [`MenuItem`], and returns `None` if it wasn't.
pub fn as_menuitem ( & self ) -> Option < & MenuItem > {
match self {
MenuItemKind ::MenuItem ( i ) = > Some ( i ) ,
_ = > None ,
}
}
2022-11-24 03:29:52 +11:00
2023-07-28 02:58:51 +10:00
/// Casts this item to a [`MenuItem`], and panics if it wasn't.
pub fn as_menuitem_unchecked ( & self ) -> & MenuItem {
match self {
MenuItemKind ::MenuItem ( i ) = > i ,
_ = > panic! ( " Not a MenuItem " ) ,
}
}
2023-07-18 11:00:25 +10:00
/// Casts this item to a [`Submenu`], and returns `None` if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_submenu ( & self ) -> Option < & Submenu > {
match self {
MenuItemKind ::Submenu ( i ) = > Some ( i ) ,
_ = > None ,
}
2023-07-18 11:00:25 +10:00
}
/// Casts this item to a [`Submenu`], and panics if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_submenu_unchecked ( & self ) -> & Submenu {
match self {
MenuItemKind ::Submenu ( i ) = > i ,
_ = > panic! ( " Not a Submenu " ) ,
}
2023-07-18 11:00:25 +10:00
}
2023-07-28 02:58:51 +10:00
/// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't.
pub fn as_predefined_menuitem ( & self ) -> Option < & PredefinedMenuItem > {
match self {
MenuItemKind ::Predefined ( i ) = > Some ( i ) ,
_ = > None ,
}
2023-07-18 11:00:25 +10:00
}
2023-07-28 02:58:51 +10:00
/// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't.
pub fn as_predefined_menuitem_unchecked ( & self ) -> & PredefinedMenuItem {
match self {
MenuItemKind ::Predefined ( i ) = > i ,
_ = > panic! ( " Not a PredefinedMenuItem " ) ,
}
2023-07-18 11:00:25 +10:00
}
/// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_check_menuitem ( & self ) -> Option < & CheckMenuItem > {
match self {
MenuItemKind ::Check ( i ) = > Some ( i ) ,
_ = > None ,
}
2023-07-18 11:00:25 +10:00
}
/// Casts this item to a [`CheckMenuItem`], and panics if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_check_menuitem_unchecked ( & self ) -> & CheckMenuItem {
match self {
MenuItemKind ::Check ( i ) = > i ,
_ = > panic! ( " Not a CheckMenuItem " ) ,
}
2023-07-18 11:00:25 +10:00
}
/// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_icon_menuitem ( & self ) -> Option < & IconMenuItem > {
match self {
MenuItemKind ::Icon ( i ) = > Some ( i ) ,
_ = > None ,
}
2023-07-18 11:00:25 +10:00
}
/// Casts this item to a [`IconMenuItem`], and panics if it wasn't.
2023-07-28 02:58:51 +10:00
pub fn as_icon_menuitem_unchecked ( & self ) -> & IconMenuItem {
match self {
MenuItemKind ::Icon ( i ) = > i ,
_ = > panic! ( " Not an IconMenuItem " ) ,
}
}
}
/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`]
///
/// # Safety
///
/// This trait is ONLY meant to be implemented internally by the crate.
pub unsafe trait IsMenuItem {
fn kind ( & self ) -> MenuItemKind ;
2023-08-05 05:17:58 +10:00
fn id ( & self ) -> & MenuId ;
2023-07-28 02:58:51 +10:00
}
#[ derive(Debug, PartialEq, PartialOrd, Clone, Copy) ]
pub ( crate ) enum MenuItemType {
MenuItem ,
Submenu ,
Predefined ,
Check ,
Icon ,
}
impl Default for MenuItemType {
fn default ( ) -> Self {
Self ::MenuItem
2023-07-18 11:00:25 +10:00
}
2022-11-24 03:29:52 +11:00
}
2022-05-05 21:50:22 +10:00
2023-07-28 02:58:51 +10:00
/// A helper trait with methods to help creating a context menu.
2022-11-24 03:29:52 +11:00
pub trait ContextMenu {
/// Get the popup [`HMENU`] for this menu.
2022-06-08 02:32:10 +10:00
///
2022-11-24 03:29:52 +11:00
/// [`HMENU`]: windows_sys::Win32::UI::WindowsAndMessaging::HMENU
#[ cfg(target_os = " windows " ) ]
fn hpopupmenu ( & self ) -> windows_sys ::Win32 ::UI ::WindowsAndMessaging ::HMENU ;
2022-05-07 02:38:39 +10:00
2022-11-24 03:29:52 +11:00
/// Shows this menu as a context menu inside a win32 window.
2022-06-07 21:05:20 +10:00
///
2023-07-27 21:56:01 +10:00
/// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
2022-05-07 02:38:39 +10:00
#[ cfg(target_os = " windows " ) ]
2023-07-27 21:56:01 +10:00
fn show_context_menu_for_hwnd ( & self , hwnd : isize , position : Option < Position > ) ;
2022-05-08 16:32:31 +10:00
2022-11-24 03:29:52 +11:00
/// Attach the menu subclass handler to the given hwnd
2023-01-03 13:07:07 +11:00
/// so you can recieve events from that window using [MenuEvent::receiver]
2022-11-24 03:29:52 +11:00
///
/// This can be used along with [`ContextMenu::hpopupmenu`] when implementing a tray icon menu.
2022-06-07 21:05:20 +10:00
#[ cfg(target_os = " windows " ) ]
2022-11-24 03:29:52 +11:00
fn attach_menu_subclass_for_hwnd ( & self , hwnd : isize ) ;
2022-06-07 21:05:20 +10:00
2022-11-24 03:29:52 +11:00
/// Remove the menu subclass handler from the given hwnd
2022-06-08 02:32:10 +10:00
#[ cfg(target_os = " windows " ) ]
2022-11-24 03:29:52 +11:00
fn detach_menu_subclass_from_hwnd ( & self , hwnd : isize ) ;
2022-06-08 02:32:10 +10:00
2022-11-24 03:29:52 +11:00
/// Shows this menu as a context menu inside a [`gtk::ApplicationWindow`]
///
2023-07-27 21:56:01 +10:00
/// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
2022-06-08 02:32:10 +10:00
#[ cfg(target_os = " linux " ) ]
2023-07-27 21:56:01 +10:00
fn show_context_menu_for_gtk_window (
& self ,
w : & gtk ::ApplicationWindow ,
position : Option < Position > ,
) ;
2023-07-26 05:01:35 +10:00
2022-11-24 03:29:52 +11:00
/// Get the underlying gtk menu reserved for context menus.
2022-06-08 02:32:10 +10:00
#[ cfg(target_os = " linux " ) ]
2022-11-24 03:29:52 +11:00
fn gtk_context_menu ( & self ) -> gtk ::Menu ;
2022-05-07 01:18:34 +10:00
2022-11-24 03:29:52 +11:00
/// Shows this menu as a context menu for the specified `NSView`.
2022-06-07 21:05:20 +10:00
///
2023-07-27 21:56:01 +10:00
/// - `position` is relative to the window top-left corner, if `None`, the cursor position is used.
2022-11-24 03:29:52 +11:00
#[ cfg(target_os = " macos " ) ]
2023-07-27 21:56:01 +10:00
fn show_context_menu_for_nsview ( & self , view : cocoa ::base ::id , position : Option < Position > ) ;
2022-11-25 03:07:11 +11:00
2023-07-26 05:01:35 +10:00
/// Get the underlying NSMenu reserved for context menus.
2022-11-25 03:07:11 +11:00
#[ cfg(target_os = " macos " ) ]
fn ns_menu ( & self ) -> * mut std ::ffi ::c_void ;
2022-05-05 21:50:22 +10:00
}
2022-11-24 03:29:52 +11:00
/// Describes a menu event emitted when a menu item is activated
2023-08-05 05:17:58 +10:00
#[ derive(Debug, Clone) ]
2022-11-24 03:29:52 +11:00
pub struct MenuEvent {
/// Id of the menu item which triggered this event
2023-08-05 05:17:58 +10:00
pub id : MenuId ,
2022-05-05 21:50:22 +10:00
}
2022-06-10 22:09:56 +10:00
2022-11-24 03:29:52 +11:00
/// A reciever that could be used to listen to menu events.
pub type MenuEventReceiver = Receiver < MenuEvent > ;
2023-01-03 13:07:07 +11:00
type MenuEventHandler = Box < dyn Fn ( MenuEvent ) + Send + Sync + 'static > ;
2022-06-12 23:42:50 +10:00
2022-11-24 03:29:52 +11:00
static MENU_CHANNEL : Lazy < ( Sender < MenuEvent > , MenuEventReceiver ) > = Lazy ::new ( unbounded ) ;
2023-01-03 13:07:07 +11:00
static MENU_EVENT_HANDLER : OnceCell < Option < MenuEventHandler > > = OnceCell ::new ( ) ;
2022-06-12 23:42:50 +10:00
2023-01-03 13:07:07 +11:00
impl MenuEvent {
2023-07-18 11:00:25 +10:00
/// Returns the id of the menu item which triggered this event
2023-08-05 05:17:58 +10:00
pub fn id ( & self ) -> & MenuId {
& self . id
2023-07-18 11:00:25 +10:00
}
2023-01-03 13:07:07 +11:00
/// Gets a reference to the event channel's [`MenuEventReceiver`]
/// which can be used to listen for menu events.
///
/// ## Note
///
/// This will not receive any events if [`MenuEvent::set_event_handler`] has been called with a `Some` value.
pub fn receiver < ' a > ( ) -> & ' a MenuEventReceiver {
& MENU_CHANNEL . 1
}
/// Set a handler to be called for new events. Useful for implementing custom event sender.
///
/// ## Note
///
/// Calling this function with a `Some` value,
/// will not send new events to the channel associated with [`MenuEvent::receiver`]
pub fn set_event_handler < F : Fn ( MenuEvent ) + Send + Sync + 'static > ( f : Option < F > ) {
if let Some ( f ) = f {
let _ = MENU_EVENT_HANDLER . set ( Some ( Box ::new ( f ) ) ) ;
} else {
let _ = MENU_EVENT_HANDLER . set ( None ) ;
}
}
pub ( crate ) fn send ( event : MenuEvent ) {
if let Some ( handler ) = MENU_EVENT_HANDLER . get_or_init ( | | None ) {
handler ( event ) ;
} else {
let _ = MENU_CHANNEL . 0. send ( event ) ;
}
}
2022-06-10 22:09:56 +10:00
}