refactor(core): add webview events (#8844)

* refactor(core): add webview events

* license header

* clippy

* fix doctests

* more doctests

* fix JS `listen` with `EventTarget::Any`

* typo

* update module import

* clippy

* remove console.log

* fix api example

* fix documentation for emiTo [skip ci]

* actually add RunEvent::WebviewEvent

* update migration

* lint

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2024-02-16 13:07:39 +02:00 committed by GitHub
parent 5618f6d2ff
commit 16e550ec15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 779 additions and 381 deletions

View File

@ -0,0 +1,9 @@
---
'@tauri-apps/api': 'patch:breaking'
---
Renamed the following enum variants of `TauriEvent` enum:
- `TauriEvent.WEBVIEW_FILE_DROP` -> `TauriEvent.FILE_DROP`
- `TauriEvent.WEBVIEW_FILE_DROP_HOVER` -> `TauriEvent.FILE_DROP_HOVER`
- `TauriEvent.WEBVIEW_FILE_DROP_CANCELLED` -> `TauriEvent.FILE_DROP_CANCELLED`

View File

@ -0,0 +1,5 @@
---
'@tauri-apps/api': 'patch:feat'
---
Add a new `webviewWindow` module that exports `WebviewWindow` class and related methods such as `getCurrent` and `getAll`.

View File

@ -0,0 +1,5 @@
---
'@tauri-apps/api': 'patch:breaking'
---
Move `WebviewWindow` class from `webview` module to a new `webviewWindow` module.

View File

@ -0,0 +1,5 @@
---
'@tauri-apps/api': 'patch:feat'
---
Add `Window.onFileDropEvent` method.

View File

@ -0,0 +1,5 @@
---
'tauri': 'patch:bug'
---
Fix JS event listeners registered using JS `listen` api with `EventTarget::Any` never fired.

View File

@ -0,0 +1,6 @@
---
'tauri-runtime': 'patch'
'tauri-runtime-wry': 'patch'
---
Add `WebviewEvent`, `RunEvent::WebviewEvent` and `WebviewDispatch::on_webview_event`.

View File

@ -0,0 +1,9 @@
---
'tauri': 'patch:feat'
---
Add webview-specific events for multi-webview windows:
- Add `WebviewEvent` enum
- Add `RunEvent::WebviewEvent` variant.
- Add `Builder::on_webview_event` and `Webview::on_webview_event` methods.

View File

@ -94,7 +94,7 @@ jobs:
- name: test (using cross)
if: ${{ matrix.platform.cross }}
run: |
cargo install cross --git https://github.com/cross-rs/cross
cargo install cross --git https://github.com/cross-rs/cross --locked
cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }}
- name: test (using cargo)

View File

@ -17,12 +17,12 @@ use tauri_runtime::{
webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler},
window::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
CursorIcon, DetachedWindow, FileDropEvent, PendingWindow, RawWindow, WindowBuilder,
WindowBuilderBase, WindowEvent, WindowId,
CursorIcon, DetachedWindow, FileDropEvent, PendingWindow, RawWindow, WebviewEvent,
WindowBuilder, WindowBuilderBase, WindowEvent, WindowId,
},
DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, RunEvent,
Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, WebviewDispatch,
WindowDispatch, WindowEventId,
WebviewEventId, WindowDispatch, WindowEventId,
};
#[cfg(target_os = "macos")]
@ -121,6 +121,8 @@ pub type WebContextStore = Arc<Mutex<HashMap<Option<PathBuf>, WebContext>>>;
// window
pub type WindowEventHandler = Box<dyn Fn(&WindowEvent) + Send>;
pub type WindowEventListeners = Arc<Mutex<HashMap<WindowEventId, WindowEventHandler>>>;
pub type WebviewEventHandler = Box<dyn Fn(&WebviewEvent) + Send>;
pub type WebviewEventListeners = Arc<Mutex<HashMap<WebviewEventId, WebviewEventHandler>>>;
#[derive(Debug, Clone, Default)]
pub struct WindowIdStore(Arc<Mutex<HashMap<TaoWindowId, WindowId>>>);
@ -172,7 +174,7 @@ pub(crate) fn send_user_message<T: UserEvent>(
&context.main_thread.window_target,
message,
UserMessageContext {
webview_id_map: context.webview_id_map.clone(),
window_id_map: context.window_id_map.clone(),
windows: context.main_thread.windows.clone(),
},
);
@ -187,7 +189,7 @@ pub(crate) fn send_user_message<T: UserEvent>(
#[derive(Clone)]
pub struct Context<T: UserEvent> {
pub webview_id_map: WindowIdStore,
pub window_id_map: WindowIdStore,
main_thread_id: ThreadId,
pub proxy: TaoEventLoopProxy<Message<T>>,
main_thread: DispatcherMainThreadContext<T>,
@ -195,6 +197,7 @@ pub struct Context<T: UserEvent> {
next_window_id: Arc<AtomicU32>,
next_webview_id: Arc<AtomicU32>,
next_window_event_id: Arc<AtomicU32>,
next_webview_event_id: Arc<AtomicU32>,
next_webcontext_id: Arc<AtomicU32>,
}
@ -222,6 +225,10 @@ impl<T: UserEvent> Context<T> {
self.next_window_event_id.fetch_add(1, Ordering::Relaxed)
}
fn next_webview_event_id(&self) -> u32 {
self.next_webview_event_id.fetch_add(1, Ordering::Relaxed)
}
fn next_webcontext_id(&self) -> u32 {
self.next_webcontext_id.fetch_add(1, Ordering::Relaxed)
}
@ -463,16 +470,6 @@ impl<'a> From<&TaoWindowEvent<'a>> for WindowEventWrapper {
}
}
impl From<WebviewEvent> for WindowEventWrapper {
fn from(event: WebviewEvent) -> Self {
let event = match event {
WebviewEvent::Focused(focused) => WindowEvent::Focused(focused),
WebviewEvent::FileDrop(event) => WindowEvent::FileDrop(event),
};
Self(Some(event))
}
}
pub struct MonitorHandleWrapper(pub MonitorHandle);
impl From<MonitorHandleWrapper> for Monitor {
@ -994,53 +991,6 @@ impl WindowBuilder for WindowBuilderWrapper {
}
}
pub struct FileDropEventWrapper(WryFileDropEvent);
// on Linux, the paths are percent-encoded
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn decode_path(path: PathBuf) -> PathBuf {
percent_encoding::percent_decode(path.display().to_string().as_bytes())
.decode_utf8_lossy()
.into_owned()
.into()
}
// on Windows and macOS, we do not need to decode the path
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
fn decode_path(path: PathBuf) -> PathBuf {
path
}
impl From<FileDropEventWrapper> for FileDropEvent {
fn from(event: FileDropEventWrapper) -> Self {
match event.0 {
WryFileDropEvent::Hovered { paths, position } => FileDropEvent::Hovered {
paths: paths.into_iter().map(decode_path).collect(),
position: PhysicalPosition::new(position.0 as f64, position.1 as f64),
},
WryFileDropEvent::Dropped { paths, position } => FileDropEvent::Dropped {
paths: paths.into_iter().map(decode_path).collect(),
position: PhysicalPosition::new(position.0 as f64, position.1 as f64),
},
// default to cancelled
// FIXME(maybe): Add `FileDropEvent::Unknown` event?
_ => FileDropEvent::Cancelled,
}
}
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
@ -1168,13 +1118,30 @@ pub enum WindowMessage {
RequestRedraw,
}
#[derive(Debug, Clone)]
pub enum SynthesizedWindowEvent {
Focused(bool),
FileDrop(FileDropEvent),
}
impl From<SynthesizedWindowEvent> for WindowEventWrapper {
fn from(event: SynthesizedWindowEvent) -> Self {
let event = match event {
SynthesizedWindowEvent::Focused(focused) => WindowEvent::Focused(focused),
SynthesizedWindowEvent::FileDrop(event) => WindowEvent::FileDrop(event),
};
Self(Some(event))
}
}
pub enum WebviewMessage {
AddEventListener(WebviewEventId, Box<dyn Fn(&WebviewEvent) + Send>),
#[cfg(not(all(feature = "tracing", not(target_os = "android"))))]
EvaluateScript(String),
#[cfg(all(feature = "tracing", not(target_os = "android")))]
EvaluateScript(String, Sender<()>, tracing::Span),
#[allow(dead_code)]
WebviewEvent(WebviewEvent),
SynthesizedWindowEvent(SynthesizedWindowEvent),
Navigate(Url),
Print,
Close,
@ -1195,13 +1162,6 @@ pub enum WebviewMessage {
IsDevToolsOpen(Sender<bool>),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum WebviewEvent {
FileDrop(FileDropEvent),
Focused(bool),
}
pub type CreateWindowClosure<T> =
Box<dyn FnOnce(&EventLoopWindowTarget<Message<T>>) -> Result<WindowWrapper> + Send>;
@ -1250,6 +1210,16 @@ impl<T: UserEvent> WebviewDispatch<T> for WryWebviewDispatcher<T> {
send_user_message(&self.context, Message::Task(Box::new(f)))
}
fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) -> WindowEventId {
let id = self.context.next_webview_event_id();
let _ = self.context.proxy.send_event(Message::Webview(
self.window_id,
self.webview_id,
WebviewMessage::AddEventListener(id, Box::new(f)),
));
id
}
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()> {
send_user_message(
&self.context,
@ -1853,9 +1823,11 @@ impl<T: UserEvent> WindowDispatch<T> for WryWindowDispatcher<T> {
#[derive(Clone)]
pub struct WebviewWrapper {
label: String,
id: WebviewId,
inner: Rc<WebView>,
context_store: WebContextStore,
webview_event_listeners: WebviewEventListeners,
// the key of the WebContext if it's not shared
context_key: Option<PathBuf>,
bounds: Option<Arc<Mutex<WebviewBounds>>>,
@ -1977,7 +1949,7 @@ impl<T: UserEvent> WryHandle<T> {
pub fn window_id(&self, window_id: TaoWindowId) -> WindowId {
*self
.context
.webview_id_map
.window_id_map
.0
.lock()
.unwrap()
@ -2132,10 +2104,10 @@ impl<T: UserEvent> Wry<T> {
let web_context = WebContextStore::default();
let windows = Rc::new(RefCell::new(HashMap::default()));
let webview_id_map = WindowIdStore::default();
let window_id_map = WindowIdStore::default();
let context = Context {
webview_id_map,
window_id_map,
main_thread_id,
proxy: event_loop.create_proxy(),
main_thread: DispatcherMainThreadContext {
@ -2149,6 +2121,7 @@ impl<T: UserEvent> Wry<T> {
next_window_id: Default::default(),
next_webview_id: Default::default(),
next_window_event_id: Default::default(),
next_webview_event_id: Default::default(),
next_webcontext_id: Default::default(),
};
@ -2347,7 +2320,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
fn run_iteration<F: FnMut(RunEvent<T>)>(&mut self, mut callback: F) {
use tao::platform::run_return::EventLoopExtRunReturn;
let windows = self.context.main_thread.windows.clone();
let webview_id_map = self.context.webview_id_map.clone();
let window_id_map = self.context.window_id_map.clone();
let web_context = &self.context.main_thread.web_context;
let plugins = self.context.plugins.clone();
@ -2372,7 +2345,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
control_flow,
EventLoopIterationContext {
callback: &mut callback,
webview_id_map: webview_id_map.clone(),
window_id_map: window_id_map.clone(),
windows: windows.clone(),
#[cfg(feature = "tracing")]
active_tracing_spans: active_tracing_spans.clone(),
@ -2391,7 +2364,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
EventLoopIterationContext {
callback: &mut callback,
windows: windows.clone(),
webview_id_map: webview_id_map.clone(),
window_id_map: window_id_map.clone(),
#[cfg(feature = "tracing")]
active_tracing_spans: active_tracing_spans.clone(),
},
@ -2401,7 +2374,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
fn run<F: FnMut(RunEvent<T>) + 'static>(self, mut callback: F) {
let windows = self.context.main_thread.windows.clone();
let webview_id_map = self.context.webview_id_map.clone();
let window_id_map = self.context.window_id_map.clone();
let web_context = self.context.main_thread.web_context;
let plugins = self.context.plugins.clone();
@ -2418,7 +2391,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
control_flow,
EventLoopIterationContext {
callback: &mut callback,
webview_id_map: webview_id_map.clone(),
window_id_map: window_id_map.clone(),
windows: windows.clone(),
#[cfg(feature = "tracing")]
active_tracing_spans: active_tracing_spans.clone(),
@ -2435,7 +2408,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
control_flow,
EventLoopIterationContext {
callback: &mut callback,
webview_id_map: webview_id_map.clone(),
window_id_map: window_id_map.clone(),
windows: windows.clone(),
#[cfg(feature = "tracing")]
active_tracing_spans: active_tracing_spans.clone(),
@ -2447,7 +2420,7 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
pub struct EventLoopIterationContext<'a, T: UserEvent> {
pub callback: &'a mut (dyn FnMut(RunEvent<T>)),
pub webview_id_map: WindowIdStore,
pub window_id_map: WindowIdStore,
pub windows: Rc<RefCell<HashMap<WindowId, WindowWrapper>>>,
#[cfg(feature = "tracing")]
pub active_tracing_spans: ActiveTraceSpanStore,
@ -2455,7 +2428,7 @@ pub struct EventLoopIterationContext<'a, T: UserEvent> {
struct UserMessageContext {
windows: Rc<RefCell<HashMap<WindowId, WindowWrapper>>>,
webview_id_map: WindowIdStore,
window_id_map: WindowIdStore,
}
fn handle_user_message<T: UserEvent>(
@ -2464,7 +2437,7 @@ fn handle_user_message<T: UserEvent>(
context: UserMessageContext,
) {
let UserMessageContext {
webview_id_map,
window_id_map,
windows,
} = context;
match message {
@ -2684,6 +2657,17 @@ fn handle_user_message<T: UserEvent>(
});
if let Some((Some(window), Some(webview))) = webview_handle {
match webview_message {
WebviewMessage::WebviewEvent(_) => { /* already handled */ }
WebviewMessage::SynthesizedWindowEvent(_) => { /* already handled */ }
WebviewMessage::AddEventListener(id, listener) => {
webview
.webview_event_listeners
.lock()
.unwrap()
.insert(id, listener);
}
#[cfg(all(feature = "tracing", not(target_os = "android")))]
WebviewMessage::EvaluateScript(script, tx, span) => {
let _span = span.entered();
@ -2743,7 +2727,6 @@ fn handle_user_message<T: UserEvent>(
WebviewMessage::SetFocus => {
webview.focus();
}
WebviewMessage::WebviewEvent(_event) => { /* already handled */ }
WebviewMessage::WithWebview(f) => {
#[cfg(any(
target_os = "linux",
@ -2850,7 +2833,7 @@ fn handle_user_message<T: UserEvent>(
let (label, builder) = handler();
let is_window_transparent = builder.window.transparent;
if let Ok(window) = builder.build(event_loop) {
webview_id_map.insert(window.id(), window_id);
window_id_map.insert(window.id(), window_id);
let window = Arc::new(window);
@ -2901,7 +2884,7 @@ fn handle_event_loop<T: UserEvent>(
) {
let EventLoopIterationContext {
callback,
webview_id_map,
window_id_map,
windows,
#[cfg(feature = "tracing")]
active_tracing_spans,
@ -2930,7 +2913,7 @@ fn handle_event_loop<T: UserEvent>(
#[cfg(any(feature = "tracing", windows))]
Event::RedrawRequested(id) => {
#[cfg(windows)]
if let Some(window_id) = webview_id_map.get(&id) {
if let Some(window_id) = window_id_map.get(&id) {
let mut windows_ref = windows.borrow_mut();
if let Some(window) = windows_ref.get_mut(&window_id) {
if window.is_window_transparent {
@ -2949,19 +2932,50 @@ fn handle_event_loop<T: UserEvent>(
Event::UserEvent(Message::Webview(
window_id,
_webview_id,
webview_id,
WebviewMessage::WebviewEvent(event),
)) => {
let windows_ref = windows.borrow();
if let Some(window) = windows_ref.get(&window_id) {
if let Some(webview) = window.webviews.iter().find(|w| w.id == webview_id) {
let label = webview.label.clone();
let webview_event_listeners = webview.webview_event_listeners.clone();
drop(windows_ref);
callback(RunEvent::WebviewEvent {
label,
event: event.clone(),
});
let listeners = webview_event_listeners.lock().unwrap();
let handlers = listeners.values();
for handler in handlers {
handler(&event);
}
}
}
}
Event::UserEvent(Message::Webview(
window_id,
_webview_id,
WebviewMessage::SynthesizedWindowEvent(event),
)) => {
if let Some(event) = WindowEventWrapper::from(event).0 {
let windows = windows.borrow();
let window = windows.get(&window_id);
let windows_ref = windows.borrow();
let window = windows_ref.get(&window_id);
if let Some(window) = window {
let label = window.label.clone();
let window_event_listeners = window.window_event_listeners.clone();
drop(windows_ref);
callback(RunEvent::WindowEvent {
label: window.label.clone(),
label,
event: event.clone(),
});
let listeners = window.window_event_listeners.lock().unwrap();
let listeners = window_event_listeners.lock().unwrap();
let handlers = listeners.values();
for handler in handlers {
handler(&event);
@ -2973,7 +2987,7 @@ fn handle_event_loop<T: UserEvent>(
Event::WindowEvent {
event, window_id, ..
} => {
if let Some(window_id) = webview_id_map.get(&window_id) {
if let Some(window_id) = window_id_map.get(&window_id) {
{
let windows_ref = windows.borrow();
if let Some(window) = windows_ref.get(&window_id) {
@ -3076,7 +3090,7 @@ fn handle_event_loop<T: UserEvent>(
event_loop,
message,
UserMessageContext {
webview_id_map,
window_id_map,
windows,
},
);
@ -3216,7 +3230,7 @@ fn create_window<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
});
}
context.webview_id_map.insert(window.id(), window_id);
context.window_id_map.insert(window.id(), window_id);
if window_builder.center {
let _ = center_window(&window, window.inner_size());
@ -3290,7 +3304,8 @@ fn create_window<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
})
}
// the kind of the webview
/// the kind of the webview
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
enum WebviewKind {
// webview is the entire window content
WindowContent,
@ -3375,12 +3390,32 @@ fn create_webview<T: UserEvent>(
if webview_attributes.file_drop_handler_enabled {
let proxy = context.proxy.clone();
webview_builder = webview_builder.with_file_drop_handler(move |event| {
let event: FileDropEvent = FileDropEventWrapper(event).into();
let _ = proxy.send_event(Message::Webview(
window_id,
id,
WebviewMessage::WebviewEvent(WebviewEvent::FileDrop(event)),
));
let event = match event {
WryFileDropEvent::Hovered {
paths,
position: (x, y),
} => FileDropEvent::Hovered {
paths,
position: PhysicalPosition::new(x as _, y as _),
},
WryFileDropEvent::Dropped {
paths,
position: (x, y),
} => FileDropEvent::Dropped {
paths,
position: PhysicalPosition::new(x as _, y as _),
},
WryFileDropEvent::Cancelled => FileDropEvent::Cancelled,
_ => unimplemented!(),
};
let message = if kind == WebviewKind::WindowContent {
WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::FileDrop(event))
} else {
WebviewMessage::WebviewEvent(WebviewEvent::FileDrop(event))
};
let _ = proxy.send_event(Message::Webview(window_id, id, message));
true
});
}
@ -3566,7 +3601,7 @@ fn create_webview<T: UserEvent>(
.map_err(|e| Error::CreateWebview(Box::new(e)))?;
#[cfg(windows)]
{
if kind == WebviewKind::WindowContent {
let controller = webview.controller();
let proxy = context.proxy.clone();
let proxy_ = proxy.clone();
@ -3574,10 +3609,10 @@ fn create_webview<T: UserEvent>(
unsafe {
controller.add_GotFocus(
&FocusChangedEventHandler::create(Box::new(move |_, _| {
let _ = proxy.send_event(Message::Webview(
let _ = proxy_.send_event(Message::Webview(
window_id,
id,
WebviewMessage::WebviewEvent(WebviewEvent::Focused(true)),
WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::Focused(true)),
));
Ok(())
})),
@ -3588,10 +3623,10 @@ fn create_webview<T: UserEvent>(
unsafe {
controller.add_LostFocus(
&FocusChangedEventHandler::create(Box::new(move |_, _| {
let _ = proxy_.send_event(Message::Webview(
let _ = proxy.send_event(Message::Webview(
window_id,
id,
WebviewMessage::WebviewEvent(WebviewEvent::Focused(false)),
WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::Focused(false)),
));
Ok(())
})),
@ -3602,9 +3637,11 @@ fn create_webview<T: UserEvent>(
}
Ok(WebviewWrapper {
label,
id,
inner: Rc::new(webview),
context_store: context.main_thread.web_context.clone(),
webview_event_listeners: Default::default(),
context_key: if automation_enabled {
None
} else {

View File

@ -27,7 +27,7 @@ pub mod window;
use monitor::Monitor;
use window::{
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent,
CursorIcon, DetachedWindow, PendingWindow, RawWindow, WebviewEvent, WindowEvent,
};
use window::{WindowBuilder, WindowId};
@ -38,6 +38,7 @@ use http::{
};
pub type WindowEventId = u32;
pub type WebviewEventId = u32;
/// Type of user attention requested on a window.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
@ -166,6 +167,13 @@ pub enum RunEvent<T: UserEvent> {
/// The detailed event.
event: WindowEvent,
},
/// An event associated with a webview.
WebviewEvent {
/// The webview label.
label: String,
/// The detailed event.
event: WebviewEvent,
},
/// Application ready.
Ready,
/// Sent if the event loop is being resumed.
@ -362,6 +370,9 @@ pub trait WebviewDispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + '
/// Run a task on the main thread.
fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> Result<()>;
/// Registers a webview event handler.
fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) -> WebviewEventId;
/// Runs a closure with the platform webview object as argument.
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()>;

View File

@ -65,6 +65,13 @@ pub enum WindowEvent {
ThemeChanged(Theme),
}
/// An event from a window.
#[derive(Debug, Clone)]
pub enum WebviewEvent {
/// An event associated with the file drop action.
FileDrop(FileDropEvent),
}
/// The file drop event payload.
#[derive(Debug, Clone)]
#[non_exhaustive]

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,8 @@ use crate::{
},
plugin::{Plugin, PluginStore},
runtime::{
window::WindowEvent as RuntimeWindowEvent, ExitRequestedEventAction,
RunEvent as RuntimeRunEvent,
window::{WebviewEvent as RuntimeWebviewEvent, WindowEvent as RuntimeWindowEvent},
ExitRequestedEventAction, RunEvent as RuntimeRunEvent,
},
sealed::{ManagerBase, RuntimeOrDispatch},
utils::config::Config,
@ -62,6 +62,8 @@ pub(crate) type GlobalMenuEventListener<T> = Box<dyn Fn(&T, crate::menu::MenuEve
pub(crate) type GlobalTrayIconEventListener<T> =
Box<dyn Fn(&T, crate::tray::TrayIconEvent) + Send + Sync>;
pub(crate) type GlobalWindowEventListener<R> = Box<dyn Fn(&Window<R>, &WindowEvent) + Send + Sync>;
pub(crate) type GlobalWebviewEventListener<R> =
Box<dyn Fn(&Webview<R>, &WebviewEvent) + Send + Sync>;
/// A closure that is run when the Tauri application is setting up.
pub type SetupHook<R> =
Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
@ -164,6 +166,22 @@ impl From<RuntimeWindowEvent> for WindowEvent {
}
}
/// An event from a window.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum WebviewEvent {
/// An event associated with the file drop action.
FileDrop(FileDropEvent),
}
impl From<RuntimeWebviewEvent> for WebviewEvent {
fn from(event: RuntimeWebviewEvent) -> Self {
match event {
RuntimeWebviewEvent::FileDrop(e) => Self::FileDrop(e),
}
}
}
/// An application event, triggered from the event loop.
///
/// See [`App::run`](crate::App#method.run) for usage examples.
@ -190,6 +208,14 @@ pub enum RunEvent {
/// The detailed event.
event: WindowEvent,
},
/// An event associated with a webview.
#[non_exhaustive]
WebviewEvent {
/// The window label.
label: String,
/// The detailed event.
event: WebviewEvent,
},
/// Application ready.
Ready,
/// Sent if the event loop is being resumed.
@ -1043,6 +1069,9 @@ pub struct Builder<R: Runtime> {
/// Window event handlers that listens to all windows.
window_event_listeners: Vec<GlobalWindowEventListener<R>>,
/// Webview event handlers that listens to all webviews.
webview_event_listeners: Vec<GlobalWebviewEventListener<R>>,
/// The device event filter.
device_event_filter: DeviceEventFilter,
}
@ -1101,6 +1130,7 @@ impl<R: Runtime> Builder<R> {
menu: None,
enable_macos_default_menu: true,
window_event_listeners: Vec::new(),
webview_event_listeners: Vec::new(),
device_event_filter: Default::default(),
}
}
@ -1400,6 +1430,27 @@ tauri::Builder::default()
self
}
/// Registers a webview event handler for all webviews.
///
/// # Examples
/// ```
/// tauri::Builder::default()
/// .on_webview_event(|window, event| match event {
/// tauri::WebviewEvent::FileDrop(event) => {
/// println!("{:?}", event);
/// }
/// _ => {}
/// });
/// ```
#[must_use]
pub fn on_webview_event<F: Fn(&Webview<R>, &WebviewEvent) + Send + Sync + 'static>(
mut self,
handler: F,
) -> Self {
self.webview_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
@ -1544,6 +1595,7 @@ tauri::Builder::default()
self.uri_scheme_protocols,
self.state,
self.window_event_listeners,
self.webview_event_listeners,
#[cfg(desktop)]
HashMap::new(),
(self.invoke_responder, self.invoke_initialization_script),
@ -1800,6 +1852,10 @@ fn on_event_loop_event<R: Runtime>(
label,
event: event.into(),
},
RuntimeRunEvent::WebviewEvent { label, event } => RunEvent::WebviewEvent {
label,
event: event.into(),
},
RuntimeRunEvent::Ready => {
// set the app icon in development
#[cfg(all(dev, target_os = "macos"))]

View File

@ -285,28 +285,49 @@ impl Listeners {
})
}
pub(crate) fn try_for_each_js<'a, R, I, F>(
pub(crate) fn emit_js_filter<'a, R, I, F>(
&self,
event: &str,
mut webviews: I,
callback: F,
event: &str,
emit_args: &EmitArgs,
filter: Option<&F>,
) -> crate::Result<()>
where
R: Runtime,
I: Iterator<Item = &'a Webview<R>>,
F: Fn(&Webview<R>, &EventTarget) -> crate::Result<()>,
F: Fn(&EventTarget) -> bool,
{
let listeners = self.inner.js_event_listeners.lock().unwrap();
webviews.try_for_each(|webview| {
if let Some(handlers) = listeners.get(webview.label()).and_then(|s| s.get(event)) {
for JsHandler { target, .. } in handlers {
callback(webview, target)?;
if *target == EventTarget::Any || filter.as_ref().map(|f| f(target)).unwrap_or(false) {
webview.emit_js(emit_args, target)?;
}
}
}
Ok(())
})
}
pub(crate) fn emit_js<'a, R, I>(
&self,
webviews: I,
event: &str,
emit_args: &EmitArgs,
) -> crate::Result<()>
where
R: Runtime,
I: Iterator<Item = &'a Webview<R>>,
{
self.emit_js_filter(
webviews,
event,
emit_args,
None::<&&dyn Fn(&EventTarget) -> bool>,
)
}
}
#[cfg(test)]

View File

@ -238,7 +238,7 @@ pub fn event_initialization_script(function: &str, listeners: &str) -> String {
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
for (let i = listeners.length - 1; i >= 0; i--) {{
const listener = listeners[i]
if ((listener.target.kind === 'Global' && target.kind === 'Global') || (listener.target.kind === target.kind && listener.target.label === target.label)) {{
if (listener.target.kind === 'Any' || (listener.target.kind === target.kind && listener.target.label === target.label)) {{
eventData.id = listener.id
listener.handler(eventData)
}}

View File

@ -206,7 +206,9 @@ pub use self::utils::TitleBarStyle;
pub use self::event::{Event, EventId, EventTarget};
pub use {
self::app::{App, AppHandle, AssetResolver, Builder, CloseRequestApi, RunEvent, WindowEvent},
self::app::{
App, AppHandle, AssetResolver, Builder, CloseRequestApi, RunEvent, WebviewEvent, WindowEvent,
},
self::manager::Asset,
self::runtime::{
webview::WebviewAttributes,
@ -800,7 +802,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
/// Fetch a single webview window from the manager.
fn get_webview_window(&self, label: &str) -> Option<WebviewWindow<R>> {
self.manager().get_webview(label).and_then(|webview| {
if webview.window().webview_window {
if webview.window().is_webview_window {
Some(WebviewWindow { webview })
} else {
None
@ -815,7 +817,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
.webviews()
.into_iter()
.filter_map(|(label, webview)| {
if webview.window().webview_window {
if webview.window().is_webview_window {
Some((label, WebviewWindow { webview }))
} else {
None

View File

@ -21,7 +21,7 @@ use tauri_utils::{
};
use crate::{
app::{AppHandle, GlobalWindowEventListener, OnPageLoad},
app::{AppHandle, GlobalWebviewEventListener, GlobalWindowEventListener, OnPageLoad},
event::{assert_event_name_is_valid, Event, EventId, EventTarget, Listeners},
ipc::{Invoke, InvokeHandler, InvokeResponder, RuntimeAuthority},
plugin::PluginStore,
@ -231,6 +231,7 @@ impl<R: Runtime> AppManager<R> {
uri_scheme_protocols: HashMap<String, Arc<webview::UriSchemeProtocol<R>>>,
state: StateManager,
window_event_listeners: Vec<GlobalWindowEventListener<R>>,
webiew_event_listeners: Vec<GlobalWebviewEventListener<R>>,
#[cfg(desktop)] window_menu_event_listeners: HashMap<
String,
crate::app::GlobalMenuEventListener<Window<R>>,
@ -255,6 +256,7 @@ impl<R: Runtime> AppManager<R> {
invoke_handler,
on_page_load,
uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
event_listeners: Arc::new(webiew_event_listeners),
invoke_responder,
invoke_initialization_script,
},
@ -485,16 +487,11 @@ impl<R: Runtime> AppManager<R> {
let listeners = self.listeners();
listeners.try_for_each_js(
event,
listeners.emit_js_filter(
self.webview.webviews_lock().values(),
|webview, target| {
if filter(target) {
webview.emit_js(&emit_args, target)
} else {
Ok(())
}
},
event,
&emit_args,
Some(&filter),
)?;
listeners.emit_filter(emit_args, Some(filter))?;
@ -511,12 +508,7 @@ impl<R: Runtime> AppManager<R> {
let listeners = self.listeners();
listeners.try_for_each_js(
event,
self.webview.webviews_lock().values(),
|webview, target| webview.emit_js(&emit_args, target),
)?;
listeners.emit_js(self.webview.webviews_lock().values(), event, &emit_args)?;
listeners.emit(emit_args)?;
Ok(())
@ -647,6 +639,7 @@ mod test {
StateManager::new(),
Default::default(),
Default::default(),
Default::default(),
(None, "".into()),
);

View File

@ -12,20 +12,26 @@ use std::{
use serde::Serialize;
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
use tauri_runtime::webview::{DetachedWebview, PendingWebview};
use tauri_runtime::{
webview::{DetachedWebview, PendingWebview},
window::FileDropEvent,
};
use tauri_utils::config::WebviewUrl;
use url::Url;
use crate::{
app::{OnPageLoad, UriSchemeResponder},
app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent},
ipc::{InvokeHandler, InvokeResponder},
pattern::PatternJavascript,
sealed::ManagerBase,
webview::PageLoadPayload,
AppHandle, EventLoopMessage, Manager, Runtime, Webview, Window,
AppHandle, EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Webview, Window,
};
use super::AppManager;
use super::{
window::{FileDropPayload, DROP_CANCELLED_EVENT, DROP_EVENT, DROP_HOVER_EVENT},
AppManager,
};
// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
// and we do not get a secure context without the custom protocol that proxies to the dev server
@ -73,6 +79,8 @@ pub struct WebviewManager<R: Runtime> {
pub on_page_load: Option<Arc<OnPageLoad<R>>>,
/// The webview protocols available to all webviews.
pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
/// Webview event listeners to all webviews.
pub event_listeners: Arc<Vec<GlobalWebviewEventListener<R>>>,
/// Responder for invoke calls.
pub invoke_responder: Option<Arc<InvokeResponder<R>>>,
@ -557,6 +565,15 @@ impl<R: Runtime> WebviewManager<R> {
) -> Webview<R> {
let webview = Webview::new(window, webview);
let webview_event_listeners = self.event_listeners.clone();
let webview_ = webview.clone();
webview.on_webview_event(move |event| {
let _ = on_webview_event(&webview_, event);
for handler in webview_event_listeners.iter() {
handler(&webview_, event);
}
});
// insert the webview into our manager
{
self
@ -600,3 +617,43 @@ impl<R: Runtime> WebviewManager<R> {
self.webviews_lock().keys().cloned().collect()
}
}
impl<R: Runtime> Webview<R> {
/// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
fn emit_to_webview<S: Serialize + Clone>(&self, event: &str, payload: S) -> crate::Result<()> {
let window_label = self.label();
self.emit_filter(event, payload, |target| match target {
EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => {
label == window_label
}
_ => false,
})
}
}
fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> crate::Result<()> {
match event {
WebviewEvent::FileDrop(event) => match event {
FileDropEvent::Hovered { paths, position } => {
let payload = FileDropPayload { paths, position };
webview.emit_to_webview(DROP_HOVER_EVENT, payload)?
}
FileDropEvent::Dropped { paths, position } => {
let scopes = webview.state::<Scopes>();
for path in paths {
if path.is_file() {
let _ = scopes.allow_file(path);
} else {
let _ = scopes.allow_directory(path, false);
}
}
let payload = FileDropPayload { paths, position };
webview.emit_to_webview(DROP_EVENT, payload)?
}
FileDropEvent::Cancelled => webview.emit_to_webview(DROP_CANCELLED_EVENT, ())?,
_ => unimplemented!(),
},
}
Ok(())
}

View File

@ -23,8 +23,6 @@ use crate::{
Icon, Manager, Runtime, Scopes, Window, WindowEvent,
};
use super::AppManager;
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
const WINDOW_MOVED_EVENT: &str = "tauri://move";
const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
@ -33,9 +31,9 @@ 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 WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
pub const DROP_EVENT: &str = "tauri://file-drop";
pub const DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
pub const DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
pub struct WindowManager<R: Runtime> {
pub windows: Mutex<HashMap<String, Window<R>>>,
@ -95,9 +93,8 @@ impl<R: Runtime> WindowManager<R> {
let window_ = window.clone();
let window_event_listeners = self.event_listeners.clone();
let manager = window.manager.clone();
window.on_window_event(move |event| {
let _ = on_window_event(&window_, &manager, event);
let _ = on_window_event(&window_, event);
for handler in window_event_listeners.iter() {
handler(&window_, event);
}
@ -152,16 +149,12 @@ impl<R: Runtime> Window<R> {
}
#[derive(Serialize, Clone)]
struct FileDropPayload<'a> {
paths: &'a Vec<PathBuf>,
position: &'a PhysicalPosition<f64>,
pub struct FileDropPayload<'a> {
pub paths: &'a Vec<PathBuf>,
pub position: &'a PhysicalPosition<f64>,
}
fn on_window_event<R: Runtime>(
window: &Window<R>,
manager: &AppManager<R>,
event: &WindowEvent,
) -> crate::Result<()> {
fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate::Result<()> {
match event {
WindowEvent::Resized(size) => window.emit_to_window(WINDOW_RESIZED_EVENT, size)?,
WindowEvent::Moved(position) => window.emit_to_window(WINDOW_MOVED_EVENT, position)?,
@ -174,7 +167,7 @@ fn on_window_event<R: Runtime>(
WindowEvent::Destroyed => {
window.emit_to_window(WINDOW_DESTROYED_EVENT, ())?;
let label = window.label();
let webviews_map = manager.webview.webviews_lock();
let webviews_map = window.manager().webview.webviews_lock();
let webviews = webviews_map.values();
for webview in webviews {
webview.eval(&format!(
@ -204,7 +197,15 @@ fn on_window_event<R: Runtime>(
WindowEvent::FileDrop(event) => match event {
FileDropEvent::Hovered { paths, position } => {
let payload = FileDropPayload { paths, position };
window.emit_to_window(WINDOW_FILE_DROP_HOVER_EVENT, payload)?
if window.is_webview_window {
window.emit_to(
EventTarget::labeled(window.label()),
DROP_HOVER_EVENT,
payload,
)?
} else {
window.emit_to_window(DROP_HOVER_EVENT, payload)?
}
}
FileDropEvent::Dropped { paths, position } => {
let scopes = window.state::<Scopes>();
@ -216,9 +217,24 @@ fn on_window_event<R: Runtime>(
}
}
let payload = FileDropPayload { paths, position };
window.emit_to_window(WINDOW_FILE_DROP_EVENT, payload)?
if window.is_webview_window {
window.emit_to(EventTarget::labeled(window.label()), DROP_EVENT, payload)?
} else {
window.emit_to_window(DROP_EVENT, payload)?
}
}
FileDropEvent::Cancelled => {
if window.is_webview_window {
window.emit_to(
EventTarget::labeled(window.label()),
DROP_CANCELLED_EVENT,
(),
)?
} else {
window.emit_to_window(DROP_CANCELLED_EVENT, ())?
}
}
FileDropEvent::Cancelled => window.emit_to_window(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
_ => unimplemented!(),
},
WindowEvent::ThemeChanged(theme) => {

View File

@ -61,6 +61,7 @@ pub struct RuntimeContext {
next_window_id: Arc<AtomicU32>,
next_webview_id: Arc<AtomicU32>,
next_window_event_id: Arc<AtomicU32>,
next_webview_event_id: Arc<AtomicU32>,
}
// SAFETY: we ensure this type is only used on the main thread.
@ -100,6 +101,10 @@ impl RuntimeContext {
fn next_window_event_id(&self) -> WindowEventId {
self.next_window_event_id.fetch_add(1, Ordering::Relaxed)
}
fn next_webview_event_id(&self) -> WindowEventId {
self.next_webview_event_id.fetch_add(1, Ordering::Relaxed)
}
}
impl fmt::Debug for RuntimeContext {
@ -460,6 +465,13 @@ impl<T: UserEvent> WebviewDispatch<T> for MockWebviewDispatcher {
self.context.send_message(Message::Task(Box::new(f)))
}
fn on_webview_event<F: Fn(&tauri_runtime::window::WebviewEvent) + Send + 'static>(
&self,
f: F,
) -> tauri_runtime::WebviewEventId {
self.context.next_window_event_id()
}
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()> {
Ok(())
}
@ -922,6 +934,7 @@ impl MockRuntime {
next_window_id: Default::default(),
next_webview_id: Default::default(),
next_window_event_id: Default::default(),
next_webview_event_id: Default::default(),
};
Self {
is_running,

View File

@ -26,7 +26,7 @@ use tauri_utils::config::{WebviewUrl, WindowConfig};
pub use url::Url;
use crate::{
app::UriSchemeResponder,
app::{UriSchemeResponder, WebviewEvent},
event::{EmitArgs, EventTarget},
ipc::{
CallbackFn, CommandArg, CommandItem, Invoke, InvokeBody, InvokeError, InvokeMessage,
@ -861,6 +861,14 @@ impl<R: Runtime> Webview<R> {
pub fn label(&self) -> &str {
&self.webview.label
}
/// Registers a window event listener.
pub fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) {
self
.webview
.dispatcher
.on_webview_event(move |event| f(&event.clone().into()));
}
}
/// Desktop webview setters and actions.
@ -875,7 +883,7 @@ impl<R: Runtime> Webview<R> {
/// Closes this webview.
pub fn close(&self) -> crate::Result<()> {
if self.window.webview_window {
if self.window.is_webview_window {
self.window.close()
} else {
self.webview.dispatcher.close()?;
@ -886,7 +894,7 @@ impl<R: Runtime> Webview<R> {
/// Resizes this webview.
pub fn set_size<S: Into<Size>>(&self, size: S) -> crate::Result<()> {
if self.window.webview_window {
if self.window.is_webview_window {
self.window.set_size(size.into())
} else {
self
@ -899,7 +907,7 @@ impl<R: Runtime> Webview<R> {
/// Sets this webviews's position.
pub fn set_position<Pos: Into<Position>>(&self, position: Pos) -> crate::Result<()> {
if self.window.webview_window {
if self.window.is_webview_window {
self.window.set_position(position.into())
} else {
self
@ -920,7 +928,7 @@ impl<R: Runtime> Webview<R> {
/// - For child webviews, returns the position of the top-left hand corner of the webviews's client area relative to the top-left hand corner of the parent window.
/// - For webview window, returns the inner position of the window.
pub fn position(&self) -> crate::Result<PhysicalPosition<i32>> {
if self.window.webview_window {
if self.window.is_webview_window {
self.window.inner_position()
} else {
self.webview.dispatcher.position().map_err(Into::into)
@ -929,7 +937,7 @@ impl<R: Runtime> Webview<R> {
/// Returns the physical size of the webviews's client area.
pub fn size(&self) -> crate::Result<PhysicalSize<u32>> {
if self.window.webview_window {
if self.window.is_webview_window {
self.window.inner_size()
} else {
self.webview.dispatcher.size().map_err(Into::into)

View File

@ -876,7 +876,7 @@ impl<'de, R: Runtime> CommandArg<'de, R> for WebviewWindow<R> {
/// Grabs the [`Window`] from the [`CommandItem`]. This will never fail.
fn from_command(command: CommandItem<'de, R>) -> Result<Self, InvokeError> {
let webview = command.message.webview();
if webview.window().webview_window {
if webview.window().is_webview_window {
Ok(Self { webview })
} else {
Err(InvokeError::from_anyhow(anyhow::anyhow!(

View File

@ -863,7 +863,7 @@ pub struct Window<R: Runtime> {
#[cfg(desktop)]
pub(crate) menu: Arc<std::sync::Mutex<Option<WindowMenu<R>>>>,
/// Whether this window is a Webview window (hosts only a single webview) or a container for multiple webviews
pub(crate) webview_window: bool,
pub(crate) is_webview_window: bool,
}
impl<R: Runtime> std::fmt::Debug for Window<R> {
@ -872,7 +872,7 @@ impl<R: Runtime> std::fmt::Debug for Window<R> {
.field("window", &self.window)
.field("manager", &self.manager)
.field("app_handle", &self.app_handle)
.field("webview_window", &self.webview_window)
.field("is_webview_window", &self.is_webview_window)
.finish()
}
}
@ -893,7 +893,7 @@ impl<R: Runtime> Clone for Window<R> {
app_handle: self.app_handle.clone(),
#[cfg(desktop)]
menu: self.menu.clone(),
webview_window: self.webview_window,
is_webview_window: self.is_webview_window,
}
}
}
@ -948,7 +948,7 @@ impl<R: Runtime> Window<R> {
window: DetachedWindow<EventLoopMessage, R>,
app_handle: AppHandle<R>,
#[cfg(desktop)] menu: Option<WindowMenu<R>>,
webview_window: bool,
is_webview_window: bool,
) -> Self {
Self {
window,
@ -956,7 +956,7 @@ impl<R: Runtime> Window<R> {
app_handle,
#[cfg(desktop)]
menu: Arc::new(std::sync::Mutex::new(menu)),
webview_window,
is_webview_window,
}
}

View File

@ -8,7 +8,7 @@
EffectState,
ProgressBarStatus
} from '@tauri-apps/api/window'
import { WebviewWindow } from '@tauri-apps/api/webview'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
const webview = WebviewWindow.getCurrent()

View File

@ -56,9 +56,9 @@ enum TauriEvent {
WINDOW_SCALE_FACTOR_CHANGED = 'tauri://scale-change',
WINDOW_THEME_CHANGED = 'tauri://theme-changed',
WEBVIEW_CREATED = 'tauri://webview-created',
WEBVIEW_FILE_DROP = 'tauri://file-drop',
WEBVIEW_FILE_DROP_HOVER = 'tauri://file-drop-hover',
WEBVIEW_FILE_DROP_CANCELLED = 'tauri://file-drop-cancelled'
FILE_DROP = 'tauri://file-drop',
FILE_DROP_HOVER = 'tauri://file-drop-hover',
FILE_DROP_CANCELLED = 'tauri://file-drop-cancelled'
}
/**
@ -183,8 +183,8 @@ async function emit(event: string, payload?: unknown): Promise<void> {
*
* @example
* ```typescript
* import { emit } from '@tauri-apps/api/event';
* await emit('frontend-loaded', { loggedIn: true, token: 'authToken' });
* import { emitTo } from '@tauri-apps/api/event';
* await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' });
* ```
*
* @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object.

View File

@ -18,9 +18,21 @@ import * as event from './event'
import * as core from './core'
import * as window from './window'
import * as webview from './webview'
import * as webviewWindow from './webviewWindow'
import * as path from './path'
import * as dpi from './dpi'
import * as tray from './tray'
import * as menu from './menu'
export { app, dpi, event, path, core, window, webview, tray, menu }
export {
app,
dpi,
event,
path,
core,
window,
webview,
webviewWindow,
tray,
menu
}

View File

@ -31,7 +31,6 @@ import {
} from './event'
import { invoke } from './core'
import { Window, getCurrent as getCurrentWindow } from './window'
import type { WindowOptions } from './window'
interface FileDropPayload {
paths: string[]
@ -47,7 +46,7 @@ type FileDropEvent =
/**
* Get an instance of `Webview` for the current webview.
*
* @since 1.0.0
* @since 2.0.0
*/
function getCurrent(): Webview {
return new Webview(
@ -63,7 +62,7 @@ function getCurrent(): Webview {
/**
* Gets a list of instances of `Webview` for all available webviews.
*
* @since 1.0.0
* @since 2.0.0
*/
function getAll(): Webview[] {
return window.__TAURI_INTERNALS__.metadata.webviews.map(
@ -298,7 +297,7 @@ class Webview {
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/webview';
* await getCurrent().emit('webview-loaded', { loggedIn: true, token: 'authToken' });
* await getCurrent().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' });
* ```
*
* @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object.
@ -506,7 +505,7 @@ class Webview {
handler: EventCallback<FileDropEvent>
): Promise<UnlistenFn> {
const unlistenFileDrop = await this.listen<FileDropPayload>(
TauriEvent.WEBVIEW_FILE_DROP,
TauriEvent.FILE_DROP,
(event) => {
handler({
...event,
@ -520,7 +519,7 @@ class Webview {
)
const unlistenFileHover = await this.listen<FileDropPayload>(
TauriEvent.WEBVIEW_FILE_DROP_HOVER,
TauriEvent.FILE_DROP_HOVER,
(event) => {
handler({
...event,
@ -534,7 +533,7 @@ class Webview {
)
const unlistenCancel = await this.listen<null>(
TauriEvent.WEBVIEW_FILE_DROP_CANCELLED,
TauriEvent.FILE_DROP_CANCELLED,
(event) => {
handler({ ...event, payload: { type: 'cancel' } })
}
@ -552,199 +551,10 @@ function mapPhysicalPosition(m: PhysicalPosition): PhysicalPosition {
return new PhysicalPosition(m.x, m.y)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
interface WebviewWindow extends Webview, Window {}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class WebviewWindow {
label: string
/** Local event listeners. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listeners: Record<string, Array<EventCallback<any>>>
/**
* Creates a new {@link Window} hosting a {@link Webview}.
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webview'
* const webview = new WebviewWindow('my-label', {
* url: 'https://github.com/tauri-apps/tauri'
* });
* webview.once('tauri://created', function () {
* // webview successfully created
* });
* webview.once('tauri://error', function (e) {
* // an error happened creating the webview
* });
* ```
*
* @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`.
* @returns The {@link WebviewWindow} instance to communicate with the window and webview.
*/
constructor(
label: WebviewLabel,
options: Omit<WebviewOptions, 'x' | 'y' | 'width' | 'height'> &
WindowOptions = {}
) {
this.label = label
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.listeners = Object.create(null)
// @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions
if (!options?.skip) {
invoke('plugin:webview|create_webview_window', {
options: {
...options,
parent:
typeof options.parent === 'string'
? options.parent
: options.parent?.label,
label
}
})
.then(async () => this.emit('tauri://created'))
.catch(async (e: string) => this.emit('tauri://error', e))
}
}
/**
* Gets the Webview for the webview associated with the given label.
* @example
* ```typescript
* import { Webview } from '@tauri-apps/api/webview';
* const mainWebview = Webview.getByLabel('main');
* ```
*
* @param label The webview label.
* @returns The Webview instance to communicate with the webview or null if the webview doesn't exist.
*/
static getByLabel(label: string): WebviewWindow | null {
const webview = getAll().find((w) => w.label === label) ?? null
if (webview) {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return new WebviewWindow(webview.label, { skip: true })
}
return null
}
/**
* Get an instance of `Webview` for the current webview.
*/
static getCurrent(): WebviewWindow {
const webview = getCurrent()
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return new WebviewWindow(webview.label, { skip: true })
}
/**
* Gets a list of instances of `Webview` for all available webviews.
*/
static getAll(): WebviewWindow[] {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return getAll().map((w) => new WebviewWindow(w.label, { skip: true }))
}
/**
* Listen to an emitted event on this webivew window.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webview';
* const unlisten = await WebviewWindow.getCurrent().listen<string>('state-changed', (event) => {
* console.log(`Got error: ${payload}`);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler.
* @returns A promise resolving to a function to unlisten to the event.
* Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
*/
async listen<T>(
event: EventName,
handler: EventCallback<T>
): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, security/detect-object-injection
const listeners = this.listeners[event]
listeners.splice(listeners.indexOf(handler), 1)
})
}
return listen(event, handler, {
target: { kind: 'WebviewWindow', label: this.label }
})
}
/**
* Listen to an emitted event on this webivew window only once.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webview';
* const unlisten = await WebviewWindow.getCurrent().once<null>('initialized', (event) => {
* console.log(`Webview initialized!`);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler.
* @returns A promise resolving to a function to unlisten to the event.
* Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
*/
async once<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// eslint-disable-next-line security/detect-object-injection
const listeners = this.listeners[event]
listeners.splice(listeners.indexOf(handler), 1)
})
}
return once(event, handler, {
target: { kind: 'WebviewWindow', label: this.label }
})
}
}
// Order matters, we use window APIs by default
applyMixins(WebviewWindow, [Window, Webview])
/** Extends a base class by other specifed classes, wihtout overriding existing properties */
function applyMixins(
baseClass: { prototype: unknown },
extendedClasses: unknown
): void {
;(Array.isArray(extendedClasses)
? extendedClasses
: [extendedClasses]
).forEach((extendedClass: { prototype: unknown }) => {
Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => {
if (
typeof baseClass.prototype === 'object' &&
baseClass.prototype &&
name in baseClass.prototype
)
return
Object.defineProperty(
baseClass.prototype,
name,
// eslint-disable-next-line
Object.getOwnPropertyDescriptor(extendedClass.prototype, name) ??
Object.create(null)
)
})
})
}
/**
* Configuration for the webview to create.
*
* @since 1.0.0
* @since 2.0.0
*/
interface WebviewOptions {
/**
@ -803,6 +613,6 @@ interface WebviewOptions {
proxyUrl?: string
}
export { Webview, WebviewWindow, getCurrent, getAll }
export { Webview, getCurrent, getAll }
export type { FileDropEvent, WebviewOptions }
export type { FileDropEvent, FileDropPayload, WebviewOptions }

View File

@ -0,0 +1,234 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import {
getCurrent as getCurrentWebview,
Webview,
WebviewLabel,
WebviewOptions
} from './webview'
import type { WindowOptions } from './window'
import { Window } from './window'
import { listen, once } from './event'
import type { EventName, EventCallback, UnlistenFn } from './event'
import { invoke } from './core'
import type { FileDropEvent, FileDropPayload } from './webview'
/**
* Get an instance of `Webview` for the current webview window.
*
* @since 2.0.0
*/
function getCurrent(): WebviewWindow {
const webview = getCurrentWebview()
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return new WebviewWindow(webview.label, { skip: true })
}
/**
* Gets a list of instances of `Webview` for all available webview windows.
*
* @since 2.0.0
*/
function getAll(): WebviewWindow[] {
return window.__TAURI_INTERNALS__.metadata.webviews.map(
(w) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
new WebviewWindow(w.label, {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
skip: true
})
)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
interface WebviewWindow extends Webview, Window {}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class WebviewWindow {
label: string
/** Local event listeners. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listeners: Record<string, Array<EventCallback<any>>>
/**
* Creates a new {@link Window} hosting a {@link Webview}.
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
* const webview = new WebviewWindow('my-label', {
* url: 'https://github.com/tauri-apps/tauri'
* });
* webview.once('tauri://created', function () {
* // webview successfully created
* });
* webview.once('tauri://error', function (e) {
* // an error happened creating the webview
* });
* ```
*
* @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`.
* @returns The {@link WebviewWindow} instance to communicate with the window and webview.
*/
constructor(
label: WebviewLabel,
options: Omit<WebviewOptions, 'x' | 'y' | 'width' | 'height'> &
WindowOptions = {}
) {
this.label = label
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.listeners = Object.create(null)
// @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions
if (!options?.skip) {
invoke('plugin:webview|create_webview_window', {
options: {
...options,
parent:
typeof options.parent === 'string'
? options.parent
: options.parent?.label,
label
}
})
.then(async () => this.emit('tauri://created'))
.catch(async (e: string) => this.emit('tauri://error', e))
}
}
/**
* Gets the Webview for the webview associated with the given label.
* @example
* ```typescript
* import { Webview } from '@tauri-apps/api/webviewWindow';
* const mainWebview = Webview.getByLabel('main');
* ```
*
* @param label The webview label.
* @returns The Webview instance to communicate with the webview or null if the webview doesn't exist.
*/
static getByLabel(label: string): WebviewWindow | null {
const webview = getAll().find((w) => w.label === label) ?? null
if (webview) {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return new WebviewWindow(webview.label, { skip: true })
}
return null
}
/**
* Get an instance of `Webview` for the current webview.
*/
static getCurrent(): WebviewWindow {
return getCurrent()
}
/**
* Gets a list of instances of `Webview` for all available webviews.
*/
static getAll(): WebviewWindow[] {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return getAll().map((w) => new WebviewWindow(w.label, { skip: true }))
}
/**
* Listen to an emitted event on this webivew window.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
* const unlisten = await WebviewWindow.getCurrent().listen<string>('state-changed', (event) => {
* console.log(`Got error: ${payload}`);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler.
* @returns A promise resolving to a function to unlisten to the event.
* Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
*/
async listen<T>(
event: EventName,
handler: EventCallback<T>
): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, security/detect-object-injection
const listeners = this.listeners[event]
listeners.splice(listeners.indexOf(handler), 1)
})
}
return listen(event, handler, {
target: { kind: 'WebviewWindow', label: this.label }
})
}
/**
* Listen to an emitted event on this webivew window only once.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
* const unlisten = await WebviewWindow.getCurrent().once<null>('initialized', (event) => {
* console.log(`Webview initialized!`);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler.
* @returns A promise resolving to a function to unlisten to the event.
* Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
*/
async once<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// eslint-disable-next-line security/detect-object-injection
const listeners = this.listeners[event]
listeners.splice(listeners.indexOf(handler), 1)
})
}
return once(event, handler, {
target: { kind: 'WebviewWindow', label: this.label }
})
}
}
// Order matters, we use window APIs by default
applyMixins(WebviewWindow, [Window, Webview])
/** Extends a base class by other specifed classes, wihtout overriding existing properties */
function applyMixins(
baseClass: { prototype: unknown },
extendedClasses: unknown
): void {
;(Array.isArray(extendedClasses)
? extendedClasses
: [extendedClasses]
).forEach((extendedClass: { prototype: unknown }) => {
Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => {
if (
typeof baseClass.prototype === 'object' &&
baseClass.prototype &&
name in baseClass.prototype
)
return
Object.defineProperty(
baseClass.prototype,
name,
// eslint-disable-next-line
Object.getOwnPropertyDescriptor(extendedClass.prototype, name) ??
Object.create(null)
)
})
})
}
export { WebviewWindow, getCurrent, getAll }
export type { FileDropEvent, FileDropPayload }

View File

@ -34,7 +34,8 @@ import {
once
} from './event'
import { invoke } from './core'
import { WebviewWindow } from './webview'
import { WebviewWindow } from './webviewWindow'
import type { FileDropEvent, FileDropPayload } from './webview'
/**
* Allows you to retrieve information about a given monitor.
@ -458,7 +459,7 @@ class Window {
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/window';
* await getCurrent().emit('window-loaded', { loggedIn: true, token: 'authToken' });
* await getCurrent().emit('main', 'window-loaded', { loggedIn: true, token: 'authToken' });
* ```
* @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object.
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
@ -1716,6 +1717,76 @@ class Window {
}
/* eslint-enable */
/**
* Listen to a file drop event.
* The listener is triggered when the user hovers the selected files on the webview,
* drops the files or cancels the operation.
*
* @example
* ```typescript
* import { getCurrent } from "@tauri-apps/api/webview";
* const unlisten = await getCurrent().onFileDropEvent((event) => {
* if (event.payload.type === 'hover') {
* console.log('User hovering', event.payload.paths);
* } else if (event.payload.type === 'drop') {
* console.log('User dropped', event.payload.paths);
* } else {
* console.log('File drop cancelled');
* }
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @returns A promise resolving to a function to unlisten to the event.
* Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted.
*/
async onFileDropEvent(
handler: EventCallback<FileDropEvent>
): Promise<UnlistenFn> {
const unlistenFileDrop = await this.listen<FileDropPayload>(
TauriEvent.FILE_DROP,
(event) => {
handler({
...event,
payload: {
type: 'drop',
paths: event.payload.paths,
position: mapPhysicalPosition(event.payload.position)
}
})
}
)
const unlistenFileHover = await this.listen<FileDropPayload>(
TauriEvent.FILE_DROP_HOVER,
(event) => {
handler({
...event,
payload: {
type: 'hover',
paths: event.payload.paths,
position: mapPhysicalPosition(event.payload.position)
}
})
}
)
const unlistenCancel = await this.listen<null>(
TauriEvent.FILE_DROP_CANCELLED,
(event) => {
handler({ ...event, payload: { type: 'cancel' } })
}
)
return () => {
unlistenFileDrop()
unlistenFileHover()
unlistenCancel()
}
}
/**
* Listen to window focus change.
*
@ -2200,5 +2271,7 @@ export type {
TitleBarStyle,
ScaleFactorChanged,
WindowOptions,
Color
Color,
FileDropEvent,
FileDropPayload
}

View File

@ -302,7 +302,7 @@ fn build_ignore_matcher(dir: &Path) -> IgnoreMatcher {
for line in crate::dev::TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE
.lines()
.flatten()
.map_while(Result::ok)
{
let _ = ignore_builder.add_line(None, &line);
}

View File

@ -43,6 +43,10 @@ pub fn migrate(app_dir: &Path, tauri_dir: &Path) -> Result<()> {
let new = "@tauri-apps/api/core".to_string();
log::info!("Replacing `{original}` with `{new}` on {}", path.display());
new
} else if module == "window" {
let new = "@tauri-apps/api/webviewWindow".to_string();
log::info!("Replacing `{original}` with `{new}` on {}", path.display());
new
} else if CORE_API_MODULES.contains(&module) {
original.to_string()
} else {