mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-02-06 17:26:48 +00:00
feat(core): allow listening to event loop events & prevent window close (#2131)
This commit is contained in:
parent
d69b1cf6d7
commit
8157a68af1
7
.changes/allow-prevent-window-close.md
Normal file
7
.changes/allow-prevent-window-close.md
Normal 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
5
.changes/app-callback.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Add `App#run` method with callback argument (event loop event handler).
|
||||
@ -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<()> {
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ impl FileDialogBuilder {
|
||||
}
|
||||
|
||||
/// Response for the ask dialog
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AskResponse {
|
||||
/// User confirmed.
|
||||
Yes,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
@ -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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user