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

This commit is contained in:
Lucas Fernandes Nogueira 2021-07-06 13:36:37 -03:00 committed by GitHub
parent d69b1cf6d7
commit 8157a68af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 169 additions and 56 deletions

View File

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

5
.changes/app-callback.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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