millennium-runtime 1.0.0-beta.3

Runtime for Millennium applications
Documentation
// Copyright 2022 pyke.io
//           2019-2021 Tauri Programme within The Commons Conservancy
//                     [https://tauri.studio/]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![allow(clippy::tabs_in_doc_comments)]

use std::{
	collections::hash_map::DefaultHasher,
	fmt,
	hash::{Hash, Hasher}
};

pub type MenuHash = u16;
pub type MenuId = String;
pub type MenuIdRef<'a> = &'a str;

/// Named images defined by the system.
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
#[derive(Debug, Clone)]
pub enum NativeImage {
	/// An add item template image.
	Add,
	/// Advanced preferences toolbar icon for the preferences window.
	Advanced,
	/// A Bluetooth template image.
	Bluetooth,
	/// Bookmarks image suitable for a template.
	Bookmarks,
	/// A caution image.
	Caution,
	/// A color panel toolbar icon.
	ColorPanel,
	/// A column view mode template image.
	ColumnView,
	/// A computer icon.
	Computer,
	/// An enter full-screen mode template image.
	EnterFullScreen,
	/// Permissions for all users.
	Everyone,
	/// An exit full-screen mode template image.
	ExitFullScreen,
	/// A cover flow view mode template image.
	FlowView,
	/// A folder image.
	Folder,
	/// A burnable folder icon.
	FolderBurnable,
	/// A smart folder icon.
	FolderSmart,
	/// A link template image.
	FollowLinkFreestanding,
	/// A font panel toolbar icon.
	FontPanel,
	/// A `go back` template image.
	GoLeft,
	/// A `go forward` template image.
	GoRight,
	/// Home image suitable for a template.
	Home,
	/// An iChat Theater template image.
	IChatTheater,
	/// An icon view mode template image.
	IconView,
	/// An information toolbar icon.
	Info,
	/// A template image used to denote invalid data.
	InvalidDataFreestanding,
	/// A generic left-facing triangle template image.
	LeftFacingTriangle,
	/// A list view mode template image.
	ListView,
	/// A locked padlock template image.
	LockLocked,
	/// An unlocked padlock template image.
	LockUnlocked,
	/// A horizontal dash, for use in menus.
	MenuMixedState,
	/// A check mark template image, for use in menus.
	MenuOnState,
	/// A MobileMe icon.
	MobileMe,
	/// A drag image for multiple items.
	MultipleDocuments,
	/// A network icon.
	Network,
	/// A path button template image.
	Path,
	/// General preferences toolbar icon for the preferences window.
	PreferencesGeneral,
	/// A Quick Look template image.
	QuickLook,
	/// A refresh template image.
	RefreshFreestanding,
	/// A refresh template image.
	Refresh,
	/// A remove item template image.
	Remove,
	/// A reveal contents template image.
	RevealFreestanding,
	/// A generic right-facing triangle template image.
	RightFacingTriangle,
	/// A share view template image.
	Share,
	/// A slideshow template image.
	Slideshow,
	/// A badge for a `smart` item.
	SmartBadge,
	/// Small green indicator, similar to iChat’s available image.
	StatusAvailable,
	/// Small clear indicator.
	StatusNone,
	/// Small yellow indicator, similar to iChat’s idle image.
	StatusPartiallyAvailable,
	/// Small red indicator, similar to iChat’s unavailable image.
	StatusUnavailable,
	/// A stop progress template image.
	StopProgressFreestanding,
	/// A stop progress button template image.
	StopProgress,

	/// An image of the empty trash can.
	TrashEmpty,
	/// An image of the full trash can.
	TrashFull,
	/// Permissions for a single user.
	User,
	/// User account toolbar icon for the preferences window.
	UserAccounts,
	/// Permissions for a group of users.
	UserGroup,
	/// Permissions for guests.
	UserGuest
}

#[derive(Debug, Clone)]
pub enum MenuUpdate {
	/// Modifies the enabled state of the menu item.
	SetEnabled(bool),
	/// Modifies the title (label) of the menu item.
	SetTitle(String),
	/// Modifies the selected state of the menu item.
	SetSelected(bool),
	/// Update native image.
	#[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<()>;
}

/// A window menu.
#[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 {
	/// Creates a new submenu with the given title and menu items.
	pub fn new<S: Into<String>>(title: S, menu: Menu) -> Self {
		Self {
			title: title.into(),
			enabled: true,
			inner: menu
		}
	}
}

impl Menu {
	/// Creates a new window menu.
	pub fn new() -> Self {
		Default::default()
	}

	/// Creates a menu with platform default menu items.
	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
	}

	/// Creates a new window menu with the given items.
	///
	/// # Examples
	/// ```
	/// # use millennium_runtime::menu::{Menu, MenuItem, CustomMenuItem, Submenu};
	/// Menu::with_items([
	/// 	MenuItem::SelectAll.into(),
	/// 	#[cfg(target_os = "macos")]
	/// 	MenuItem::Redo.into(),
	/// 	CustomMenuItem::new("toggle", "Toggle visibility").into(),
	/// 	Submenu::new("View", Menu::new()).into()
	/// ]);
	/// ```
	pub fn with_items<I: IntoIterator<Item = MenuEntry>>(items: I) -> Self {
		Self { items: items.into_iter().collect() }
	}

	/// Adds the custom menu item to the menu.
	#[must_use]
	pub fn add_item(mut self, item: CustomMenuItem) -> Self {
		self.items.push(MenuEntry::CustomItem(item));
		self
	}

	/// Adds a native item to the menu.
	#[must_use]
	pub fn add_native_item(mut self, item: MenuItem) -> Self {
		self.items.push(MenuEntry::NativeItem(item));
		self
	}

	/// Adds an entry with submenu.
	#[must_use]
	pub fn add_submenu(mut self, submenu: Submenu) -> Self {
		self.items.push(MenuEntry::Submenu(submenu));
		self
	}
}

/// A custom menu item.
#[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 {
	/// Create new custom menu item.
	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
		}
	}

	/// Assign a keyboard shortcut to the menu action.
	#[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]
	/// A native image do render on the menu item.
	pub fn native_image(mut self, image: NativeImage) -> Self {
		self.native_image.replace(image);
		self
	}

	/// Mark the item as disabled.
	#[must_use]
	pub fn disabled(mut self) -> Self {
		self.enabled = false;
		self
	}

	/// Mark the item as selected.
	#[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
	}
}

/// A system tray menu.
#[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 {
	/// Creates a new submenu with the given title and menu items.
	pub fn new<S: Into<String>>(title: S, menu: SystemTrayMenu) -> Self {
		Self {
			title: title.into(),
			enabled: true,
			inner: menu
		}
	}
}

impl SystemTrayMenu {
	/// Creates a new system tray menu.
	pub fn new() -> Self {
		Default::default()
	}

	/// Adds the custom menu item to the system tray menu.
	#[must_use]
	pub fn add_item(mut self, item: CustomMenuItem) -> Self {
		self.items.push(SystemTrayMenuEntry::CustomItem(item));
		self
	}

	/// Adds a native item to the system tray menu.
	#[must_use]
	pub fn add_native_item(mut self, item: SystemTrayMenuItem) -> Self {
		self.items.push(SystemTrayMenuEntry::NativeItem(item));
		self
	}

	/// Adds an entry with submenu.
	#[must_use]
	pub fn add_submenu(mut self, submenu: SystemTraySubmenu) -> Self {
		self.items.push(SystemTrayMenuEntry::Submenu(submenu));
		self
	}
}

/// An entry on the system tray menu.
#[derive(Debug, Clone)]
pub enum SystemTrayMenuEntry {
	/// A custom item.
	CustomItem(CustomMenuItem),
	/// A native item.
	NativeItem(SystemTrayMenuItem),
	/// An entry with submenu.
	Submenu(SystemTraySubmenu)
}

/// System tray menu item.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SystemTrayMenuItem {
	/// A separator.
	Separator
}

/// An entry on the system tray menu.
#[derive(Debug, Clone)]
pub enum MenuEntry {
	/// A custom item.
	CustomItem(CustomMenuItem),
	/// A native item.
	NativeItem(MenuItem),
	/// An entry with submenu.
	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)
	}
}

/// Application metadata for the [`MenuItem::About`] action.
///
/// ## Platform-specific
///
/// - **Windows / macOS / Android / iOS:** The metadata is ignored on these platforms.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AboutMetadata {
	/// The application name.
	pub version: Option<String>,
	/// The authors of the application.
	pub authors: Option<Vec<String>>,
	/// Application comments.
	pub comments: Option<String>,
	/// The copyright of the application.
	pub copyright: Option<String>,
	/// The license of the application.
	pub license: Option<String>,
	/// The application website.
	pub website: Option<String>,
	/// The website label.
	pub website_label: Option<String>
}

impl AboutMetadata {
	/// Creates the default metadata for the [`MenuItem::About`] action, which is just empty.
	pub fn new() -> Self {
		Default::default()
	}

	/// Defines the application version.
	pub fn version(mut self, version: impl Into<String>) -> Self {
		self.version.replace(version.into());
		self
	}

	/// Defines the application authors.
	pub fn authors(mut self, authors: Vec<String>) -> Self {
		self.authors.replace(authors);
		self
	}

	/// Defines the application comments.
	pub fn comments(mut self, comments: impl Into<String>) -> Self {
		self.comments.replace(comments.into());
		self
	}

	/// Defines the application copyright.
	pub fn copyright(mut self, copyright: impl Into<String>) -> Self {
		self.copyright.replace(copyright.into());
		self
	}

	/// Defines the application license.
	pub fn license(mut self, license: impl Into<String>) -> Self {
		self.license.replace(license.into());
		self
	}

	/// Defines the application version.
	pub fn website(mut self, website: impl Into<String>) -> Self {
		self.website.replace(website.into());
		self
	}

	/// Defines the application version.
	pub fn website_label(mut self, website_label: impl Into<String>) -> Self {
		self.website_label.replace(website_label.into());
		self
	}
}

/// A menu item, bound to a pre-defined action or `Custom` emit an event. Note
/// that status bar only supports `Custom` menu item variants. And on the menu
/// bar, some platforms might not support some of the variants. Unsupported
/// variant will be no-op on such platform.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum MenuItem {
	/// Shows a standard "About" item.
	///
	/// The first value is the application name, and the second is its metadata.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Android / iOS:** Unsupported
	/// - **Linux**: The metadata is only applied on Linux.
	About(String, AboutMetadata),

	/// A standard "hide the app" menu item.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS:** Unsupported
	Hide,

	/// A standard "Services" menu item.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	Services,

	/// A "hide all other windows" menu item.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	HideOthers,

	/// A menu item to show all the windows for this app.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	ShowAll,

	/// Close the current window.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS:** Unsupported
	CloseWindow,

	/// A "quit this app" menu icon.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS:** Unsupported
	Quit,

	/// A menu item for enabling copying (often text) from responders.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS / Linux:** Unsupported
	Copy,

	/// A menu item for enabling cutting (often text) from responders.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS / Linux:** Unsupported
	Cut,

	/// An "undo" menu item; particularly useful for supporting the
	/// cut/copy/paste/undo lifecycle of events.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	Undo,

	/// An "redo" menu item; particularly useful for supporting the
	/// cut/copy/paste/undo lifecycle of events.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	Redo,

	/// A menu item for selecting all (often text) from responders.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Android / iOS / Linux:** Unsupported
	SelectAll,

	/// A menu item for pasting (often text) into responders.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS / Linux:** Unsupported
	Paste,

	/// A standard "enter full screen" item.
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	EnterFullScreen,

	/// An item for minimizing the window with the standard system controls.
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS:** Unsupported
	Minimize,

	/// An item for instructing the app to zoom
	///
	/// ## Platform-specific
	///
	/// - **Windows / Linux / Android / iOS:** Unsupported
	Zoom,

	/// Represents a Separator
	///
	/// ## Platform-specific
	///
	/// - **Android / iOS:** Unsupported
	Separator
}