Răsfoiți Sursa

feat(macos): add `tabbing_identifier` option, closes #2804, #3912 (#5399)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Caesar Schinas 2 ani în urmă
părinte
comite
4137ab44a8

+ 6 - 0
.changes/automatic-tabbing.md

@@ -0,0 +1,6 @@
+---
+"tauri": minor
+"tauri-runtime-wry": minor
+---
+
+Disable automatic window tabbing on macOS when the `tabbing_identifier` option is not defined, the window is transparent or does not have decorations.

+ 5 - 0
.changes/tabbing-identifier-api.md

@@ -0,0 +1,5 @@
+---
+"api": minor
+---
+
+Added `tabbingIdentifier` window option for macOS.

+ 8 - 0
.changes/tabbing-identifier.md

@@ -0,0 +1,8 @@
+---
+"tauri": minor
+"tauri-runtime": minor
+"tauri-runtime-wry": minor
+"api": minor
+---
+
+Added `tabbing_identifier` to the window builder on macOS.

+ 22 - 0
core/tauri-runtime-wry/src/lib.rs

@@ -680,6 +680,8 @@ impl From<CursorIcon> for CursorIconWrapper {
 pub struct WindowBuilderWrapper {
   inner: WryWindowBuilder,
   center: bool,
+  #[cfg(target_os = "macos")]
+  tabbing_identifier: Option<String>,
   menu: Option<Menu>,
 }
 
@@ -711,6 +713,9 @@ impl WindowBuilder for WindowBuilderWrapper {
       window = window
         .hidden_title(config.hidden_title)
         .title_bar_style(config.title_bar_style);
+      if let Some(identifier) = &config.tabbing_identifier {
+        window = window.tabbing_identifier(identifier);
+      }
     }
 
     #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
@@ -878,6 +883,13 @@ impl WindowBuilder for WindowBuilderWrapper {
     self
   }
 
+  #[cfg(target_os = "macos")]
+  fn tabbing_identifier(mut self, identifier: &str) -> Self {
+    self.inner = self.inner.with_tabbing_identifier(identifier);
+    self.tabbing_identifier.replace(identifier.into());
+    self
+  }
+
   fn icon(mut self, icon: Icon) -> Result<Self> {
     self.inner = self
       .inner
@@ -2930,6 +2942,16 @@ fn create_webview<T: UserEvent>(
       .with_drag_and_drop(webview_attributes.file_drop_handler_enabled);
   }
 
+  #[cfg(target_os = "macos")]
+  {
+    if window_builder.tabbing_identifier.is_none()
+      || window_builder.inner.window.transparent
+      || !window_builder.inner.window.decorations
+    {
+      window_builder.inner = window_builder.inner.with_automatic_window_tabbing(false);
+    }
+  }
+
   let is_window_transparent = window_builder.inner.window.transparent;
   let menu_items = if let Some(menu) = window_builder.menu {
     let mut menu_items = HashMap::new();

+ 10 - 0
core/tauri-runtime/src/webview.rs

@@ -219,6 +219,16 @@ pub trait WindowBuilder: WindowBuilderBase {
   #[must_use]
   fn hidden_title(self, hidden: bool) -> Self;
 
+  /// Defines the window [tabbing identifier] for macOS.
+  ///
+  /// Windows with matching tabbing identifiers will be grouped together.
+  /// If the tabbing identifier is not set, automatic tabbing will be disabled.
+  ///
+  /// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
+  #[cfg(target_os = "macos")]
+  #[must_use]
+  fn tabbing_identifier(self, identifier: &str) -> Self;
+
   /// Forces a theme or uses the system settings if None was provided.
   fn theme(self, theme: Option<Theme>) -> Self;
 

+ 12 - 1
core/tauri-utils/src/config.rs

@@ -873,6 +873,14 @@ pub struct WindowConfig {
   /// Whether clicking an inactive window also clicks through to the webview.
   #[serde(default, alias = "accept-first-mouse")]
   pub accept_first_mouse: bool,
+  /// Defines the window [tabbing identifier] for macOS.
+  ///
+  /// Windows with matching tabbing identifiers will be grouped together.
+  /// If the tabbing identifier is not set, automatic tabbing will be disabled.
+  ///
+  /// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
+  #[serde(default, alias = "tabbing-identifier")]
+  pub tabbing_identifier: Option<String>,
 }
 
 impl Default for WindowConfig {
@@ -905,6 +913,7 @@ impl Default for WindowConfig {
       title_bar_style: Default::default(),
       hidden_title: false,
       accept_first_mouse: false,
+      tabbing_identifier: None,
     }
   }
 }
@@ -3037,6 +3046,7 @@ mod build {
       let title_bar_style = &self.title_bar_style;
       let hidden_title = self.hidden_title;
       let accept_first_mouse = self.accept_first_mouse;
+      let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
 
       literal_struct!(
         tokens,
@@ -3067,7 +3077,8 @@ mod build {
         theme,
         title_bar_style,
         hidden_title,
-        accept_first_mouse
+        accept_first_mouse,
+        tabbing_identifier
       );
     }
   }

+ 5 - 0
core/tauri/src/test/mock_runtime.rs

@@ -283,6 +283,11 @@ impl WindowBuilder for MockWindowBuilder {
     self
   }
 
+  #[cfg(target_os = "macos")]
+  fn tabbing_identifier(self, identifier: &str) -> Self {
+    self
+  }
+
   fn theme(self, theme: Option<Theme>) -> Self {
     self
   }

+ 13 - 0
core/tauri/src/window.rs

@@ -462,6 +462,19 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
+  /// Defines the window [tabbing identifier] for macOS.
+  ///
+  /// Windows with matching tabbing identifiers will be grouped together.
+  /// If the tabbing identifier is not set, automatic tabbing will be disabled.
+  ///
+  /// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
+  #[cfg(target_os = "macos")]
+  #[must_use]
+  pub fn tabbing_identifier(mut self, identifier: &str) -> Self {
+    self.window_builder = self.window_builder.tabbing_identifier(identifier);
+    self
+  }
+
   // ------------------------------------------- Webview attributes -------------------------------------------
 
   /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created,

+ 75 - 70
examples/multiwindow/index.html

@@ -1,84 +1,89 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <style>
-      #response {
-        white-space: pre-wrap;
-      }
-    </style>
-  </head>
 
-  <body>
-    <div id="window-label"></div>
-    <div id="container"></div>
-    <div id="response"></div>
+<head>
+  <style>
+    #response {
+      white-space: pre-wrap;
+    }
+  </style>
+</head>
 
-    <script>
-      var WebviewWindow = window.__TAURI__.window.WebviewWindow
-      var appWindow = window.__TAURI__.window.appWindow
-      var windowLabel = appWindow.label
-      var windowLabelContainer = document.getElementById('window-label')
-      windowLabelContainer.innerText = 'This is the ' + windowLabel + ' window.'
+<body>
+  <div id="window-label"></div>
+  <div id="container"></div>
+  <div id="response"></div>
 
-      var container = document.getElementById('container')
+  <script>
+    var WebviewWindow = window.__TAURI__.window.WebviewWindow
+    var appWindow = window.__TAURI__.window.appWindow
+    var windowLabel = appWindow.label
+    var windowLabelContainer = document.getElementById('window-label')
+    windowLabelContainer.innerText = 'This is the ' + windowLabel + ' window.'
 
-      function createWindowMessageBtn(label) {
-        var tauriWindow = WebviewWindow.getByLabel(label)
-        var button = document.createElement('button')
-        button.innerText = 'Send message to ' + label
-        button.addEventListener('click', function () {
-          tauriWindow.emit('clicked', 'message from ' + windowLabel)
-        })
-        container.appendChild(button)
-      }
+    var container = document.getElementById('container')
 
-      // global listener
-      window.__TAURI__.event.listen('clicked', function (event) {
-        responseContainer.innerHTML +=
-          'Got ' + JSON.stringify(event) + ' on global listener\n\n'
-      })
-      window.__TAURI__.event.listen('tauri://window-created', function (event) {
-        createWindowMessageBtn(event.payload.label)
+    function createWindowMessageBtn(label) {
+      var tauriWindow = WebviewWindow.getByLabel(label)
+      var button = document.createElement('button')
+      button.innerText = 'Send message to ' + label
+      button.addEventListener('click', function () {
+        tauriWindow.emit('clicked', 'message from ' + windowLabel)
       })
+      container.appendChild(button)
+    }
 
-      var responseContainer = document.getElementById('response')
-      // listener tied to this window
-      appWindow.listen('clicked', function (event) {
-        responseContainer.innerText +=
-          'Got ' + JSON.stringify(event) + ' on window listener\n\n'
-      })
+    // global listener
+    window.__TAURI__.event.listen('clicked', function (event) {
+      responseContainer.innerHTML +=
+        'Got ' + JSON.stringify(event) + ' on global listener\n\n'
+    })
+    window.__TAURI__.event.listen('tauri://window-created', function (event) {
+      createWindowMessageBtn(event.payload.label)
+    })
 
-      var createWindowButton = document.createElement('button')
-      createWindowButton.innerHTML = 'Create window'
-      createWindowButton.addEventListener('click', function () {
-        var webviewWindow = new WebviewWindow(
-          Math.random().toString().replace('.', '')
-        )
-        webviewWindow.once('tauri://created', function () {
-          responseContainer.innerHTML += 'Created new webview'
-        })
-        webviewWindow.once('tauri://error', function (e) {
-          responseContainer.innerHTML += 'Error creating new webview'
-        })
-      })
-      container.appendChild(createWindowButton)
+    var responseContainer = document.getElementById('response')
+    // listener tied to this window
+    appWindow.listen('clicked', function (event) {
+      responseContainer.innerText +=
+        'Got ' + JSON.stringify(event) + ' on window listener\n\n'
+    })
 
-      var globalMessageButton = document.createElement('button')
-      globalMessageButton.innerHTML = 'Send global message'
-      globalMessageButton.addEventListener('click', function () {
-        // emit to all windows
-        window.__TAURI__.event.emit('clicked', 'message from ' + windowLabel)
+    var createWindowButton = document.createElement('button')
+    createWindowButton.innerHTML = 'Create window'
+    createWindowButton.addEventListener('click', function () {
+      var webviewWindow = new WebviewWindow(
+        Math.random().toString().replace('.', ''),
+        {
+          tabbingIdentifier: windowLabel
+        }
+      )
+      webviewWindow.once('tauri://created', function () {
+        responseContainer.innerHTML += 'Created new webview'
+      })
+      webviewWindow.once('tauri://error', function (e) {
+        responseContainer.innerHTML += 'Error creating new webview'
       })
-      container.appendChild(globalMessageButton)
+    })
+    container.appendChild(createWindowButton)
 
-      var allWindows = window.__TAURI__.window.getAll()
-      for (var index in allWindows) {
-        var label = allWindows[index].label
-        if (label === windowLabel) {
-          continue
-        }
-        createWindowMessageBtn(label)
+    var globalMessageButton = document.createElement('button')
+    globalMessageButton.innerHTML = 'Send global message'
+    globalMessageButton.addEventListener('click', function () {
+      // emit to all windows
+      window.__TAURI__.event.emit('clicked', 'message from ' + windowLabel)
+    })
+    container.appendChild(globalMessageButton)
+
+    var allWindows = window.__TAURI__.window.getAll()
+    for (var index in allWindows) {
+      var label = allWindows[index].label
+      if (label === windowLabel) {
+        continue
       }
-    </script>
-  </body>
-</html>
+      createWindowMessageBtn(label)
+    }
+  </script>
+</body>
+
+</html>

+ 8 - 4
examples/multiwindow/main.rs

@@ -18,13 +18,17 @@ fn main() {
       });
     })
     .setup(|app| {
-      WindowBuilder::new(
+      #[allow(unused_mut)]
+      let mut builder = WindowBuilder::new(
         app,
         "Rust".to_string(),
         tauri::WindowUrl::App("index.html".into()),
-      )
-      .title("Tauri - Rust")
-      .build()?;
+      );
+      #[cfg(target_os = "macos")]
+      {
+        builder = builder.tabbing_identifier("Rust");
+      }
+      let _window = builder.title("Tauri - Rust").build()?;
       Ok(())
     })
     .run(tauri::generate_context!(

+ 2 - 0
examples/multiwindow/tauri.conf.json

@@ -35,12 +35,14 @@
       {
         "label": "Main",
         "title": "Tauri - Main",
+        "tabbingIdentifier": "Main",
         "width": 800,
         "height": 600
       },
       {
         "label": "Secondary",
         "title": "Tauri - Secondary",
+        "tabbingIdentifier": "Secondary",
         "width": 600,
         "height": 400
       }

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

@@ -2046,6 +2046,13 @@ interface WindowOptions {
    * Whether clicking an inactive window also clicks through to the webview.
    */
   acceptFirstMouse?: boolean
+  /**
+   * Defines the window [tabbing identifier](https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier) on macOS.
+   *
+   * Windows with the same tabbing identifier will be grouped together.
+   * If the tabbing identifier is not set, automatic tabbing will be disabled.
+   */
+  tabbingIdentifier?: string
   /**
    * The user agent for the webview.
    */

+ 7 - 0
tooling/cli/schema.json

@@ -675,6 +675,13 @@
           "description": "Whether clicking an inactive window also clicks through to the webview.",
           "default": false,
           "type": "boolean"
+        },
+        "tabbingIdentifier": {
+          "description": "Defines the window [tabbing identifier].\n\n[tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>",
+          "type": [
+            "string",
+            "null"
+          ]
         }
       },
       "additionalProperties": false