Sfoglia il codice sorgente

feat(core): window menus (#1745)

Lucas Fernandes Nogueira 4 anni fa
parent
commit
41d5d6aff2

+ 5 - 0
.changes/menu.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Implemented window menus APIs.

+ 1 - 1
core/tauri/Cargo.toml

@@ -24,7 +24,7 @@ thiserror = "1.0.24"
 once_cell = "1.7.2"
 tauri-macros = { version = "1.0.0-beta-rc.1", path = "../tauri-macros" }
 tauri-utils = { version = "1.0.0-beta-rc.1", path = "../tauri-utils" }
-wry = { git = "https://github.com/tauri-apps/wry", rev = "0570dcab90087af5b1d29218d9d25186a7ade357" }
+wry = { git = "https://github.com/tauri-apps/wry", rev = "6bc97aff525644b83a3a00537316c46d7afb985b" }
 rand = "0.8"
 reqwest = { version = "0.11", features = [ "json", "multipart" ] }
 tempfile = "3"

+ 4 - 2
core/tauri/src/lib.rs

@@ -59,10 +59,12 @@ pub use {
     Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokeResolver, InvokeResponse, OnPageLoad,
     PageLoadPayload, SetupHook,
   },
-  self::runtime::app::{App, Builder},
+  self::runtime::app::{App, Builder, WindowMenuEvent},
   self::runtime::flavors::wry::Wry,
   self::runtime::monitor::Monitor,
-  self::runtime::webview::{WebviewAttributes, WindowBuilder},
+  self::runtime::webview::{
+    CustomMenuItem, Menu, MenuItem, MenuItemId, WebviewAttributes, WindowBuilder,
+  },
   self::runtime::window::{
     export::{
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},

+ 46 - 1
core/tauri/src/runtime/app.rs

@@ -10,7 +10,7 @@ use crate::{
     flavors::wry::Wry,
     manager::{Args, WindowManager},
     tag::Tag,
-    webview::{CustomProtocol, WebviewAttributes, WindowBuilder},
+    webview::{CustomProtocol, Menu, MenuItemId, WebviewAttributes, WindowBuilder},
     window::PendingWindow,
     Dispatch, Runtime,
   },
@@ -23,6 +23,26 @@ use std::{collections::HashMap, sync::Arc};
 #[cfg(feature = "updater")]
 use crate::updater;
 
+pub(crate) type GlobalMenuEventListener<P> = Box<dyn Fn(WindowMenuEvent<P>) + Send + Sync>;
+
+/// A menu event that was triggered on a window.
+pub struct WindowMenuEvent<P: Params> {
+  pub(crate) menu_item_id: MenuItemId,
+  pub(crate) window: Window<P>,
+}
+
+impl<P: Params> WindowMenuEvent<P> {
+  /// The menu item id.
+  pub fn menu_item_id(&self) -> MenuItemId {
+    self.menu_item_id
+  }
+
+  /// The window that the menu belongs to.
+  pub fn window(&self) -> &Window<P> {
+    &self.window
+  }
+}
+
 /// A handle to the currently running application.
 ///
 /// This type implements [`Manager`] which allows for manipulation of global application items.
@@ -154,6 +174,12 @@ where
 
   /// App state.
   state: StateManager,
+
+  /// The menu set to all windows.
+  menu: Vec<Menu>,
+
+  /// Menu event handlers that listens to all windows.
+  menu_event_listeners: Vec<GlobalMenuEventListener<Args<E, L, A, R>>>,
 }
 
 impl<E, L, A, R> Builder<E, L, A, R>
@@ -173,6 +199,8 @@ where
       plugins: PluginStore::default(),
       uri_scheme_protocols: Default::default(),
       state: StateManager::new(),
+      menu: Vec::new(),
+      menu_event_listeners: Vec::new(),
     }
   }
 
@@ -286,6 +314,21 @@ where
     self
   }
 
+  /// Sets the menu to use on all windows.
+  pub fn menu(mut self, menu: Vec<Menu>) -> Self {
+    self.menu = menu;
+    self
+  }
+
+  /// Registers a menu event handler for all windows.
+  pub fn on_menu_event<F: Fn(WindowMenuEvent<Args<E, L, A, R>>) + Send + Sync + 'static>(
+    mut self,
+    handler: F,
+  ) -> Self {
+    self.menu_event_listeners.push(Box::new(handler));
+    self
+  }
+
   /// Registers a URI scheme protocol available to all webviews.
   /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS,
   /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows
@@ -321,6 +364,8 @@ where
       self.on_page_load,
       self.uri_scheme_protocols,
       self.state,
+      self.menu,
+      self.menu_event_listeners,
     );
 
     // set up all the windows defined in the config

+ 143 - 48
core/tauri/src/runtime/flavors/wry.rs

@@ -8,12 +8,12 @@ use crate::{
   api::config::WindowConfig,
   runtime::{
     webview::{
-      FileDropEvent, FileDropHandler, RpcRequest, WebviewRpcHandler, WindowBuilder,
-      WindowBuilderBase,
+      CustomMenuItem, FileDropEvent, FileDropHandler, Menu, MenuItem, MenuItemId, RpcRequest,
+      WebviewRpcHandler, WindowBuilder, WindowBuilderBase,
     },
     window::{
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
-      DetachedWindow, PendingWindow, WindowEvent,
+      DetachedWindow, MenuEvent, PendingWindow, WindowEvent,
     },
     Dispatch, Monitor, Params, Runtime,
   },
@@ -31,6 +31,10 @@ use wry::{
     },
     event::{Event, WindowEvent as WryWindowEvent},
     event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget},
+    menu::{
+      CustomMenu as WryCustomMenu, Menu as WryMenu, MenuId as WryMenuId, MenuItem as WryMenuItem,
+      MenuType,
+    },
     monitor::MonitorHandle,
     window::{Fullscreen, Icon as WindowIcon, Window, WindowBuilder as WryWindowBuilder, WindowId},
   },
@@ -54,6 +58,8 @@ type CreateWebviewHandler =
 type MainThreadTask = Box<dyn FnOnce() + Send>;
 type WindowEventHandler = Box<dyn Fn(&WindowEvent) + Send>;
 type WindowEventListeners = Arc<Mutex<HashMap<Uuid, WindowEventHandler>>>;
+type MenuEventHandler = Box<dyn Fn(&MenuEvent) + Send>;
+type MenuEventListeners = Arc<Mutex<HashMap<Uuid, MenuEventHandler>>>;
 
 #[repr(C)]
 #[derive(Debug)]
@@ -195,6 +201,50 @@ impl From<Position> for WryPosition {
   }
 }
 
+impl From<CustomMenuItem> for WryCustomMenu {
+  fn from(item: CustomMenuItem) -> Self {
+    Self {
+      id: WryMenuId(item.id.0),
+      name: item.name,
+      keyboard_accelerators: None,
+    }
+  }
+}
+
+impl From<MenuItem> for WryMenuItem {
+  fn from(item: MenuItem) -> Self {
+    match item {
+      MenuItem::Custom(custom) => Self::Custom(custom.into()),
+      MenuItem::About(v) => Self::About(v),
+      MenuItem::Hide => Self::Hide,
+      MenuItem::Services => Self::Services,
+      MenuItem::HideOthers => Self::HideOthers,
+      MenuItem::ShowAll => Self::ShowAll,
+      MenuItem::CloseWindow => Self::CloseWindow,
+      MenuItem::Quit => Self::Quit,
+      MenuItem::Copy => Self::Copy,
+      MenuItem::Cut => Self::Cut,
+      MenuItem::Undo => Self::Undo,
+      MenuItem::Redo => Self::Redo,
+      MenuItem::SelectAll => Self::SelectAll,
+      MenuItem::Paste => Self::Paste,
+      MenuItem::EnterFullScreen => Self::EnterFullScreen,
+      MenuItem::Minimize => Self::Minimize,
+      MenuItem::Zoom => Self::Zoom,
+      MenuItem::Separator => Self::Separator,
+    }
+  }
+}
+
+impl From<Menu> for WryMenu {
+  fn from(menu: Menu) -> Self {
+    Self {
+      title: menu.title,
+      items: menu.items.into_iter().map(Into::into).collect(),
+    }
+  }
+}
+
 impl WindowBuilderBase for WryWindowBuilder {}
 impl WindowBuilder for WryWindowBuilder {
   fn new() -> Self {
@@ -226,6 +276,10 @@ impl WindowBuilder for WryWindowBuilder {
     window
   }
 
+  fn menu(self, menu: Vec<Menu>) -> Self {
+    self.with_menu(menu.into_iter().map(Into::into).collect::<Vec<WryMenu>>())
+  }
+
   fn position(self, x: f64, y: f64) -> Self {
     self.with_position(WryLogicalPosition::new(x, y))
   }
@@ -285,6 +339,10 @@ impl WindowBuilder for WryWindowBuilder {
   fn has_icon(&self) -> bool {
     self.window.window_icon.is_some()
   }
+
+  fn has_menu(&self) -> bool {
+    self.window.window_menu.is_some()
+  }
 }
 
 impl From<WryRpcRequest> for RpcRequest {
@@ -353,19 +411,26 @@ enum Message {
   CreateWebview(Arc<Mutex<Option<CreateWebviewHandler>>>, Sender<WindowId>),
 }
 
-/// The Tauri [`Dispatch`] for [`Wry`].
 #[derive(Clone)]
-pub struct WryDispatcher {
-  window_id: WindowId,
+struct DispatcherContext {
   proxy: EventLoopProxy<Message>,
   task_tx: Sender<MainThreadTask>,
   window_event_listeners: WindowEventListeners,
+  menu_event_listeners: MenuEventListeners,
+}
+
+/// The Tauri [`Dispatch`] for [`Wry`].
+#[derive(Clone)]
+pub struct WryDispatcher {
+  window_id: WindowId,
+  context: DispatcherContext,
 }
 
 macro_rules! dispatcher_getter {
   ($self: ident, $message: expr) => {{
     let (tx, rx) = channel();
     $self
+      .context
       .proxy
       .send_event(Message::Window($self.window_id, $message(tx)))
       .map_err(|_| crate::Error::FailedToSendMessage)?;
@@ -398,6 +463,7 @@ impl Dispatch for WryDispatcher {
 
   fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> crate::Result<()> {
     self
+      .context
       .task_tx
       .send(Box::new(f))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -406,6 +472,7 @@ impl Dispatch for WryDispatcher {
   fn on_window_event<F: Fn(&WindowEvent) + Send + 'static>(&self, f: F) -> Uuid {
     let id = Uuid::new_v4();
     self
+      .context
       .window_event_listeners
       .lock()
       .unwrap()
@@ -413,6 +480,17 @@ impl Dispatch for WryDispatcher {
     id
   }
 
+  fn on_menu_event<F: Fn(&MenuEvent) + Send + 'static>(&self, f: F) -> Uuid {
+    let id = Uuid::new_v4();
+    self
+      .context
+      .menu_event_listeners
+      .lock()
+      .unwrap()
+      .insert(id, Box::new(f));
+    id
+  }
+
   // Getters
 
   fn scale_factor(&self) -> crate::Result<f64> {
@@ -464,6 +542,7 @@ impl Dispatch for WryDispatcher {
 
   fn print(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Webview(self.window_id, WebviewMessage::Print))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -475,14 +554,13 @@ impl Dispatch for WryDispatcher {
   ) -> crate::Result<DetachedWindow<M>> {
     let (tx, rx) = channel();
     let label = pending.label.clone();
-    let proxy = self.proxy.clone();
-    let task_tx = self.task_tx.clone();
-    let window_event_listeners = self.window_event_listeners.clone();
+    let context = self.context.clone();
     self
+      .context
       .proxy
       .send_event(Message::CreateWebview(
         Arc::new(Mutex::new(Some(Box::new(move |event_loop| {
-          create_webview(event_loop, proxy, task_tx, window_event_listeners, pending)
+          create_webview(event_loop, context, pending)
         })))),
         tx,
       ))
@@ -490,15 +568,14 @@ impl Dispatch for WryDispatcher {
     let window_id = rx.recv().unwrap();
     let dispatcher = WryDispatcher {
       window_id,
-      proxy: self.proxy.clone(),
-      task_tx: self.task_tx.clone(),
-      window_event_listeners: self.window_event_listeners.clone(),
+      context: self.context.clone(),
     };
     Ok(DetachedWindow { label, dispatcher })
   }
 
   fn set_resizable(&self, resizable: bool) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -509,6 +586,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_title<S: Into<String>>(&self, title: S) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -519,6 +597,7 @@ impl Dispatch for WryDispatcher {
 
   fn maximize(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Maximize))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -526,6 +605,7 @@ impl Dispatch for WryDispatcher {
 
   fn unmaximize(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Unmaximize))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -533,6 +613,7 @@ impl Dispatch for WryDispatcher {
 
   fn minimize(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Minimize))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -540,6 +621,7 @@ impl Dispatch for WryDispatcher {
 
   fn unminimize(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Unminimize))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -547,6 +629,7 @@ impl Dispatch for WryDispatcher {
 
   fn show(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Show))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -554,6 +637,7 @@ impl Dispatch for WryDispatcher {
 
   fn hide(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Hide))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -561,6 +645,7 @@ impl Dispatch for WryDispatcher {
 
   fn close(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::Close))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -568,6 +653,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_decorations(&self, decorations: bool) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -578,6 +664,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -588,6 +675,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_size(&self, size: Size) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -598,6 +686,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_min_size(&self, size: Option<Size>) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -608,6 +697,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_max_size(&self, size: Option<Size>) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -618,6 +708,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_position(&self, position: Position) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -628,6 +719,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -638,6 +730,7 @@ impl Dispatch for WryDispatcher {
 
   fn set_icon(&self, icon: Icon) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(
         self.window_id,
@@ -648,6 +741,7 @@ impl Dispatch for WryDispatcher {
 
   fn start_dragging(&self) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Window(self.window_id, WindowMessage::DragWindow))
       .map_err(|_| crate::Error::FailedToSendMessage)
@@ -655,6 +749,7 @@ impl Dispatch for WryDispatcher {
 
   fn eval_script<S: Into<String>>(&self, script: S) -> crate::Result<()> {
     self
+      .context
       .proxy
       .send_event(Message::Webview(
         self.window_id,
@@ -670,6 +765,7 @@ pub struct Wry {
   webviews: HashMap<WindowId, WebView>,
   task_tx: Sender<MainThreadTask>,
   window_event_listeners: WindowEventListeners,
+  menu_event_listeners: MenuEventListeners,
   task_rx: Receiver<MainThreadTask>,
 }
 
@@ -685,6 +781,7 @@ impl Runtime for Wry {
       task_tx,
       task_rx,
       window_event_listeners: Default::default(),
+      menu_event_listeners: Default::default(),
     })
   }
 
@@ -696,17 +793,23 @@ impl Runtime for Wry {
     let proxy = self.event_loop.create_proxy();
     let webview = create_webview(
       &self.event_loop,
-      proxy.clone(),
-      self.task_tx.clone(),
-      self.window_event_listeners.clone(),
+      DispatcherContext {
+        proxy: proxy.clone(),
+        task_tx: self.task_tx.clone(),
+        window_event_listeners: self.window_event_listeners.clone(),
+        menu_event_listeners: self.menu_event_listeners.clone(),
+      },
       pending,
     )?;
 
     let dispatcher = WryDispatcher {
       window_id: webview.window().id(),
-      proxy,
-      task_tx: self.task_tx.clone(),
-      window_event_listeners: self.window_event_listeners.clone(),
+      context: DispatcherContext {
+        proxy,
+        task_tx: self.task_tx.clone(),
+        window_event_listeners: self.window_event_listeners.clone(),
+        menu_event_listeners: self.menu_event_listeners.clone(),
+      },
     };
 
     self.webviews.insert(webview.window().id(), webview);
@@ -718,6 +821,7 @@ impl Runtime for Wry {
     let mut webviews = self.webviews;
     let task_rx = self.task_rx;
     let window_event_listeners = self.window_event_listeners.clone();
+    let menu_event_listeners = self.menu_event_listeners.clone();
     self.event_loop.run(move |event, event_loop, control_flow| {
       *control_flow = ControlFlow::Wait;
 
@@ -732,6 +836,17 @@ impl Runtime for Wry {
       }
 
       match event {
+        Event::MenuEvent {
+          menu_id,
+          origin: MenuType::Menubar,
+        } => {
+          let event = MenuEvent {
+            menu_item_id: MenuItemId(menu_id.0),
+          };
+          for handler in menu_event_listeners.lock().unwrap().values() {
+            handler(&event);
+          }
+        }
         Event::WindowEvent { event, window_id } => {
           if let Some(event) = WindowEventWrapper::from(&event).0 {
             for handler in window_event_listeners.lock().unwrap().values() {
@@ -859,9 +974,7 @@ impl Runtime for Wry {
 
 fn create_webview<M: Params<Runtime = Wry>>(
   event_loop: &EventLoopWindowTarget<Message>,
-  proxy: EventLoopProxy<Message>,
-  task_tx: Sender<MainThreadTask>,
-  window_event_listeners: WindowEventListeners,
+  context: DispatcherContext,
   pending: PendingWindow<M>,
 ) -> crate::Result<WebView> {
   let PendingWindow {
@@ -880,22 +993,12 @@ fn create_webview<M: Params<Runtime = Wry>>(
     .with_url(&url)
     .unwrap(); // safe to unwrap because we validate the URL beforehand
   if let Some(handler) = rpc_handler {
-    webview_builder = webview_builder.with_rpc_handler(create_rpc_handler(
-      proxy.clone(),
-      task_tx.clone(),
-      window_event_listeners.clone(),
-      label.clone(),
-      handler,
-    ));
+    webview_builder =
+      webview_builder.with_rpc_handler(create_rpc_handler(context.clone(), label.clone(), handler));
   }
   if let Some(handler) = file_drop_handler {
-    webview_builder = webview_builder.with_file_drop_handler(create_file_drop_handler(
-      proxy,
-      task_tx,
-      window_event_listeners,
-      label,
-      handler,
-    ));
+    webview_builder =
+      webview_builder.with_file_drop_handler(create_file_drop_handler(context, label, handler));
   }
   for (scheme, protocol) in webview_attributes.uri_scheme_protocols {
     webview_builder = webview_builder.with_custom_protocol(scheme, move |_window, url| {
@@ -916,9 +1019,7 @@ fn create_webview<M: Params<Runtime = Wry>>(
 
 /// Create a wry rpc handler from a tauri rpc handler.
 fn create_rpc_handler<M: Params<Runtime = Wry>>(
-  proxy: EventLoopProxy<Message>,
-  task_tx: Sender<MainThreadTask>,
-  window_event_listeners: WindowEventListeners,
+  context: DispatcherContext,
   label: M::Label,
   handler: WebviewRpcHandler<M>,
 ) -> Box<dyn Fn(&Window, WryRpcRequest) -> Option<RpcResponse> + 'static> {
@@ -927,9 +1028,7 @@ fn create_rpc_handler<M: Params<Runtime = Wry>>(
       DetachedWindow {
         dispatcher: WryDispatcher {
           window_id: window.id(),
-          proxy: proxy.clone(),
-          task_tx: task_tx.clone(),
-          window_event_listeners: window_event_listeners.clone(),
+          context: context.clone(),
         },
         label: label.clone(),
       },
@@ -941,9 +1040,7 @@ fn create_rpc_handler<M: Params<Runtime = Wry>>(
 
 /// Create a wry file drop handler from a tauri file drop handler.
 fn create_file_drop_handler<M: Params<Runtime = Wry>>(
-  proxy: EventLoopProxy<Message>,
-  task_tx: Sender<MainThreadTask>,
-  window_event_listeners: WindowEventListeners,
+  context: DispatcherContext,
   label: M::Label,
   handler: FileDropHandler<M>,
 ) -> Box<dyn Fn(&Window, WryFileDropEvent) -> bool + 'static> {
@@ -953,9 +1050,7 @@ fn create_file_drop_handler<M: Params<Runtime = Wry>>(
       DetachedWindow {
         dispatcher: WryDispatcher {
           window_id: window.id(),
-          proxy: proxy.clone(),
-          task_tx: task_tx.clone(),
-          window_event_listeners: window_event_listeners.clone(),
+          context: context.clone(),
         },
         label: label.clone(),
       },

+ 39 - 2
core/tauri/src/runtime/manager.rs

@@ -13,12 +13,13 @@ use crate::{
   hooks::{InvokeHandler, OnPageLoad, PageLoadPayload},
   plugin::PluginStore,
   runtime::{
+    app::{GlobalMenuEventListener, WindowMenuEvent},
     tag::{tags_to_javascript_array, Tag, TagRef, ToJsString},
     webview::{
-      CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, WebviewRpcHandler,
+      CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, Menu, WebviewRpcHandler,
       WindowBuilder,
     },
-    window::{dpi::PhysicalSize, DetachedWindow, PendingWindow, WindowEvent},
+    window::{dpi::PhysicalSize, DetachedWindow, MenuEvent, PendingWindow, WindowEvent},
     Icon, Runtime,
   },
   sealed::ParamsBase,
@@ -43,6 +44,7 @@ const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
 const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
 const WINDOW_BLUR_EVENT: &str = "tauri://blur";
 const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
+const MENU_EVENT: &str = "tauri://menu";
 
 /// Parse a string representing an internal tauri event into [`Params::Event`]
 ///
@@ -79,6 +81,10 @@ pub struct InnerWindowManager<P: Params> {
   package_info: PackageInfo,
   /// The webview protocols protocols available to all windows.
   uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
+  /// The menu set to all windows.
+  menu: Vec<Menu>,
+  /// Menu event listeners to all windows.
+  menu_event_listeners: Arc<Vec<GlobalMenuEventListener<P>>>,
 }
 
 /// A [Zero Sized Type] marker representing a full [`Params`].
@@ -125,6 +131,7 @@ impl<P: Params> Clone for WindowManager<P> {
 }
 
 impl<P: Params> WindowManager<P> {
+  #[allow(clippy::too_many_arguments)]
   pub(crate) fn with_handlers(
     context: Context<P::Assets>,
     plugins: PluginStore<P>,
@@ -132,6 +139,8 @@ impl<P: Params> WindowManager<P> {
     on_page_load: Box<OnPageLoad<P>>,
     uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
     state: StateManager,
+    menu: Vec<Menu>,
+    menu_event_listeners: Vec<GlobalMenuEventListener<P>>,
   ) -> Self {
     Self {
       inner: Arc::new(InnerWindowManager {
@@ -147,6 +156,8 @@ impl<P: Params> WindowManager<P> {
         salts: Mutex::default(),
         package_info: context.package_info,
         uri_scheme_protocols,
+        menu,
+        menu_event_listeners: Arc::new(menu_event_listeners),
       }),
       _marker: Args::default(),
     }
@@ -209,6 +220,10 @@ impl<P: Params> WindowManager<P> {
       }
     }
 
+    if !pending.window_attributes.has_menu() {
+      pending.window_attributes = pending.window_attributes.menu(self.inner.menu.clone());
+    }
+
     for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
       if !webview_attributes.has_uri_scheme_protocol(uri_scheme) {
         let protocol = protocol.clone();
@@ -414,6 +429,8 @@ mod test {
       Box::new(|_, _| ()),
       Default::default(),
       StateManager::new(),
+      Vec::new(),
+      Default::default(),
     );
 
     #[cfg(custom_protocol)]
@@ -498,6 +515,17 @@ impl<P: Params> WindowManager<P> {
     window.on_window_event(move |event| {
       let _ = on_window_event(&window_, event);
     });
+    let window_ = window.clone();
+    let menu_event_listeners = self.inner.menu_event_listeners.clone();
+    window.on_menu_event(move |event| {
+      let _ = on_menu_event(&window_, event);
+      for handler in menu_event_listeners.iter() {
+        handler(WindowMenuEvent {
+          window: window_.clone(),
+          menu_item_id: event.menu_item_id,
+        });
+      }
+    });
 
     // insert the window into our manager
     {
@@ -686,3 +714,12 @@ struct ScaleFactorChanged {
   scale_factor: f64,
   size: PhysicalSize<u32>,
 }
+
+fn on_menu_event<P: Params>(window: &Window<P>, event: &MenuEvent) -> crate::Result<()> {
+  window.emit(
+    &MENU_EVENT
+      .parse()
+      .unwrap_or_else(|_| panic!("unhandled event")),
+    Some(event),
+  )
+}

+ 4 - 1
core/tauri/src/runtime/mod.rs

@@ -22,7 +22,7 @@ pub mod window;
 use monitor::Monitor;
 use window::{
   dpi::{PhysicalPosition, PhysicalSize, Position, Size},
-  WindowEvent,
+  MenuEvent, WindowEvent,
 };
 
 /// The webview runtime interface.
@@ -57,6 +57,9 @@ pub trait Dispatch: Clone + Send + Sized + 'static {
   /// Registers a window event handler.
   fn on_window_event<F: Fn(&WindowEvent) + Send + 'static>(&self, f: F) -> Uuid;
 
+  /// Registers a window event handler.
+  fn on_menu_event<F: Fn(&MenuEvent) + Send + 'static>(&self, f: F) -> Uuid;
+
   // GETTERS
 
   /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.

+ 219 - 2
core/tauri/src/runtime/webview.rs

@@ -9,9 +9,13 @@ use crate::{
   api::config::{WindowConfig, WindowUrl},
   runtime::window::DetachedWindow,
 };
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
-use std::{collections::HashMap, path::PathBuf};
+use std::{
+  collections::{hash_map::DefaultHasher, HashMap},
+  hash::{Hash, Hasher},
+  path::PathBuf,
+};
 
 type UriSchemeProtocol = dyn Fn(&str) -> crate::Result<Vec<u8>> + Send + Sync + 'static;
 
@@ -92,6 +96,9 @@ pub trait WindowBuilder: WindowBuilderBase {
   /// Initializes a new webview builder from a [`WindowConfig`]
   fn with_config(config: WindowConfig) -> Self;
 
+  /// Sets the menu for the window.
+  fn menu(self, menu: Vec<Menu>) -> Self;
+
   /// The initial position of the window's.
   fn position(self, x: f64, y: f64) -> Self;
 
@@ -134,6 +141,9 @@ pub trait WindowBuilder: WindowBuilderBase {
 
   /// Whether the icon was set or not.
   fn has_icon(&self) -> bool;
+
+  /// Whether the menu was set or not.
+  fn has_menu(&self) -> bool;
 }
 
 /// Rpc request.
@@ -179,3 +189,210 @@ pub(crate) struct InvokePayload {
   #[serde(flatten)]
   pub(crate) inner: JsonValue,
 }
+
+/// A window or system tray menu.
+#[derive(Debug, Clone)]
+pub struct Menu {
+  pub(crate) title: String,
+  pub(crate) items: Vec<MenuItem>,
+}
+
+impl Menu {
+  /// Creates a new menu with the given title and items.
+  pub fn new<T: Into<String>>(title: T, items: Vec<MenuItem>) -> Self {
+    Self {
+      title: title.into(),
+      items,
+    }
+  }
+}
+
+/// Identifier of a custom menu item.
+///
+/// Whenever you receive an event arising from a particular menu, this event contains a `MenuId` which
+/// identifies its origin.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize)]
+pub struct MenuItemId(pub(crate) u32);
+
+impl MenuItemId {
+  fn new<T: Into<String>>(menu_title: T) -> Self {
+    Self(hash_string_to_u32(menu_title.into()))
+  }
+}
+
+fn hash_string_to_u32(title: String) -> u32 {
+  let mut s = DefaultHasher::new();
+  title.hash(&mut s);
+  s.finish() as u32
+}
+
+/// A custom menu item.
+#[derive(Debug, Clone)]
+pub struct CustomMenuItem {
+  pub(crate) id: MenuItemId,
+  pub(crate) name: String,
+}
+
+impl CustomMenuItem {
+  /// Create new custom menu item.
+  pub fn new<T: Into<String>>(title: T) -> Self {
+    let title = title.into();
+    Self {
+      id: MenuItemId::new(&title),
+      name: title,
+    }
+  }
+
+  /// Return unique menu ID. Works only with `MenuItem::Custom`.
+  pub fn id(self) -> MenuItemId {
+    self.id
+  }
+}
+
+/// 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 {
+  /// A custom menu item emits an event inside the EventLoop.
+  Custom(CustomMenuItem),
+
+  /// Shows a standard "About" item
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  About(String),
+
+  /// A standard "hide the app" menu item.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / 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
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  CloseWindow,
+
+  /// A "quit this app" menu icon.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Quit,
+
+  /// A menu item for enabling copying (often text) from responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Copy,
+
+  /// A menu item for enabling cutting (often text) from responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** 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:** Unsupported
+  ///
+  SelectAll,
+
+  /// A menu item for pasting (often text) into responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** 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
+  ///
+  /// - **Windows / 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
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Separator,
+}

+ 20 - 1
core/tauri/src/runtime/window.rs

@@ -10,7 +10,7 @@ use crate::{
   hooks::{InvokeMessage, InvokeResolver, PageLoadPayload},
   runtime::{
     tag::ToJsString,
-    webview::{FileDropHandler, InvokePayload, WebviewAttributes, WebviewRpcHandler},
+    webview::{FileDropHandler, InvokePayload, MenuItemId, WebviewAttributes, WebviewRpcHandler},
     Dispatch, Monitor, Runtime,
   },
   sealed::{ManagerBase, RuntimeOrDispatch},
@@ -55,6 +55,20 @@ pub enum WindowEvent {
   },
 }
 
+/// A menu event.
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MenuEvent {
+  pub(crate) menu_item_id: MenuItemId,
+}
+
+impl MenuEvent {
+  /// Returns the id of the menu item that triggered the event.
+  pub fn item_id(&self) -> MenuItemId {
+    self.menu_item_id
+  }
+}
+
 /// A webview window that has yet to be built.
 pub struct PendingWindow<M: Params> {
   /// The label that the window will be named.
@@ -378,6 +392,11 @@ pub(crate) mod export {
       self.window.dispatcher.on_window_event(f);
     }
 
+    /// Registers a menu event listener.
+    pub fn on_menu_event<F: Fn(&MenuEvent) + Send + 'static>(&self, f: F) {
+      self.window.dispatcher.on_menu_event(f);
+    }
+
     // Getters
 
     /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.

+ 5 - 0
examples/api/src-tauri/src/main.rs

@@ -8,6 +8,7 @@
 )]
 
 mod cmd;
+mod menu;
 
 use serde::Serialize;
 
@@ -31,6 +32,10 @@ fn main() {
           .expect("failed to emit");
       });
     })
+    .menu(menu::get_menu())
+    .on_menu_event(|event| {
+      println!("{:?}", event.menu_item_id());
+    })
     .invoke_handler(tauri::generate_handler![
       cmd::log_operation,
       cmd::perform_request

+ 72 - 0
examples/api/src-tauri/src/menu.rs

@@ -0,0 +1,72 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use tauri::{CustomMenuItem, Menu, MenuItem};
+
+pub fn get_menu() -> Vec<Menu> {
+  let custom_print_menu = MenuItem::Custom(CustomMenuItem::new("Print"));
+  let other_test_menu = MenuItem::Custom(CustomMenuItem::new("Custom"));
+  let quit_menu = MenuItem::Custom(CustomMenuItem::new("Quit"));
+
+  // macOS require to have at least Copy, Paste, Select all etc..
+  // to works fine. You should always add them.
+  #[cfg(any(target_os = "linux", target_os = "macos"))]
+  let menu = vec![
+    Menu::new(
+      // on macOS first menu is always app name
+      "Tauri API",
+      vec![
+        // All's non-custom menu, do NOT return event's
+        // they are handled by the system automatically
+        MenuItem::About("Tauri".to_string()),
+        MenuItem::Services,
+        MenuItem::Separator,
+        MenuItem::Hide,
+        MenuItem::HideOthers,
+        MenuItem::ShowAll,
+        MenuItem::Separator,
+        quit_menu,
+      ],
+    ),
+    Menu::new(
+      "File",
+      vec![
+        custom_print_menu,
+        MenuItem::Separator,
+        other_test_menu,
+        MenuItem::CloseWindow,
+      ],
+    ),
+    Menu::new(
+      "Edit",
+      vec![
+        MenuItem::Undo,
+        MenuItem::Redo,
+        MenuItem::Separator,
+        MenuItem::Cut,
+        MenuItem::Copy,
+        MenuItem::Paste,
+        MenuItem::Separator,
+        MenuItem::SelectAll,
+      ],
+    ),
+    Menu::new("View", vec![MenuItem::EnterFullScreen]),
+    Menu::new("Window", vec![MenuItem::Minimize, MenuItem::Zoom]),
+    Menu::new(
+      "Help",
+      vec![MenuItem::Custom(CustomMenuItem::new("Custom help"))],
+    ),
+  ];
+
+  // Attention, Windows only support custom menu for now.
+  // If we add any `MenuItem::*` they'll not render
+  // We need to use custom menu with `Menu::new()` and catch
+  // the events in the EventLoop.
+  #[cfg(target_os = "windows")]
+  let menu = vec![
+    Menu::new("File", vec![other_test_menu]),
+    Menu::new("Other menu", vec![quit_menu]),
+  ];
+  menu
+}