use std::{
collections::hash_map::DefaultHasher,
fmt,
hash::{Hash, Hasher},
};
pub type MenuHash = u16;
pub type MenuId = String;
pub type MenuIdRef<'a> = &'a str;
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
#[derive(Debug, Clone)]
pub enum NativeImage {
Add,
Advanced,
Bluetooth,
Bookmarks,
Caution,
ColorPanel,
ColumnView,
Computer,
EnterFullScreen,
Everyone,
ExitFullScreen,
FlowView,
Folder,
FolderBurnable,
FolderSmart,
FollowLinkFreestanding,
FontPanel,
GoLeft,
GoRight,
Home,
IChatTheater,
IconView,
Info,
InvalidDataFreestanding,
LeftFacingTriangle,
ListView,
LockLocked,
LockUnlocked,
MenuMixedState,
MenuOnState,
MobileMe,
MultipleDocuments,
Network,
Path,
PreferencesGeneral,
QuickLook,
RefreshFreestanding,
Refresh,
Remove,
RevealFreestanding,
RightFacingTriangle,
Share,
Slideshow,
SmartBadge,
StatusAvailable,
StatusNone,
StatusPartiallyAvailable,
StatusUnavailable,
StopProgressFreestanding,
StopProgress,
TrashEmpty,
TrashFull,
User,
UserAccounts,
UserGroup,
UserGuest,
}
#[derive(Debug, Clone)]
pub enum MenuUpdate {
SetEnabled(bool),
SetTitle(String),
SetSelected(bool),
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
SetNativeImage(NativeImage),
}
pub trait TrayHandle: fmt::Debug + Clone + Send + Sync {
fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
fn set_menu(&self, menu: crate::menu::SystemTrayMenu) -> crate::Result<()>;
fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>;
#[cfg(target_os = "macos")]
fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>;
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct Menu {
pub items: Vec<MenuEntry>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Submenu {
pub title: String,
pub enabled: bool,
pub inner: Menu,
}
impl Submenu {
pub fn new<S: Into<String>>(title: S, menu: Menu) -> Self {
Self {
title: title.into(),
enabled: true,
inner: menu,
}
}
}
impl Menu {
pub fn new() -> Self {
Default::default()
}
pub fn os_default(#[allow(unused)] app_name: &str) -> Self {
let mut menu = Menu::new();
#[cfg(target_os = "macos")]
{
menu = menu.add_submenu(Submenu::new(
app_name,
Menu::new()
.add_native_item(MenuItem::About(
app_name.to_string(),
AboutMetadata::default(),
))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
));
}
let mut file_menu = Menu::new();
file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
#[cfg(not(target_os = "macos"))]
{
file_menu = file_menu.add_native_item(MenuItem::Quit);
}
menu = menu.add_submenu(Submenu::new("File", file_menu));
#[cfg(not(target_os = "linux"))]
let mut edit_menu = Menu::new();
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::Undo);
edit_menu = edit_menu.add_native_item(MenuItem::Redo);
edit_menu = edit_menu.add_native_item(MenuItem::Separator);
}
#[cfg(not(target_os = "linux"))]
{
edit_menu = edit_menu.add_native_item(MenuItem::Cut);
edit_menu = edit_menu.add_native_item(MenuItem::Copy);
edit_menu = edit_menu.add_native_item(MenuItem::Paste);
}
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
}
#[cfg(not(target_os = "linux"))]
{
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
}
#[cfg(target_os = "macos")]
{
menu = menu.add_submenu(Submenu::new(
"View",
Menu::new().add_native_item(MenuItem::EnterFullScreen),
));
}
let mut window_menu = Menu::new();
window_menu = window_menu.add_native_item(MenuItem::Minimize);
#[cfg(target_os = "macos")]
{
window_menu = window_menu.add_native_item(MenuItem::Zoom);
window_menu = window_menu.add_native_item(MenuItem::Separator);
}
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
menu = menu.add_submenu(Submenu::new("Window", window_menu));
menu
}
pub fn with_items<I: IntoIterator<Item = MenuEntry>>(items: I) -> Self {
Self {
items: items.into_iter().collect(),
}
}
#[must_use]
pub fn add_item(mut self, item: CustomMenuItem) -> Self {
self.items.push(MenuEntry::CustomItem(item));
self
}
#[must_use]
pub fn add_native_item(mut self, item: MenuItem) -> Self {
self.items.push(MenuEntry::NativeItem(item));
self
}
#[must_use]
pub fn add_submenu(mut self, submenu: Submenu) -> Self {
self.items.push(MenuEntry::Submenu(submenu));
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CustomMenuItem {
pub id: MenuHash,
pub id_str: MenuId,
pub title: String,
pub keyboard_accelerator: Option<String>,
pub enabled: bool,
pub selected: bool,
#[cfg(target_os = "macos")]
pub native_image: Option<NativeImage>,
}
impl CustomMenuItem {
pub fn new<I: Into<String>, T: Into<String>>(id: I, title: T) -> Self {
let id_str = id.into();
Self {
id: Self::hash(&id_str),
id_str,
title: title.into(),
keyboard_accelerator: None,
enabled: true,
selected: false,
#[cfg(target_os = "macos")]
native_image: None,
}
}
#[must_use]
pub fn accelerator<T: Into<String>>(mut self, accelerator: T) -> Self {
self.keyboard_accelerator.replace(accelerator.into());
self
}
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
#[must_use]
pub fn native_image(mut self, image: NativeImage) -> Self {
self.native_image.replace(image);
self
}
#[must_use]
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
#[must_use]
pub fn selected(mut self) -> Self {
self.selected = true;
self
}
fn hash(id: &str) -> MenuHash {
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
hasher.finish() as MenuHash
}
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct SystemTrayMenu {
pub items: Vec<SystemTrayMenuEntry>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SystemTraySubmenu {
pub title: String,
pub enabled: bool,
pub inner: SystemTrayMenu,
}
impl SystemTraySubmenu {
pub fn new<S: Into<String>>(title: S, menu: SystemTrayMenu) -> Self {
Self {
title: title.into(),
enabled: true,
inner: menu,
}
}
}
impl SystemTrayMenu {
pub fn new() -> Self {
Default::default()
}
#[must_use]
pub fn add_item(mut self, item: CustomMenuItem) -> Self {
self.items.push(SystemTrayMenuEntry::CustomItem(item));
self
}
#[must_use]
pub fn add_native_item(mut self, item: SystemTrayMenuItem) -> Self {
self.items.push(SystemTrayMenuEntry::NativeItem(item));
self
}
#[must_use]
pub fn add_submenu(mut self, submenu: SystemTraySubmenu) -> Self {
self.items.push(SystemTrayMenuEntry::Submenu(submenu));
self
}
}
#[derive(Debug, Clone)]
pub enum SystemTrayMenuEntry {
CustomItem(CustomMenuItem),
NativeItem(SystemTrayMenuItem),
Submenu(SystemTraySubmenu),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SystemTrayMenuItem {
Separator,
}
#[derive(Debug, Clone)]
pub enum MenuEntry {
CustomItem(CustomMenuItem),
NativeItem(MenuItem),
Submenu(Submenu),
}
impl From<CustomMenuItem> for MenuEntry {
fn from(item: CustomMenuItem) -> Self {
Self::CustomItem(item)
}
}
impl From<MenuItem> for MenuEntry {
fn from(item: MenuItem) -> Self {
Self::NativeItem(item)
}
}
impl From<Submenu> for MenuEntry {
fn from(submenu: Submenu) -> Self {
Self::Submenu(submenu)
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AboutMetadata {
pub version: Option<String>,
pub authors: Option<Vec<String>>,
pub comments: Option<String>,
pub copyright: Option<String>,
pub license: Option<String>,
pub website: Option<String>,
pub website_label: Option<String>,
}
impl AboutMetadata {
pub fn new() -> Self {
Default::default()
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version.replace(version.into());
self
}
pub fn authors(mut self, authors: Vec<String>) -> Self {
self.authors.replace(authors);
self
}
pub fn comments(mut self, comments: impl Into<String>) -> Self {
self.comments.replace(comments.into());
self
}
pub fn copyright(mut self, copyright: impl Into<String>) -> Self {
self.copyright.replace(copyright.into());
self
}
pub fn license(mut self, license: impl Into<String>) -> Self {
self.license.replace(license.into());
self
}
pub fn website(mut self, website: impl Into<String>) -> Self {
self.website.replace(website.into());
self
}
pub fn website_label(mut self, website_label: impl Into<String>) -> Self {
self.website_label.replace(website_label.into());
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MenuItem {
About(String, AboutMetadata),
Hide,
Services,
HideOthers,
ShowAll,
CloseWindow,
Quit,
Copy,
Cut,
Undo,
Redo,
SelectAll,
Paste,
EnterFullScreen,
Minimize,
Zoom,
Separator,
}