#![allow(clippy::unnecessary_cast)]
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::ffi::c_void;
use std::ptr;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use dispatch2::MainThreadBound;
use dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
Position, Size,
};
use objc2::rc::{Retained, autoreleasepool};
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2::{
ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, Message, available, define_class,
msg_send, sel,
};
use objc2_app_kit::{
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization,
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions,
NSAutoresizingMaskOptions, NSBackingStoreType, NSColor, NSDragOperation, NSDraggingContext,
NSDraggingDestination, NSDraggingInfo, NSDraggingSession, NSDraggingSource,
NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound,
NSPasteboardTypeString, NSPasteboardTypeTIFF, NSRequestUserAttentionType, NSScreen, NSToolbar,
NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility,
NSWindowToolbarStyle,
};
#[cfg(not(feature = "private-apple-apis"))]
use objc2_app_kit::{
NSVisualEffectBlendingMode, NSVisualEffectMaterial, NSVisualEffectState, NSVisualEffectView,
};
use objc2_core_foundation::{CGFloat, CGPoint};
use objc2_core_graphics::{
CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture,
CGDisplayFade, CGDisplayRelease, CGDisplaySetDisplayMode, CGReleaseDisplayFadeReservation,
CGRestorePermanentDisplayConfiguration, CGShieldingWindowLevel, CGWarpMouseCursorPosition,
kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, kCGDisplayFadeReservationInvalidToken,
kCGFloatingWindowLevel, kCGNormalWindowLevel,
};
use objc2_foundation::{
NSArray, NSDictionary, NSEdgeInsets, NSKeyValueChangeKey, NSKeyValueChangeNewKey,
NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSNotificationCenter, NSObject,
NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint,
NSRect, NSSize, NSString, ns_string,
};
use tracing::{debug_span, trace, warn};
use winit_common::core_foundation::MainRunLoop;
use winit_common::positioner::place_window;
use winit_core::cursor::Cursor;
use winit_core::data_transfer::DataTransferId;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
use winit_core::icon::Icon;
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider};
use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowPositioner,
WindowType,
};
use super::app_state::AppState;
use super::cursor::{CustomCursor, cursor_from_icon};
use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_display_id};
use super::util::cgerr;
use super::view::WinitView;
use super::window::{WinitPanel, WinitWindow, window_id};
use crate::app_state::DragState;
use crate::dnd::{
dnd_action_to_ns_drag_operation, ns_drag_operation_to_dnd_action, preferred_drag_operation,
};
use crate::{BlurMaterial, OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS};
#[derive(Debug)]
pub(crate) struct State {
app_state: Rc<AppState>,
window: Retained<NSWindow>,
view: Retained<WinitView>,
#[cfg(not(feature = "private-apple-apis"))]
blur_view: RefCell<Option<Retained<NSVisualEffectView>>>,
blur_material: Cell<BlurMaterial>,
previous_position: Cell<NSPoint>,
previous_scale_factor: Cell<f64>,
surface_resize_increments: Cell<NSSize>,
decorations: Cell<bool>,
resizable: Cell<bool>,
maximized: Cell<bool>,
save_presentation_opts: Cell<Option<NSApplicationPresentationOptions>>,
initial_fullscreen: Cell<bool>,
fullscreen: RefCell<Option<Fullscreen>>,
target_fullscreen: RefCell<Option<Option<Fullscreen>>>,
in_fullscreen_transition: Cell<bool>,
standard_frame: Cell<Option<NSRect>>,
is_simple_fullscreen: Cell<bool>,
saved_style: Cell<Option<NSWindowStyleMask>>,
is_borderless_game: Cell<bool>,
window_type: WindowType,
anchored: bool,
positioner: RefCell<WindowPositioner>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "WinitWindowDelegate"]
#[ivars = State]
pub(crate) struct WindowDelegate;
unsafe impl NSObjectProtocol for WindowDelegate {}
unsafe impl NSWindowDelegate for WindowDelegate {
#[unsafe(method(windowShouldClose:))]
fn window_should_close(&self, _: Option<&AnyObject>) -> bool {
let _entered = debug_span!("windowShouldClose:").entered();
self.queue_event(WindowEvent::CloseRequested);
false
}
#[unsafe(method(windowWillClose:))]
fn window_will_close(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowWillClose:").entered();
autoreleasepool(|_| {
self.window().setDelegate(None);
});
self.queue_event(WindowEvent::Destroyed);
}
#[unsafe(method(windowDidResize:))]
fn window_did_resize(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidResize:").entered();
self.emit_move_event();
self.reposition_child_windows();
}
#[unsafe(method(windowWillStartLiveResize:))]
fn window_will_start_live_resize(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowWillStartLiveResize:").entered();
let increments = self.ivars().surface_resize_increments.get();
self.set_resize_increments_inner(increments);
}
#[unsafe(method(windowDidEndLiveResize:))]
fn window_did_end_live_resize(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidEndLiveResize:").entered();
self.set_resize_increments_inner(NSSize::new(1., 1.));
}
#[unsafe(method(windowDidMove:))]
fn window_did_move(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidMove:").entered();
self.emit_move_event();
self.reposition_child_windows();
}
#[unsafe(method(windowDidChangeBackingProperties:))]
fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidChangeBackingProperties:").entered();
let scale_factor = self.scale_factor();
if scale_factor == self.ivars().previous_scale_factor.get() {
return;
};
self.ivars().previous_scale_factor.set(scale_factor);
let mtm = MainThreadMarker::from(self);
let this = self.retain();
MainRunLoop::get(mtm).queue_closure(move || {
this.handle_scale_factor_changed(scale_factor);
});
}
#[unsafe(method(windowDidBecomeKey:))]
fn window_did_become_key(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidBecomeKey:").entered();
self.queue_event(WindowEvent::Focused(true));
}
#[unsafe(method(windowDidResignKey:))]
fn window_did_resign_key(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidResignKey:").entered();
self.view().reset_modifiers();
self.queue_event(WindowEvent::Focused(false));
}
#[unsafe(method(windowWillEnterFullScreen:))]
fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowWillEnterFullScreen:").entered();
self.ivars().maximized.set(self.is_zoomed());
let mut fullscreen = self.ivars().fullscreen.borrow_mut();
match &*fullscreen {
Some(Fullscreen::Exclusive(..)) => (),
Some(Fullscreen::Borderless(_)) => (),
Some(_) => (),
None => {
let current_monitor = self
.current_monitor_inner()
.map(|monitor| CoreMonitorHandle(Arc::new(monitor)));
*fullscreen = Some(Fullscreen::Borderless(current_monitor));
},
}
self.ivars().in_fullscreen_transition.set(true);
}
#[unsafe(method(windowWillExitFullScreen:))]
fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowWillExitFullScreen:").entered();
self.ivars().in_fullscreen_transition.set(true);
}
#[unsafe(method(window:willUseFullScreenPresentationOptions:))]
fn window_will_use_fullscreen_presentation_options(
&self,
_: Option<&AnyObject>,
proposed_options: NSApplicationPresentationOptions,
) -> NSApplicationPresentationOptions {
let _entered = debug_span!("window:willUseFullScreenPresentationOptions:").entered();
let mut options = proposed_options;
let fullscreen = self.ivars().fullscreen.borrow();
if let Some(Fullscreen::Exclusive(..)) = &*fullscreen {
options = NSApplicationPresentationOptions::FullScreen
| NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar;
}
options
}
#[unsafe(method(windowDidEnterFullScreen:))]
fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidEnterFullScreen:").entered();
self.ivars().initial_fullscreen.set(false);
self.ivars().in_fullscreen_transition.set(false);
if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() {
self.set_fullscreen(target_fullscreen);
}
}
#[unsafe(method(windowDidExitFullScreen:))]
fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidExitFullScreen:").entered();
self.restore_state_from_fullscreen();
self.ivars().in_fullscreen_transition.set(false);
if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() {
self.set_fullscreen(target_fullscreen);
}
}
#[unsafe(method(windowDidFailToEnterFullScreen:))]
fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidFailToEnterFullScreen:").entered();
self.ivars().in_fullscreen_transition.set(false);
self.ivars().target_fullscreen.replace(None);
if self.ivars().initial_fullscreen.get() {
unsafe {
self.window().performSelector_withObject_afterDelay(
sel!(toggleFullScreen:),
None,
0.5,
)
};
} else {
self.restore_state_from_fullscreen();
}
}
#[unsafe(method(windowDidChangeOcclusionState:))]
fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidChangeOcclusionState:").entered();
let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible);
self.queue_event(WindowEvent::Occluded(!visible));
if self.is_borderless_game()
&& matches!(*self.ivars().fullscreen.borrow(), Some(Fullscreen::Borderless(_)))
{
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
app.setPresentationOptions(
NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar,
);
}
}
#[unsafe(method(windowDidChangeScreen:))]
fn window_did_change_screen(&self, _: Option<&AnyObject>) {
let _entered = debug_span!("windowDidChangeScreen:").entered();
let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get();
if is_simple_fullscreen {
if let Some(screen) = self.window().screen() {
self.window().setFrame_display(screen.frame(), true);
}
}
}
}
unsafe impl NSDraggingSource for WindowDelegate {
#[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
fn dragging_session_source_operation_mask(
&self,
_: &NSDraggingSession,
_: NSDraggingContext,
) -> NSDragOperation {
self.view().drag_operations()
}
#[unsafe(method(draggingSession:endedAtPoint:operation:))]
fn dragging_session_ended_at_point(
&self,
session: &NSDraggingSession,
_: NSPoint,
operation: NSDragOperation,
) {
let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64);
if operation == NSDragOperation::None {
self.queue_event(WindowEvent::OutgoingDragCanceled { id });
} else {
self.queue_event(WindowEvent::OutgoingDragDropped {
id,
action: ns_drag_operation_to_dnd_action(operation),
});
}
self.view().clear_dragging_session(session);
}
}
unsafe impl NSDraggingDestination for WindowDelegate {
#[unsafe(method(draggingEntered:))]
fn dragging_entered(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
let _entered = debug_span!("draggingEntered:").entered();
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
let window_id = self.id();
let vars = self.ivars();
let source_operations = sender.draggingSourceOperationMask();
let transfer_id = DataTransferId::from_raw(sender.draggingSequenceNumber() as i64);
vars.app_state.pasteboards().insert(transfer_id, &pb, window_id);
vars.app_state
.drag_state()
.replace(Some(DragState { id: transfer_id, valid_actions: vec![] }));
self.queue_event(WindowEvent::DragEntered {
id: transfer_id,
position: Some(position),
});
let drag_state = vars.app_state.drag_state().borrow();
drag_state
.as_ref()
.and_then(|drag_state| {
preferred_drag_operation(source_operations, &drag_state.valid_actions)
})
.map(dnd_action_to_ns_drag_operation)
.unwrap_or(NSDragOperation::empty())
}
#[unsafe(method(wantsPeriodicDraggingUpdates))]
fn wants_periodic_dragging_updates(&self) -> bool {
let _entered = debug_span!("wantsPeriodicDraggingUpdates:").entered();
true
}
#[unsafe(method(draggingUpdated:))]
fn dragging_updated(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
let _entered = debug_span!("draggingUpdated:").entered();
let vars = self.ivars();
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return NSDragOperation::empty();
};
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let source_operations = sender.draggingSourceOperationMask();
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
let drag_state = vars.app_state.drag_state().borrow();
drag_state
.as_ref()
.and_then(|drag_state| {
preferred_drag_operation(source_operations, &drag_state.valid_actions)
})
.map(dnd_action_to_ns_drag_operation)
.unwrap_or(NSDragOperation::empty())
}
#[unsafe(method(prepareForDragOperation:))]
fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool {
let _entered = debug_span!("prepareForDragOperation:").entered();
true
}
#[unsafe(method(performDragOperation:))]
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
let _entered = debug_span!("performDragOperation:").entered();
let vars = self.ivars();
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return false.into();
};
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let source_operations = sender.draggingSourceOperationMask();
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragDropped { id: transfer_id, proposed_action });
true
}
#[unsafe(method(concludeDragOperation:))]
fn conclude_drag_operation(&self, _sender: Option<&NSObject>) {
let _entered = debug_span!("concludeDragOperation:").entered();
let vars = self.ivars();
vars.app_state.pasteboards().remove_deloaded_pasteboards();
vars.app_state.drag_state().take();
}
#[unsafe(method(draggingExited:))]
fn dragging_exited(&self, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
let _entered = debug_span!("draggingExited:").entered();
let vars = self.ivars();
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return;
};
if let Some(sender) = sender {
let pb = MainThreadBound::new(
sender.draggingPasteboard(),
MainThreadMarker::new().unwrap(),
);
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
let source_operations = sender.draggingSourceOperationMask();
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
}
self.queue_event(WindowEvent::DragLeft { id: transfer_id });
vars.app_state.drag_state().take();
}
}
impl WindowDelegate {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn observe_value(
&self,
key_path: Option<&NSString>,
_object: Option<&AnyObject>,
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
let _entered = debug_span!("observeValueForKeyPath:ofObject:change:context:").entered();
if key_path == Some(ns_string!("effectiveAppearance")) {
let change = change.expect(
"requested a change dictionary in `addObserver`, but none was provided",
);
let old = change
.objectForKey(unsafe { NSKeyValueChangeOldKey })
.expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`");
let new = change
.objectForKey(unsafe { NSKeyValueChangeNewKey })
.expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`");
let old = old.downcast::<NSAppearance>().unwrap();
let new = new.downcast::<NSAppearance>().unwrap();
trace!(old = %old.name(), new = %new.name(), "effectiveAppearance changed");
if self.window().appearance().is_some() {
return;
}
let old = appearance_to_theme(&old);
let new = appearance_to_theme(&new);
if old == new {
return;
}
self.queue_event(WindowEvent::ThemeChanged(new));
} else {
panic!("unknown observed keypath {key_path:?}");
}
}
}
);
impl Drop for WindowDelegate {
fn drop(&mut self) {
unsafe { self.window().removeObserver_forKeyPath(self, ns_string!("effectiveAppearance")) };
}
}
fn new_window(
app_state: &Rc<AppState>,
attrs: &WindowAttributes,
macos_attrs: &WindowAttributesMacOS,
anchored: bool,
mtm: MainThreadMarker,
) -> Option<(Retained<NSWindow>, Retained<WinitView>)> {
autoreleasepool(|_| {
let screen = match attrs.fullscreen.clone() {
Some(Fullscreen::Borderless(Some(monitor)))
| Some(Fullscreen::Exclusive(monitor, _)) => {
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm))
},
Some(Fullscreen::Borderless(None)) => NSScreen::mainScreen(mtm),
Some(_) => NSScreen::mainScreen(mtm),
None => None,
};
let frame = match &screen {
Some(screen) => screen.frame(),
None => {
let scale_factor = NSScreen::mainScreen(mtm)
.map(|screen| screen.backingScaleFactor() as f64)
.unwrap_or(1.0);
let size = match attrs.surface_size {
Some(size) => {
let size = size.to_logical(scale_factor);
NSSize::new(size.width, size.height)
},
None => NSSize::new(800.0, 600.0),
};
let position = match attrs.position {
_ if anchored && attrs.parent_window().is_some() => NSPoint::new(0.0, 0.0),
Some(position) => {
let position = position.to_logical(scale_factor);
flip_window_screen_coordinates(NSRect::new(
NSPoint::new(position.x, position.y),
size,
))
},
None => NSPoint::new(0.0, 0.0),
};
NSRect::new(position, size)
},
};
let mut masks = if (!attrs.decorations && screen.is_none()) || macos_attrs.titlebar_hidden {
NSWindowStyleMask::Borderless
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Miniaturizable
} else {
NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Titled
};
if !attrs.resizable {
masks &= !NSWindowStyleMask::Resizable;
}
if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) {
masks &= !NSWindowStyleMask::Miniaturizable;
}
if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) {
masks &= !NSWindowStyleMask::Closable;
}
if macos_attrs.fullsize_content_view {
masks |= NSWindowStyleMask::FullSizeContentView;
}
let window: Retained<NSWindow> = if macos_attrs.panel {
masks |= NSWindowStyleMask::NonactivatingPanel;
let window: Option<Retained<WinitPanel>> = unsafe {
msg_send![
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame,
styleMask: masks,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
window?.as_super().as_super().retain()
} else {
let window: Option<Retained<WinitWindow>> = unsafe {
msg_send![
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame,
styleMask: masks,
backing: NSBackingStoreType::Buffered,
defer: false,
]
};
window?.as_super().retain()
};
unsafe { window.setReleasedWhenClosed(false) };
window.setTitle(&NSString::from_str(&attrs.title));
window.setAcceptsMouseMovedEvents(true);
if let Some(identifier) = &macos_attrs.tabbing_identifier {
window.setTabbingIdentifier(&NSString::from_str(identifier));
window.setTabbingMode(NSWindowTabbingMode::Preferred);
}
if attrs.content_protected {
window.setSharingType(NSWindowSharingType::None);
}
if macos_attrs.titlebar_transparent {
window.setTitlebarAppearsTransparent(true);
}
if macos_attrs.title_hidden {
window.setTitleVisibility(NSWindowTitleVisibility::Hidden);
}
if macos_attrs.titlebar_buttons_hidden {
for titlebar_button in &[
#[allow(deprecated)]
objc2_app_kit::NSWindowFullScreenButton,
NSWindowButton::MiniaturizeButton,
NSWindowButton::CloseButton,
NSWindowButton::ZoomButton,
] {
if let Some(button) = window.standardWindowButton(*titlebar_button) {
button.setHidden(true);
}
}
}
if macos_attrs.movable_by_window_background {
window.setMovableByWindowBackground(true);
}
if macos_attrs.unified_titlebar {
window.setToolbar(Some(&NSToolbar::new(mtm)));
window.setToolbarStyle(NSWindowToolbarStyle::Unified);
}
if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) {
if let Some(button) = window.standardWindowButton(NSWindowButton::ZoomButton) {
button.setEnabled(false);
}
}
if !macos_attrs.has_shadow {
window.setHasShadow(false);
}
if macos_attrs.fullscreen_auxiliary {
window.setCollectionBehavior(
window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary,
);
}
if attrs.position.is_none() && !(anchored && attrs.parent_window().is_some()) {
window.center();
}
let view = WinitView::new(app_state, macos_attrs.option_as_alt, mtm);
#[allow(deprecated)]
view.setWantsBestResolutionOpenGLSurface(!macos_attrs.disallow_hidpi);
if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 {
view.setWantsLayer(true);
}
let content_view = NSView::new(mtm);
window.setContentView(Some(&content_view));
view.setFrame(content_view.bounds());
view.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
content_view.addSubview(&view);
window.setInitialFirstResponder(Some(&view));
view.setPostsFrameChangedNotifications(true);
let notification_center = NSNotificationCenter::defaultCenter();
unsafe {
notification_center.addObserver_selector_name_object(
&view,
sel!(viewFrameDidChangeNotification:),
Some(NSViewFrameDidChangeNotification),
Some(&view),
)
}
if attrs.transparent {
window.setOpaque(false);
window.setBackgroundColor(Some(&NSColor::clearColor()));
}
Some((window, view))
})
}
impl WindowDelegate {
pub(super) fn new(
app_state: &Rc<AppState>,
mut attrs: WindowAttributes,
mtm: MainThreadMarker,
) -> Result<Retained<Self>, RequestError> {
let mut macos_attrs = attrs
.platform
.take()
.and_then(|attrs| attrs.cast::<WindowAttributesMacOS>().ok())
.unwrap_or_default();
let window_type = attrs.window_type();
let is_popup = matches!(window_type, WindowType::Popup);
let anchored = is_popup || attrs.positioner.is_some();
if is_popup {
attrs.decorations = false;
attrs.enabled_buttons = WindowButtons::empty();
if !attrs.active {
macos_attrs.panel = true;
}
}
let (window, view) = new_window(app_state, &attrs, &macos_attrs, anchored, mtm)
.ok_or_else(|| os_error!("couldn't create `NSWindow`"))?;
match attrs.parent_window() {
Some(rwh_06::RawWindowHandle::AppKit(handle)) => {
let parent_view: Retained<NSView> =
unsafe { Retained::retain(handle.ns_view.as_ptr().cast()) }.unwrap();
let parent = parent_view
.window()
.ok_or_else(|| os_error!("parent view should be installed in a window"))?;
unsafe { parent.addChildWindow_ordered(&window, NSWindowOrderingMode::Above) };
},
Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"),
None if is_popup => {
return Err(RequestError::NotSupported(NotSupportedError::new(
"a popup window requires a parent window",
)));
},
None => (),
}
let surface_resize_increments = match attrs
.surface_resize_increments
.map(|i| i.to_logical(window.backingScaleFactor() as _))
{
Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => {
NSSize::new(width, height)
},
_ => NSSize::new(1., 1.),
};
let scale_factor = window.backingScaleFactor() as _;
if let Some(appearance) = theme_to_appearance(attrs.preferred_theme) {
window.setAppearance(Some(&appearance));
}
let delegate = mtm.alloc().set_ivars(State {
app_state: Rc::clone(app_state),
window: window.retain(),
view,
#[cfg(not(feature = "private-apple-apis"))]
blur_view: RefCell::new(None),
blur_material: Cell::new(macos_attrs.blur_material),
previous_position: Cell::new(flip_window_screen_coordinates(window.frame())),
previous_scale_factor: Cell::new(scale_factor),
surface_resize_increments: Cell::new(surface_resize_increments),
decorations: Cell::new(attrs.decorations),
resizable: Cell::new(attrs.resizable),
maximized: Cell::new(attrs.maximized),
save_presentation_opts: Cell::new(None),
initial_fullscreen: Cell::new(attrs.fullscreen.is_some()),
fullscreen: RefCell::new(None),
target_fullscreen: RefCell::new(None),
in_fullscreen_transition: Cell::new(false),
standard_frame: Cell::new(None),
is_simple_fullscreen: Cell::new(false),
saved_style: Cell::new(None),
is_borderless_game: Cell::new(macos_attrs.borderless_game),
window_type,
anchored,
positioner: RefCell::new(attrs.positioner.unwrap_or_default()),
});
let delegate: Retained<WindowDelegate> = unsafe { msg_send![super(delegate), init] };
window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
let drag_types = unsafe {
NSArray::from_slice(&[
NSPasteboardTypeFileURL,
NSPasteboardTypeHTML,
NSPasteboardTypePNG,
NSPasteboardTypeSound,
NSPasteboardTypeString,
NSPasteboardTypeTIFF,
])
};
window.registerForDraggedTypes(&drag_types);
unsafe {
window.addObserver_forKeyPath_options_context(
&delegate,
ns_string!("effectiveAppearance"),
NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Old,
ptr::null_mut(),
)
};
if attrs.blur {
delegate.set_blur(attrs.blur);
}
if let Some(dim) = attrs.min_surface_size {
delegate.set_min_surface_size(Some(dim));
}
if let Some(dim) = attrs.max_surface_size {
delegate.set_max_surface_size(Some(dim));
}
delegate.set_window_level(attrs.window_level);
if anchored {
let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into());
delegate.set_outer_position(position);
}
delegate.set_cursor(attrs.cursor);
delegate.set_fullscreen(attrs.fullscreen);
if attrs.visible {
if attrs.active {
window.makeKeyAndOrderFront(None);
} else {
window.orderFront(None);
}
}
if attrs.maximized {
delegate.set_maximized(attrs.maximized);
}
Ok(delegate)
}
pub(super) fn view(&self) -> Retained<WinitView> {
self.ivars().view.clone()
}
#[track_caller]
pub(super) fn window(&self) -> &NSWindow {
&self.ivars().window
}
#[track_caller]
pub(crate) fn id(&self) -> WindowId {
window_id(self.window())
}
pub(crate) fn queue_event(&self, event: WindowEvent) {
let window_id = window_id(self.window());
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, event);
});
}
fn defer_if_handling_event(&self, f: impl FnOnce(Retained<Self>) + 'static) -> bool {
if !self.ivars().app_state.is_handling_event() {
return false;
}
let mtm = MainThreadMarker::from(self);
let this = self.retain();
MainRunLoop::get(mtm).queue_closure(move || f(this));
true
}
fn handle_scale_factor_changed(&self, scale_factor: CGFloat) {
let window = self.window();
let suggested_size = self.view().surface_size();
let new_surface_size = Arc::new(Mutex::new(suggested_size));
self.queue_event(WindowEvent::ScaleFactorChanged {
scale_factor,
surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&new_surface_size)),
});
let physical_size = *new_surface_size.lock().unwrap();
drop(new_surface_size);
if physical_size != suggested_size {
let logical_size = physical_size.to_logical(scale_factor);
let size = NSSize::new(logical_size.width, logical_size.height);
window.setContentSize(size);
}
self.view().surface_resized();
}
fn emit_move_event(&self) {
let position = flip_window_screen_coordinates(self.window().frame());
if self.ivars().previous_position.get() == position {
return;
}
self.ivars().previous_position.set(position);
let position =
LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor());
self.queue_event(WindowEvent::Moved(position));
}
fn set_style_mask(&self, mask: NSWindowStyleMask) {
self.window().setStyleMask(mask);
let _ = self.window().makeFirstResponder(Some(&self.view()));
}
pub fn set_title(&self, title: &str) {
self.window().setTitle(&NSString::from_str(title))
}
pub fn set_transparent(&self, transparent: bool) {
self.window().setOpaque(!transparent);
let color =
if transparent { NSColor::clearColor() } else { NSColor::windowBackgroundColor() };
self.window().setBackgroundColor(Some(&color));
}
pub fn set_blur(&self, blur: bool) {
#[cfg(feature = "private-apple-apis")]
{
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
pub fn CGSMainConnectionID() -> *mut objc2::runtime::AnyObject;
pub fn CGSSetWindowBackgroundBlurRadius(
connection_id: *mut objc2::runtime::AnyObject,
window_id: objc2_foundation::NSInteger,
radius: i64,
) -> i32;
}
let radius = if blur { 80 } else { 0 };
let window_number = self.window().windowNumber();
unsafe {
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, radius)
};
}
#[cfg(not(feature = "private-apple-apis"))]
{
if !blur {
let installed = self.ivars().blur_view.borrow_mut().take();
if let Some(installed) = installed {
installed.removeFromSuperview();
}
return;
}
if self.ivars().blur_view.borrow().is_some() {
return;
}
let mtm = MainThreadMarker::from(self);
let view = self.view();
let content_view =
self.window().contentView().expect("window always has a content view");
let new_blur_view = NSVisualEffectView::new(mtm);
new_blur_view.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
new_blur_view.setState(NSVisualEffectState::Active);
if let Some(material) = ns_visual_effect_material(self.ivars().blur_material.get()) {
new_blur_view.setMaterial(material);
}
new_blur_view.setFrame(content_view.bounds());
new_blur_view.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
content_view.addSubview_positioned_relativeTo(
&new_blur_view,
NSWindowOrderingMode::Below,
Some(&view),
);
*self.ivars().blur_view.borrow_mut() = Some(new_blur_view);
}
}
pub fn set_visible(&self, visible: bool) {
match visible {
true => self.window().makeKeyAndOrderFront(None),
false => self.window().orderOut(None),
}
}
#[inline]
pub fn is_visible(&self) -> Option<bool> {
Some(self.window().isVisible())
}
pub fn request_redraw(&self) {
self.ivars().app_state.queue_redraw(window_id(self.window()));
}
#[inline]
pub fn pre_present_notify(&self) {}
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
let position = flip_window_screen_coordinates(self.window().frame());
let position = self
.translate_anchored_position_to_parent(LogicalPosition::new(position.x, position.y));
Ok(position.to_physical(self.scale_factor()))
}
pub fn surface_position(&self) -> PhysicalPosition<i32> {
let window_position = flip_window_screen_coordinates(self.window().frame());
let view_position = flip_window_screen_coordinates(
self.window().contentRectForFrameRect(self.window().frame()),
);
let surface_position =
NSPoint::new(view_position.x - window_position.x, view_position.y - window_position.y);
let logical = LogicalPosition::new(surface_position.x, surface_position.y);
logical.to_physical(self.scale_factor())
}
pub fn set_outer_position(&self, position: Position) {
let position = position.to_logical(self.scale_factor());
let position = self.translate_anchored_position(position);
let point = flip_window_screen_coordinates(NSRect::new(
NSPoint::new(position.x, position.y),
self.window().frame().size,
));
self.window().setFrameOrigin(point);
}
fn translate_anchored_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> {
if !self.ivars().anchored {
return position;
}
let Some(parent) = self.window().parentWindow() else {
return position;
};
let parent_origin =
flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame()));
LogicalPosition::new(parent_origin.x + position.x, parent_origin.y + position.y)
}
fn translate_anchored_position_to_parent(
&self,
position: LogicalPosition<f64>,
) -> LogicalPosition<f64> {
if !self.ivars().anchored {
return position;
}
let Some(parent) = self.window().parentWindow() else {
return position;
};
let parent_origin =
flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame()));
LogicalPosition::new(position.x - parent_origin.x, position.y - parent_origin.y)
}
pub fn parent_content_origin(&self) -> Option<LogicalPosition<f64>> {
if !self.ivars().anchored {
return None;
}
let parent = self.window().parentWindow()?;
let origin = flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame()));
Some(LogicalPosition::new(origin.x, origin.y))
}
pub fn window_type(&self) -> WindowType {
self.ivars().window_type
}
pub fn popup_positioner(&self) -> WindowPositioner {
*self.ivars().positioner.borrow()
}
pub fn set_popup_positioner(&self, positioner: WindowPositioner) {
*self.ivars().positioner.borrow_mut() = positioner;
self.reposition();
}
pub(crate) fn reposition(&self) {
if !self.ivars().anchored {
return;
}
let positioner = *self.ivars().positioner.borrow();
let parent_origin = self.parent_content_origin().unwrap_or_default();
let Some(monitor) = self.current_monitor() else { return };
let Some((work_area_position, work_area_size)) = monitor.work_area() else { return };
let scale_factor = self.scale_factor();
let work_area_position = work_area_position.to_logical::<f64>(scale_factor);
let clip_position = LogicalPosition::new(
work_area_position.x - parent_origin.x,
work_area_position.y - parent_origin.y,
);
let clip_size = work_area_size.to_logical::<f64>(scale_factor);
let current_outer_size = self.outer_size().to_logical::<f64>(scale_factor);
let (origin, new_outer_size) =
place_window(&positioner, scale_factor, current_outer_size, (clip_position, clip_size));
self.set_outer_position(Position::Logical(origin));
if new_outer_size != current_outer_size {
let frame = NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(new_outer_size.width, new_outer_size.height),
);
let content_size = self.window().contentRectForFrameRect(frame).size;
let content_size = LogicalSize::new(content_size.width, content_size.height);
let _ = self.request_surface_size(Size::Logical(content_size));
}
}
fn reposition_child_windows(&self) {
let Some(children) = self.window().childWindows() else { return };
for child in children.iter() {
let Some(child_delegate) = child.delegate() else { continue };
let Ok(child_delegate) = child_delegate.downcast::<WindowDelegate>() else { continue };
if child_delegate.ivars().anchored {
child_delegate.reposition();
}
}
}
#[inline]
pub fn surface_size(&self) -> PhysicalSize<u32> {
self.view().surface_size()
}
#[inline]
pub fn outer_size(&self) -> PhysicalSize<u32> {
let frame = self.window().frame();
let logical = LogicalSize::new(frame.size.width, frame.size.height);
logical.to_physical(self.scale_factor())
}
pub fn safe_area(&self) -> PhysicalInsets<u32> {
let insets = if self.view().respondsToSelector(sel!(safeAreaInsets)) {
self.view().safeAreaInsets()
} else {
let window_rect = self.window().convertRectFromScreen(
self.window().contentRectForFrameRect(self.window().frame()),
);
let layout_rect = self.window().contentLayoutRect();
NSEdgeInsets {
top: (window_rect.size.height + window_rect.origin.y)
- (layout_rect.size.height + layout_rect.origin.y),
left: layout_rect.origin.x - window_rect.origin.x,
bottom: layout_rect.origin.y - window_rect.origin.y,
right: (window_rect.size.width + window_rect.origin.x)
- (layout_rect.size.width + layout_rect.origin.x),
}
};
let insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right);
insets.to_physical(self.scale_factor())
}
#[inline]
pub fn request_surface_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
let scale_factor = self.scale_factor();
let size = size.to_logical(scale_factor);
self.window().setContentSize(NSSize::new(size.width, size.height));
None
}
pub fn set_min_surface_size(&self, dimensions: Option<Size>) {
let dimensions =
dimensions.unwrap_or(Size::Logical(LogicalSize { width: 0.0, height: 0.0 }));
let min_size = dimensions.to_logical::<CGFloat>(self.scale_factor());
let min_size = NSSize::new(min_size.width, min_size.height);
self.window().setContentMinSize(min_size);
let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size;
if current_size.width < min_size.width {
current_size.width = min_size.width;
}
if current_size.height < min_size.height {
current_size.height = min_size.height;
}
self.window().setContentSize(current_size);
}
pub fn set_max_surface_size(&self, dimensions: Option<Size>) {
let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize {
width: f32::MAX as f64,
height: f32::MAX as f64,
}));
let scale_factor = self.scale_factor();
let max_size = dimensions.to_logical::<CGFloat>(scale_factor);
let max_size = NSSize::new(max_size.width, max_size.height);
self.window().setContentMaxSize(max_size);
let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size;
if max_size.width < current_size.width {
current_size.width = max_size.width;
}
if max_size.height < current_size.height {
current_size.height = max_size.height;
}
self.window().setContentSize(current_size);
}
pub fn surface_resize_increments(&self) -> Option<PhysicalSize<u32>> {
let increments = self.ivars().surface_resize_increments.get();
let (w, h) = (increments.width, increments.height);
if w > 1.0 || h > 1.0 {
Some(LogicalSize::new(w, h).to_physical(self.scale_factor()))
} else {
None
}
}
pub fn set_surface_resize_increments(&self, increments: Option<Size>) {
self.ivars().surface_resize_increments.set(
increments
.map(|increments| {
let logical = increments.to_logical::<f64>(self.scale_factor());
NSSize::new(logical.width.max(1.0), logical.height.max(1.0))
})
.unwrap_or_else(|| NSSize::new(1.0, 1.0)),
);
}
pub(crate) fn set_resize_increments_inner(&self, size: NSSize) {
self.window().setContentResizeIncrements(size);
}
#[inline]
pub fn set_resizable(&self, resizable: bool) {
self.ivars().resizable.set(resizable);
let fullscreen = self.ivars().fullscreen.borrow().is_some();
if !fullscreen {
let mut mask = self.window().styleMask();
if resizable {
mask |= NSWindowStyleMask::Resizable;
} else {
mask &= !NSWindowStyleMask::Resizable;
}
self.set_style_mask(mask);
}
}
#[inline]
pub fn is_resizable(&self) -> bool {
self.window().isResizable()
}
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let mut mask = self.window().styleMask();
if buttons.contains(WindowButtons::CLOSE) {
mask |= NSWindowStyleMask::Closable;
} else {
mask &= !NSWindowStyleMask::Closable;
}
if buttons.contains(WindowButtons::MINIMIZE) {
mask |= NSWindowStyleMask::Miniaturizable;
} else {
mask &= !NSWindowStyleMask::Miniaturizable;
}
self.set_style_mask(mask);
if let Some(button) = self.window().standardWindowButton(NSWindowButton::ZoomButton) {
button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE));
}
}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
let mut buttons = WindowButtons::empty();
if self.window().isMiniaturizable() {
buttons |= WindowButtons::MINIMIZE;
}
if self
.window()
.standardWindowButton(NSWindowButton::ZoomButton)
.map(|b| b.isEnabled())
.unwrap_or(true)
{
buttons |= WindowButtons::MAXIMIZE;
}
if self.window().hasCloseBox() {
buttons |= WindowButtons::CLOSE;
}
buttons
}
pub fn set_cursor(&self, cursor: Cursor) {
let view = self.view();
let cursor = match cursor {
Cursor::Icon(icon) => cursor_from_icon(icon),
Cursor::Custom(cursor) => match cursor.cast_ref::<CustomCursor>() {
Some(cursor) => cursor.0.clone(),
None => {
tracing::error!("unrecognized cursor passed to macOS backend");
return;
},
},
};
if view.cursor_icon() == cursor {
return;
}
view.set_cursor_icon(cursor);
self.window().invalidateCursorRectsForView(&view);
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> {
let associate_mouse_cursor = match mode {
CursorGrabMode::Locked => false,
CursorGrabMode::None => true,
CursorGrabMode::Confined => {
return Err(NotSupportedError::new("confined cursor is not supported").into());
},
};
cgerr(CGAssociateMouseAndMouseCursorPosition(associate_mouse_cursor))?;
Ok(())
}
#[inline]
pub fn set_cursor_visible(&self, visible: bool) {
let view = self.view();
let state_changed = view.set_cursor_visible(visible);
if state_changed {
self.window().invalidateCursorRectsForView(&view);
}
}
#[inline]
pub fn scale_factor(&self) -> f64 {
self.window().backingScaleFactor() as _
}
#[inline]
pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), RequestError> {
let content_rect = self.window().contentRectForFrameRect(self.window().frame());
let window_position = flip_window_screen_coordinates(content_rect);
let cursor_position = cursor_position.to_logical::<CGFloat>(self.scale_factor());
let point = CGPoint {
x: window_position.x + cursor_position.x,
y: window_position.y + cursor_position.y,
};
cgerr(CGWarpMouseCursorPosition(point))?;
cgerr(CGAssociateMouseAndMouseCursorPosition(true))?;
Ok(())
}
#[inline]
pub fn drag_window(&self) -> Result<(), RequestError> {
let mtm = MainThreadMarker::from(self);
let event =
NSApplication::sharedApplication(mtm).currentEvent().ok_or(RequestError::Ignored)?;
self.window().performWindowDragWithEvent(&event);
Ok(())
}
#[inline]
pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), NotSupportedError> {
Err(NotSupportedError::new("drag_resize_window is not supported"))
}
#[inline]
pub fn show_window_menu(&self, _position: Position) {}
#[inline]
pub fn set_cursor_hittest(&self, hittest: bool) {
self.window().setIgnoresMouseEvents(!hittest);
}
pub(crate) fn is_zoomed(&self) -> bool {
let curr_mask = self.window().styleMask();
let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable;
let needs_temp_mask = !curr_mask.contains(required);
if needs_temp_mask {
self.set_style_mask(required);
}
let is_zoomed = self.window().isZoomed();
if needs_temp_mask {
self.set_style_mask(curr_mask);
}
is_zoomed
}
fn saved_style(&self) -> NSWindowStyleMask {
let base_mask =
self.ivars().saved_style.take().unwrap_or_else(|| self.window().styleMask());
if self.ivars().resizable.get() {
base_mask | NSWindowStyleMask::Resizable
} else {
base_mask & !NSWindowStyleMask::Resizable
}
}
pub(crate) fn restore_state_from_fullscreen(&self) {
self.ivars().fullscreen.replace(None);
let maximized = self.ivars().maximized.get();
let mask = self.saved_style();
self.set_style_mask(mask);
self.set_maximized(maximized);
}
#[inline]
pub fn set_minimized(&self, minimized: bool) {
let is_minimized = self.window().isMiniaturized();
if is_minimized == minimized {
return;
}
if minimized {
self.window().miniaturize(Some(self));
} else {
self.window().deminiaturize(Some(self));
}
}
#[inline]
pub fn is_minimized(&self) -> Option<bool> {
Some(self.window().isMiniaturized())
}
#[inline]
pub fn set_maximized(&self, maximized: bool) {
if self.defer_if_handling_event(move |this| this.set_maximized(maximized)) {
return;
}
let mtm = MainThreadMarker::from(self);
let is_zoomed = self.is_zoomed();
if is_zoomed == maximized {
return;
};
if !is_zoomed {
self.ivars().standard_frame.set(Some(self.window().frame()));
}
self.ivars().maximized.set(maximized);
if self.ivars().fullscreen.borrow().is_some() {
return;
}
if self.window().styleMask().contains(NSWindowStyleMask::Resizable) {
self.window().zoom(None);
} else {
let new_rect = if maximized {
let screen = NSScreen::mainScreen(mtm).expect("no screen found");
screen.visibleFrame()
} else {
self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME)
};
self.window().setFrame_display(new_rect, false);
}
}
#[inline]
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
self.ivars().fullscreen.borrow().clone()
}
#[inline]
pub fn is_maximized(&self) -> bool {
self.is_zoomed()
}
#[inline]
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
if self.ivars().is_simple_fullscreen.get() {
return;
}
if fullscreen.is_some()
&& self
.window()
.collectionBehavior()
.contains(NSWindowCollectionBehavior::FullScreenAuxiliary)
{
warn!(
"cannot fullscreen a window marked as fullscreen auxiliary; call \
`set_fullscreen_auxiliary(false)` first"
);
return;
}
if self.ivars().in_fullscreen_transition.get() {
self.ivars().target_fullscreen.replace(Some(fullscreen));
return;
}
let old_fullscreen = self.ivars().fullscreen.borrow().clone();
if fullscreen == old_fullscreen {
return;
}
if !self.ivars().initial_fullscreen.get()
&& self.defer_if_handling_event({
let fullscreen = fullscreen.clone();
move |this| this.set_fullscreen(fullscreen)
})
{
return;
}
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
if let Some(ref fullscreen) = fullscreen {
let new_screen = match fullscreen {
Fullscreen::Borderless(Some(monitor)) | Fullscreen::Exclusive(monitor, _) => {
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
monitor.ns_screen(mtm)
},
_ => {
if let Some(monitor) = self.current_monitor_inner() {
monitor.ns_screen(mtm)
} else {
return;
}
},
}
.unwrap();
let old_screen = self.window().screen().unwrap();
if old_screen != new_screen {
self.window().setFrameOrigin(new_screen.frame().origin);
}
}
if let Some(Fullscreen::Exclusive(ref monitor, ref video_mode)) = fullscreen {
let display_id = monitor.native_id() as _;
let mut fade_token = kCGDisplayFadeReservationInvalidToken;
if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) {
self.ivars().save_presentation_opts.replace(Some(app.presentationOptions()));
}
if cgerr(unsafe { CGAcquireDisplayFadeReservation(5.0, &mut fade_token) }).is_ok() {
CGDisplayFade(
fade_token,
0.3,
kCGDisplayBlendNormal,
kCGDisplayBlendSolidColor,
0.0,
0.0,
0.0,
true,
);
}
cgerr(CGDisplayCapture(display_id)).unwrap();
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
let video_mode =
match monitor.video_mode_handles().find(|mode| &mode.mode == video_mode) {
Some(video_mode) => video_mode,
None => return,
};
cgerr(unsafe {
CGDisplaySetDisplayMode(display_id, Some(&video_mode.native_mode.0), None)
})
.expect("failed to set video mode");
if fade_token != kCGDisplayFadeReservationInvalidToken {
CGDisplayFade(
fade_token,
0.6,
kCGDisplayBlendSolidColor,
kCGDisplayBlendNormal,
0.0,
0.0,
0.0,
false,
);
CGReleaseDisplayFadeReservation(fade_token);
}
}
self.ivars().fullscreen.replace(fullscreen.clone());
fn toggle_fullscreen(window: &NSWindow) {
window.setLevel(kCGNormalWindowLevel as NSWindowLevel);
window.toggleFullScreen(None);
}
match (old_fullscreen, fullscreen) {
(None, Some(_)) => {
let curr_mask = self.window().styleMask();
let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable;
if !curr_mask.contains(required) {
self.set_style_mask(required);
self.ivars().saved_style.set(Some(curr_mask));
}
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Borderless(_)), None) => {
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Exclusive(monitor, _)), None) => {
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
restore_and_release_display(monitor);
toggle_fullscreen(self.window());
},
(Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(..))) => {
self.ivars().save_presentation_opts.set(Some(app.presentationOptions()));
let presentation_options = NSApplicationPresentationOptions::FullScreen
| NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar;
app.setPresentationOptions(presentation_options);
let window_level = CGShieldingWindowLevel() as NSWindowLevel + 1;
self.window().setLevel(window_level);
},
(Some(Fullscreen::Exclusive(monitor, _)), Some(Fullscreen::Borderless(_))) => {
let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or(
NSApplicationPresentationOptions::FullScreen
| NSApplicationPresentationOptions::AutoHideDock
| NSApplicationPresentationOptions::AutoHideMenuBar,
);
app.setPresentationOptions(presentation_options);
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
restore_and_release_display(monitor);
self.window().setLevel(kCGNormalWindowLevel as NSWindowLevel);
},
_ => {},
};
}
#[inline]
pub fn set_decorations(&self, decorations: bool) {
if decorations == self.ivars().decorations.get() {
return;
}
self.ivars().decorations.set(decorations);
let fullscreen = self.ivars().fullscreen.borrow().is_some();
let resizable = self.ivars().resizable.get();
if fullscreen {
return;
}
let new_mask = {
let mut new_mask = if decorations {
NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable
| NSWindowStyleMask::Titled
} else {
NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable
};
if !resizable {
new_mask &= !NSWindowStyleMask::Resizable;
}
new_mask
};
self.set_style_mask(new_mask);
}
#[inline]
pub fn is_decorated(&self) -> bool {
self.ivars().decorations.get()
}
#[inline]
pub fn set_window_level(&self, level: WindowLevel) {
let level = match level {
WindowLevel::AlwaysOnTop => kCGFloatingWindowLevel as NSWindowLevel,
WindowLevel::AlwaysOnBottom => (kCGNormalWindowLevel - 1) as NSWindowLevel,
WindowLevel::Normal => kCGNormalWindowLevel as NSWindowLevel,
};
self.window().setLevel(level);
}
#[inline]
pub fn set_window_icon(&self, _icon: Option<Icon>) {
}
pub fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> {
let current_caps = self.view().ime_capabilities();
let request_data = match request {
ImeRequest::Enable(enable) => {
let (capabilities, request_data) = enable.into_raw();
if current_caps.is_some() {
return Err(ImeRequestError::AlreadyEnabled);
}
self.view().enable_ime(capabilities);
request_data
},
ImeRequest::Update(request_data) => {
if current_caps.is_none() {
return Err(ImeRequestError::NotEnabled);
}
request_data
},
ImeRequest::Disable => {
self.view().disable_ime();
return Ok(());
},
_ => return Err(ImeRequestError::NotSupported),
};
if let Some((spot, size)) = request_data.cursor_area {
if self.view().ime_capabilities().unwrap().cursor_area() {
let scale_factor = self.scale_factor();
let logical_spot = spot.to_logical(scale_factor);
let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y);
let size = size.to_logical(scale_factor);
let size = NSSize::new(size.width, size.height);
self.view().set_ime_cursor_area(logical_spot, size);
} else {
warn!("discarding IME cursor area update without capability enabled.");
}
}
Ok(())
}
pub fn ime_capabilities(&self) -> Option<ImeCapabilities> {
self.view().ime_capabilities()
}
#[inline]
pub fn focus_window(&self) {
let mtm = MainThreadMarker::from(self);
let is_minimized = self.window().isMiniaturized();
let is_visible = self.window().isVisible();
if !is_minimized && is_visible {
#[allow(deprecated)]
NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true);
self.window().makeKeyAndOrderFront(None);
}
}
#[inline]
pub fn request_user_attention(&self, request_type: Option<UserAttentionType>) {
let mtm = MainThreadMarker::from(self);
let ns_request_type = request_type.map(|ty| match ty {
UserAttentionType::Critical => NSRequestUserAttentionType::CriticalRequest,
UserAttentionType::Informational => NSRequestUserAttentionType::InformationalRequest,
});
if let Some(ty) = ns_request_type {
NSApplication::sharedApplication(mtm).requestUserAttention(ty);
}
}
#[inline]
pub(crate) fn current_monitor_inner(&self) -> Option<MonitorHandle> {
let display_id = get_display_id(&*self.window().screen()?);
if let Some(monitor) = MonitorHandle::new(display_id) {
Some(monitor)
} else {
warn!(display_id, "got screen with invalid display ID");
None
}
}
#[inline]
pub fn current_monitor(&self) -> Option<MonitorHandle> {
self.current_monitor_inner()
}
#[inline]
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
monitor::available_monitors()
}
#[inline]
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
let monitor = monitor::primary_monitor();
Some(monitor)
}
#[inline]
pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle {
let window_handle = rwh_06::AppKitWindowHandle::new({
let ptr = Retained::as_ptr(&self.view()) as *mut _;
std::ptr::NonNull::new(ptr).expect("Retained<T> should never be null")
});
rwh_06::RawWindowHandle::AppKit(window_handle)
}
fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) {
let current_style_mask = self.window().styleMask();
if on {
self.set_style_mask(current_style_mask | mask);
} else {
self.set_style_mask(current_style_mask & !mask);
}
}
#[inline]
pub fn has_focus(&self) -> bool {
self.window().isKeyWindow()
}
pub fn theme(&self) -> Option<Theme> {
self.window().appearance().map(|appearance| appearance_to_theme(&appearance)).or_else(
|| {
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
if available!(macos = 10.14) {
Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance()))
} else {
Some(Theme::Light)
}
},
)
}
pub fn set_theme(&self, theme: Option<Theme>) {
self.window().setAppearance(theme_to_appearance(theme).as_deref());
}
#[inline]
pub fn set_content_protected(&self, protected: bool) {
self.window().setSharingType(if protected {
NSWindowSharingType::None
} else {
NSWindowSharingType::ReadOnly
})
}
pub fn title(&self) -> String {
self.window().title().to_string()
}
pub fn reset_dead_keys(&self) {
}
}
fn restore_and_release_display(monitor: &MonitorHandle) {
let available_monitors = monitor::available_monitors();
if available_monitors.contains(monitor) {
CGRestorePermanentDisplayConfiguration();
cgerr(CGDisplayRelease(monitor.native_id() as _)).unwrap();
} else {
warn!(
monitor = monitor.name().map(|name| name.to_string()),
"Tried to restore exclusive fullscreen on a monitor that is no longer available"
);
}
}
#[cfg(not(feature = "private-apple-apis"))]
fn ns_visual_effect_material(material: BlurMaterial) -> Option<NSVisualEffectMaterial> {
match material {
BlurMaterial::Titlebar => Some(NSVisualEffectMaterial::Titlebar),
BlurMaterial::Selection => Some(NSVisualEffectMaterial::Selection),
BlurMaterial::Menu => available!(macos = 10.11).then_some(NSVisualEffectMaterial::Menu),
BlurMaterial::Popover => {
available!(macos = 10.11).then_some(NSVisualEffectMaterial::Popover)
},
BlurMaterial::Sidebar => {
available!(macos = 10.11).then_some(NSVisualEffectMaterial::Sidebar)
},
BlurMaterial::HeaderView => {
available!(macos = 10.14).then_some(NSVisualEffectMaterial::HeaderView)
},
BlurMaterial::FullScreenUI => {
available!(macos = 10.14).then_some(NSVisualEffectMaterial::FullScreenUI)
},
BlurMaterial::HudWindow => {
available!(macos = 10.14).then_some(NSVisualEffectMaterial::HUDWindow)
},
BlurMaterial::ToolTip => {
available!(macos = 10.14).then_some(NSVisualEffectMaterial::ToolTip)
},
BlurMaterial::UnderWindowBackground => {
available!(macos = 10.14).then_some(NSVisualEffectMaterial::UnderWindowBackground)
},
}
}
impl WindowExtMacOS for WindowDelegate {
#[inline]
fn simple_fullscreen(&self) -> bool {
self.ivars().is_simple_fullscreen.get()
}
#[inline]
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
let mtm = MainThreadMarker::from(self);
let app = NSApplication::sharedApplication(mtm);
let is_native_fullscreen = self.ivars().fullscreen.borrow().is_some();
let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get();
if is_native_fullscreen
|| (fullscreen && is_simple_fullscreen)
|| (!fullscreen && !is_simple_fullscreen)
{
return false;
}
if fullscreen {
self.ivars()
.standard_frame
.set(Some(self.window().contentRectForFrameRect(self.window().frame())));
self.ivars().saved_style.set(Some(self.window().styleMask()));
self.ivars().save_presentation_opts.set(Some(app.presentationOptions()));
self.ivars().is_simple_fullscreen.set(true);
let presentation_options = if self.is_borderless_game() {
NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar
} else {
NSApplicationPresentationOptions::AutoHideDock
| NSApplicationPresentationOptions::AutoHideMenuBar
};
app.setPresentationOptions(presentation_options);
self.toggle_style_mask(NSWindowStyleMask::Titled, false);
let screen = self.window().screen().expect("expected screen to be available");
self.window().setFrame_display(screen.frame(), true);
if NSScreen::class().responds_to(sel!(safeAreaInsets)) {
self.view().setAdditionalSafeAreaInsets(screen.safeAreaInsets());
}
self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false);
self.toggle_style_mask(NSWindowStyleMask::Resizable, false);
self.window().setMovable(false);
} else {
let new_mask = self.saved_style();
self.ivars().is_simple_fullscreen.set(false);
let save_presentation_opts = self.ivars().save_presentation_opts.get();
let frame = self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME);
if let Some(presentation_opts) = save_presentation_opts {
app.setPresentationOptions(presentation_opts);
}
if NSScreen::class().responds_to(sel!(safeAreaInsets)) {
self.view().setAdditionalSafeAreaInsets(NSEdgeInsets {
top: 0.0,
left: 0.0,
bottom: 0.0,
right: 0.0,
});
}
self.window().setFrame_display(frame, true);
self.window().setMovable(true);
self.set_style_mask(new_mask);
}
true
}
#[inline]
fn has_shadow(&self) -> bool {
self.window().hasShadow()
}
#[inline]
fn set_has_shadow(&self, has_shadow: bool) {
self.window().setHasShadow(has_shadow)
}
#[inline]
fn set_tabbing_identifier(&self, identifier: &str) {
self.window().setTabbingIdentifier(&NSString::from_str(identifier))
}
#[inline]
fn tabbing_identifier(&self) -> String {
self.window().tabbingIdentifier().to_string()
}
#[inline]
fn select_next_tab(&self) {
self.window().selectNextTab(None)
}
#[inline]
fn select_previous_tab(&self) {
self.window().selectPreviousTab(None)
}
#[inline]
fn select_tab_at_index(&self, index: usize) {
if !available!(macos = 10.13) {
tracing::warn!("window tab groups are only available on macOS 10.13+");
return;
}
if let Some(group) = self.window().tabGroup() {
if let Some(windows) = self.window().tabbedWindows() {
if index < windows.len() {
group.setSelectedWindow(Some(&windows.objectAtIndex(index)));
}
}
}
}
#[inline]
fn num_tabs(&self) -> usize {
self.window().tabbedWindows().map(|windows| windows.len()).unwrap_or(1)
}
fn is_document_edited(&self) -> bool {
self.window().isDocumentEdited()
}
fn set_document_edited(&self, edited: bool) {
self.window().setDocumentEdited(edited)
}
fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) {
self.view().set_option_as_alt(option_as_alt);
}
fn set_blur_material(&self, blur_material: BlurMaterial) {
self.ivars().blur_material.set(blur_material);
#[cfg(not(feature = "private-apple-apis"))]
if let Some(blur_view) = self.ivars().blur_view.borrow().as_ref() {
if let Some(material) = ns_visual_effect_material(blur_material) {
blur_view.setMaterial(material);
}
}
}
fn blur_material(&self) -> BlurMaterial {
self.ivars().blur_material.get()
}
fn option_as_alt(&self) -> OptionAsAlt {
self.view().option_as_alt()
}
fn set_borderless_game(&self, borderless_game: bool) {
self.ivars().is_borderless_game.set(borderless_game);
}
fn is_borderless_game(&self) -> bool {
self.ivars().is_borderless_game.get()
}
fn set_unified_titlebar(&self, unified_titlebar: bool) {
let window = self.window();
if unified_titlebar {
let mtm = MainThreadMarker::from(self);
window.setToolbar(Some(&NSToolbar::new(mtm)));
window.setToolbarStyle(NSWindowToolbarStyle::Unified);
} else {
window.setToolbar(None);
window.setToolbarStyle(NSWindowToolbarStyle::Automatic);
}
}
fn unified_titlebar(&self) -> bool {
let window = self.window();
window.toolbar().is_some() && window.toolbarStyle() == NSWindowToolbarStyle::Unified
}
#[inline]
fn set_fullscreen_auxiliary(&self, fullscreen_auxiliary: bool) {
let window = self.window();
let behavior = window.collectionBehavior();
if fullscreen_auxiliary {
window
.setCollectionBehavior(behavior | NSWindowCollectionBehavior::FullScreenAuxiliary);
} else {
window
.setCollectionBehavior(behavior - NSWindowCollectionBehavior::FullScreenAuxiliary);
}
}
#[inline]
fn fullscreen_auxiliary(&self) -> bool {
self.window().collectionBehavior().contains(NSWindowCollectionBehavior::FullScreenAuxiliary)
}
}
const DEFAULT_STANDARD_FRAME: NSRect =
NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0));
fn dark_appearance_name() -> &'static NSString {
ns_string!("NSAppearanceNameDarkAqua")
}
pub fn appearance_to_theme(appearance: &NSAppearance) -> Theme {
let best_match = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_slice(&[
unsafe { NSAppearanceNameAqua },
dark_appearance_name(),
]));
if let Some(best_match) = best_match {
if *best_match == *dark_appearance_name() { Theme::Dark } else { Theme::Light }
} else {
warn!(?appearance, "failed to determine the theme of the appearance");
Theme::Light
}
}
fn theme_to_appearance(theme: Option<Theme>) -> Option<Retained<NSAppearance>> {
let appearance = match theme? {
Theme::Light => unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua) },
Theme::Dark => NSAppearance::appearanceNamed(dark_appearance_name()),
};
if let Some(appearance) = appearance {
Some(appearance)
} else {
warn!(?theme, "could not find appearance for theme");
None
}
}