use std::{cell::RefCell, mem, rc::Rc};
use crate::{
accelerator::{Accelerator, Code, MenuAccelerator, Modifiers, CMD_OR_CTRL},
platform_impl::PlatformMenuItem,
util, AboutMetadata, IsMenuItem, MenuId, MenuItemAction, MenuItemKind, StateCell,
};
#[derive(Clone)]
pub struct PredefinedMenuItem {
pub(crate) id: Rc<MenuId>,
pub(crate) state: StateCell<PredefinedMenuItemState>,
pub(crate) platform: Rc<RefCell<PlatformMenuItem>>,
}
#[derive(Debug, Clone)]
pub(crate) struct PredefinedMenuItemState {
pub text: String,
pub predefined_item_type: PredefinedMenuItemType,
pub enabled: bool,
}
impl crate::sealed::Sealed for PredefinedMenuItem {}
impl IsMenuItem for PredefinedMenuItem {
fn kind(&self) -> MenuItemKind {
MenuItemKind::Predefined(self.clone())
}
fn id(&self) -> &MenuId {
self.id()
}
fn into_id(self) -> MenuId {
self.into_id()
}
}
impl PredefinedMenuItem {
pub fn separator() -> PredefinedMenuItem {
PredefinedMenuItem::new::<&str>(PredefinedMenuItemType::Separator, None)
}
pub fn section_header(text: &str) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::SectionHeader, Some(text))
}
pub fn copy(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Copy, text)
}
pub fn cut(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Cut, text)
}
pub fn paste(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Paste, text)
}
pub fn paste_and_match_style(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::PasteAndMatchStyle, text)
}
pub fn delete(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Delete, text)
}
pub fn select_all(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::SelectAll, text)
}
pub fn undo(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Undo, text)
}
pub fn redo(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Redo, text)
}
pub fn minimize(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Minimize, text)
}
pub fn maximize(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Maximize, text)
}
pub fn zoom(text: Option<&str>) -> PredefinedMenuItem {
Self::maximize(text)
}
pub fn actual_size(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::ActualSize, text)
}
pub fn zoom_in(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::ZoomIn, text)
}
pub fn zoom_out(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::ZoomOut, text)
}
pub fn fullscreen(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Fullscreen, text)
}
pub fn hide(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Hide, text)
}
pub fn hide_others(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::HideOthers, text)
}
pub fn show_all(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::ShowAll, text)
}
pub fn close_window(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::CloseWindow, text)
}
pub fn quit(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Quit, text)
}
pub fn about(text: Option<&str>, metadata: Option<AboutMetadata>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::About(metadata), text)
}
pub fn services(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::Services, text)
}
pub fn bring_all_to_front(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::BringAllToFront, text)
}
pub fn start_speaking(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::StartSpeaking, text)
}
pub fn stop_speaking(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::StopSpeaking, text)
}
pub fn start_dictation(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::StartDictation, text)
}
pub fn emoji_and_symbols(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredefinedMenuItemType::EmojiAndSymbols, text)
}
fn new<S: AsRef<str>>(item: PredefinedMenuItemType, text: Option<S>) -> Self {
let id = util::next_id(None);
let resolved_text = text
.as_ref()
.map(|text| text.as_ref().to_string())
.unwrap_or_else(|| item.default_text(app_name().as_deref()));
let enabled = item.is_enabled();
let state = StateCell::new(PredefinedMenuItemState {
text: resolved_text,
predefined_item_type: item,
enabled,
});
let click = MenuItemAction::Predefined(state.downgrade());
let platform = PlatformMenuItem::new(click);
Self {
id: Rc::new(id),
state,
platform: Rc::new(RefCell::new(platform)),
}
}
pub fn id(&self) -> &MenuId {
&self.id
}
pub fn text(&self) -> String {
let text = self.platform.borrow().text();
text.unwrap_or_else(|| self.state.borrow().text.clone())
}
pub fn set_text<S: AsRef<str>>(&self, text: S) {
let accelerator = {
let mut state = self.state.borrow_mut();
state.text = text.as_ref().to_string();
state.predefined_item_type.accelerator()
};
self.platform
.borrow_mut()
.set_text(text.as_ref(), accelerator.as_ref())
}
pub fn into_id(mut self) -> MenuId {
if let Some(id) = Rc::get_mut(&mut self.id) {
mem::take(id)
} else {
self.id().clone()
}
}
}
fn app_name() -> Option<String> {
#[cfg(target_os = "macos")]
{
crate::platform_impl::app_name()
}
#[cfg(not(target_os = "macos"))]
{
None
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub(crate) enum PredefinedMenuItemType {
Separator,
SectionHeader,
Copy,
Cut,
Paste,
PasteAndMatchStyle,
Delete,
SelectAll,
Undo,
Redo,
Minimize,
Maximize,
ActualSize,
ZoomIn,
ZoomOut,
Fullscreen,
Hide,
HideOthers,
ShowAll,
CloseWindow,
Quit,
About(Option<AboutMetadata>),
Services,
BringAllToFront,
StartSpeaking,
StopSpeaking,
StartDictation,
EmojiAndSymbols,
}
impl PredefinedMenuItemType {
pub(crate) fn text(&self) -> &str {
match self {
PredefinedMenuItemType::Separator => "",
PredefinedMenuItemType::SectionHeader => "",
PredefinedMenuItemType::Copy => "&Copy",
PredefinedMenuItemType::Cut => "Cu&t",
PredefinedMenuItemType::Paste => "&Paste",
PredefinedMenuItemType::PasteAndMatchStyle => "Paste and Match Style",
PredefinedMenuItemType::Delete => "&Delete",
PredefinedMenuItemType::SelectAll => "Select &All",
PredefinedMenuItemType::Undo => "Undo",
PredefinedMenuItemType::Redo => "Redo",
PredefinedMenuItemType::Minimize => "&Minimize",
#[cfg(target_os = "macos")]
PredefinedMenuItemType::Maximize => "Zoom",
#[cfg(not(target_os = "macos"))]
PredefinedMenuItemType::Maximize => "Ma&ximize",
PredefinedMenuItemType::ActualSize => "Actual Size",
PredefinedMenuItemType::ZoomIn => "Zoom In",
PredefinedMenuItemType::ZoomOut => "Zoom Out",
PredefinedMenuItemType::Fullscreen => "Toggle Full Screen",
PredefinedMenuItemType::Hide => "&Hide",
PredefinedMenuItemType::HideOthers => "Hide Others",
PredefinedMenuItemType::ShowAll => "Show All",
#[cfg(windows)]
PredefinedMenuItemType::CloseWindow => "Close",
#[cfg(not(windows))]
PredefinedMenuItemType::CloseWindow => "C&lose Window",
#[cfg(windows)]
PredefinedMenuItemType::Quit => "&Exit",
#[cfg(not(windows))]
PredefinedMenuItemType::Quit => "&Quit",
PredefinedMenuItemType::About(_) => "&About",
PredefinedMenuItemType::Services => "Services",
PredefinedMenuItemType::BringAllToFront => "Bring All to Front",
PredefinedMenuItemType::StartSpeaking => "Start Speaking",
PredefinedMenuItemType::StopSpeaking => "Stop Speaking",
PredefinedMenuItemType::StartDictation => "Start Dictation…",
PredefinedMenuItemType::EmojiAndSymbols => "Emoji & Symbols",
}
}
pub(crate) fn default_text(&self, app_name: Option<&str>) -> String {
#[cfg(target_os = "macos")]
{
let named = |verb: &str| match app_name {
Some(name) if !name.trim().is_empty() => {
format!("{verb} {}", escape_mnemonic(name.trim()))
}
_ => verb.to_string(),
};
match self {
PredefinedMenuItemType::About(_) => return named("About"),
PredefinedMenuItemType::Hide => return named("Hide"),
PredefinedMenuItemType::Quit => return named("Quit"),
_ => {}
}
}
#[cfg(not(target_os = "macos"))]
let _ = app_name;
self.text().to_string()
}
#[cfg(target_os = "windows")]
pub(crate) fn is_supported(&self) -> bool {
matches!(
self,
PredefinedMenuItemType::Separator
| PredefinedMenuItemType::Copy
| PredefinedMenuItemType::Cut
| PredefinedMenuItemType::Paste
| PredefinedMenuItemType::SelectAll
| PredefinedMenuItemType::Undo
| PredefinedMenuItemType::Redo
| PredefinedMenuItemType::Minimize
| PredefinedMenuItemType::Maximize
| PredefinedMenuItemType::Hide
| PredefinedMenuItemType::CloseWindow
| PredefinedMenuItemType::Quit
| PredefinedMenuItemType::About(_)
)
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
not(any(feature = "gtk3", feature = "gtk4"))
))]
pub(crate) fn is_supported(&self) -> bool {
matches!(self, PredefinedMenuItemType::Separator)
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3",
not(feature = "gtk4")
))]
pub(crate) fn is_supported(&self) -> bool {
matches!(
self,
PredefinedMenuItemType::Separator
| PredefinedMenuItemType::Copy
| PredefinedMenuItemType::Cut
| PredefinedMenuItemType::Paste
| PredefinedMenuItemType::SelectAll
| PredefinedMenuItemType::About(_)
)
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk4"
))]
pub(crate) fn is_supported(&self) -> bool {
matches!(
self,
PredefinedMenuItemType::Separator
| PredefinedMenuItemType::Minimize
| PredefinedMenuItemType::Maximize
| PredefinedMenuItemType::Fullscreen
| PredefinedMenuItemType::Hide
| PredefinedMenuItemType::CloseWindow
| PredefinedMenuItemType::Quit
| PredefinedMenuItemType::About(_)
)
}
#[cfg(target_os = "macos")]
pub(crate) fn is_supported(&self) -> bool {
matches!(
self,
PredefinedMenuItemType::Separator
| PredefinedMenuItemType::SectionHeader
| PredefinedMenuItemType::Copy
| PredefinedMenuItemType::Cut
| PredefinedMenuItemType::Paste
| PredefinedMenuItemType::PasteAndMatchStyle
| PredefinedMenuItemType::Delete
| PredefinedMenuItemType::SelectAll
| PredefinedMenuItemType::Undo
| PredefinedMenuItemType::Redo
| PredefinedMenuItemType::Minimize
| PredefinedMenuItemType::Maximize
| PredefinedMenuItemType::ActualSize
| PredefinedMenuItemType::ZoomIn
| PredefinedMenuItemType::ZoomOut
| PredefinedMenuItemType::Fullscreen
| PredefinedMenuItemType::Hide
| PredefinedMenuItemType::HideOthers
| PredefinedMenuItemType::ShowAll
| PredefinedMenuItemType::CloseWindow
| PredefinedMenuItemType::Quit
| PredefinedMenuItemType::About(_)
| PredefinedMenuItemType::Services
| PredefinedMenuItemType::BringAllToFront
| PredefinedMenuItemType::StartSpeaking
| PredefinedMenuItemType::StopSpeaking
| PredefinedMenuItemType::StartDictation
| PredefinedMenuItemType::EmojiAndSymbols
)
}
fn is_enabled(&self) -> bool {
self.is_supported() && !matches!(self, PredefinedMenuItemType::SectionHeader)
}
pub(crate) fn accelerator(&self) -> Option<MenuAccelerator> {
match self {
PredefinedMenuItemType::Copy => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyC)),
PredefinedMenuItemType::Cut => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyX)),
PredefinedMenuItemType::Paste => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyV)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::PasteAndMatchStyle => Some(physical_accelerator(
CMD_OR_CTRL | Modifiers::ALT | Modifiers::SHIFT,
Code::KeyV,
)),
PredefinedMenuItemType::Undo => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyZ)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::Redo => Some(physical_accelerator(
CMD_OR_CTRL | Modifiers::SHIFT,
Code::KeyZ,
)),
#[cfg(not(target_os = "macos"))]
PredefinedMenuItemType::Redo => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyY)),
PredefinedMenuItemType::SelectAll => {
Some(physical_accelerator(CMD_OR_CTRL, Code::KeyA))
}
PredefinedMenuItemType::Minimize => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyM)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::ActualSize => {
Some(physical_accelerator(CMD_OR_CTRL, Code::Digit0))
}
#[cfg(target_os = "macos")]
PredefinedMenuItemType::ZoomIn => Some(physical_accelerator(
CMD_OR_CTRL | Modifiers::SHIFT,
Code::Equal,
)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::ZoomOut => Some(physical_accelerator(CMD_OR_CTRL, Code::Minus)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::Fullscreen => Some(physical_accelerator(
Modifiers::META | Modifiers::CONTROL,
Code::KeyF,
)),
PredefinedMenuItemType::Hide => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyH)),
PredefinedMenuItemType::HideOthers => Some(physical_accelerator(
CMD_OR_CTRL | Modifiers::ALT,
Code::KeyH,
)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::CloseWindow => {
Some(physical_accelerator(CMD_OR_CTRL, Code::KeyW))
}
#[cfg(not(target_os = "macos"))]
PredefinedMenuItemType::CloseWindow => {
Some(physical_accelerator(Modifiers::ALT, Code::F4))
}
#[cfg(target_os = "macos")]
PredefinedMenuItemType::Quit => Some(physical_accelerator(CMD_OR_CTRL, Code::KeyQ)),
#[cfg(target_os = "macos")]
PredefinedMenuItemType::EmojiAndSymbols => Some(physical_accelerator(
Modifiers::META | Modifiers::CONTROL,
Code::Space,
)),
_ => None,
}
}
}
fn physical_accelerator(modifiers: Modifiers, key: Code) -> MenuAccelerator {
MenuAccelerator::Physical(Accelerator::new(modifiers, key))
}
#[cfg(target_os = "macos")]
fn escape_mnemonic(text: &str) -> String {
text.replace('&', "&&")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_about_metadata() {
assert_eq!(
AboutMetadata {
..Default::default()
}
.full_version(),
None
);
assert_eq!(
AboutMetadata {
version: Some("Version: 1.inner".into()),
..Default::default()
}
.full_version(),
Some("Version: 1.inner".into())
);
assert_eq!(
AboutMetadata {
version: Some("Version: 1.inner".into()),
short_version: Some("Universal".into()),
..Default::default()
}
.full_version(),
Some("Version: 1.inner (Universal)".into())
);
}
}