feat(api): add abstractions to updater and window event listeners (#4569)

This commit is contained in:
Lucas Fernandes Nogueira 2022-07-05 16:57:53 -03:00 committed by GitHub
parent e29fff2566
commit b02fc90f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 431 additions and 147 deletions

View File

@ -0,0 +1,5 @@
---
"api": patch
---
Added helper functions to listen to updater and window events.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -214,24 +214,27 @@ fn main() {
event: WindowEvent::CloseRequested { api, .. },
..
} => {
let app_handle = app_handle.clone();
let window = app_handle.get_window(&label).unwrap();
// use the exposed close api, and prevent the event loop to close
api.prevent_close();
// ask the user if he wants to quit
ask(
Some(&window),
"Tauri API",
"Are you sure that you want to close this window?",
move |answer| {
if answer {
// .close() cannot be called on the main thread
std::thread::spawn(move || {
app_handle.get_window(&label).unwrap().close().unwrap();
});
}
},
);
// for other windows, we handle it in JS
if label == "main" {
let app_handle = app_handle.clone();
let window = app_handle.get_window(&label).unwrap();
// use the exposed close api, and prevent the event loop to close
api.prevent_close();
// ask the user if he wants to quit
ask(
Some(&window),
"Tauri API",
"Are you sure that you want to close this window?",
move |answer| {
if answer {
// .close() cannot be called on the main thread
std::thread::spawn(move || {
app_handle.get_window(&label).unwrap().close().unwrap();
});
}
},
);
}
}
// Keep the event loop running even if all windows are closed

View File

@ -22,8 +22,18 @@
import { listen } from '@tauri-apps/api/event'
import { ask } from '@tauri-apps/api/dialog'
appWindow.listen('tauri://file-drop', function (event) {
onMessage(`File drop: ${event.payload}`)
if (appWindow.label !== 'main') {
appWindow.onCloseRequested(async (event) => {
const confirmed = await confirm('Are you sure?')
if (!confirmed) {
// user did not confirm closing the window; let's prevent it
event.preventDefault()
}
})
}
appWindow.onFileDropEvent((event) => {
onMessage(`File drop: ${JSON.stringify(event.payload)}`)
})
const views = [

View File

@ -19,6 +19,7 @@ import type {
/**
* Listen to an event from the backend.
*
* @example
* ```typescript
* import { listen } from '@tauri-apps/api/event';
@ -26,13 +27,14 @@ import type {
* console.log(`Got error in window ${event.windowLabel}, payload: ${payload}`);
* });
*
* // removes the listener later
* await unlisten();
* // 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 callback.
* @return 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 function listen<T>(
event: EventName,
@ -43,6 +45,7 @@ async function listen<T>(
/**
* Listen to an one-off event from the backend.
*
* @example
* ```typescript
* import { once } from '@tauri-apps/api/event';
@ -53,11 +56,15 @@ async function listen<T>(
* const unlisten = await once<LoadedPayload>('loaded', (event) => {
* console.log(`App is loaded, logggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`);
* });
*
* // 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 callback.
* @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 function once<T>(
event: EventName,

View File

@ -5,7 +5,6 @@
import { WindowLabel } from '../window'
import { invokeTauriCommand } from './tauri'
import { transformCallback } from '../tauri'
import { LiteralUnion } from 'type-fest'
export interface Event<T> {
/** Event name */
@ -18,25 +17,7 @@ export interface Event<T> {
payload: T
}
export type EventName = LiteralUnion<
| 'tauri://update'
| 'tauri://update-available'
| 'tauri://update-download-progress'
| 'tauri://update-install'
| 'tauri://update-status'
| 'tauri://resize'
| 'tauri://move'
| 'tauri://close-requested'
| 'tauri://focus'
| 'tauri://blur'
| 'tauri://scale-change'
| 'tauri://menu'
| 'tauri://file-drop'
| 'tauri://file-drop-hover'
| 'tauri://file-drop-cancelled'
| 'tauri://theme-changed',
string
>
export type EventName = string
export type EventCallback<T> = (event: Event<T>) => void

View File

@ -29,6 +29,31 @@ interface UpdateResult {
shouldUpdate: boolean
}
/**
* Listen to an updater event.
* @example
* ```typescript
* import { onUpdaterEvent } from "@tauri-apps/api/updater";
* const unlisten = await onUpdaterEvent(({ error, status }) => {
* console.log('Updater event', error, status);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 function onUpdaterEvent(
handler: (status: UpdateStatusResult) => void
): Promise<UnlistenFn> {
return listen('tauri://update-status', (data: { payload: any }) => {
handler(data?.payload as UpdateStatusResult)
})
}
/**
* Install the update if there's one available.
* @example
@ -68,9 +93,7 @@ async function installUpdate(): Promise<void> {
}
// listen status change
listen('tauri://update-status', (data: { payload: any }) => {
onStatusChange(data?.payload as UpdateStatusResult)
})
onUpdaterEvent(onStatusChange)
.then((fn) => {
unlistenerFn = fn
})
@ -144,9 +167,7 @@ async function checkUpdate(): Promise<UpdateResult> {
})
// listen status change
listen('tauri://update-status', (data: { payload: any }) => {
onStatusChange(data?.payload as UpdateStatusResult)
})
onUpdaterEvent(onStatusChange)
.then((fn) => {
unlistenerFn = fn
})
@ -167,4 +188,4 @@ async function checkUpdate(): Promise<UpdateResult> {
export type { UpdateStatus, UpdateStatusResult, UpdateManifest, UpdateResult }
export { installUpdate, checkUpdate }
export { onUpdaterEvent, installUpdate, checkUpdate }

View File

@ -53,67 +53,8 @@
*
* Events can be listened using `appWindow.listen`:
* ```typescript
* import { appWindow } from "@tauri-apps/api/window"
* appWindow.listen("tauri://move", ({ event, payload }) => {
* const { x, y } = payload; // payload here is a `PhysicalPosition`
* });
* ```
*
* Window-specific events emitted by the backend:
*
* #### 'tauri://resize'
* Emitted when the size of the window has changed.
* *EventPayload*:
* ```typescript
* type ResizePayload = PhysicalSize
* ```
*
* #### 'tauri://move'
* Emitted when the position of the window has changed.
* *EventPayload*:
* ```typescript
* type MovePayload = PhysicalPosition
* ```
*
* #### 'tauri://close-requested'
* Emitted when the user requests the window to be closed.
* If a listener is registered for this event, Tauri won't close the window so you must call `appWindow.close()` manually.
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* import { confirm } from '@tauri-apps/api/dialog';
* appWindow.listen("tauri://close-requested", async ({ event, payload }) => {
* const confirmed = await confirm('Are you sure?');
* if (confirmed) {
* await appWindow.close();
* }
* });
* ```
*
* #### 'tauri://focus'
* Emitted when the window gains focus.
*
* #### 'tauri://blur'
* Emitted when the window loses focus.
*
* #### 'tauri://scale-change'
* Emitted when the window's scale factor has changed.
* The following user actions can cause DPI changes:
* - Changing the display's resolution.
* - Changing the display's scale factor (e.g. in Control Panel on Windows).
* - Moving the window to a display with a different scale factor.
* *Event payload*:
* ```typescript
* interface ScaleFactorChanged {
* scaleFactor: number
* size: PhysicalSize
* }
* ```
*
* #### 'tauri://menu'
* Emitted when a menu item is clicked.
* *EventPayload*:
* ```typescript
* type MenuClicked = string
* appWindow.listen("my-window-event", ({ event, payload }) => { });
* ```
*
* @module
@ -121,7 +62,7 @@
import { invokeTauriCommand } from './helpers/tauri'
import type { EventName, EventCallback, UnlistenFn } from './event'
import { emit, listen, once } from './helpers/event'
import { emit, Event, listen, once } from './helpers/event'
type Theme = 'light' | 'dark'
@ -137,6 +78,20 @@ interface Monitor {
scaleFactor: number
}
/** The payload for the `scaleChange` event. */
interface ScaleFactorChanged {
/** The new window scale factor. */
scaleFactor: number
/** The new window size */
size: PhysicalSize
}
/** The file drop event types. */
type FileDropEvent =
| { type: 'hover'; paths: string[] }
| { type: 'drop'; paths: string[] }
| { type: 'cancel' }
/** A size represented in logical pixels. */
class LogicalSize {
type = 'Logical'
@ -335,9 +290,21 @@ class WebviewWindowHandle {
/**
* Listen to an event emitted by the backend that is tied to the webview window.
*
* @example
* ```typescript
* import { appWindow } from '@tauri-apps/api/window';
* const unlisten = await appWindow.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,
@ -356,9 +323,21 @@ class WebviewWindowHandle {
/**
* Listen to an one-off event emitted by the backend that is tied to the webview window.
*
* @example
* ```typescript
* import { appWindow } from '@tauri-apps/api/window';
* const unlisten = await appWindow.once<null>('initialized', (event) => {
* console.log(`Window 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)) {
@ -373,6 +352,11 @@ class WebviewWindowHandle {
/**
* Emits an event to the backend, tied to the webview window.
* @example
* ```typescript
* import { appWindow } from '@tauri-apps/api/window';
* await appWindow.emit('window-loaded', { loggedIn: true, token: 'authToken' });
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param payload Event payload.
@ -1517,6 +1501,278 @@ class WindowManager extends WebviewWindowHandle {
}
})
}
// Listeners
/**
* Listen to window resize.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onResized(({ payload: size }) => {
* console.log('Window resized', size);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onResized(handler: EventCallback<PhysicalSize>): Promise<UnlistenFn> {
return this.listen<PhysicalSize>('tauri://resize', handler)
}
/**
* Listen to window move.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onMoved(({ payload: position }) => {
* console.log('Window moved', position);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onMoved(handler: EventCallback<PhysicalPosition>): Promise<UnlistenFn> {
return this.listen<PhysicalPosition>('tauri://move', handler)
}
/**
* Listen to window close requested. Emitted when the user requests to closes the window.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* import { confirm } from '@tauri-apps/api/dialog';
* const unlisten = await appWindow.onCloseRequested(async (event) => {
* const confirmed = await confirm('Are you sure?');
* if (!confirmed) {
* // user did not confirm closing the window; let's prevent it
* event.preventDefault();
* }
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onCloseRequested(
handler: (event: CloseRequestedEvent) => void
): Promise<UnlistenFn> {
return this.listen<null>('tauri://close-requested', (event) => {
const evt = new CloseRequestedEvent(event)
void Promise.resolve(handler(evt)).then(() => {
if (!evt.isPreventDefault()) {
return this.close()
}
})
})
}
/**
* Listen to window focus change.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onFocusChanged(({ payload: focused }) => {
* console.log('Focus changed, window is focused? ' + focused);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onFocusChanged(handler: EventCallback<boolean>): Promise<UnlistenFn> {
const unlistenFocus = await this.listen<PhysicalPosition>(
'tauri://focus',
(event) => {
handler({ ...event, payload: true })
}
)
const unlistenBlur = await this.listen<PhysicalPosition>(
'tauri://blur',
(event) => {
handler({ ...event, payload: false })
}
)
return () => {
unlistenFocus()
unlistenBlur()
}
}
/**
* Listen to window scale change. Emitted when the window's scale factor has changed.
* The following user actions can cause DPI changes:
* - Changing the display's resolution.
* - Changing the display's scale factor (e.g. in Control Panel on Windows).
* - Moving the window to a display with a different scale factor.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onScaleChanged(({ payload }) => {
* console.log('Scale changed', payload.scaleFactor, payload.size);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onScaleChanged(
handler: EventCallback<ScaleFactorChanged>
): Promise<UnlistenFn> {
return this.listen<ScaleFactorChanged>('tauri://scale-change', handler)
}
/**
* Listen to the window menu item click. The payload is the item id.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onMenuClicked(({ payload: menuId }) => {
* console.log('Menu clicked: ' + menuId);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onMenuClicked(handler: EventCallback<string>): Promise<UnlistenFn> {
return this.listen<string>('tauri://menu', handler)
}
/**
* Listen to a file drop event.
* The listener is triggered when the user hovers the selected files on the window,
* drops the files or cancels the operation.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.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();
* ```
*
* @param 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 onFileDropEvent(
handler: EventCallback<FileDropEvent>
): Promise<UnlistenFn> {
const unlistenFileDrop = await this.listen<string[]>(
'tauri://file-drop',
(event) => {
handler({ ...event, payload: { type: 'drop', paths: event.payload } })
}
)
const unlistenFileHover = await this.listen<string[]>(
'tauri://file-drop-hover',
(event) => {
handler({ ...event, payload: { type: 'hover', paths: event.payload } })
}
)
const unlistenCancel = await this.listen<null>(
'tauri://file-drop-cancelled',
(event) => {
handler({ ...event, payload: { type: 'cancel' } })
}
)
return () => {
unlistenFileDrop()
unlistenFileHover()
unlistenCancel()
}
}
/**
* Listen to the system theme change.
*
* @example
* ```typescript
* import { appWindow } from "@tauri-apps/api/window";
* const unlisten = await appWindow.onThemeChanged(({ payload: theme }) => {
* console.log('New theme: ' + theme);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
* unlisten();
* ```
*
* @param 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 onThemeChanged(handler: EventCallback<Theme>): Promise<UnlistenFn> {
return this.listen<Theme>('tauri://theme-changed', handler)
}
}
class CloseRequestedEvent {
/** Event name */
event: EventName
/** The label of the window that emitted this event. */
windowLabel: string
/** Event identifier used to unlisten */
id: number
private _preventDefault = false
constructor(event: Event<null>) {
this.event = event.event
this.windowLabel = event.windowLabel
this.id = event.id
}
preventDefault(): void {
this._preventDefault = true
}
isPreventDefault(): boolean {
return this._preventDefault
}
}
/**
@ -1769,6 +2025,7 @@ export {
WebviewWindow,
WebviewWindowHandle,
WindowManager,
CloseRequestedEvent,
getCurrent,
getAll,
appWindow,
@ -1782,4 +2039,4 @@ export {
availableMonitors
}
export type { Theme, Monitor, WindowOptions }
export type { Theme, Monitor, ScaleFactorChanged, FileDropEvent, WindowOptions }