Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add traffic light inset #1446

Merged
merged 7 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/traffic-light-inset.md
Original file line number Diff line number Diff line change
@@ -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()`.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",))]
Expand All @@ -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",))]
Expand All @@ -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)]
Expand Down Expand Up @@ -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")]
Expand All @@ -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.
Expand Down
69 changes: 66 additions & 3 deletions src/wkwebview/class/wry_web_view_parent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)>>,
FabianLars marked this conversation as resolved.
Show resolved Hide resolved
}

declare_class!(
pub struct WryWebViewParent;
Expand Down Expand Up @@ -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)};
}
}
}
);

Expand All @@ -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);
}
}
30 changes: 26 additions & 4 deletions src/wkwebview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -459,7 +462,7 @@ impl InnerWebView {
}
}

let w = Self {
let mut w = Self {
id: webview_id,
webview: webview.clone(),
manager: manager.clone(),
Expand All @@ -473,6 +476,8 @@ impl InnerWebView {
ui_delegate,
protocol_ptrs,
is_child,
#[cfg(target_os = "macos")]
parent_view: None,
};

// Initialize scripts
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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> {
Expand Down
Loading