use std::num::NonZeroU32;
use crate::kurbo::Point;
use crate::shell::{HotKey, KeyCompare, Menu as PlatformMenu, RawMods, SysMods};
use crate::{commands, Command, Data, Env, KeyCode, LocalizedString, Selector};
#[derive(Clone)]
pub struct MenuDesc<T> {
item: MenuItem<T>,
items: Vec<MenuEntry<T>>,
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum MenuEntry<T> {
Item(MenuItem<T>),
SubMenu(MenuDesc<T>),
Separator,
}
#[derive(Debug, Clone)]
pub struct MenuItem<T> {
title: LocalizedString<T>,
command: Command,
hotkey: Option<HotKey>,
tool_tip: Option<LocalizedString<T>>,
selected: bool,
enabled: bool, platform_id: MenuItemId,
}
#[derive(Debug, Clone)]
pub struct ContextMenu<T> {
pub(crate) menu: MenuDesc<T>,
pub(crate) location: Point,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MenuItemId(Option<NonZeroU32>);
impl<T> MenuItem<T> {
pub fn new(title: LocalizedString<T>, command: impl Into<Command>) -> Self {
MenuItem {
title,
command: command.into(),
hotkey: None,
tool_tip: None,
selected: false,
enabled: true,
platform_id: MenuItemId::PLACEHOLDER,
}
}
pub fn hotkey(mut self, mods: impl Into<Option<RawMods>>, key: impl Into<KeyCompare>) -> Self {
self.hotkey = Some(HotKey::new(mods, key));
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn disabled_if(mut self, mut p: impl FnMut() -> bool) -> Self {
if p() {
self.enabled = false;
}
self
}
pub fn selected(mut self) -> Self {
self.selected = true;
self
}
pub fn selected_if(mut self, mut p: impl FnMut() -> bool) -> Self {
if p() {
self.selected = true;
}
self
}
}
impl<T: Data> MenuDesc<T> {
pub fn empty() -> Self {
Self::new(LocalizedString::new(""))
}
pub fn new(title: LocalizedString<T>) -> Self {
let item = MenuItem::new(title, Selector::NOOP);
MenuDesc {
item,
items: Vec::new(),
}
}
#[allow(unreachable_code)]
pub fn platform_default() -> Option<MenuDesc<T>> {
#[cfg(target_os = "macos")]
return Some(MenuDesc::empty().append(sys::mac::application::default()));
#[cfg(target_os = "windows")]
return None;
log::warn!("MenuDesc::platform_default is not implemented for this platform.");
None
}
pub fn append_iter<I: Iterator<Item = MenuItem<T>>>(mut self, f: impl FnOnce() -> I) -> Self {
for item in f() {
self.items.push(item.into());
}
self
}
pub fn append(mut self, item: impl Into<MenuEntry<T>>) -> Self {
self.items.push(item.into());
self
}
pub fn append_if(mut self, item: impl Into<MenuEntry<T>>, mut p: impl FnMut() -> bool) -> Self {
if p() {
self.items.push(item.into());
}
self
}
pub fn append_separator(mut self) -> Self {
self.items.push(MenuEntry::Separator);
self
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub(crate) fn build_window_menu(&mut self, data: &T, env: &Env) -> PlatformMenu {
self.build_native_menu(data, env, false)
}
pub(crate) fn build_popup_menu(&mut self, data: &T, env: &Env) -> PlatformMenu {
self.build_native_menu(data, env, true)
}
fn build_native_menu(&mut self, data: &T, env: &Env, for_popup: bool) -> PlatformMenu {
let mut menu = if for_popup {
PlatformMenu::new_for_popup()
} else {
PlatformMenu::new()
};
for item in &mut self.items {
match item {
MenuEntry::Item(ref mut item) => {
item.title.resolve(data, env);
item.platform_id = MenuItemId::next();
menu.add_item(
item.platform_id.as_u32(),
item.title.localized_str(),
item.hotkey.as_ref(),
item.enabled,
item.selected,
);
}
MenuEntry::Separator => menu.add_separator(),
MenuEntry::SubMenu(ref mut submenu) => {
let sub = submenu.build_native_menu(data, env, false);
submenu.item.title.resolve(data, env);
menu.add_dropdown(
sub,
&submenu.item.title.localized_str(),
submenu.item.enabled,
);
}
}
}
menu
}
pub(crate) fn command_for_id(&self, id: u32) -> Option<Command> {
for item in &self.items {
match item {
MenuEntry::Item(item) if item.platform_id.as_u32() == id => {
return Some(item.command.clone())
}
MenuEntry::SubMenu(menu) => {
if let Some(cmd) = menu.command_for_id(id) {
return Some(cmd);
}
}
_ => (),
}
}
None
}
}
impl<T> ContextMenu<T> {
pub fn new(menu: MenuDesc<T>, location: Point) -> Self {
ContextMenu { menu, location }
}
}
impl MenuItemId {
const PLACEHOLDER: MenuItemId = MenuItemId(None);
fn next() -> Self {
use std::sync::atomic::{AtomicU32, Ordering};
static MENU_ID: AtomicU32 = AtomicU32::new(1);
let raw = NonZeroU32::new(MENU_ID.fetch_add(2, Ordering::Relaxed));
MenuItemId(raw)
}
fn as_u32(self) -> u32 {
match self.0 {
Some(val) => val.get(),
None => 0,
}
}
}
impl<T> std::fmt::Debug for MenuDesc<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
fn menu_debug_impl<T>(
menu: &MenuDesc<T>,
f: &mut std::fmt::Formatter,
level: usize,
) -> std::fmt::Result {
static TABS: &str =
" ";
let indent = &TABS[..level * 2];
let child_indent = &TABS[..(level + 1) * 2];
writeln!(f, "{}{}", indent, menu.item.title.key)?;
for item in &menu.items {
match item {
MenuEntry::Item(item) => writeln!(f, "{}{}", child_indent, item.title.key)?,
MenuEntry::Separator => writeln!(f, "{} --------- ", child_indent)?,
MenuEntry::SubMenu(ref menu) => menu_debug_impl(menu, f, level + 1)?,
}
}
Ok(())
}
menu_debug_impl(self, f, 0)
}
}
impl<T> From<MenuItem<T>> for MenuEntry<T> {
fn from(src: MenuItem<T>) -> MenuEntry<T> {
MenuEntry::Item(src)
}
}
impl<T> From<MenuDesc<T>> for MenuEntry<T> {
fn from(src: MenuDesc<T>) -> MenuEntry<T> {
MenuEntry::SubMenu(src)
}
}
pub mod sys {
use super::*;
pub mod common {
use super::*;
pub fn cut<T: Data>() -> MenuItem<T> {
MenuItem::new(LocalizedString::new("common-menu-cut"), commands::CUT)
.hotkey(SysMods::Cmd, "x")
}
pub fn copy<T: Data>() -> MenuItem<T> {
MenuItem::new(LocalizedString::new("common-menu-copy"), commands::COPY)
.hotkey(SysMods::Cmd, "c")
}
pub fn paste<T: Data>() -> MenuItem<T> {
MenuItem::new(LocalizedString::new("common-menu-paste"), commands::PASTE)
.hotkey(SysMods::Cmd, "v")
}
pub fn undo<T: Data>() -> MenuItem<T> {
MenuItem::new(LocalizedString::new("common-menu-undo"), commands::UNDO)
.hotkey(SysMods::Cmd, "z")
}
pub fn redo<T: Data>() -> MenuItem<T> {
let item = MenuItem::new(LocalizedString::new("common-menu-redo"), commands::REDO);
#[cfg(target_os = "windows")]
{
item.hotkey(RawMods::Ctrl, "y")
}
#[cfg(not(target_os = "windows"))]
{
item.hotkey(SysMods::CmdShift, "z")
}
}
}
pub mod win {
use super::*;
pub mod file {
use super::*;
pub fn default<T: Data>() -> MenuDesc<T> {
MenuDesc::new(LocalizedString::new("common-menu-file-menu"))
.append(new())
.append(open())
.append(close())
.append(save().disabled())
.append(save_as().disabled())
.append(print().disabled())
.append(page_setup().disabled())
.append_separator()
.append(exit())
}
pub fn new<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-new"),
commands::NEW_FILE,
)
.hotkey(RawMods::Ctrl, "n")
}
pub fn open<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-open"),
commands::OPEN_FILE,
)
.hotkey(RawMods::Ctrl, "o")
}
pub fn close<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-close"),
commands::CLOSE_WINDOW,
)
}
pub fn save<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save"),
commands::SAVE_FILE,
)
.hotkey(RawMods::Ctrl, "s")
}
pub fn save_ellipsis<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save"),
commands::SAVE_FILE,
)
.hotkey(RawMods::Ctrl, "s")
}
pub fn save_as<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save-as"),
commands::SHOW_SAVE_PANEL,
)
.hotkey(RawMods::CtrlShift, "s")
}
pub fn print<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-print"),
commands::PRINT,
)
.hotkey(RawMods::Ctrl, "p")
}
pub fn print_preview<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-print-preview"),
commands::PRINT_PREVIEW,
)
}
pub fn page_setup<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-page-setup"),
commands::PRINT_SETUP,
)
}
pub fn exit<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("win-menu-file-exit"),
commands::QUIT_APP,
)
.hotkey(RawMods::Alt, KeyCode::F4)
}
}
}
pub mod mac {
use super::*;
pub fn menu_bar<T: Data>() -> MenuDesc<T> {
MenuDesc::new(LocalizedString::new(""))
.append(application::default())
.append(file::default())
}
pub mod application {
use super::*;
pub fn default<T: Data>() -> MenuDesc<T> {
MenuDesc::new(LocalizedString::new("macos-menu-application-menu"))
.append(about())
.append_separator()
.append(preferences().disabled())
.append_separator()
.append(hide())
.append(hide_others())
.append(show_all().disabled())
.append_separator()
.append(quit())
}
pub fn about<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-about-app"),
commands::SHOW_ABOUT,
)
}
pub fn preferences<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-preferences"),
commands::SHOW_PREFERENCES,
)
.hotkey(RawMods::Meta, ",")
}
pub fn hide<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-hide-app"),
commands::HIDE_APPLICATION,
)
.hotkey(RawMods::Meta, "h")
}
pub fn hide_others<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-hide-others"),
commands::HIDE_OTHERS,
)
.hotkey(RawMods::AltMeta, "h")
}
pub fn show_all<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-show-all"),
commands::SHOW_ALL,
)
}
pub fn quit<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("macos-menu-quit-app"),
commands::QUIT_APP,
)
.hotkey(RawMods::Meta, "q")
}
}
pub mod file {
use super::*;
pub fn default<T: Data>() -> MenuDesc<T> {
MenuDesc::new(LocalizedString::new("common-menu-file-menu"))
.append(new_file())
.append(open_file())
.append_separator()
.append(close())
.append(save().disabled())
.append(save_as().disabled())
.append_separator()
.append(page_setup().disabled())
.append(print().disabled())
}
pub fn new_file<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-new"),
commands::NEW_FILE,
)
.hotkey(RawMods::Meta, "n")
}
pub fn open_file<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-open"),
commands::OPEN_FILE,
)
.hotkey(RawMods::Meta, "o")
}
pub fn close<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-close"),
commands::CLOSE_WINDOW,
)
.hotkey(RawMods::Meta, "w")
}
pub fn save<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save"),
commands::SAVE_FILE,
)
.hotkey(RawMods::Meta, "s")
}
pub fn save_ellipsis<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save-ellipsis"),
commands::SAVE_FILE,
)
.hotkey(RawMods::Meta, "s")
}
pub fn save_as<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-save-as"),
commands::SHOW_SAVE_PANEL,
)
.hotkey(RawMods::MetaShift, "s")
}
pub fn page_setup<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-page-setup"),
commands::PRINT_SETUP,
)
.hotkey(RawMods::MetaShift, "p")
}
pub fn print<T: Data>() -> MenuItem<T> {
MenuItem::new(
LocalizedString::new("common-menu-file-print"),
commands::PRINT,
)
.hotkey(RawMods::Meta, "p")
}
}
}
}