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, } #[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>(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>(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>(&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>(&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>, +} 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 { let delegate = mtm .alloc::() - .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, protocol_ptrs: Vec<*mut Box>, RequestAsyncResponder)>>, + #[cfg(target_os = "macos")] + // We need this to update the traffic light inset + parent_view: Option>, } 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 {