use std::{cell::Cell, ffi::c_void};
use objc2::{
DefinedClass, MainThreadOnly, define_class,
rc::Retained,
runtime::{NSObject, NSObjectProtocol},
sel,
};
use objc2_app_kit::{
NSBackingStoreType, NSColor, NSView, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowDidBecomeKeyNotification, NSWindowDidChangeBackingPropertiesNotification,
NSWindowDidChangeScreenNotification, NSWindowDidDeminiaturizeNotification,
NSWindowDidEndLiveResizeNotification, NSWindowDidExitFullScreenNotification,
NSWindowDidResizeNotification, NSWindowStyleMask,
};
use objc2_foundation::{
MainThreadMarker, NSNotification, NSNotificationCenter, NSObjectNSDelayedPerforming, NSPoint,
};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use tauri_runtime::dpi::Position;
use tauri_utils::{TitleBarStyle, config::Color};
use crate::window::AppWindow;
use super::utils;
impl AppWindow {
pub(crate) fn cef_host_handle(&self) -> cef::sys::cef_window_handle_t {
let nsview = self.nsview();
Retained::as_ptr(&nsview).cast_mut().cast()
}
pub(crate) fn nsview(&self) -> Retained<NSView> {
let handle = self
.window
.window_handle()
.expect("failed to get window handle");
match handle.as_raw() {
RawWindowHandle::AppKit(handle) => unsafe {
Retained::<NSView>::retain(handle.ns_view.as_ptr().cast::<NSView>())
.expect("failed to retain NSView")
},
other => panic!("expected AppKit window handle, got {other:?}"),
}
}
pub(crate) fn set_enabled(&self, enabled: bool) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
if enabled {
if let Some(attached) = nswindow.attachedSheet() {
nswindow.endSheet(&attached);
}
} else {
if nswindow.attachedSheet().is_some() {
return;
}
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let frame = nswindow.frame();
let sheet = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
mtm.alloc(),
frame,
NSWindowStyleMask::Titled,
NSBackingStoreType::Buffered,
false,
)
};
sheet.setAlphaValue(0.5);
nswindow.beginSheet_completionHandler(&sheet, None);
}
}
pub(crate) fn is_enabled(&self) -> bool {
self
.nsview()
.window()
.map(|nswindow| nswindow.attachedSheet().is_none())
.unwrap_or(true)
}
pub(crate) fn set_traffic_light_position(&self, position: &Position) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
let pos = position.to_logical::<f64>(nswindow.backingScaleFactor());
let pos = NSPoint::new(pos.x, pos.y);
inset_traffic_lights(&nswindow, pos.x, pos.y);
observe_traffic_light_resets(&nswindow, pos);
}
pub(crate) fn reapply_traffic_light_position_after_appearance_change(&self) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
let Some(observer) = traffic_light_observer(&nswindow) else {
return;
};
unsafe {
observer.performSelector_withObject_afterDelay(
sel!(reapplyTrafficLightPosition:),
Some(&nswindow),
0.0,
);
}
}
pub(crate) fn set_title_bar_style(&self, style: TitleBarStyle) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
match style {
TitleBarStyle::Visible => {
nswindow.setTitlebarAppearsTransparent(false);
let mut mask = nswindow.styleMask();
mask.remove(NSWindowStyleMask::FullSizeContentView);
nswindow.setStyleMask(mask);
}
TitleBarStyle::Transparent => {
nswindow.setTitlebarAppearsTransparent(true);
let mut mask = nswindow.styleMask();
mask.remove(NSWindowStyleMask::FullSizeContentView);
nswindow.setStyleMask(mask);
}
TitleBarStyle::Overlay => {
nswindow.setTitlebarAppearsTransparent(true);
let mut mask = nswindow.styleMask();
mask.insert(NSWindowStyleMask::FullSizeContentView);
nswindow.setStyleMask(mask);
}
_ => {}
}
}
pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
let mut collection_behavior = nswindow.collectionBehavior();
collection_behavior.set(NSWindowCollectionBehavior::CanJoinAllSpaces, visible);
nswindow.setCollectionBehavior(collection_behavior);
}
pub(crate) fn set_background_color(&self, color: Option<Color>) {
let nsview = self.nsview();
let Some(nswindow) = nsview.window() else {
return;
};
let nscolor = color
.map(utils::ns_color_from_tauri_color)
.unwrap_or_else(NSColor::windowBackgroundColor);
nswindow.setOpaque(color.map(|color| color.3 == u8::MAX).unwrap_or(true));
nswindow.setBackgroundColor(Some(&nscolor));
}
}
fn inset_traffic_lights(nswindow: &NSWindow, x: f64, y: f64) {
let Some(close) = nswindow.standardWindowButton(NSWindowButton::CloseButton) else {
return;
};
let Some(miniaturize) = nswindow.standardWindowButton(NSWindowButton::MiniaturizeButton) else {
return;
};
let Some(zoom) = nswindow.standardWindowButton(NSWindowButton::ZoomButton) else {
return;
};
let title_bar_container_view = unsafe { close.superview().and_then(|view| view.superview()) };
let Some(title_bar_container_view) = title_bar_container_view else {
return;
};
let close_rect = close.frame();
let title_bar_frame_height = close_rect.size.height + y;
let mut title_bar_rect = title_bar_container_view.frame();
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = nswindow.frame().size.height - title_bar_frame_height;
title_bar_container_view.setFrame(title_bar_rect);
let space_between = miniaturize.frame().origin.x - close_rect.origin.x;
for (index, button) in [close, miniaturize, zoom].into_iter().enumerate() {
let mut origin = button.frame().origin;
origin.x = x + (index as f64 * space_between);
button.setFrameOrigin(origin);
}
}
fn observe_traffic_light_resets(nswindow: &NSWindow, position: NSPoint) {
if let Some(observer) = traffic_light_observer(nswindow) {
observer.ivars().position.set(position);
return;
}
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let observer = TrafficLightObserver::new(mtm, position);
let center = NSNotificationCenter::defaultCenter();
for name in unsafe {
[
NSWindowDidResizeNotification,
NSWindowDidEndLiveResizeNotification,
NSWindowDidExitFullScreenNotification,
NSWindowDidDeminiaturizeNotification,
NSWindowDidChangeScreenNotification,
NSWindowDidChangeBackingPropertiesNotification,
NSWindowDidBecomeKeyNotification,
]
} {
unsafe {
center.addObserver_selector_name_object(
&observer,
sel!(handleWindowNotification:),
Some(name),
Some(nswindow),
);
}
}
set_traffic_light_observer(nswindow, &observer);
}
fn reapply_traffic_light_position(nswindow: &NSWindow, position: NSPoint) {
if nswindow.styleMask().contains(NSWindowStyleMask::FullScreen) {
return;
}
inset_traffic_lights(nswindow, position.x, position.y);
}
#[derive(Default)]
struct TrafficLightObserverIvars {
position: Cell<NSPoint>,
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "TauriCefTrafficLightObserver"]
#[ivars = TrafficLightObserverIvars]
#[thread_kind = MainThreadOnly]
struct TrafficLightObserver;
unsafe impl NSObjectProtocol for TrafficLightObserver {}
impl TrafficLightObserver {
#[unsafe(method(handleWindowNotification:))]
fn handle_window_notification(&self, notification: &NSNotification) {
let Some(object) = notification.object() else {
return;
};
let nswindow = unsafe { Retained::cast_unchecked::<NSWindow>(object) };
reapply_traffic_light_position(&nswindow, self.ivars().position.get());
}
#[unsafe(method(reapplyTrafficLightPosition:))]
fn reapply_traffic_light_position_deferred(&self, nswindow: &NSWindow) {
reapply_traffic_light_position(nswindow, self.ivars().position.get());
}
}
);
impl TrafficLightObserver {
fn new(mtm: MainThreadMarker, position: NSPoint) -> Retained<Self> {
let observer = Self::alloc(mtm).set_ivars(TrafficLightObserverIvars {
position: Cell::new(position),
});
unsafe { objc2::msg_send![super(observer), init] }
}
}
fn traffic_light_observer_key() -> *const c_void {
static TRAFFIC_LIGHT_OBSERVER_KEY: u8 = 0;
&TRAFFIC_LIGHT_OBSERVER_KEY as *const u8 as *const c_void
}
fn traffic_light_observer(nswindow: &NSWindow) -> Option<Retained<TrafficLightObserver>> {
let observer = utils::associated_object(nswindow, traffic_light_observer_key())?;
Some(unsafe { Retained::cast_unchecked::<TrafficLightObserver>(observer) })
}
fn set_traffic_light_observer(nswindow: &NSWindow, observer: &TrafficLightObserver) {
utils::set_associated_object(nswindow, traffic_light_observer_key(), observer);
}