Bläddra i källkod

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

Lucas Fernandes Nogueira 3 år sedan
förälder
incheckning
b02fc90f45

+ 5 - 0
.changes/api-event-listeners.md

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
core/tauri/scripts/bundle.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
examples/api/dist/assets/index.js


+ 21 - 18
examples/api/src-tauri/src/main.rs

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

+ 12 - 2
examples/api/src/App.svelte

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

+ 9 - 2
tooling/api/src/event.ts

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

+ 1 - 20
tooling/api/src/helpers/event.ts

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

+ 28 - 7
tooling/api/src/updater.ts

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

+ 319 - 62
tooling/api/src/window.ts

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

Vissa filer visades inte eftersom för många filer har ändrats