Browse Source

feat(core): add `request_user_attention` API, closes #2023 (#2026)

* feat(core): add `request_user_attention` API

* fix: api lint

* fix build without window allowlist
Lucas Fernandes Nogueira 4 years ago
parent
commit
7dcca6e928

+ 5 - 0
.changes/api-request-user-attention.md

@@ -0,0 +1,5 @@
+---
+"api": patch
+---
+
+Adds `requestUserAttention` API to the `window` module.

+ 5 - 0
.changes/core-request-user-attention.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Adds `request_user_attention` API to the `Window` struct.

+ 6 - 0
.changes/runtime-request-user-attention.md

@@ -0,0 +1,6 @@
+---
+"tauri-runtime": patch
+"tauri-runtime-wry": patch
+---
+
+Adds `request_user_attention` API to the `Dispatcher` trait.

+ 33 - 1
core/tauri-runtime-wry/src/lib.rs

@@ -14,6 +14,7 @@ use tauri_runtime::{
     DetachedWindow, PendingWindow, WindowEvent,
   },
   Dispatch, Error, Icon, Params, Result, RunEvent, RunIteration, Runtime, RuntimeHandle,
+  UserAttentionType,
 };
 
 #[cfg(feature = "menu")]
@@ -39,7 +40,10 @@ use wry::{
     event::{Event, WindowEvent as WryWindowEvent},
     event_loop::{ControlFlow, EventLoop, EventLoopProxy, EventLoopWindowTarget},
     monitor::MonitorHandle,
-    window::{Fullscreen, Icon as WindowIcon, Window, WindowBuilder as WryWindowBuilder, WindowId},
+    window::{
+      Fullscreen, Icon as WindowIcon, UserAttentionType as WryUserAttentionType, Window,
+      WindowBuilder as WryWindowBuilder, WindowId,
+    },
   },
   webview::{
     FileDropEvent as WryFileDropEvent, RpcRequest as WryRpcRequest, RpcResponse, WebContext,
@@ -245,6 +249,19 @@ impl From<Position> for PositionWrapper {
   }
 }
 
+#[derive(Debug, Clone)]
+struct UserAttentionTypeWrapper(WryUserAttentionType);
+
+impl From<UserAttentionType> for UserAttentionTypeWrapper {
+  fn from(request_type: UserAttentionType) -> UserAttentionTypeWrapper {
+    let o = match request_type {
+      UserAttentionType::Critical => WryUserAttentionType::Critical,
+      UserAttentionType::Informational => WryUserAttentionType::Informational,
+    };
+    Self(o)
+  }
+}
+
 #[derive(Debug, Clone, Default)]
 pub struct WindowBuilderWrapper {
   inner: WryWindowBuilder,
@@ -467,6 +484,7 @@ enum WindowMessage {
   Hwnd(Sender<Hwnd>),
   // Setters
   Center(Sender<Result<()>>),
+  RequestUserAttention(Option<UserAttentionTypeWrapper>),
   SetResizable(bool),
   SetTitle(String),
   Maximize,
@@ -680,6 +698,17 @@ impl Dispatch for WryDispatcher {
       .map_err(|_| Error::FailedToSendMessage)
   }
 
+  fn request_user_attention(&self, request_type: Option<UserAttentionType>) -> Result<()> {
+    self
+      .context
+      .proxy
+      .send_event(Message::Window(
+        self.window_id,
+        WindowMessage::RequestUserAttention(request_type.map(Into::into)),
+      ))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
+
   // Creates a window by dispatching a message to the event loop.
   // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock.
   fn create_window<P: Params<Runtime = Self::Runtime>>(
@@ -1341,6 +1370,9 @@ fn handle_event_loop(
             WindowMessage::Center(tx) => {
               tx.send(center_window(window)).unwrap();
             }
+            WindowMessage::RequestUserAttention(request_type) => {
+              window.request_user_attention(request_type.map(|r| r.0));
+            }
             WindowMessage::SetResizable(resizable) => window.set_resizable(resizable),
             WindowMessage::SetTitle(title) => window.set_title(&title),
             WindowMessage::Maximize => window.set_maximized(true),

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

@@ -8,7 +8,7 @@
 
 use std::{fmt::Debug, hash::Hash, path::PathBuf};
 
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use tauri_utils::assets::Assets;
 use uuid::Uuid;
 
@@ -76,6 +76,20 @@ impl<I: MenuId> SystemTray<I> {
   }
 }
 
+/// Type of user attention requested on a window.
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
+#[serde(tag = "type")]
+pub enum UserAttentionType {
+  /// ## Platform-specific
+  /// - **macOS:** Bounces the dock icon until the application is in focus.
+  /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus.
+  Critical,
+  /// ## Platform-specific
+  /// - **macOS:** Bounces the dock icon once.
+  /// - **Windows:** Flashes the taskbar button until the application is in focus.
+  Informational,
+}
+
 #[derive(Debug, thiserror::Error)]
 #[non_exhaustive]
 pub enum Error {
@@ -338,6 +352,11 @@ pub trait Dispatch: Clone + Send + Sized + 'static {
   /// Opens the dialog to prints the contents of the webview.
   fn print(&self) -> crate::Result<()>;
 
+  /// Requests user attention to the window.
+  ///
+  /// Providing `None` will unset the request for user attention.
+  fn request_user_attention(&self, request_type: Option<UserAttentionType>) -> crate::Result<()>;
+
   /// Create a new webview window.
   fn create_window<P: Params<Runtime = Self::Runtime>>(
     &mut self,

File diff suppressed because it is too large
+ 0 - 0
core/tauri/scripts/bundle.js


+ 6 - 1
core/tauri/src/endpoints/window.rs

@@ -7,7 +7,10 @@ use crate::runtime::{webview::WindowBuilder, Dispatch, Runtime};
 use crate::{
   api::config::WindowConfig,
   endpoints::InvokeResponse,
-  runtime::window::dpi::{Position, Size},
+  runtime::{
+    window::dpi::{Position, Size},
+    UserAttentionType,
+  },
   Params, Window,
 };
 use serde::Deserialize;
@@ -54,6 +57,7 @@ pub enum Cmd {
   AvailableMonitors,
   // Setters
   Center,
+  RequestUserAttention(Option<UserAttentionType>),
   SetResizable(bool),
   SetTitle(String),
   Maximize,
@@ -143,6 +147,7 @@ impl Cmd {
         Self::AvailableMonitors => return Ok(window.available_monitors()?.into()),
         // Setters
         Self::Center => window.center()?,
+        Self::RequestUserAttention(request_type) => window.request_user_attention(request_type)?,
         Self::SetResizable(resizable) => window.set_resizable(resizable)?,
         Self::SetTitle(title) => window.set_title(&title)?,
         Self::Maximize => window.maximize()?,

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

@@ -91,7 +91,7 @@ pub use {
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},
       WindowEvent,
     },
-    Icon, MenuId, Params, RunIteration,
+    Icon, MenuId, Params, RunIteration, UserAttentionType,
   },
   self::state::{State, StateManager},
   self::window::{Monitor, Window},

+ 22 - 1
core/tauri/src/window.rs

@@ -19,7 +19,7 @@ use crate::{
       dpi::{PhysicalPosition, PhysicalSize, Position, Size},
       DetachedWindow, PendingWindow, WindowEvent,
     },
-    Dispatch, Icon, Params, Runtime,
+    Dispatch, Icon, Params, Runtime, UserAttentionType,
   },
   sealed::ManagerBase,
   sealed::RuntimeOrDispatch,
@@ -490,6 +490,27 @@ impl<P: Params> Window<P> {
     self.window.dispatcher.center().map_err(Into::into)
   }
 
+  /// Requests user attention to the window, this has no effect if the application
+  /// is already focused. How requesting for user attention manifests is platform dependent,
+  /// see `UserAttentionType` for details.
+  ///
+  /// Providing `None` will unset the request for user attention. Unsetting the request for
+  /// user attention might not be done automatically by the WM when the window receives input.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **macOS:** `None` has no effect.
+  pub fn request_user_attention(
+    &self,
+    request_type: Option<UserAttentionType>,
+  ) -> crate::Result<()> {
+    self
+      .window
+      .dispatcher
+      .request_user_attention(request_type)
+      .map_err(Into::into)
+  }
+
   /// Opens the dialog to prints the contents of the webview.
   /// Currently only supported on macOS on `wry`.
   /// `window.print()` works on all platforms.

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


+ 12 - 1
examples/api/src/components/Window.svelte

@@ -1,5 +1,5 @@
 <script>
-  import { appWindow, WebviewWindow, LogicalSize, LogicalPosition } from "@tauri-apps/api/window";
+  import { appWindow, WebviewWindow, LogicalSize, LogicalPosition, UserAttentionType } from "@tauri-apps/api/window";
   import { open as openDialog } from "@tauri-apps/api/dialog";
   import { open } from "@tauri-apps/api/shell";
 
@@ -21,7 +21,10 @@
     setFullscreen,
     setIcon,
     center,
+    requestUserAttention,
   } = appWindow;
+  window.app = appWindow;
+  window.UserAttentionType = UserAttentionType;
 
   export let onMessage;
 
@@ -74,6 +77,13 @@
     })
   }
 
+  async function requestUserAttention_() {
+    await minimize();
+    await requestUserAttention(UserAttentionType.Critical);
+    await new Promise(resolve => setTimeout(resolve, 3000));
+    await requestUserAttention(null);
+  }
+
   $: setResizable(resizable);
   $: maximized ? maximize() : unmaximize();
   $: setDecorations(decorations);
@@ -179,6 +189,7 @@
   <input id="url" bind:value={urlValue} />
   <button class="button" id="open-url"> Open URL </button>
 </form>
+<button class="button" on:click={requestUserAttention_} title="Minimizes the window, requests attention for 3s and then resets it">Request attention</button>
 <button class="button" on:click={createWindow}>New window</button>
 
 <style>

+ 52 - 0
tooling/api/src/window.ts

@@ -113,6 +113,22 @@ declare global {
   }
 }
 
+/** Attention type to request on a window. */
+enum UserAttentionType {
+  /**
+   * ## Platform-specific
+   *  - **macOS:** Bounces the dock icon until the application is in focus.
+   * - **Windows:** Flashes both the window and the taskbar button until the application is in focus.
+   */
+  Critical = 1,
+  /**
+   * ## Platform-specific
+   * - **macOS:** Bounces the dock icon once.
+   * - **Windows:** Flashes the taskbar button until the application is in focus.
+   */
+  Informational
+}
+
 /**
  * Get a handle to the current webview window. Allows emitting and listening to events from the backend that are tied to the window.
  *
@@ -409,6 +425,41 @@ class WindowManager {
     })
   }
 
+  /**
+   *  Requests user attention to the window, this has no effect if the application
+   * is already focused. How requesting for user attention manifests is platform dependent,
+   * see `UserAttentionType` for details.
+   *
+   * Providing `null` will unset the request for user attention. Unsetting the request for
+   * user attention might not be done automatically by the WM when the window receives input.
+   *
+   * ## Platform-specific
+   *
+   * - **macOS:** `null` has no effect.
+   *
+   * @param resizable
+   * @returns A promise indicating the success or failure of the operation.
+   */
+  async requestUserAttention(
+    requestType: UserAttentionType | null
+  ): Promise<void> {
+    let requestType_ = null
+    if (requestType) {
+      if (requestType === UserAttentionType.Critical) {
+        requestType_ = { type: 'Critical' }
+      } else {
+        requestType_ = { type: 'Informational' }
+      }
+    }
+    return invokeTauriCommand({
+      __tauriModule: 'Window',
+      message: {
+        cmd: 'requestUserAttention',
+        data: requestType_
+      }
+    })
+  }
+
   /**
    * Updates the window resizable flag.
    *
@@ -887,6 +938,7 @@ export {
   PhysicalSize,
   LogicalPosition,
   PhysicalPosition,
+  UserAttentionType,
   currentMonitor,
   primaryMonitor,
   availableMonitors

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