Repository: tauri-apps/tauri (rust)

Issue:
[feat] Allowing for native inset OS X traffic lights on NSWindow
### Describe the problem

Many apps (also electron apps) have customized traffic light position to fit their style better. In electron this is achieved through a config file that calls native objc code. I would like this in tauri as it would allow for cleaner more intentional designs.

### Describe the solution you'd like

I have already worked on this in issue #2663. I used [the electron source](https://github.com/electron/electron/pull/21781/commits/63c1caa8d8f966eca68754b859b50d1f7c9c7088) as inspiration.

A file called `window_ext.rs`:

```rust
use tauri::{Runtime, Window};

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, transparent: bool);
    fn position_traffic_lights(&self, x: f64, y: f64);
}

impl<R: Runtime> WindowExt for Window<R> {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, transparent: bool) {
        use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};

        let window = self.ns_window().unwrap() as cocoa::base::id;

        unsafe {
            window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);

            if transparent {
                window.setTitlebarAppearsTransparent_(cocoa::base::YES);
            } else {
                window.setTitlebarAppearsTransparent_(cocoa::base::NO);
            }
        }
    }

    #[cfg(target_os = "macos")]
    fn position_traffic_lights(&self, x: f64, y: f64) {
        use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
        use cocoa::foundation::NSRect;

        let window = self.ns_window().unwrap() as cocoa::base::id;

        unsafe {
            let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
            let miniaturize =
                window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
            let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);

            let title_bar_container_view = close.superview().superview();

            let close_rect: NSRect = msg_send![close, frame];
            let button_height = close_rect.size.height;

            let title_bar_frame_height = button_height + y;
            let mut title_bar_rect = NSView::frame(title_bar_container_view);
            title_bar_rect.size.height = title_bar_frame_height;
            title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
            let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];

            let window_buttons = vec![close, miniaturize, zoom];
            let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;

            for (i, button) in window_buttons.into_iter().enumerate() {
                let mut rect: NSRect = NSView::frame(button);
                rect.origin.x = x + (i as f64 * space_between);
                button.setFrameOrigin(rect.origin);
            }
        }
    }
}
```

`main.rs`:

```rust
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;

use tauri::{Manager, WindowEvent};
use window_ext::WindowExt;

mod window_ext;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let win = app.get_window("main").unwrap();
            win.set_transparent_titlebar(true);
            win.position_traffic_lights(30.0, 30.0);
            Ok(())
        })
        .on_window_event(|e| {
            if let WindowEvent::Resized(..) = e.event() {
                let win = e.window();
                win.position_traffic_lights(30., 30.);
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");

}
```

In general this works:

<img width="592" alt="Screenshot 2022-07-27 at 14 36 04" src="https://user-images.githubusercontent.com/104835302/181439210-ea94aaf5-5ae6-4d06-bd74-3857c98e4587.png">


The `on_window_event` listener is there because without it the traffic lights would take their original position when the window is resized. This comes however with an annoying problem: The `NSWindow` handle becomes only available _after_ the window is already rendered. That causes some annoying artifacts. You can see the original traffic lights before they are repositioned for a very short time.

So the task is somehow tapping into the tao render pipeline or something else to draw the traffic lights before the window is rendered.

### Alternatives considered

I don't really have any. Maybe somehow controlling the NSWindow completely ourselves. _Please_ make suggestions if you know more about the tauri source.

### Additional context

There are still some `#[cfg(target_os = "macos")]` missing. Feel free to add them.

Pull request description:
feat: traffic light position
closes #4789

Patch:
diff --git a/.changes/traffic-light-builder.md b/.changes/traffic-light-builder.md
new file mode 100644
index 000000000..47c303628
--- /dev/null
+++ b/.changes/traffic-light-builder.md
@@ -0,0 +1,5 @@
+---
+"tauri": "minor:feat"
+---
+
+Added `WebviewWindowBuilder::traffic_light_position` to set the traffic light buttons position on macOS.
diff --git a/.changes/traffic-light-config.md b/.changes/traffic-light-config.md
new file mode 100644
index 000000000..f28b883f8
--- /dev/null
+++ b/.changes/traffic-light-config.md
@@ -0,0 +1,10 @@
+---
+"tauri-utils": "minor:feat"
+"tauri-runtime": "minor:feat"
+"tauri-runtime-wry": "minor:feat"
+"tauri": "minor:feat"
+"@tauri-apps/cli": "minor:feat"
+"tauri-cli": "minor:feat"
+---
+
+Added `trafficLightPosition` window configuration to set the traffic light buttons position on macOS.
diff --git a/.changes/traffic-light-runtime.md b/.changes/traffic-light-runtime.md
new file mode 100644
index 000000000..418fb22b5
--- /dev/null
+++ b/.changes/traffic-light-runtime.md
@@ -0,0 +1,6 @@
+---
+"tauri-runtime": "minor:feat"
+"tauri-runtime-wry": "minor:feat"
+---
+
+Added `traffic_light_position` window builder method to set the traffic light buttons position on macOS.
diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json
index 02195b6cc..83bc22188 100644
--- a/crates/tauri-cli/config.schema.json
+++ b/crates/tauri-cli/config.schema.json
@@ -227,7 +227,7 @@
           "type": "string"
         },
         "create": {
-          "description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2.0.0-rc/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).",
+          "description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).",
           "default": true,
           "type": "boolean"
         },
@@ -424,6 +424,17 @@
             }
           ]
         },
+        "trafficLightPosition": {
+          "description": "The position of the window controls on macOS.\n\n Requires titleBarStyle: Overlay and decorations: true.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/LogicalPosition"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "hiddenTitle": {
           "description": "If `true`, sets the window title to be hidden on macOS.",
           "default": false,
@@ -600,6 +611,27 @@
         }
       ]
     },
+    "LogicalPosition": {
+      "description": "Position coordinates struct.",
+      "type": "object",
+      "required": [
+        "x",
+        "y"
+      ],
+      "properties": {
+        "x": {
+          "description": "X coordinate.",
+          "type": "number",
+          "format": "double"
+        },
+        "y": {
+          "description": "Y coordinate.",
+          "type": "number",
+          "format": "double"
+        }
+      },
+      "additionalProperties": false
+    },
     "WindowEffectsConfig": {
       "description": "The window effects configuration object",
       "type": "object",
diff --git a/crates/tauri-cli/templates/tauri.conf.json b/crates/tauri-cli/templates/tauri.conf.json
index e7f0514a5..6b8354f78 100644
--- a/crates/tauri-cli/templates/tauri.conf.json
+++ b/crates/tauri-cli/templates/tauri.conf.json
@@ -1,5 +1,5 @@
 {
-  "$schema": "https://schema.tauri.app/config/2.0.0-rc",
+  "$schema": "https://schema.tauri.app/config/2",
   "productName": "{{ app_name }}",
   "version": "0.1.0",
   "identifier": "com.tauri.dev",
diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs
index 8682a1ca7..c97ada060 100644
--- a/crates/tauri-runtime-wry/src/lib.rs
+++ b/crates/tauri-runtime-wry/src/lib.rs
@@ -788,6 +788,11 @@ impl WindowBuilder for WindowBuilderWrapper {
       if let Some(identifier) = &config.tabbing_identifier {
         window = window.tabbing_identifier(identifier);
       }
+      if let Some(position) = &config.traffic_light_position {
+        window = window.traffic_light_position(tauri_runtime::dpi::LogicalPosition::new(
+          position.x, position.y,
+        ));
+      }
     }
 
     #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
@@ -1069,6 +1074,12 @@ impl WindowBuilder for WindowBuilderWrapper {
     self
   }
 
+  #[cfg(target_os = "macos")]
+  fn traffic_light_position<P: Into<Position>>(mut self, position: P) -> Self {
+    self.inner = self.inner.with_traffic_light_inset(position.into());
+    self
+  }
+
   #[cfg(target_os = "macos")]
   fn hidden_title(mut self, hidden: bool) -> Self {
     self.inner = self.inner.with_title_hidden(hidden);
@@ -1276,6 +1287,7 @@ pub enum WindowMessage {
   SetOverlayIcon(Option<TaoIcon>),
   SetProgressBar(ProgressBarState),
   SetTitleBarStyle(tauri_utils::TitleBarStyle),
+  SetTrafficLightPosition(Position),
   SetTheme(Option<Theme>),
   SetBackgroundColor(Option<Color>),
   DragWindow,
@@ -2194,6 +2206,16 @@ impl<T: UserEvent> WindowDispatch<T> for WryWindowDispatcher<T> {
     )
   }
 
+  fn set_traffic_light_position(&self, position: Position) -> Result<()> {
+    send_user_message(
+      &self.context,
+      Message::Window(
+        self.window_id,
+        WindowMessage::SetTrafficLightPosition(position),
+      ),
+    )
+  }
+
   fn set_theme(&self, theme: Option<Theme>) -> Result<()> {
     send_user_message(
       &self.context,
@@ -3248,6 +3270,10 @@ fn handle_user_message<T: UserEvent>(
               }
             };
           }
+          WindowMessage::SetTrafficLightPosition(_position) => {
+            #[cfg(target_os = "macos")]
+            window.set_traffic_light_inset(_position);
+          }
           WindowMessage::SetTheme(theme) => {
             window.set_theme(match theme {
               Some(Theme::Light) => Some(TaoTheme::Light),
@@ -4451,6 +4477,14 @@ fn create_webview<T: UserEvent>(
     }
   }
 
+  #[cfg(target_os = "macos")]
+  {
+    use wry::WebViewBuilderExtDarwin;
+    if let Some(position) = &webview_attributes.traffic_light_position {
+      webview_builder = webview_builder.with_traffic_light_inset(*position);
+    }
+  }
+
   webview_builder = webview_builder.with_ipc_handler(create_ipc_handler(
     kind,
     window_id.clone(),
diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs
index 77b4189d0..ce97fabdc 100644
--- a/crates/tauri-runtime/src/lib.rs
+++ b/crates/tauri-runtime/src/lib.rs
@@ -238,7 +238,7 @@ pub enum RunEvent<T: UserEvent> {
   Resumed,
   /// Emitted when all of the event loop's input events have been processed and redraw processing is about to begin.
   ///
-  /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the “main body” of your event loop.
+  /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the "main body" of your event loop.
   MainEventsCleared,
   /// Emitted when the user wants to open the specified resource with the app.
   #[cfg(any(target_os = "macos", target_os = "ios"))]
@@ -875,6 +875,15 @@ pub trait WindowDispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 's
   /// - **Linux / Windows / iOS / Android:** Unsupported.
   fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()>;
 
+  /// Change the position of the window controls. Available on macOS only.
+  ///
+  /// Requires titleBarStyle: Overlay and decorations: true.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Linux / Windows / iOS / Android:** Unsupported.
+  fn set_traffic_light_position(&self, position: Position) -> Result<()>;
+
   /// Sets the theme for this window.
   ///
   /// ## Platform-specific
diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs
index aef72b49b..895c10141 100644
--- a/crates/tauri-runtime/src/webview.rs
+++ b/crates/tauri-runtime/src/webview.rs
@@ -217,6 +217,7 @@ pub struct WebviewAttributes {
   pub use_https_scheme: bool,
   pub devtools: Option<bool>,
   pub background_color: Option<Color>,
+  pub traffic_light_position: Option<dpi::Position>,
   pub background_throttling: Option<BackgroundThrottlingPolicy>,
   pub javascript_disabled: bool,
 }
@@ -236,6 +237,13 @@ impl From<&WindowConfig> for WebviewAttributes {
     {
       builder = builder.transparent(config.transparent);
     }
+    #[cfg(target_os = "macos")]
+    {
+      if let Some(position) = &config.traffic_light_position {
+        builder =
+          builder.traffic_light_position(dpi::LogicalPosition::new(position.x, position.y).into());
+      }
+    }
     builder = builder.accept_first_mouse(config.accept_first_mouse);
     if !config.drag_drop_enabled {
       builder = builder.disable_drag_drop_handler();
@@ -286,6 +294,7 @@ impl WebviewAttributes {
       use_https_scheme: false,
       devtools: None,
       background_color: None,
+      traffic_light_position: None,
       background_throttling: None,
       javascript_disabled: false,
     }
@@ -454,7 +463,20 @@ impl WebviewAttributes {
     self
   }
 
-  /// Change the default background throttling behaviour.
+  /// Change the position of the window controls. Available on macOS only.
+  ///
+  /// Requires titleBarStyle: Overlay and decorations: true.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Linux / Windows / iOS / Android:** Unsupported.
+  #[must_use]
+  pub fn traffic_light_position(mut self, position: dpi::Position) -> Self {
+    self.traffic_light_position = Some(position);
+    self
+  }
+
+  /// Change the default background throttling behavior.
   ///
   /// By default, browsers use a suspend policy that will throttle timers and even unload
   /// the whole tab (view) to free resources after roughly 5 minutes when a view became
diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs
index dc843100d..911bb6064 100644
--- a/crates/tauri-runtime/src/window.rs
+++ b/crates/tauri-runtime/src/window.rs
@@ -423,6 +423,13 @@ pub trait WindowBuilder: WindowBuilderBase {
   #[must_use]
   fn title_bar_style(self, style: tauri_utils::TitleBarStyle) -> Self;
 
+  /// Change the position of the window controls on macOS.
+  ///
+  /// Requires titleBarStyle: Overlay and decorations: true.
+  #[cfg(target_os = "macos")]
+  #[must_use]
+  fn traffic_light_position<P: Into<dpi::Position>>(self, position: P) -> Self;
+
   /// Hide the window title.
   #[cfg(target_os = "macos")]
   #[must_use]
diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json
index 02195b6cc..83bc22188 100644
--- a/crates/tauri-schema-generator/schemas/config.schema.json
+++ b/crates/tauri-schema-generator/schemas/config.schema.json
@@ -227,7 +227,7 @@
           "type": "string"
         },
         "create": {
-          "description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2.0.0-rc/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).",
+          "description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).",
           "default": true,
           "type": "boolean"
         },
@@ -424,6 +424,17 @@
             }
           ]
         },
+        "trafficLightPosition": {
+          "description": "The position of the window controls on macOS.\n\n Requires titleBarStyle: Overlay and decorations: true.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/LogicalPosition"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "hiddenTitle": {
           "description": "If `true`, sets the window title to be hidden on macOS.",
           "default": false,
@@ -600,6 +611,27 @@
         }
       ]
     },
+    "LogicalPosition": {
+      "description": "Position coordinates struct.",
+      "type": "object",
+      "required": [
+        "x",
+        "y"
+      ],
+      "properties": {
+        "x": {
+          "description": "X coordinate.",
+          "type": "number",
+          "format": "double"
+        },
+        "y": {
+          "description": "Y coordinate.",
+          "type": "number",
+          "format": "double"
+        }
+      },
+      "additionalProperties": false
+    },
     "WindowEffectsConfig": {
       "description": "The window effects configuration object",
       "type": "object",
diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs
index d066e7745..27e30496a 100644
--- a/crates/tauri-utils/src/config.rs
+++ b/crates/tauri-utils/src/config.rs
@@ -511,6 +511,17 @@ pub struct Position {
   pub y: u32,
 }
 
+/// Position coordinates struct.
+#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct LogicalPosition {
+  /// X coordinate.
+  pub x: f64,
+  /// Y coordinate.
+  pub y: f64,
+}
+
 /// Size of the window.
 #[derive(Default, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
@@ -1465,7 +1476,7 @@ pub struct WindowConfig {
   /// Whether Tauri should create this window at app startup or not.
   ///
   /// When this is set to `false` you must manually grab the config object via `app.config().app.windows`
-  /// and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2.0.0-rc/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).
+  /// and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).
   #[serde(default = "default_true")]
   pub create: bool,
   /// The window webview URL.
@@ -1582,6 +1593,11 @@ pub struct WindowConfig {
   /// The style of the macOS title bar.
   #[serde(default, alias = "title-bar-style")]
   pub title_bar_style: TitleBarStyle,
+  /// The position of the window controls on macOS.
+  ///
+  /// Requires titleBarStyle: Overlay and decorations: true.
+  #[serde(default, alias = "traffic-light-position")]
+  pub traffic_light_position: Option<LogicalPosition>,
   /// If `true`, sets the window title to be hidden on macOS.
   #[serde(default, alias = "hidden-title")]
   pub hidden_title: bool,
@@ -1758,6 +1774,7 @@ impl Default for WindowConfig {
       window_classname: None,
       theme: None,
       title_bar_style: Default::default(),
+      traffic_light_position: None,
       hidden_title: false,
       accept_first_mouse: false,
       tabbing_identifier: None,
@@ -2951,6 +2968,13 @@ mod build {
     }
   }
 
+  impl ToTokens for LogicalPosition {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let LogicalPosition { x, y } = self;
+      literal_struct!(tokens, ::tauri::utils::config::LogicalPosition, x, y)
+    }
+  }
+
   impl ToTokens for crate::WindowEffect {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let prefix = quote! { ::tauri::utils::WindowEffect };
@@ -3037,6 +3061,7 @@ mod build {
       let window_classname = opt_str_lit(self.window_classname.as_ref());
       let theme = opt_lit(self.theme.as_ref());
       let title_bar_style = &self.title_bar_style;
+      let traffic_light_position = opt_lit(self.traffic_light_position.as_ref());
       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());
@@ -3090,6 +3115,7 @@ mod build {
         window_classname,
         theme,
         title_bar_style,
+        traffic_light_position,
         hidden_title,
         accept_first_mouse,
         tabbing_identifier,
diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs
index 223463eff..2b6ef1af4 100644
--- a/crates/tauri/src/webview/webview_window.rs
+++ b/crates/tauri/src/webview/webview_window.rs
@@ -706,6 +706,19 @@ impl<'a, R: Runtime, M: Manager<R>> WebviewWindowBuilder<'a, R, M> {
     self
   }
 
+  /// Change the position of the window controls on macOS.
+  ///
+  /// Requires titleBarStyle: Overlay and decorations: true.
+  #[cfg(target_os = "macos")]
+  #[must_use]
+  pub fn traffic_light_position<P: Into<Position>>(mut self, position: P) -> Self {
+    self.webview_builder.webview_attributes = self
+      .webview_builder
+      .webview_attributes
+      .traffic_light_position(position.into());
+    self
+  }
+
   /// Hide the window title.
   #[cfg(target_os = "macos")]
   #[must_use]


Test patch:
diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs
index db1a2f0fa..98f71be23 100644
--- a/crates/tauri/src/test/mock_runtime.rs
+++ b/crates/tauri/src/test/mock_runtime.rs
@@ -482,6 +482,11 @@ impl WindowBuilder for MockWindowBuilder {
     self
   }
 
+  #[cfg(target_os = "macos")]
+  fn traffic_light_position<P: Into<Position>>(self, position: P) -> Self {
+    self
+  }
+
   #[cfg(target_os = "macos")]
   fn hidden_title(self, transparent: bool) -> Self {
     self
@@ -1014,6 +1019,10 @@ impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
     Ok(())
   }
 
+  fn set_traffic_light_position(&self, position: Position) -> Result<()> {
+    Ok(())
+  }
+
   fn set_size_constraints(
     &self,
     constraints: tauri_runtime::window::WindowSizeConstraints,