Repository: tauri-apps/wry (language: rust)

Issue:
Traffic light inset not working with wry webview
## Description

When applying `with_traffic_light_inset(...)` method to reexported tao windowbuilder without passing it to a webview the macos traffic lights are inset as expected (of course the webview isn't working because it is never called). But when using the method on the windowbuilder and then passing the newly built window to `WebviewBuilder::new(...)` no traffic light inset can be observed. Everything looks like `with_traffic_light_inset(...)` was never called.

I.e.

```rust
  let window = WindowBuilder::new()
    .with_title("Hello World")
    .with_traffic_light_inset(Position::Logical(LogicalPosition::new(32., 60.)))
    .build(&event_loop)?;
  let _webview = WebViewBuilder::new(window)?
    .with_url("https://www.netflix.com/browse")?
    .build()?;
```

works when commenting out the webview part but doesn't when constructing the webview. `set_traffic_light_inset(...)` on `window` doesn't work too.

## Steps To Reproduce

Use the `simple` example and add the `.with_traffic_light_inset(Position::Logical(LogicalPosition::new(32., 60.)))` to the tao `WindowBuilder` call chain. 

**Expected behavior**

Works only on macos: I expect the traffic lights to be inset on the webview wry window.

Platform and Versions (please complete the following information):

OS: OSX Ventura (not 100% sure)
Rustc: 1.73 (I don't have my mac here but I'm pretty sure it was the latest version)

**Would you want to assign yourself to resolve this bug?**
- [x] Yes
- [ ] No

related to [this PR](https://github.com/tauri-apps/tauri/issues/4789)


Discussion / hints:
Update: Now that tao 24.0 was released I tested it again and it still doesn't work in the same way as before. Also `WebViewBuilder::new_as_child` **works**.
I think that's because the new method `new_as_child` won't replace original view, while previous implementation will always provide a new one.
@wusyong Any idea on how to fix this without breaking everything else? I honestly have no idea...
You have to use new_as_child instead. because new method will always replace original view. We choose this as default behavior because it can avoid lots of  subtle bugs we encountered before.
@wusyong But the main goal is too make this available in [tauri](https://github.com/tauri-apps/tauri/issues/4789). If the default Tauri window is _not_ constructed using `new_as_child` your proposal isn't an option, right?


Fix patch:
diff --git a/.changes/traffic-light-inset.md b/.changes/traffic-light-inset.md
new file mode 100644
index 000000000..ad8221531
--- /dev/null
+++ b/.changes/traffic-light-inset.md
@@ -0,0 +1,5 @@
+---
+wry: patch
+---
+
+Add functionality to set the traffic light inset on macOS. This is required to prevent flickers if the WebView is injected via `build()` instead of `build_as_child()`.
diff --git a/Cargo.toml b/Cargo.toml
index 1bec5fd12..ab5b634ad 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -156,6 +156,8 @@ objc2-ui-kit = { version = "0.2.2", features = [
 [target."cfg(target_os = \"macos\")".dependencies]
 objc2-app-kit = { version = "0.2.0", features = [
   "NSApplication",
+  "NSButton",
+  "NSControl",
   "NSEvent",
   "NSWindow",
   "NSView",
diff --git a/src/lib.rs b/src/lib.rs
index abce41501..c35b5d4e9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1238,6 +1238,7 @@ impl<'a> WebViewBuilder<'a> {
 #[derive(Clone, Default)]
 pub(crate) struct PlatformSpecificWebViewAttributes {
   data_store_identifier: Option<[u8; 16]>,
+  traffic_light_inset: Option<dpi::Position>,
 }
 
 #[cfg(any(target_os = "macos", target_os = "ios",))]
@@ -1247,6 +1248,12 @@ pub trait WebViewBuilderExtDarwin {
   ///
   /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17
   fn with_data_store_identifier(self, identifier: [u8; 16]) -> Self;
+  /// Move the window controls to the specified position.
+  /// Normally this is handled by the Window but because `WebViewBuilder::build()` overwrites the window's NSView the controls will flicker on resizing.
+  /// Note: This method has no effects if the WebView is injected via `WebViewBuilder::build_as_child();` and there should be no flickers.
+  /// Warning: Do not use this if your chosen window library does not support traffic light insets.
+  /// Warning: Only use this in **decorated** windows with a **hidden titlebar**!
+  fn with_traffic_light_inset<P: Into<dpi::Position>>(self, position: P) -> Self;
 }
 
 #[cfg(any(target_os = "macos", target_os = "ios",))]
@@ -1257,6 +1264,13 @@ impl WebViewBuilderExtDarwin for WebViewBuilder<'_> {
       Ok(b)
     })
   }
+
+  fn with_traffic_light_inset<P: Into<dpi::Position>>(self, position: P) -> Self {
+    self.and_then(|mut b| {
+      b.platform_specific.traffic_light_inset = Some(position.into());
+      Ok(b)
+    })
+  }
 }
 
 #[cfg(windows)]
@@ -1915,6 +1929,12 @@ pub trait WebViewExtMacOS {
   fn reparent(&self, window: *mut NSWindow) -> Result<()>;
   // Prints with extra options
   fn print_with_options(&self, options: &PrintOptions) -> Result<()>;
+  /// Move the window controls to the specified position.
+  /// Normally this is handled by the Window but because `WebViewBuilder::build()` overwrites the window's NSView the controls will flicker on resizing.
+  /// Note: This method has no effects if the WebView is injected via `WebViewBuilder::build_as_child();` and there should be no flickers.
+  /// Warning: Do not use this if your chosen window library does not support traffic light insets.
+  /// Warning: Only use this in **decorated** windows with a **hidden titlebar**!
+  fn set_traffic_light_inset<P: Into<dpi::Position>>(&self, position: P) -> Result<()>;
 }
 
 #[cfg(target_os = "macos")]
@@ -1938,6 +1958,10 @@ impl WebViewExtMacOS for WebView {
   fn print_with_options(&self, options: &PrintOptions) -> Result<()> {
     self.webview.print_with_options(options)
   }
+
+  fn set_traffic_light_inset<P: Into<dpi::Position>>(&self, position: P) -> Result<()> {
+    self.webview.set_traffic_light_inset(position.into())
+  }
 }
 
 /// Additional methods on `WebView` that are specific to iOS.
diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs
index 95ef05c50..521729b8d 100644
--- a/src/wkwebview/class/wry_web_view_parent.rs
+++ b/src/wkwebview/class/wry_web_view_parent.rs
@@ -2,16 +2,23 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use std::cell::Cell;
+
 use objc2::{
   declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, ClassType, DeclaredClass,
 };
 #[cfg(target_os = "macos")]
-use objc2_app_kit::{NSApplication, NSEvent, NSView};
+use objc2_app_kit::{NSApplication, NSEvent, NSView, NSWindow, NSWindowButton};
 use objc2_foundation::MainThreadMarker;
+#[cfg(target_os = "macos")]
+use objc2_foundation::NSRect;
 #[cfg(target_os = "ios")]
 use objc2_ui_kit::UIView as NSView;
 
-pub struct WryWebViewParentIvars {}
+pub struct WryWebViewParentIvars {
+  #[cfg(target_os = "macos")]
+  traffic_light_inset: Cell<Option<(f64, f64)>>,
+}
 
 declare_class!(
   pub struct WryWebViewParent;
@@ -41,6 +48,14 @@ declare_class!(
         }
       }
     }
+
+    #[cfg(target_os = "macos")]
+    #[method(drawRect:)]
+    fn draw(&self, _dirty_rect: NSRect) {
+      if let Some((x, y)) = self.ivars().traffic_light_inset.get() {
+        unsafe {inset_traffic_lights(&self.window().unwrap(), x, y)};
+      }
+    }
   }
 );
 
@@ -49,7 +64,55 @@ impl WryWebViewParent {
   pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
     let delegate = mtm
       .alloc::<WryWebViewParent>()
-      .set_ivars(WryWebViewParentIvars {});
+      .set_ivars(WryWebViewParentIvars {
+        #[cfg(target_os = "macos")]
+        traffic_light_inset: Default::default(),
+      });
     unsafe { msg_send_id![super(delegate), init] }
   }
+
+  #[cfg(target_os = "macos")]
+  pub fn set_traffic_light_inset(&self, ns_window: &NSWindow, position: dpi::Position) {
+    let scale_factor = NSWindow::backingScaleFactor(ns_window);
+    let position = position.to_logical(scale_factor);
+    self
+      .ivars()
+      .traffic_light_inset
+      .replace(Some((position.x, position.y)));
+
+    unsafe {
+      inset_traffic_lights(ns_window, position.x, position.y);
+    }
+  }
+}
+
+#[cfg(target_os = "macos")]
+pub unsafe fn inset_traffic_lights(window: &NSWindow, x: f64, y: f64) {
+  let close = window
+    .standardWindowButton(NSWindowButton::NSWindowCloseButton)
+    .unwrap();
+  let miniaturize = window
+    .standardWindowButton(NSWindowButton::NSWindowMiniaturizeButton)
+    .unwrap();
+  let zoom = window
+    .standardWindowButton(NSWindowButton::NSWindowZoomButton)
+    .unwrap();
+
+  let title_bar_container_view = close.superview().unwrap().superview().unwrap();
+
+  let close_rect = NSView::frame(&close);
+  let title_bar_frame_height = close_rect.size.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 = window.frame().size.height - title_bar_frame_height;
+  title_bar_container_view.setFrame(title_bar_rect);
+
+  let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x;
+  let window_buttons = vec![close, miniaturize, zoom];
+
+  for (i, button) in window_buttons.into_iter().enumerate() {
+    let mut rect = NSView::frame(&button);
+    rect.origin.x = x + (i as f64 * space_between);
+    button.setFrameOrigin(rect.origin);
+  }
 }
diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs
index afa4f0aeb..80f5da7c4 100644
--- a/src/wkwebview/mod.rs
+++ b/src/wkwebview/mod.rs
@@ -135,6 +135,9 @@ pub(crate) struct InnerWebView {
   // We need this the keep the reference count
   ui_delegate: Retained<WryWebViewUIDelegate>,
   protocol_ptrs: Vec<*mut Box<dyn Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder)>>,
+  #[cfg(target_os = "macos")]
+  // We need this to update the traffic light inset
+  parent_view: Option<Retained<WryWebViewParent>>,
 }
 
 impl InnerWebView {
@@ -459,7 +462,7 @@ impl InnerWebView {
         }
       }
 
-      let w = Self {
+      let mut w = Self {
         id: webview_id,
         webview: webview.clone(),
         manager: manager.clone(),
@@ -473,6 +476,8 @@ impl InnerWebView {
         ui_delegate,
         protocol_ptrs,
         is_child,
+        #[cfg(target_os = "macos")]
+        parent_view: None,
       };
 
       // Initialize scripts
@@ -504,19 +509,27 @@ r#"Object.defineProperty(window, 'ipc', {
         if is_child {
           ns_view.addSubview(&webview);
         } else {
+          // inject the webview into the window
+          let ns_window = ns_view.window().unwrap();
+
           let parent_view = WryWebViewParent::new(mtm);
+
+          if let Some(position) = pl_attrs.traffic_light_inset {
+            parent_view.set_traffic_light_inset(&ns_window, position);
+          }
+
           parent_view.setAutoresizingMask(
             NSAutoresizingMaskOptions::NSViewHeightSizable
               | NSAutoresizingMaskOptions::NSViewWidthSizable,
           );
           parent_view.addSubview(&webview.clone());
 
-          // inject the webview into the window
-          let ns_window = ns_view.window().unwrap();
           // Tell the webview receive keyboard events in the window.
           // See https://github.com/tauri-apps/wry/issues/739
           ns_window.setContentView(Some(&parent_view));
           ns_window.makeFirstResponder(Some(&webview));
+
+          w.parent_view = Some(parent_view);
         }
 
         // make sure the window is always on top when we create a new webview
@@ -884,7 +897,7 @@ r#"Object.defineProperty(window, 'ipc', {
             (secure && url.scheme() == "https") ||
             // or cookie is secure and is localhost
             (
-              secure && url.scheme() == "http" && 
+              secure && url.scheme() == "http" &&
               (url.domain() == Some("localhost") || url.domain().and_then(|d| Ipv4Addr::from_str(d).ok()).map(|ip| ip.is_loopback()).unwrap_or(false))
             ) ||
             // or cookie is not secure
@@ -926,6 +939,15 @@ r#"Object.defineProperty(window, 'ipc', {
 
     Ok(())
   }
+
+  #[cfg(target_os = "macos")]
+  pub(crate) fn set_traffic_light_inset(&self, position: dpi::Position) -> crate::Result<()> {
+    if let Some(parent_view) = &self.parent_view {
+      parent_view.set_traffic_light_inset(&self.webview.window().unwrap(), position);
+    }
+
+    Ok(())
+  }
 }
 
 pub fn url_from_webview(webview: &WKWebView) -> Result<String> {


Test patch: