From e07139504b0e07013a9b784ae9aee46a8deb40b4 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Mon, 6 Jan 2025 17:58:39 +0100 Subject: [PATCH 1/7] feat: Add traffic light inset --- .changes/traffic-light-inset.md | 5 ++ src/lib.rs | 22 ++++++++ src/wkwebview/class/wry_web_view_parent.rs | 66 ++++++++++++++++++++-- src/wkwebview/mod.rs | 35 ++++++++++-- 4 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 .changes/traffic-light-inset.md 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/src/lib.rs b/src/lib.rs index abce41501..001219c58 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, } #[cfg(any(target_os = "macos", target_os = "ios",))] @@ -1247,6 +1248,11 @@ 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. + /// Note: Do not use this if your chosen window library does not support traffic light insets. + fn with_traffic_light_inset>(self, position: P) -> Self; } #[cfg(any(target_os = "macos", target_os = "ios",))] @@ -1257,6 +1263,13 @@ impl WebViewBuilderExtDarwin for WebViewBuilder<'_> { Ok(b) }) } + + fn with_traffic_light_inset>(self, position: P) -> Self { + self.and_then(|mut b| { + b.platform_specific.traffic_light_inset = Some(position.into()); + Ok(b) + }) + } } #[cfg(windows)] @@ -1915,6 +1928,11 @@ 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. + /// Note: Do not use this if your chosen window library does not support traffic light insets. + fn set_traffic_light_inset>(&self, position: P) -> Result<()>; } #[cfg(target_os = "macos")] @@ -1938,6 +1956,10 @@ impl WebViewExtMacOS for WebView { fn print_with_options(&self, options: &PrintOptions) -> Result<()> { self.webview.print_with_options(options) } + + fn set_traffic_light_inset>(&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..f54cfd152 100644 --- a/src/wkwebview/class/wry_web_view_parent.rs +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -2,16 +2,21 @@ // 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_foundation::MainThreadMarker; +use objc2_app_kit::{NSWindow, NSWindowButton}; +use objc2_foundation::{MainThreadMarker, NSRect}; #[cfg(target_os = "ios")] use objc2_ui_kit::UIView as NSView; -pub struct WryWebViewParentIvars {} +pub struct WryWebViewParentIvars { + traffic_light_inset: Cell>, +} declare_class!( pub struct WryWebViewParent; @@ -41,15 +46,68 @@ 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)}; + } + } } ); impl WryWebViewParent { #[allow(dead_code)] - pub fn new(mtm: MainThreadMarker) -> Retained { + pub fn new(mtm: MainThreadMarker, traffic_light_inset: Option<(f64, f64)>) -> Retained { let delegate = mtm .alloc::() - .set_ivars(WryWebViewParentIvars {}); + .set_ivars(WryWebViewParentIvars { + traffic_light_inset: traffic_light_inset.into(), + }); unsafe { msg_send_id![super(delegate), init] } } + + 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); + } + } +} + +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..2af78784e 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -135,6 +135,8 @@ pub(crate) struct InnerWebView { // We need this the keep the reference count ui_delegate: Retained, protocol_ptrs: Vec<*mut Box>, RequestAsyncResponder)>>, + // We need this to update the traffic light inset + parent_view: Option>, } impl InnerWebView { @@ -459,7 +461,7 @@ impl InnerWebView { } } - let w = Self { + let mut w = Self { id: webview_id, webview: webview.clone(), manager: manager.clone(), @@ -473,6 +475,7 @@ impl InnerWebView { ui_delegate, protocol_ptrs, is_child, + parent_view: None, }; // Initialize scripts @@ -504,19 +507,30 @@ r#"Object.defineProperty(window, 'ipc', { if is_child { ns_view.addSubview(&webview); } else { - let parent_view = WryWebViewParent::new(mtm); + // inject the webview into the window + let ns_window = ns_view.window().unwrap(); + let scale_factor = ns_window.backingScaleFactor(); + + let parent_view = WryWebViewParent::new( + mtm, + pl_attrs + .traffic_light_inset + .map(|p| p.to_logical(scale_factor)) + .map(|p| (p.x, p.y)), + ); + 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 +898,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 +940,17 @@ r#"Object.defineProperty(window, 'ipc', { Ok(()) } + + pub(crate) fn set_traffic_light_inset(&self, position: dpi::Position) -> crate::Result<()> { + #[cfg(target_os = "macos")] + if !self.is_child { + 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 { From 8f48145125b0c80096d4248cbed67fc91bb71a26 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Mon, 6 Jan 2025 18:08:34 +0100 Subject: [PATCH 2/7] fix? --- Cargo.toml | 2 ++ src/wkwebview/class/wry_web_view_parent.rs | 29 +++++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) 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/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs index f54cfd152..fe968632d 100644 --- a/src/wkwebview/class/wry_web_view_parent.rs +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -8,9 +8,10 @@ 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::{NSWindow, NSWindowButton}; -use objc2_foundation::{MainThreadMarker, NSRect}; +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; @@ -69,19 +70,23 @@ impl WryWebViewParent { } 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")] + { + 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) From b711f0207c06d61165c6ddbcb3ff373a6a969078 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Mon, 6 Jan 2025 18:41:32 +0100 Subject: [PATCH 3/7] ios --- src/wkwebview/class/wry_web_view_parent.rs | 22 ++++++++++------------ src/wkwebview/mod.rs | 9 ++++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs index fe968632d..3942b9959 100644 --- a/src/wkwebview/class/wry_web_view_parent.rs +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -69,19 +69,17 @@ impl WryWebViewParent { unsafe { msg_send_id![super(delegate), init] } } + #[cfg(target_os = "macos")] pub fn set_traffic_light_inset(&self, ns_window: &NSWindow, position: dpi::Position) { - #[cfg(target_os = "macos")] - { - 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); - } + 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); } } } diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 2af78784e..d6a0acfb3 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -135,6 +135,7 @@ pub(crate) struct InnerWebView { // We need this the keep the reference count ui_delegate: Retained, protocol_ptrs: Vec<*mut Box>, RequestAsyncResponder)>>, + #[cfg(target_os = "macos")] // We need this to update the traffic light inset parent_view: Option>, } @@ -941,12 +942,10 @@ r#"Object.defineProperty(window, 'ipc', { Ok(()) } + #[cfg(target_os = "macos")] pub(crate) fn set_traffic_light_inset(&self, position: dpi::Position) -> crate::Result<()> { - #[cfg(target_os = "macos")] - if !self.is_child { - if let Some(parent_view) = &self.parent_view { - parent_view.set_traffic_light_inset(&self.webview.window().unwrap(), position); - } + if let Some(parent_view) = &self.parent_view { + parent_view.set_traffic_light_inset(&self.webview.window().unwrap(), position); } Ok(()) From 9ae8e0ac66807e8faba4092360cc2cbfac319da7 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Mon, 6 Jan 2025 18:48:39 +0100 Subject: [PATCH 4/7] ios --- src/wkwebview/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index d6a0acfb3..baa8f4705 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -476,6 +476,7 @@ impl InnerWebView { ui_delegate, protocol_ptrs, is_child, + #[cfg(target_os = "macos")] parent_view: None, }; From 3fe0a35f91b7aa4cd899edfbb5a281c42165fc64 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Tue, 7 Jan 2025 12:09:21 +0100 Subject: [PATCH 5/7] don't set inset in new() --- src/wkwebview/class/wry_web_view_parent.rs | 4 ++-- src/wkwebview/mod.rs | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs index 3942b9959..f39d1bb29 100644 --- a/src/wkwebview/class/wry_web_view_parent.rs +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -60,11 +60,11 @@ declare_class!( impl WryWebViewParent { #[allow(dead_code)] - pub fn new(mtm: MainThreadMarker, traffic_light_inset: Option<(f64, f64)>) -> Retained { + pub fn new(mtm: MainThreadMarker) -> Retained { let delegate = mtm .alloc::() .set_ivars(WryWebViewParentIvars { - traffic_light_inset: traffic_light_inset.into(), + traffic_light_inset: Default::default(), }); unsafe { msg_send_id![super(delegate), init] } } diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index baa8f4705..80f5da7c4 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -511,15 +511,12 @@ r#"Object.defineProperty(window, 'ipc', { } else { // inject the webview into the window let ns_window = ns_view.window().unwrap(); - let scale_factor = ns_window.backingScaleFactor(); - - let parent_view = WryWebViewParent::new( - mtm, - pl_attrs - .traffic_light_inset - .map(|p| p.to_logical(scale_factor)) - .map(|p| (p.x, p.y)), - ); + + 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 From cc887d7ffc518c91e3313d4bef655a2dae235c84 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Tue, 7 Jan 2025 12:10:08 +0100 Subject: [PATCH 6/7] guard Cell behind cfg macos --- src/wkwebview/class/wry_web_view_parent.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs index f39d1bb29..521729b8d 100644 --- a/src/wkwebview/class/wry_web_view_parent.rs +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -16,6 +16,7 @@ use objc2_foundation::NSRect; use objc2_ui_kit::UIView as NSView; pub struct WryWebViewParentIvars { + #[cfg(target_os = "macos")] traffic_light_inset: Cell>, } @@ -64,6 +65,7 @@ impl WryWebViewParent { let delegate = mtm .alloc::() .set_ivars(WryWebViewParentIvars { + #[cfg(target_os = "macos")] traffic_light_inset: Default::default(), }); unsafe { msg_send_id![super(delegate), init] } From 17f5bb8c9cece4fbfb00d3e47b04e266e18430f0 Mon Sep 17 00:00:00 2001 From: fabianlars Date: Tue, 7 Jan 2025 12:15:42 +0100 Subject: [PATCH 7/7] usage warnings --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 001219c58..c35b5d4e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1251,7 +1251,8 @@ pub trait WebViewBuilderExtDarwin { /// 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. - /// Note: Do not use this if your chosen window library does not support traffic light insets. + /// 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>(self, position: P) -> Self; } @@ -1931,7 +1932,8 @@ pub trait WebViewExtMacOS { /// 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. - /// Note: Do not use this if your chosen window library does not support traffic light insets. + /// 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>(&self, position: P) -> Result<()>; }