// Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{collections::HashMap, sync::Mutex}; use serde::{Deserialize, Serialize}; use tauri_runtime::dpi::Position; use super::{sealed::ContextMenuBase, *}; use crate::{ command, image::JsImage, ipc::{channel::JavaScriptChannelId, Channel}, plugin::{Builder, TauriPlugin}, resources::ResourceId, sealed::ManagerBase, Manager, ResourceTable, RunEvent, Runtime, State, Webview, Window, }; use tauri_macros::do_menu_item; #[derive(Deserialize, Serialize)] pub(crate) enum ItemKind { Menu, MenuItem, Predefined, Submenu, Check, Icon, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct AboutMetadata { pub name: Option, pub version: Option, pub short_version: Option, pub authors: Option>, pub comments: Option, pub copyright: Option, pub license: Option, pub website: Option, pub website_label: Option, pub credits: Option, pub icon: Option, } impl AboutMetadata { pub fn into_metadata( self, resources_table: &ResourceTable, ) -> crate::Result> { let icon = match self.icon { Some(i) => Some(i.into_img(resources_table)?.as_ref().clone()), None => None, }; Ok(super::AboutMetadata { name: self.name, version: self.version, short_version: self.short_version, authors: self.authors, comments: self.comments, copyright: self.copyright, license: self.license, website: self.website, website_label: self.website_label, credits: self.credits, icon, }) } } #[allow(clippy::large_enum_variant)] #[derive(Deserialize)] enum Predefined { Separator, Copy, Cut, Paste, SelectAll, Undo, Redo, Minimize, Maximize, Fullscreen, Hide, HideOthers, ShowAll, CloseWindow, Quit, About(Option), Services, } #[derive(Deserialize)] struct SubmenuPayload { id: Option, text: String, enabled: Option, items: Vec, } impl SubmenuPayload { pub fn create_item( self, webview: &Webview, resources_table: &ResourceTable, ) -> crate::Result> { let mut builder = if let Some(id) = self.id { SubmenuBuilder::with_id(webview, id, self.text) } else { SubmenuBuilder::new(webview, self.text) }; if let Some(enabled) = self.enabled { builder = builder.enabled(enabled); } for item in self.items { builder = item.with_item(webview, resources_table, |i| Ok(builder.item(i)))?; } builder.build() } } #[derive(Deserialize)] struct CheckMenuItemPayload { handler: Option, id: Option, text: String, checked: bool, enabled: Option, accelerator: Option, } impl CheckMenuItemPayload { pub fn create_item(self, webview: &Webview) -> crate::Result> { let mut builder = if let Some(id) = self.id { CheckMenuItemBuilder::with_id(id, self.text) } else { CheckMenuItemBuilder::new(self.text) }; if let Some(accelerator) = self.accelerator { builder = builder.accelerator(accelerator); } if let Some(enabled) = self.enabled { builder = builder.enabled(enabled); } let item = builder.checked(self.checked).build(webview)?; if let Some(handler) = self.handler { let handler = handler.channel_on(webview.clone()); webview .state::() .0 .lock() .unwrap() .insert(item.id().clone(), handler); } Ok(item) } } #[derive(Deserialize)] #[serde(untagged)] enum Icon { Native(NativeIcon), Icon(JsImage), } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct IconMenuItemPayload { handler: Option, id: Option, text: String, icon: Icon, enabled: Option, accelerator: Option, } impl IconMenuItemPayload { pub fn create_item( self, webview: &Webview, resources_table: &ResourceTable, ) -> crate::Result> { let mut builder = if let Some(id) = self.id { IconMenuItemBuilder::with_id(id, self.text) } else { IconMenuItemBuilder::new(self.text) }; if let Some(accelerator) = self.accelerator { builder = builder.accelerator(accelerator); } if let Some(enabled) = self.enabled { builder = builder.enabled(enabled); } builder = match self.icon { Icon::Native(native_icon) => builder.native_icon(native_icon), Icon::Icon(icon) => builder.icon(icon.into_img(resources_table)?.as_ref().clone()), }; let item = builder.build(webview)?; if let Some(handler) = self.handler { let handler = handler.channel_on(webview.clone()); webview .state::() .0 .lock() .unwrap() .insert(item.id().clone(), handler); } Ok(item) } } #[derive(Deserialize)] struct MenuItemPayload { handler: Option, id: Option, text: String, enabled: Option, accelerator: Option, } impl MenuItemPayload { pub fn create_item(self, webview: &Webview) -> crate::Result> { let mut builder = if let Some(id) = self.id { MenuItemBuilder::with_id(id, self.text) } else { MenuItemBuilder::new(self.text) }; if let Some(accelerator) = self.accelerator { builder = builder.accelerator(accelerator); } if let Some(enabled) = self.enabled { builder = builder.enabled(enabled); } let item = builder.build(webview)?; if let Some(handler) = self.handler { let handler = handler.channel_on(webview.clone()); webview .state::() .0 .lock() .unwrap() .insert(item.id().clone(), handler); } Ok(item) } } #[derive(Deserialize)] struct PredefinedMenuItemPayload { item: Predefined, text: Option, } impl PredefinedMenuItemPayload { pub fn create_item( self, webview: &Webview, resources_table: &ResourceTable, ) -> crate::Result> { match self.item { Predefined::Separator => PredefinedMenuItem::separator(webview), Predefined::Copy => PredefinedMenuItem::copy(webview, self.text.as_deref()), Predefined::Cut => PredefinedMenuItem::cut(webview, self.text.as_deref()), Predefined::Paste => PredefinedMenuItem::paste(webview, self.text.as_deref()), Predefined::SelectAll => PredefinedMenuItem::select_all(webview, self.text.as_deref()), Predefined::Undo => PredefinedMenuItem::undo(webview, self.text.as_deref()), Predefined::Redo => PredefinedMenuItem::redo(webview, self.text.as_deref()), Predefined::Minimize => PredefinedMenuItem::minimize(webview, self.text.as_deref()), Predefined::Maximize => PredefinedMenuItem::maximize(webview, self.text.as_deref()), Predefined::Fullscreen => PredefinedMenuItem::fullscreen(webview, self.text.as_deref()), Predefined::Hide => PredefinedMenuItem::hide(webview, self.text.as_deref()), Predefined::HideOthers => PredefinedMenuItem::hide_others(webview, self.text.as_deref()), Predefined::ShowAll => PredefinedMenuItem::show_all(webview, self.text.as_deref()), Predefined::CloseWindow => PredefinedMenuItem::close_window(webview, self.text.as_deref()), Predefined::Quit => PredefinedMenuItem::quit(webview, self.text.as_deref()), Predefined::About(metadata) => { let metadata = match metadata { Some(m) => Some(m.into_metadata(resources_table)?), None => None, }; PredefinedMenuItem::about(webview, self.text.as_deref(), metadata) } Predefined::Services => PredefinedMenuItem::services(webview, self.text.as_deref()), } } } #[derive(Deserialize)] #[serde(untagged)] // Note, order matters for untagged enum deserialization enum MenuItemPayloadKind { ExistingItem((ResourceId, ItemKind)), Predefined(PredefinedMenuItemPayload), Check(CheckMenuItemPayload), Icon(IconMenuItemPayload), Submenu(SubmenuPayload), MenuItem(MenuItemPayload), } impl MenuItemPayloadKind { pub fn with_item) -> crate::Result>( self, webview: &Webview, resources_table: &ResourceTable, f: F, ) -> crate::Result { match self { Self::ExistingItem((rid, kind)) => { do_menu_item!(resources_table, rid, kind, |i| f(&*i)) } Self::Submenu(i) => f(&i.create_item(webview, resources_table)?), Self::Predefined(i) => f(&i.create_item(webview, resources_table)?), Self::Check(i) => f(&i.create_item(webview)?), Self::Icon(i) => f(&i.create_item(webview, resources_table)?), Self::MenuItem(i) => f(&i.create_item(webview)?), } } } #[derive(Deserialize, Default)] #[serde(rename_all = "camelCase")] struct NewOptions { id: Option, text: Option, enabled: Option, checked: Option, accelerator: Option, #[serde(rename = "item")] predefined_item: Option, icon: Option, items: Option>, } #[command(root = "crate")] fn new( app: Webview, webview: Webview, kind: ItemKind, options: Option, channels: State<'_, MenuChannels>, handler: Channel, ) -> crate::Result<(ResourceId, MenuId)> { let options = options.unwrap_or_default(); let mut resources_table = app.resources_table(); let (rid, id) = match kind { ItemKind::Menu => { let mut builder = MenuBuilder::new(&app); if let Some(id) = options.id { builder = builder.id(id); } if let Some(items) = options.items { for item in items { builder = item.with_item(&webview, &resources_table, |i| Ok(builder.item(i)))?; } } let menu = builder.build()?; let id = menu.id().clone(); let rid = resources_table.add(menu); (rid, id) } ItemKind::Submenu => { let submenu = SubmenuPayload { id: options.id, text: options.text.unwrap_or_default(), enabled: options.enabled, items: options.items.unwrap_or_default(), } .create_item(&webview, &resources_table)?; let id = submenu.id().clone(); let rid = resources_table.add(submenu); (rid, id) } ItemKind::MenuItem => { let item = MenuItemPayload { // handler managed in this function instead handler: None, id: options.id, text: options.text.unwrap_or_default(), enabled: options.enabled, accelerator: options.accelerator, } .create_item(&webview)?; let id = item.id().clone(); let rid = resources_table.add(item); (rid, id) } ItemKind::Predefined => { let item = PredefinedMenuItemPayload { item: options.predefined_item.unwrap(), text: options.text, } .create_item(&webview, &resources_table)?; let id = item.id().clone(); let rid = resources_table.add(item); (rid, id) } ItemKind::Check => { let item = CheckMenuItemPayload { // handler managed in this function instead handler: None, id: options.id, text: options.text.unwrap_or_default(), checked: options.checked.unwrap_or_default(), enabled: options.enabled, accelerator: options.accelerator, } .create_item(&webview)?; let id = item.id().clone(); let rid = resources_table.add(item); (rid, id) } ItemKind::Icon => { let item = IconMenuItemPayload { // handler managed in this function instead handler: None, id: options.id, text: options.text.unwrap_or_default(), icon: options.icon.unwrap_or(Icon::Native(NativeIcon::User)), enabled: options.enabled, accelerator: options.accelerator, } .create_item(&webview, &resources_table)?; let id = item.id().clone(); let rid = resources_table.add(item); (rid, id) } }; channels.0.lock().unwrap().insert(id.clone(), handler); Ok((rid, id)) } #[command(root = "crate")] fn append( webview: Webview, rid: ResourceId, kind: ItemKind, items: Vec, ) -> crate::Result<()> { let resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| menu.append(i))?; } } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| submenu.append(i))?; } } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(()) } #[command(root = "crate")] fn prepend( webview: Webview, rid: ResourceId, kind: ItemKind, items: Vec, ) -> crate::Result<()> { let resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| menu.prepend(i))?; } } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| submenu.prepend(i))?; } } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(()) } #[command(root = "crate")] fn insert( webview: Webview, rid: ResourceId, kind: ItemKind, items: Vec, mut position: usize, ) -> crate::Result<()> { let resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| menu.insert(i, position))?; position += 1 } } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; for item in items { item.with_item(&webview, &resources_table, |i| submenu.insert(i, position))?; position += 1 } } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(()) } #[command(root = "crate")] fn remove( webview: Webview, rid: ResourceId, kind: ItemKind, item: (ResourceId, ItemKind), ) -> crate::Result<()> { let resources_table = webview.resources_table(); let (item_rid, item_kind) = item; match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; do_menu_item!(resources_table, item_rid, item_kind, |i| menu.remove(&*i))?; } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; do_menu_item!(resources_table, item_rid, item_kind, |i| submenu .remove(&*i))?; } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(()) } macro_rules! make_item_resource { ($resources_table:ident, $item:ident) => {{ let id = $item.id().clone(); let (rid, kind) = match $item { MenuItemKind::MenuItem(i) => ($resources_table.add(i), ItemKind::MenuItem), MenuItemKind::Submenu(i) => ($resources_table.add(i), ItemKind::Submenu), MenuItemKind::Predefined(i) => ($resources_table.add(i), ItemKind::Predefined), MenuItemKind::Check(i) => ($resources_table.add(i), ItemKind::Check), MenuItemKind::Icon(i) => ($resources_table.add(i), ItemKind::Icon), }; (rid, id, kind) }}; } #[command(root = "crate")] fn remove_at( webview: Webview, rid: ResourceId, kind: ItemKind, position: usize, ) -> crate::Result> { let mut resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; if let Some(item) = menu.remove_at(position)? { return Ok(Some(make_item_resource!(resources_table, item))); } } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; if let Some(item) = submenu.remove_at(position)? { return Ok(Some(make_item_resource!(resources_table, item))); } } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(None) } #[command(root = "crate")] fn items( webview: Webview, rid: ResourceId, kind: ItemKind, ) -> crate::Result> { let mut resources_table = webview.resources_table(); let items = match kind { ItemKind::Menu => resources_table.get::>(rid)?.items()?, ItemKind::Submenu => resources_table.get::>(rid)?.items()?, _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok( items .into_iter() .map(|i| make_item_resource!(resources_table, i)) .collect::>(), ) } #[command(root = "crate")] fn get( webview: Webview, rid: ResourceId, kind: ItemKind, id: MenuId, ) -> crate::Result> { let mut resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; if let Some(item) = menu.get(&id) { return Ok(Some(make_item_resource!(resources_table, item))); } } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; if let Some(item) = submenu.get(&id) { return Ok(Some(make_item_resource!(resources_table, item))); } } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; Ok(None) } #[command(root = "crate")] async fn popup( webview: Webview, current_window: Window, rid: ResourceId, kind: ItemKind, window: Option, at: Option, ) -> crate::Result<()> { let window = window .map(|w| webview.manager().get_window(&w)) .unwrap_or(Some(current_window)); if let Some(window) = window { let resources_table = webview.resources_table(); match kind { ItemKind::Menu => { let menu = resources_table.get::>(rid)?; menu.popup_inner(window, at)?; } ItemKind::Submenu => { let submenu = resources_table.get::>(rid)?; submenu.popup_inner(window, at)?; } _ => return Err(anyhow::anyhow!("unexpected menu item kind").into()), }; } Ok(()) } #[command(root = "crate")] fn create_default( app: AppHandle, webview: Webview, ) -> crate::Result<(ResourceId, MenuId)> { let mut resources_table = webview.resources_table(); let menu = Menu::default(&app)?; let id = menu.id().clone(); let rid = resources_table.add(menu); Ok((rid, id)) } #[command(root = "crate")] async fn set_as_app_menu( webview: Webview, rid: ResourceId, ) -> crate::Result> { let mut resources_table = webview.resources_table(); let menu = resources_table.get::>(rid)?; if let Some(menu) = menu.set_as_app_menu()? { let id = menu.id().clone(); let rid = resources_table.add(menu); return Ok(Some((rid, id))); } Ok(None) } #[command(root = "crate")] async fn set_as_window_menu( webview: Webview, current_window: Window, rid: ResourceId, window: Option, ) -> crate::Result> { let window = window .map(|w| webview.manager().get_window(&w)) .unwrap_or(Some(current_window)); if let Some(window) = window { let mut resources_table = webview.resources_table(); let menu = resources_table.get::>(rid)?; if let Some(menu) = menu.set_as_window_menu(&window)? { let id = menu.id().clone(); let rid = resources_table.add(menu); return Ok(Some((rid, id))); } } Ok(None) } #[command(root = "crate")] fn text(webview: Webview, rid: ResourceId, kind: ItemKind) -> crate::Result { let resources_table = webview.resources_table(); do_menu_item!(resources_table, rid, kind, |i| i.text()) } #[command(root = "crate")] fn set_text( webview: Webview, rid: ResourceId, kind: ItemKind, text: String, ) -> crate::Result<()> { let resources_table = webview.resources_table(); do_menu_item!(resources_table, rid, kind, |i| i.set_text(text)) } #[command(root = "crate")] fn is_enabled( webview: Webview, rid: ResourceId, kind: ItemKind, ) -> crate::Result { let resources_table = webview.resources_table(); do_menu_item!(resources_table, rid, kind, |i| i.is_enabled(), !Predefined) } #[command(root = "crate")] fn set_enabled( webview: Webview, rid: ResourceId, kind: ItemKind, enabled: bool, ) -> crate::Result<()> { let resources_table = webview.resources_table(); do_menu_item!( resources_table, rid, kind, |i| i.set_enabled(enabled), !Predefined ) } #[command(root = "crate")] fn set_accelerator( webview: Webview, rid: ResourceId, kind: ItemKind, accelerator: Option, ) -> crate::Result<()> { let resources_table = webview.resources_table(); do_menu_item!( resources_table, rid, kind, |i| i.set_accelerator(accelerator), !Predefined | !Submenu ) } #[command(root = "crate")] fn set_as_windows_menu_for_nsapp( webview: Webview, rid: ResourceId, ) -> crate::Result<()> { #[cfg(target_os = "macos")] { let resources_table = webview.resources_table(); let submenu = resources_table.get::>(rid)?; submenu.set_as_help_menu_for_nsapp()?; } let _ = rid; let _ = webview; Ok(()) } #[command(root = "crate")] fn set_as_help_menu_for_nsapp( webview: Webview, rid: ResourceId, ) -> crate::Result<()> { #[cfg(target_os = "macos")] { let resources_table = webview.resources_table(); let submenu = resources_table.get::>(rid)?; submenu.set_as_help_menu_for_nsapp()?; } let _ = rid; let _ = webview; Ok(()) } #[command(root = "crate")] fn is_checked(webview: Webview, rid: ResourceId) -> crate::Result { let resources_table = webview.resources_table(); let check_item = resources_table.get::>(rid)?; check_item.is_checked() } #[command(root = "crate")] fn set_checked( webview: Webview, rid: ResourceId, checked: bool, ) -> crate::Result<()> { let resources_table = webview.resources_table(); let check_item = resources_table.get::>(rid)?; check_item.set_checked(checked) } #[command(root = "crate")] fn set_icon( webview: Webview, rid: ResourceId, icon: Option, ) -> crate::Result<()> { let resources_table = webview.resources_table(); let icon_item = resources_table.get::>(rid)?; match icon { Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)), Some(Icon::Icon(icon)) => { icon_item.set_icon(Some(icon.into_img(&resources_table)?.as_ref().clone())) } None => { icon_item.set_icon(None)?; icon_item.set_native_icon(None)?; Ok(()) } } } struct MenuChannels(Mutex>>); pub(crate) fn init() -> TauriPlugin { Builder::new("menu") .setup(|app, _api| { app.manage(MenuChannels(Mutex::default())); Ok(()) }) .on_event(|app, e| { if let RunEvent::MenuEvent(e) = e { if let Some(channel) = app.state::().0.lock().unwrap().get(&e.id) { let _ = channel.send(e.id.clone()); } } }) .invoke_handler(crate::generate_handler![ new, append, prepend, insert, remove, remove_at, items, get, popup, create_default, set_as_app_menu, set_as_window_menu, text, set_text, is_enabled, set_enabled, set_accelerator, set_as_windows_menu_for_nsapp, set_as_help_menu_for_nsapp, is_checked, set_checked, set_icon, ]) .build() }