Browse Source

feat(core): allow listening to event loop events & prevent window close (#2131)

Lucas Fernandes Nogueira 4 years ago
parent
commit
8157a68af1

+ 7 - 0
.changes/allow-prevent-window-close.md

@@ -0,0 +1,7 @@
+---
+"tauri": patch
+"tauri-runtime": patch
+"tauri-runtime-wry": patch
+---
+
+Allow preventing window close when the user requests it.

+ 5 - 0
.changes/app-callback.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Add `App#run` method with callback argument (event loop event handler).

+ 43 - 19
core/tauri-runtime-wry/src/lib.rs

@@ -1490,6 +1490,11 @@ fn handle_event_loop(
     #[cfg(feature = "system-tray")]
     tray_context,
   } = context;
+  if *control_flow == ControlFlow::Exit {
+    return RunIteration {
+      webview_count: webviews.len(),
+    };
+  }
   *control_flow = ControlFlow::Wait;
 
   match event {
@@ -1562,14 +1567,26 @@ fn handle_event_loop(
       }
       match event {
         WryWindowEvent::CloseRequested => {
-          on_window_close(
-            callback,
-            window_id,
-            &mut webviews,
-            control_flow,
-            #[cfg(feature = "menu")]
-            menu_event_listeners.clone(),
-          );
+          let (tx, rx) = channel();
+          if let Some(w) = webviews.get(&window_id) {
+            callback(RunEvent::CloseRequested {
+              label: w.label.clone(),
+              signal_tx: tx,
+            });
+            if let Ok(true) = rx.try_recv() {
+            } else {
+              on_window_close(
+                callback,
+                window_id,
+                &mut webviews,
+                control_flow,
+                #[cfg(target_os = "linux")]
+                window_event_listeners,
+                #[cfg(feature = "menu")]
+                menu_event_listeners.clone(),
+              );
+            }
+          }
         }
         // we also resize the webview on `Moved` to fix https://github.com/tauri-apps/tauri/issues/1911
         WryWindowEvent::Resized(_) | WryWindowEvent::Moved(_) => {
@@ -1666,22 +1683,13 @@ fn handle_event_loop(
             WindowMessage::Show => window.set_visible(true),
             WindowMessage::Hide => window.set_visible(false),
             WindowMessage::Close => {
-              for handler in window_event_listeners
-                .lock()
-                .unwrap()
-                .get(&window.id())
-                .unwrap()
-                .lock()
-                .unwrap()
-                .values()
-              {
-                handler(&WindowEvent::CloseRequested);
-              }
               on_window_close(
                 callback,
                 id,
                 &mut webviews,
                 control_flow,
+                #[cfg(target_os = "linux")]
+                window_event_listeners,
                 #[cfg(feature = "menu")]
                 menu_event_listeners.clone(),
               );
@@ -1851,6 +1859,7 @@ fn on_window_close<'a>(
   window_id: WindowId,
   webviews: &mut MutexGuard<'a, HashMap<WindowId, WebviewWrapper>>,
   control_flow: &mut ControlFlow,
+  #[cfg(target_os = "linux")] window_event_listeners: &WindowEventListeners,
   #[cfg(feature = "menu")] menu_event_listeners: MenuEventListeners,
 ) {
   if let Some(webview) = webviews.remove(&window_id) {
@@ -1862,6 +1871,21 @@ fn on_window_close<'a>(
     *control_flow = ControlFlow::Exit;
     callback(RunEvent::Exit);
   }
+  // TODO: tao does not fire the destroyed event properly
+  #[cfg(target_os = "linux")]
+  {
+    for handler in window_event_listeners
+      .lock()
+      .unwrap()
+      .get(&window_id)
+      .unwrap()
+      .lock()
+      .unwrap()
+      .values()
+    {
+      handler(&WindowEvent::Destroyed);
+    }
+  }
 }
 
 fn center_window(window: &Window) -> Result<()> {

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

@@ -6,7 +6,7 @@
 
 #![cfg_attr(doc_cfg, feature(doc_cfg))]
 
-use std::{fmt::Debug, hash::Hash, path::PathBuf};
+use std::{fmt::Debug, hash::Hash, path::PathBuf, sync::mpsc::Sender};
 
 use serde::{Deserialize, Serialize};
 use tauri_utils::assets::Assets;
@@ -189,9 +189,17 @@ impl Icon {
 }
 
 /// Event triggered on the event loop run.
+#[non_exhaustive]
 pub enum RunEvent {
   /// Event loop is exiting.
   Exit,
+  /// Window close was requested by the user.
+  CloseRequested {
+    /// The window label.
+    label: String,
+    /// A signal sender. If a `true` value is emitted, the window won't be closed.
+    signal_tx: Sender<bool>,
+  },
   /// Window closed.
   WindowClose(String),
 }

+ 1 - 0
core/tauri/src/api/dialog.rs

@@ -65,6 +65,7 @@ impl FileDialogBuilder {
 }
 
 /// Response for the ask dialog
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum AskResponse {
   /// User confirmed.
   Yes,

+ 71 - 20
core/tauri/src/app.rs

@@ -25,7 +25,11 @@ use crate::{
 
 use tauri_utils::PackageInfo;
 
-use std::{collections::HashMap, path::PathBuf, sync::Arc};
+use std::{
+  collections::HashMap,
+  path::PathBuf,
+  sync::{mpsc::Sender, Arc},
+};
 
 #[cfg(feature = "menu")]
 use crate::runtime::menu::Menu;
@@ -45,6 +49,33 @@ pub(crate) type GlobalWindowEventListener<P> = Box<dyn Fn(GlobalWindowEvent<P>)
 type SystemTrayEventListener<P> =
   Box<dyn Fn(&AppHandle<P>, tray::SystemTrayEvent<<P as Params>::SystemTrayMenuId>) + Send + Sync>;
 
+/// Api exposed on the `CloseRequested` event.
+pub struct CloseRequestApi(Sender<bool>);
+
+impl CloseRequestApi {
+  /// Prevents the window from being closed.
+  pub fn prevent_close(&self) {
+    self.0.send(true).unwrap();
+  }
+}
+
+/// An application event, triggered from the event loop.
+#[non_exhaustive]
+pub enum Event<P: Params> {
+  /// Event loop is exiting.
+  Exit,
+  /// Window close was requested by the user.
+  #[non_exhaustive]
+  CloseRequested {
+    /// The window label.
+    label: P::Label,
+    /// Event API.
+    api: CloseRequestApi,
+  },
+  /// Window closed.
+  WindowClosed(P::Label),
+}
+
 crate::manager::default_args! {
   /// A menu event that was triggered on a window.
   #[cfg(feature = "menu")]
@@ -271,6 +302,42 @@ impl<P: Params> App<P> {
     self.handle.clone()
   }
 
+  /// Runs the application.
+  pub fn run<F: Fn(&AppHandle<P>, Event<P>) + 'static>(mut self, callback: F) {
+    let app_handle = self.handle();
+    let manager = self.manager.clone();
+    self.runtime.take().unwrap().run(move |event| match event {
+      RunEvent::Exit => {
+        #[cfg(shell_execute)]
+        {
+          crate::api::process::kill_children();
+        }
+        #[cfg(all(windows, feature = "system-tray"))]
+        {
+          let _ = app_handle.remove_system_tray();
+        }
+        callback(&app_handle, Event::Exit);
+      }
+      _ => {
+        on_event_loop_event(&event, &manager);
+        callback(
+          &app_handle,
+          match event {
+            RunEvent::Exit => Event::Exit,
+            RunEvent::CloseRequested { label, signal_tx } => Event::CloseRequested {
+              label: label.parse().unwrap_or_else(|_| unreachable!()),
+              api: CloseRequestApi(signal_tx),
+            },
+            RunEvent::WindowClose(label) => {
+              Event::WindowClosed(label.parse().unwrap_or_else(|_| unreachable!()))
+            }
+            _ => unimplemented!(),
+          },
+        );
+      }
+    });
+  }
+
   /// Runs a iteration of the runtime event loop and immediately return.
   ///
   /// Note that when using this API, app cleanup is not automatically done.
@@ -297,7 +364,7 @@ impl<P: Params> App<P> {
       .runtime
       .as_mut()
       .unwrap()
-      .run_iteration(move |event| on_event_loop_event(event, &manager))
+      .run_iteration(move |event| on_event_loop_event(&event, &manager))
   }
 }
 
@@ -855,28 +922,12 @@ where
 
   /// Runs the configured Tauri application.
   pub fn run(self, context: Context<A>) -> crate::Result<()> {
-    let mut app = self.build(context)?;
-    #[cfg(all(windows, feature = "system-tray"))]
-    let app_handle = app.handle();
-    let manager = app.manager.clone();
-    app.runtime.take().unwrap().run(move |event| match event {
-      RunEvent::Exit => {
-        #[cfg(shell_execute)]
-        {
-          crate::api::process::kill_children();
-        }
-        #[cfg(all(windows, feature = "system-tray"))]
-        {
-          let _ = app_handle.remove_system_tray();
-        }
-      }
-      _ => on_event_loop_event(event, &manager),
-    });
+    self.build(context)?.run(|_, _| {});
     Ok(())
   }
 }
 
-fn on_event_loop_event<P: Params>(event: RunEvent, manager: &WindowManager<P>) {
+fn on_event_loop_event<P: Params>(event: &RunEvent, manager: &WindowManager<P>) {
   if let RunEvent::WindowClose(label) = event {
     manager.on_window_close(label);
   }

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

@@ -59,7 +59,7 @@ pub type Result<T> = std::result::Result<T, Error>;
 pub type SyncTask = Box<dyn FnOnce() + Send>;
 
 use crate::{
-  event::{Event, EventHandler},
+  event::{Event as EmittedEvent, EventHandler},
   runtime::window::PendingWindow,
 };
 use serde::Serialize;
@@ -83,7 +83,7 @@ pub use {
     config::{Config, WindowUrl},
     PackageInfo,
   },
-  self::app::{App, AppHandle, Builder, GlobalWindowEvent},
+  self::app::{App, AppHandle, Builder, CloseRequestApi, Event, GlobalWindowEvent},
   self::hooks::{
     Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokeResolver, InvokeResponse, OnPageLoad,
     PageLoadPayload, SetupHook,
@@ -283,7 +283,7 @@ pub trait Manager<P: Params>: sealed::ManagerBase<P> {
   /// Listen to a global event.
   fn listen_global<E: Into<P::Event>, F>(&self, event: E, handler: F) -> EventHandler
   where
-    F: Fn(Event) + Send + 'static,
+    F: Fn(EmittedEvent) + Send + 'static,
   {
     self.manager().listen(event.into(), None, handler)
   }
@@ -291,7 +291,7 @@ pub trait Manager<P: Params>: sealed::ManagerBase<P> {
   /// Listen to a global event only once.
   fn once_global<E: Into<P::Event>, F>(&self, event: E, handler: F) -> EventHandler
   where
-    F: Fn(Event) + Send + 'static,
+    F: Fn(EmittedEvent) + Send + 'static,
   {
     self.manager().once(event.into(), None, handler)
   }

+ 9 - 7
core/tauri/src/manager.rs

@@ -753,7 +753,7 @@ impl<P: Params> WindowManager<P> {
     window
   }
 
-  pub(crate) fn on_window_close(&self, label: String) {
+  pub(crate) fn on_window_close(&self, label: &str) {
     self
       .windows_lock()
       .remove(&label.parse().unwrap_or_else(|_| panic!("bad label")));
@@ -886,6 +886,14 @@ fn on_window_event<P: Params>(
           .unwrap_or_else(|_| panic!("unhandled event")),
         Some(()),
       )?;
+    }
+    WindowEvent::Destroyed => {
+      window.emit(
+        &WINDOW_DESTROYED_EVENT
+          .parse()
+          .unwrap_or_else(|_| panic!("unhandled event")),
+        Some(()),
+      )?;
       let label = window.label();
       for window in manager.inner.windows.lock().unwrap().values() {
         window.eval(&format!(
@@ -894,12 +902,6 @@ fn on_window_event<P: Params>(
         ))?;
       }
     }
-    WindowEvent::Destroyed => window.emit(
-      &WINDOW_DESTROYED_EVENT
-        .parse()
-        .unwrap_or_else(|_| panic!("unhandled event")),
-      Some(()),
-    )?,
     WindowEvent::Focused(focused) => window.emit(
       &if *focused {
         WINDOW_FOCUS_EVENT

File diff suppressed because it is too large
+ 0 - 0
examples/api/public/build/bundle.js


File diff suppressed because it is too large
+ 0 - 0
examples/api/public/build/bundle.js.map


+ 11 - 3
examples/api/src-tauri/src/main.rs

@@ -16,7 +16,8 @@ mod menu;
 
 use serde::Serialize;
 use tauri::{
-  CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder, WindowUrl,
+  CustomMenuItem, Event, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
+  WindowUrl,
 };
 
 #[derive(Serialize)]
@@ -98,6 +99,13 @@ fn main() {
       cmd::perform_request,
       menu_toggle,
     ])
-    .run(tauri::generate_context!())
-    .expect("error while running tauri application");
+    .build(tauri::generate_context!())
+    .expect("error while building tauri application")
+    .run(|app_handle, e| {
+      if let Event::CloseRequested { label, api, .. } = e {
+        api.prevent_close();
+        let window = app_handle.get_window(&label).unwrap();
+        window.emit("close-requested", ()).unwrap();
+      }
+    })
 }

+ 7 - 0
examples/api/src/App.svelte

@@ -4,6 +4,7 @@
   import hotkeys from "hotkeys-js";
   import { open } from "@tauri-apps/api/shell";
   import { invoke } from "@tauri-apps/api/tauri";
+  import { appWindow, getCurrent } from "@tauri-apps/api/window";
 
   import Welcome from "./components/Welcome.svelte";
   import Cli from "./components/Cli.svelte";
@@ -24,6 +25,12 @@
     hotkeys(MENU_TOGGLE_HOTKEY, () => {
       invoke('menu_toggle');
     });
+
+    getCurrent().listen('close-requested', async () => {
+      if (await confirm('Are you sure?')) {
+        await appWindow.close()
+      }
+    })
   });
 
   const views = [

Some files were not shown because too many files changed in this diff