feat(core): window menus (#1745)

This commit is contained in:
Lucas Fernandes Nogueira 2021-05-08 12:11:40 -03:00 committed by GitHub
parent 10715e63b1
commit 41d5d6aff2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 558 additions and 58 deletions

5
.changes/menu.md Normal file
View File

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

View File

@ -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"

View File

@ -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},

View File

@ -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

View File

@ -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>),
}
#[derive(Clone)]
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,
proxy: EventLoopProxy<Message>,
task_tx: Sender<MainThreadTask>,
window_event_listeners: WindowEventListeners,
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(),
},

View File

@ -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),
)
}

View File

@ -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.

View File

@ -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,
}

View File

@ -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.

View File

@ -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

View File

@ -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
}