Kaynağa Gözat

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>
Amr Bashir 1 yıl önce
ebeveyn
işleme
16e550ec15

+ 9 - 0
.changes/api-tauri-event-file-drop-rename.md

@@ -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 - 0
.changes/api-webview-window-new-methods.md

@@ -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 - 0
.changes/api-webview-window.md

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

+ 5 - 0
.changes/api-window-on-filedrop.md

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

+ 5 - 0
.changes/core-js-event-anytarget.md

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

+ 6 - 0
.changes/tauri-runtime-webview-events.md

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

+ 9 - 0
.changes/tauri-webview-events.md

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

+ 1 - 1
.github/workflows/test-core.yml

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

+ 143 - 106
core/tauri-runtime-wry/src/lib.rs

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

+ 12 - 1
core/tauri-runtime/src/lib.rs

@@ -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<()>;
 

+ 7 - 0
core/tauri-runtime/src/window.rs

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
core/tauri/scripts/bundle.global.js


+ 58 - 2
core/tauri/src/app.rs

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

+ 26 - 5
core/tauri/src/event/listener.rs

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

+ 1 - 1
core/tauri/src/event/mod.rs

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

+ 5 - 3
core/tauri/src/lib.rs

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

+ 9 - 16
core/tauri/src/manager/mod.rs

@@ -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()),
     );
 

+ 61 - 4
core/tauri/src/manager/webview.rs

@@ -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(())
+}

+ 35 - 19
core/tauri/src/manager/window.rs

@@ -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) => {

+ 13 - 0
core/tauri/src/test/mock_runtime.rs

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

+ 14 - 6
core/tauri/src/webview/mod.rs

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

+ 1 - 1
core/tauri/src/webview/webview_window.rs

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

+ 5 - 5
core/tauri/src/window/mod.rs

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

+ 1 - 1
examples/api/src/views/Window.svelte

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

+ 5 - 5
tooling/api/src/event.ts

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

+ 13 - 1
tooling/api/src/index.ts

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

+ 9 - 199
tooling/api/src/webview.ts

@@ -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 - 0
tooling/api/src/webviewWindow.ts

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

+ 76 - 3
tooling/api/src/window.ts

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

+ 1 - 1
tooling/cli/src/interface/rust.rs

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

+ 4 - 0
tooling/cli/src/migrate/frontend.rs

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

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor