mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-08-17 03:40:23 +03:00
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:
parent
5618f6d2ff
commit
16e550ec15
9
.changes/api-tauri-event-file-drop-rename.md
Normal file
9
.changes/api-tauri-event-file-drop-rename.md
Normal 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`
|
5
.changes/api-webview-window-new-methods.md
Normal file
5
.changes/api-webview-window-new-methods.md
Normal 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`.
|
5
.changes/api-webview-window.md
Normal file
5
.changes/api-webview-window.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'@tauri-apps/api': 'patch:breaking'
|
||||
---
|
||||
|
||||
Move `WebviewWindow` class from `webview` module to a new `webviewWindow` module.
|
5
.changes/api-window-on-filedrop.md
Normal file
5
.changes/api-window-on-filedrop.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'@tauri-apps/api': 'patch:feat'
|
||||
---
|
||||
|
||||
Add `Window.onFileDropEvent` method.
|
5
.changes/core-js-event-anytarget.md
Normal file
5
.changes/core-js-event-anytarget.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'tauri': 'patch:bug'
|
||||
---
|
||||
|
||||
Fix JS event listeners registered using JS `listen` api with `EventTarget::Any` never fired.
|
6
.changes/tauri-runtime-webview-events.md
Normal file
6
.changes/tauri-runtime-webview-events.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
'tauri-runtime': 'patch'
|
||||
'tauri-runtime-wry': 'patch'
|
||||
---
|
||||
|
||||
Add `WebviewEvent`, `RunEvent::WebviewEvent` and `WebviewDispatch::on_webview_event`.
|
9
.changes/tauri-webview-events.md
Normal file
9
.changes/tauri-webview-events.md
Normal 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.
|
2
.github/workflows/test-core.yml
vendored
2
.github/workflows/test-core.yml
vendored
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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<()>;
|
||||
|
||||
|
@ -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
@ -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"))]
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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
|
||||
|
@ -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()),
|
||||
);
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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!(
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
|
234
tooling/api/src/webviewWindow.ts
Normal file
234
tooling/api/src/webviewWindow.ts
Normal 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 }
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user