diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 2e4e5e4c..38561239 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -75,7 +75,7 @@ impl PanicInfo { #[derive(Debug)] pub struct EventLoopWindowTarget { delegate: Id, - mtm: MainThreadMarker, + pub(super) mtm: MainThreadMarker, } impl EventLoopWindowTarget { diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index d29e19f4..65bea82f 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -22,7 +22,8 @@ pub(crate) use self::{ EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes, }, monitor::{MonitorHandle, VideoModeHandle}, - window::{PlatformSpecificWindowBuilderAttributes, WindowId}, + window::WindowId, + window_delegate::PlatformSpecificWindowBuilderAttributes, }; use crate::event::DeviceId as RootDeviceId; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 0b7d0027..89533c86 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -34,7 +34,7 @@ use crate::{ WindowEvent, }, keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}, - platform::macos::{OptionAsAlt, WindowExtMacOS}, + platform::macos::OptionAsAlt, }; #[derive(Debug)] @@ -141,6 +141,9 @@ pub struct ViewState { // Weak reference because the window keeps a strong reference to the view _ns_window: WeakId, + + /// The state of the `Option` as `Alt`. + option_as_alt: Cell, } declare_class!( @@ -437,7 +440,7 @@ declare_class!( // Get the characters from the event. let old_ime_state = self.ivars().ime_state.get(); self.ivars().forward_key_to_app.set(false); - let event = replace_event(event, self.window().option_as_alt()); + let event = replace_event(event, self.option_as_alt()); // The `interpretKeyEvents` function might call // `setMarkedText`, `insertText`, and `doCommandBySelector`. @@ -483,7 +486,7 @@ declare_class!( fn key_up(&self, event: &NSEvent) { trace_scope!("keyUp:"); - let event = replace_event(event, self.window().option_as_alt()); + let event = replace_event(event, self.option_as_alt()); self.update_modifiers(&event, false); // We want to send keyboard input when we are currently in the ground state. @@ -759,11 +762,16 @@ declare_class!( ); impl WinitView { - pub(super) fn new(window: &WinitWindow, accepts_first_mouse: bool) -> Id { + pub(super) fn new( + window: &WinitWindow, + accepts_first_mouse: bool, + option_as_alt: OptionAsAlt, + ) -> Id { let mtm = MainThreadMarker::from(window); let this = mtm.alloc().set_ivars(ViewState { accepts_first_mouse, _ns_window: WeakId::new(&window.retain()), + option_as_alt: Cell::new(option_as_alt), ..Default::default() }); let this: Id = unsafe { msg_send_id![super(this), init] }; @@ -883,6 +891,14 @@ impl WinitView { } } + pub(super) fn set_option_as_alt(&self, value: OptionAsAlt) { + self.ivars().option_as_alt.set(value) + } + + pub(super) fn option_as_alt(&self) -> OptionAsAlt { + self.ivars().option_as_alt.get() + } + /// Update modifiers if `event` has something different fn update_modifiers(&self, ns_event: &NSEvent, is_flags_changed_event: bool) { use ElementState::{Pressed, Released}; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index c95dffa8..bdaa9c49 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1,61 +1,20 @@ #![allow(clippy::unnecessary_cast)] -use std::cell::{Cell, RefCell}; -use std::collections::VecDeque; -use std::f64; - -use core_graphics::display::{CGDisplay, CGPoint}; -use icrate::AppKit::{ - NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication, - NSApplicationPresentationAutoHideDock, NSApplicationPresentationAutoHideMenuBar, - NSApplicationPresentationFullScreen, NSApplicationPresentationHideDock, - NSApplicationPresentationHideMenuBar, NSApplicationPresentationOptions, NSBackingStoreBuffered, - NSColor, NSCriticalRequest, NSFilenamesPboardType, NSInformationalRequest, NSResponder, - NSScreen, NSView, NSWindow, NSWindowAbove, NSWindowCloseButton, NSWindowFullScreenButton, - NSWindowLevel, NSWindowMiniaturizeButton, NSWindowSharingNone, NSWindowSharingReadOnly, - NSWindowStyleMask, NSWindowStyleMaskBorderless, NSWindowStyleMaskClosable, - NSWindowStyleMaskFullSizeContentView, NSWindowStyleMaskMiniaturizable, - NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, NSWindowTabbingModePreferred, - NSWindowTitleHidden, NSWindowZoomButton, -}; -use icrate::Foundation::{ - CGFloat, MainThreadBound, MainThreadMarker, NSArray, NSCopying, NSObject, NSPoint, NSRect, - NSSize, NSString, -}; +use icrate::AppKit::{NSResponder, NSWindow}; +use icrate::Foundation::{MainThreadBound, MainThreadMarker, NSObject}; use objc2::rc::{autoreleasepool, Id}; -use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; -use super::app_delegate::ApplicationDelegate; -use super::cursor::cursor_from_icon; use super::event_loop::EventLoopWindowTarget; -use super::ffi::{ - self, kCGFloatingWindowLevel, kCGNormalWindowLevel, CGSMainConnectionID, - CGSSetWindowBackgroundBlurRadius, -}; -use super::monitor::{self, MonitorHandle, VideoModeHandle}; -use super::monitor::{flip_window_screen_coordinates, get_display_id}; -use super::view::WinitView; -use super::window_delegate::WinitWindowDelegate; -use super::{Fullscreen, OsError}; -use crate::{ - cursor::Cursor, - dpi::{ - LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, - }, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - event::WindowEvent, - icon::Icon, - platform::macos::{OptionAsAlt, WindowExtMacOS}, - window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowLevel, - }, -}; +use super::window_delegate::WindowDelegate; +use super::PlatformSpecificWindowBuilderAttributes; +use crate::error::OsError as RootOsError; +use crate::window::WindowAttributes; pub(crate) struct Window { window: MainThreadBound>, - // We keep this around so that it doesn't get dropped until the window does. - _delegate: MainThreadBound>, + /// The window only keeps a weak reference to this, so we must keep it around here. + delegate: MainThreadBound>, } impl Drop for Window { @@ -67,30 +26,28 @@ impl Drop for Window { impl Window { pub(crate) fn new( - _window_target: &EventLoopWindowTarget, + window_target: &EventLoopWindowTarget, attributes: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, ) -> Result { - let mtm = MainThreadMarker::new() - .expect("windows can only be created on the main thread on macOS"); - let (window, _delegate) = - autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs, mtm))?; + let mtm = window_target.mtm; + let delegate = autoreleasepool(|_| WindowDelegate::new(attributes, pl_attribs, mtm))?; Ok(Window { - window: MainThreadBound::new(window, mtm), - _delegate: MainThreadBound::new(_delegate, mtm), + window: MainThreadBound::new(delegate.window().retain(), mtm), + delegate: MainThreadBound::new(delegate, mtm), }) } - pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WinitWindow) + Send + 'static) { + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WindowDelegate) + Send + 'static) { // For now, don't actually do queuing, since it may be less predictable self.maybe_wait_on_main(f) } pub(crate) fn maybe_wait_on_main( &self, - f: impl FnOnce(&WinitWindow) -> R + Send, + f: impl FnOnce(&WindowDelegate) -> R + Send, ) -> R { - self.window.get_on_main(|window| f(window)) + self.delegate.get_on_main(|delegate| f(delegate)) } #[cfg(feature = "rwh_06")] @@ -99,7 +56,7 @@ impl Window { &self, ) -> Result { if let Some(mtm) = MainThreadMarker::new() { - Ok(self.window.get(mtm).raw_window_handle_rwh_06()) + Ok(self.delegate.get(mtm).raw_window_handle_rwh_06()) } else { Err(rwh_06::HandleError::Unavailable) } @@ -137,40 +94,6 @@ impl From for WindowId { } } -#[derive(Clone)] -pub struct PlatformSpecificWindowBuilderAttributes { - pub movable_by_window_background: bool, - pub titlebar_transparent: bool, - pub title_hidden: bool, - pub titlebar_hidden: bool, - pub titlebar_buttons_hidden: bool, - pub fullsize_content_view: bool, - pub disallow_hidpi: bool, - pub has_shadow: bool, - pub accepts_first_mouse: bool, - pub tabbing_identifier: Option, - pub option_as_alt: OptionAsAlt, -} - -impl Default for PlatformSpecificWindowBuilderAttributes { - #[inline] - fn default() -> Self { - Self { - movable_by_window_background: false, - titlebar_transparent: false, - title_hidden: false, - titlebar_hidden: false, - titlebar_buttons_hidden: false, - fullsize_content_view: false, - disallow_hidpi: false, - has_shadow: true, - accepts_first_mouse: true, - tabbing_identifier: None, - option_as_alt: Default::default(), - } - } -} - declare_class!( #[derive(Debug)] pub struct WinitWindow; @@ -182,9 +105,7 @@ declare_class!( const NAME: &'static str = "WinitWindow"; } - impl DeclaredClass for WinitWindow { - type Ivars = State; - } + impl DeclaredClass for WinitWindow {} unsafe impl WinitWindow { #[method(canBecomeMainWindow)] @@ -201,1334 +122,8 @@ declare_class!( } ); -#[derive(Debug, Default)] -pub struct State { - resizable: Cell, - /// This field tracks the current fullscreen state of the window - /// (as seen by `WindowDelegate`). - pub(super) fullscreen: RefCell>, - // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen - // or windowWillExitFullScreen and windowDidExitFullScreen. - // We must not toggle fullscreen when this is true. - pub(super) in_fullscreen_transition: Cell, - // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, - // Set target_fullscreen and do after fullscreen transition is end. - pub(crate) target_fullscreen: RefCell>>, - pub(super) maximized: Cell, - standard_frame: Cell>, - pub(crate) is_simple_fullscreen: Cell, - pub(super) saved_style: Cell>, - /// Presentation options saved before entering `set_simple_fullscreen`, and - /// restored upon exiting it. Also used when transitioning from Borderless to - /// Exclusive fullscreen in `set_fullscreen` because we need to disable the menu - /// bar in exclusive fullscreen but want to restore the original options when - /// transitioning back to borderless fullscreen. - save_presentation_opts: Cell>, - pub(super) current_theme: Cell>, - - /// The current resize increments for the window content. - pub(crate) resize_increments: Cell, - /// The state of the `Option` as `Alt`. - option_as_alt: Cell, - /// Whether the window is showing decorations. - decorations: Cell, -} - impl WinitWindow { - #[allow(clippy::type_complexity)] - fn new( - attrs: WindowAttributes, - pl_attrs: PlatformSpecificWindowBuilderAttributes, - mtm: MainThreadMarker, - ) -> Result<(Id, Id), RootOsError> { - trace_scope!("WinitWindow::new"); - - let this = autoreleasepool(|_| { - let screen = match attrs.fullscreen.clone().map(Into::into) { - Some(Fullscreen::Borderless(Some(monitor))) - | Some(Fullscreen::Exclusive(VideoModeHandle { monitor, .. })) => { - monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm)) - } - Some(Fullscreen::Borderless(None)) => 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.inner_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 { - Some(position) => { - let position = position.to_logical(scale_factor); - flip_window_screen_coordinates(NSRect::new( - NSPoint::new(position.x, position.y), - size, - )) - } - // This value is ignored by calling win.center() below - None => NSPoint::new(0.0, 0.0), - }; - NSRect::new(position, size) - } - }; - - let mut masks = if (!attrs.decorations && screen.is_none()) || pl_attrs.titlebar_hidden - { - // Resizable without a titlebar or borders - // if decorations is set to false, ignore pl_attrs - // - // if the titlebar is hidden, ignore other pl_attrs - NSWindowStyleMaskBorderless - | NSWindowStyleMaskResizable - | NSWindowStyleMaskMiniaturizable - } else { - // default case, resizable window with titlebar and titlebar buttons - NSWindowStyleMaskClosable - | NSWindowStyleMaskMiniaturizable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskTitled - }; - - if !attrs.resizable { - masks &= !NSWindowStyleMaskResizable; - } - - if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) { - masks &= !NSWindowStyleMaskMiniaturizable; - } - - if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) { - masks &= !NSWindowStyleMaskClosable; - } - - if pl_attrs.fullsize_content_view { - masks |= NSWindowStyleMaskFullSizeContentView; - } - - let this = mtm.alloc().set_ivars(State { - resizable: Cell::new(attrs.resizable), - maximized: Cell::new(attrs.maximized), - decorations: Cell::new(attrs.decorations), - ..Default::default() - }); - let this: Option> = unsafe { - msg_send_id![ - super(this), - initWithContentRect: frame, - styleMask: masks, - backing: NSBackingStoreBuffered, - defer: false, - ] - }; - let this = this?; - - // It is very important for correct memory management that we - // disable the extra release that would otherwise happen when - // calling `close` on the window. - unsafe { this.setReleasedWhenClosed(false) }; - - let resize_increments = match attrs - .resize_increments - .map(|i| i.to_logical::(this.scale_factor())) - { - Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { - NSSize::new(width, height) - } - _ => NSSize::new(1., 1.), - }; - - this.ivars().resize_increments.set(resize_increments); - - this.setTitle(&NSString::from_str(&attrs.title)); - this.setAcceptsMouseMovedEvents(true); - - if let Some(identifier) = pl_attrs.tabbing_identifier { - this.setTabbingIdentifier(&NSString::from_str(&identifier)); - this.setTabbingMode(NSWindowTabbingModePreferred); - } - - if attrs.content_protected { - this.setSharingType(NSWindowSharingNone); - } - - if pl_attrs.titlebar_transparent { - this.setTitlebarAppearsTransparent(true); - } - if pl_attrs.title_hidden { - this.setTitleVisibility(NSWindowTitleHidden); - } - if pl_attrs.titlebar_buttons_hidden { - for titlebar_button in &[ - #[allow(deprecated)] - NSWindowFullScreenButton, - NSWindowMiniaturizeButton, - NSWindowCloseButton, - NSWindowZoomButton, - ] { - if let Some(button) = this.standardWindowButton(*titlebar_button) { - button.setHidden(true); - } - } - } - if pl_attrs.movable_by_window_background { - this.setMovableByWindowBackground(true); - } - - if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { - if let Some(button) = this.standardWindowButton(NSWindowZoomButton) { - button.setEnabled(false); - } - } - - if !pl_attrs.has_shadow { - this.setHasShadow(false); - } - if attrs.position.is_none() { - this.center(); - } - - this.set_option_as_alt(pl_attrs.option_as_alt); - - Some(this) - }) - .ok_or_else(|| os_error!(OsError::CreationError("Couldn't create `NSWindow`")))?; - - #[cfg(feature = "rwh_06")] - match attrs.parent_window.map(|handle| handle.0) { - Some(rwh_06::RawWindowHandle::AppKit(handle)) => { - // SAFETY: Caller ensures the pointer is valid or NULL - // Unwrap is fine, since the pointer comes from `NonNull`. - let parent_view: Id = - unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); - let parent = parent_view.window().ok_or_else(|| { - os_error!(OsError::CreationError( - "parent view should be installed in a window" - )) - })?; - - // SAFETY: We know that there are no parent -> child -> parent cycles since the only place in `winit` - // where we allow making a window a child window is right here, just after it's been created. - unsafe { parent.addChildWindow_ordered(&this, NSWindowAbove) }; - } - Some(raw) => panic!("Invalid raw window handle {raw:?} on macOS"), - None => (), - } - - let view = WinitView::new(&this, pl_attrs.accepts_first_mouse); - - // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until - // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid - // always the default system value in favour of the user's code - #[allow(deprecated)] - view.setWantsBestResolutionOpenGLSurface(!pl_attrs.disallow_hidpi); - - // On Mojave, views automatically become layer-backed shortly after being added to - // a window. Changing the layer-backedness of a view breaks the association between - // the view and its associated OpenGL context. To work around this, on Mojave we - // explicitly make the view layer-backed up front so that AppKit doesn't do it - // itself and break the association with its context. - if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 { - view.setWantsLayer(true); - } - - // Configure the new view as the "key view" for the window - this.setContentView(Some(&view)); - this.setInitialFirstResponder(Some(&view)); - - if attrs.transparent { - this.setOpaque(false); - this.setBackgroundColor(Some(unsafe { &NSColor::clearColor() })); - } - - if attrs.blur { - this.set_blur(attrs.blur); - } - - if let Some(dim) = attrs.min_inner_size { - this.set_min_inner_size(Some(dim)); - } - if let Some(dim) = attrs.max_inner_size { - this.set_max_inner_size(Some(dim)); - } - - this.set_window_level(attrs.window_level); - - // register for drag and drop operations. - this.registerForDraggedTypes(&NSArray::from_id_slice(&[ - unsafe { NSFilenamesPboardType }.copy() - ])); - - match attrs.preferred_theme { - Some(theme) => { - this.ivars().current_theme.set(Some(theme)); - } - None => { - this.ivars().current_theme.set(Some(get_ns_theme(mtm))); - } - } - - this.set_cursor(attrs.cursor); - - let delegate = WinitWindowDelegate::new(&this, attrs.fullscreen.is_some()); - - // XXX Send `Focused(false)` right after creating the window delegate, so we won't - // obscure the real focused events on the startup. - delegate.queue_event(WindowEvent::Focused(false)); - - // Set fullscreen mode after we setup everything - this.set_fullscreen(attrs.fullscreen.map(Into::into)); - - // Setting the window as key has to happen *after* we set the fullscreen - // state, since otherwise we'll briefly see the window at normal size - // before it transitions. - if attrs.visible { - if attrs.active { - // Tightly linked with `app_state::window_activation_hack` - this.makeKeyAndOrderFront(None); - } else { - this.orderFront(None); - } - } - - if attrs.maximized { - this.set_maximized(attrs.maximized); - } - - Ok((this, delegate)) - } - - #[track_caller] - pub(super) fn view(&self) -> Id { - // SAFETY: The view inside WinitWindow is always `WinitView` - unsafe { Id::cast(self.contentView().unwrap()) } - } - - fn set_style_mask(&self, mask: NSWindowStyleMask) { - self.setStyleMask(mask); - // If we don't do this, key handling will break - // (at least until the window is clicked again/etc.) - let _ = self.makeFirstResponder(Some(&self.contentView().unwrap())); - } -} - -impl WinitWindow { - pub fn id(&self) -> WindowId { + pub(super) fn id(&self) -> WindowId { WindowId(self as *const Self as usize) } - - pub fn set_title(&self, title: &str) { - self.setTitle(&NSString::from_str(title)) - } - - pub fn set_transparent(&self, transparent: bool) { - self.setOpaque(!transparent) - } - - pub fn set_blur(&self, blur: bool) { - // NOTE: in general we want to specify the blur radius, but the choice of 80 - // should be a reasonable default. - let radius = if blur { 80 } else { 0 }; - let window_number = unsafe { self.windowNumber() }; - unsafe { - CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, radius); - } - } - - pub fn set_visible(&self, visible: bool) { - match visible { - true => self.makeKeyAndOrderFront(None), - false => self.orderOut(None), - } - } - - #[inline] - pub fn is_visible(&self) -> Option { - Some(self.isVisible()) - } - - pub fn request_redraw(&self) { - let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); - app_delegate.queue_redraw(self.id()); - } - - #[inline] - pub fn pre_present_notify(&self) {} - - pub fn outer_position(&self) -> Result, NotSupportedError> { - let position = flip_window_screen_coordinates(self.frame()); - Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) - } - - pub fn inner_position(&self) -> Result, NotSupportedError> { - let content_rect = self.contentRectForFrameRect(self.frame()); - let position = flip_window_screen_coordinates(content_rect); - Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) - } - - pub fn set_outer_position(&self, position: Position) { - let position = position.to_logical(self.scale_factor()); - let point = flip_window_screen_coordinates(NSRect::new( - NSPoint::new(position.x, position.y), - self.frame().size, - )); - unsafe { self.setFrameOrigin(point) }; - } - - #[inline] - pub fn inner_size(&self) -> PhysicalSize { - let content_rect = self.contentRectForFrameRect(self.frame()); - let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height); - logical.to_physical(self.scale_factor()) - } - - #[inline] - pub fn outer_size(&self) -> PhysicalSize { - let frame = self.frame(); - let logical = LogicalSize::new(frame.size.width, frame.size.height); - logical.to_physical(self.scale_factor()) - } - - #[inline] - pub fn request_inner_size(&self, size: Size) -> Option> { - let scale_factor = self.scale_factor(); - let size = size.to_logical(scale_factor); - self.setContentSize(NSSize::new(size.width, size.height)); - None - } - - pub fn set_min_inner_size(&self, dimensions: Option) { - let dimensions = dimensions.unwrap_or(Logical(LogicalSize { - width: 0.0, - height: 0.0, - })); - let min_size = dimensions.to_logical::(self.scale_factor()); - - let min_size = NSSize::new(min_size.width, min_size.height); - unsafe { self.setContentMinSize(min_size) }; - - // If necessary, resize the window to match constraint - let mut current_size = self.contentRectForFrameRect(self.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.setContentSize(current_size); - } - - pub fn set_max_inner_size(&self, dimensions: Option) { - let dimensions = dimensions.unwrap_or(Logical(LogicalSize { - width: std::f32::MAX as f64, - height: std::f32::MAX as f64, - })); - let scale_factor = self.scale_factor(); - let max_size = dimensions.to_logical::(scale_factor); - - let max_size = NSSize::new(max_size.width, max_size.height); - unsafe { self.setContentMaxSize(max_size) }; - - // If necessary, resize the window to match constraint - let mut current_size = self.contentRectForFrameRect(self.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.setContentSize(current_size); - } - - pub fn resize_increments(&self) -> Option> { - let increments = self.ivars().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_resize_increments(&self, increments: Option) { - // XXX the resize increments are only used during live resizes. - self.ivars().resize_increments.set( - increments - .map(|increments| { - let logical = increments.to_logical::(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) { - // It was concluded (#2411) that there is never a use-case for - // "outer" resize increments, hence we set "inner" ones here. - // ("outer" in macOS being just resizeIncrements, and "inner" - contentResizeIncrements) - // This is consistent with X11 size hints behavior - self.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.styleMask(); - if resizable { - mask |= NSWindowStyleMaskResizable; - } else { - mask &= !NSWindowStyleMaskResizable; - } - self.set_style_mask(mask); - } - // Otherwise, we don't change the mask until we exit fullscreen. - } - - #[inline] - pub fn is_resizable(&self) -> bool { - self.isResizable() - } - - #[inline] - pub fn set_enabled_buttons(&self, buttons: WindowButtons) { - let mut mask = self.styleMask(); - - if buttons.contains(WindowButtons::CLOSE) { - mask |= NSWindowStyleMaskClosable; - } else { - mask &= !NSWindowStyleMaskClosable; - } - - if buttons.contains(WindowButtons::MINIMIZE) { - mask |= NSWindowStyleMaskMiniaturizable; - } else { - mask &= !NSWindowStyleMaskMiniaturizable; - } - - // This must happen before the button's "enabled" status has been set, - // hence we do it synchronously. - self.set_style_mask(mask); - - // We edit the button directly instead of using `NSResizableWindowMask`, - // since that mask also affect the resizability of the window (which is - // controllable by other means in `winit`). - if let Some(button) = self.standardWindowButton(NSWindowZoomButton) { - button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE)); - } - } - - #[inline] - pub fn enabled_buttons(&self) -> WindowButtons { - let mut buttons = WindowButtons::empty(); - if self.isMiniaturizable() { - buttons |= WindowButtons::MINIMIZE; - } - if self - .standardWindowButton(NSWindowZoomButton) - .map(|b| b.isEnabled()) - .unwrap_or(true) - { - buttons |= WindowButtons::MAXIMIZE; - } - if self.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) => cursor.inner.0, - }; - - if view.cursor_icon() == cursor { - return; - } - - view.set_cursor_icon(cursor); - self.invalidateCursorRectsForView(&view); - } - - #[inline] - pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - let associate_mouse_cursor = match mode { - CursorGrabMode::Locked => false, - CursorGrabMode::None => true, - CursorGrabMode::Confined => { - return Err(ExternalError::NotSupported(NotSupportedError::new())) - } - }; - - // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 - CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) - .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) - } - - #[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.invalidateCursorRectsForView(&view); - } - } - - #[inline] - pub fn scale_factor(&self) -> f64 { - self.backingScaleFactor() as f64 - } - - #[inline] - pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), ExternalError> { - let physical_window_position = self.inner_position().unwrap(); - let scale_factor = self.scale_factor(); - let window_position = physical_window_position.to_logical::(scale_factor); - let logical_cursor_position = cursor_position.to_logical::(scale_factor); - let point = CGPoint { - x: logical_cursor_position.x + window_position.x, - y: logical_cursor_position.y + window_position.y, - }; - CGDisplay::warp_mouse_cursor_position(point) - .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; - CGDisplay::associate_mouse_and_mouse_cursor_position(true) - .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; - - Ok(()) - } - - #[inline] - pub fn drag_window(&self) -> Result<(), ExternalError> { - let mtm = MainThreadMarker::from(self); - let event = NSApplication::sharedApplication(mtm) - .currentEvent() - .unwrap(); - self.performWindowDragWithEvent(&event); - Ok(()) - } - - #[inline] - pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { - Err(ExternalError::NotSupported(NotSupportedError::new())) - } - - #[inline] - pub fn show_window_menu(&self, _position: Position) {} - - #[inline] - pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { - self.setIgnoresMouseEvents(!hittest); - Ok(()) - } - - pub(crate) fn is_zoomed(&self) -> bool { - // because `isZoomed` doesn't work if the window's borderless, - // we make it resizable temporalily. - let curr_mask = self.styleMask(); - - let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; - let needs_temp_mask = !mask_contains(curr_mask, required); - if needs_temp_mask { - self.set_style_mask(required); - } - - let is_zoomed = self.isZoomed(); - - // Roll back temp styles - 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.styleMask()); - if self.ivars().resizable.get() { - base_mask | NSWindowStyleMaskResizable - } else { - base_mask & !NSWindowStyleMaskResizable - } - } - - /// This is called when the window is exiting fullscreen, whether by the - /// user clicking on the green fullscreen button or programmatically by - /// `toggleFullScreen:` - 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.isMiniaturized(); - if is_minimized == minimized { - return; - } - - if minimized { - self.miniaturize(Some(self)); - } else { - unsafe { self.deminiaturize(Some(self)) }; - } - } - - #[inline] - pub fn is_minimized(&self) -> Option { - Some(self.isMiniaturized()) - } - - #[inline] - pub fn set_maximized(&self, maximized: bool) { - let mtm = MainThreadMarker::from(self); - let is_zoomed = self.is_zoomed(); - if is_zoomed == maximized { - return; - }; - - // Save the standard frame sized if it is not zoomed - if !is_zoomed { - self.ivars().standard_frame.set(Some(self.frame())); - } - - self.ivars().maximized.set(maximized); - - if self.ivars().fullscreen.borrow().is_some() { - // Handle it in window_did_exit_fullscreen - return; - } - - if mask_contains(self.styleMask(), NSWindowStyleMaskResizable) { - // Just use the native zoom if resizable - self.zoom(None); - } else { - // if it's not resizable, we set the frame directly - 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.setFrame_display(new_rect, false); - } - } - - #[inline] - pub(crate) fn fullscreen(&self) -> Option { - self.ivars().fullscreen.borrow().clone() - } - - #[inline] - pub fn is_maximized(&self) -> bool { - self.is_zoomed() - } - - #[inline] - pub(crate) fn set_fullscreen(&self, fullscreen: Option) { - let mtm = MainThreadMarker::from(self); - let app = NSApplication::sharedApplication(mtm); - - if self.ivars().is_simple_fullscreen.get() { - return; - } - if self.ivars().in_fullscreen_transition.get() { - // We can't set fullscreen here. - // Set fullscreen after transition. - self.ivars().target_fullscreen.replace(Some(fullscreen)); - return; - } - let old_fullscreen = self.ivars().fullscreen.borrow().clone(); - if fullscreen == old_fullscreen { - return; - } - - // If the fullscreen is on a different monitor, we must move the window - // to that monitor before we toggle fullscreen (as `toggleFullScreen` - // does not take a screen parameter, but uses the current screen) - if let Some(ref fullscreen) = fullscreen { - let new_screen = match fullscreen { - Fullscreen::Borderless(Some(monitor)) => monitor.clone(), - Fullscreen::Borderless(None) => { - if let Some(monitor) = self.current_monitor_inner() { - monitor - } else { - return; - } - } - Fullscreen::Exclusive(video_mode) => video_mode.monitor(), - } - .ns_screen(mtm) - .unwrap(); - - let old_screen = self.screen().unwrap(); - if old_screen != new_screen { - unsafe { self.setFrameOrigin(new_screen.frame().origin) }; - } - } - - if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { - // Note: `enterFullScreenMode:withOptions:` seems to do the exact - // same thing as we're doing here (captures the display, sets the - // video mode, and hides the menu bar and dock), with the exception - // of that I couldn't figure out how to set the display mode with - // it. I think `enterFullScreenMode:withOptions:` is still using the - // older display mode API where display modes were of the type - // `CFDictionary`, but this has changed, so we can't obtain the - // correct parameter for this any longer. Apple's code samples for - // this function seem to just pass in "YES" for the display mode - // parameter, which is not consistent with the docs saying that it - // takes a `NSDictionary`.. - - let display_id = video_mode.monitor().native_identifier(); - - let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; - - if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { - self.ivars() - .save_presentation_opts - .replace(Some(app.presentationOptions())); - } - - unsafe { - // Fade to black (and wait for the fade to complete) to hide the - // flicker from capturing the display and switching display mode - if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) - == ffi::kCGErrorSuccess - { - ffi::CGDisplayFade( - fade_token, - 0.3, - ffi::kCGDisplayBlendNormal, - ffi::kCGDisplayBlendSolidColor, - 0.0, - 0.0, - 0.0, - ffi::TRUE, - ); - } - - assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); - } - - unsafe { - let result = ffi::CGDisplaySetDisplayMode( - display_id, - video_mode.native_mode.0, - std::ptr::null(), - ); - assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); - - // After the display has been configured, fade back in - // asynchronously - if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { - ffi::CGDisplayFade( - fade_token, - 0.6, - ffi::kCGDisplayBlendSolidColor, - ffi::kCGDisplayBlendNormal, - 0.0, - 0.0, - 0.0, - ffi::FALSE, - ); - ffi::CGReleaseDisplayFadeReservation(fade_token); - } - } - } - - self.ivars().fullscreen.replace(fullscreen.clone()); - - fn toggle_fullscreen(window: &WinitWindow) { - // Window level must be restored from `CGShieldingWindowLevel() - // + 1` back to normal in order for `toggleFullScreen` to do - // anything - window.setLevel(kCGNormalWindowLevel as NSWindowLevel); - window.toggleFullScreen(None); - } - - match (old_fullscreen, fullscreen) { - (None, Some(_)) => { - // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we - // set a normal style temporarily. The previous state will be - // restored in `WindowDelegate::window_did_exit_fullscreen`. - let curr_mask = self.styleMask(); - let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; - if !mask_contains(curr_mask, required) { - self.set_style_mask(required); - self.ivars().saved_style.set(Some(curr_mask)); - } - toggle_fullscreen(self); - } - (Some(Fullscreen::Borderless(_)), None) => { - // State is restored by `window_did_exit_fullscreen` - toggle_fullscreen(self); - } - (Some(Fullscreen::Exclusive(ref video_mode)), None) => { - unsafe { - ffi::CGRestorePermanentDisplayConfiguration(); - assert_eq!( - ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), - ffi::kCGErrorSuccess - ); - }; - toggle_fullscreen(self); - } - (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { - // If we're already in fullscreen mode, calling - // `CGDisplayCapture` will place the shielding window on top of - // our window, which results in a black display and is not what - // we want. So, we must place our window on top of the shielding - // window. Unfortunately, this also makes our window be on top - // of the menu bar, and this looks broken, so we must make sure - // that the menu bar is disabled. This is done in the window - // delegate in `window:willUseFullScreenPresentationOptions:`. - self.ivars() - .save_presentation_opts - .set(Some(app.presentationOptions())); - - let presentation_options = NSApplicationPresentationFullScreen - | NSApplicationPresentationHideDock - | NSApplicationPresentationHideMenuBar; - app.setPresentationOptions(presentation_options); - - let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1; - self.setLevel(window_level); - } - (Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => { - let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or( - NSApplicationPresentationFullScreen - | NSApplicationPresentationAutoHideDock - | NSApplicationPresentationAutoHideMenuBar, - ); - app.setPresentationOptions(presentation_options); - - unsafe { - ffi::CGRestorePermanentDisplayConfiguration(); - assert_eq!( - ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), - ffi::kCGErrorSuccess - ); - }; - - // Restore the normal window level following the Borderless fullscreen - // `CGShieldingWindowLevel() + 1` hack. - self.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 we're in fullscreen mode, we wait to apply decoration changes - // until we're in `window_did_exit_fullscreen`. - if fullscreen { - return; - } - - let new_mask = { - let mut new_mask = if decorations { - NSWindowStyleMaskClosable - | NSWindowStyleMaskMiniaturizable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskTitled - } else { - NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable - }; - if !resizable { - new_mask &= !NSWindowStyleMaskResizable; - } - 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.setLevel(level); - } - - #[inline] - pub fn set_window_icon(&self, _icon: Option) { - // macOS doesn't have window icons. Though, there is - // `setRepresentedFilename`, but that's semantically distinct and should - // only be used when the window is in some way representing a specific - // file/directory. For instance, Terminal.app uses this for the CWD. - // Anyway, that should eventually be implemented as - // `WindowBuilderExt::with_represented_file` or something, and doesn't - // have anything to do with `set_window_icon`. - // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html - } - - #[inline] - pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { - 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); - } - - #[inline] - pub fn set_ime_allowed(&self, allowed: bool) { - self.view().set_ime_allowed(allowed); - } - - #[inline] - pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} - - #[inline] - pub fn focus_window(&self) { - let mtm = MainThreadMarker::from(self); - let is_minimized = self.isMiniaturized(); - let is_visible = self.isVisible(); - - if !is_minimized && is_visible { - #[allow(deprecated)] - NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); - self.makeKeyAndOrderFront(None); - } - } - - #[inline] - pub fn request_user_attention(&self, request_type: Option) { - let mtm = MainThreadMarker::from(self); - let ns_request_type = request_type.map(|ty| match ty { - UserAttentionType::Critical => NSCriticalRequest, - UserAttentionType::Informational => NSInformationalRequest, - }); - if let Some(ty) = ns_request_type { - NSApplication::sharedApplication(mtm).requestUserAttention(ty); - } - } - - #[inline] - // Allow directly accessing the current monitor internally without unwrapping. - pub(crate) fn current_monitor_inner(&self) -> Option { - let display_id = get_display_id(&*self.screen()?); - Some(MonitorHandle::new(display_id)) - } - - #[inline] - pub fn current_monitor(&self) -> Option { - self.current_monitor_inner() - } - - #[inline] - pub fn available_monitors(&self) -> VecDeque { - monitor::available_monitors() - } - - #[inline] - pub fn primary_monitor(&self) -> Option { - let monitor = monitor::primary_monitor(); - Some(monitor) - } - - #[cfg(feature = "rwh_04")] - #[inline] - pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { - let mut window_handle = rwh_04::AppKitHandle::empty(); - window_handle.ns_window = self as *const Self as *mut _; - window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _; - rwh_04::RawWindowHandle::AppKit(window_handle) - } - - #[cfg(feature = "rwh_05")] - #[inline] - pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { - let mut window_handle = rwh_05::AppKitWindowHandle::empty(); - window_handle.ns_window = self as *const Self as *mut _; - window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _; - rwh_05::RawWindowHandle::AppKit(window_handle) - } - - #[cfg(feature = "rwh_05")] - #[inline] - pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { - rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) - } - - #[cfg(feature = "rwh_06")] - #[inline] - pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { - let window_handle = rwh_06::AppKitWindowHandle::new({ - let ptr = Id::as_ptr(&self.contentView().unwrap()) as *mut _; - std::ptr::NonNull::new(ptr).expect("Id should never be null") - }); - rwh_06::RawWindowHandle::AppKit(window_handle) - } - - fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) { - let current_style_mask = self.styleMask(); - if on { - self.set_style_mask(current_style_mask | mask); - } else { - self.set_style_mask(current_style_mask & (!mask)); - } - } - - #[inline] - pub fn theme(&self) -> Option { - self.ivars().current_theme.get() - } - - #[inline] - pub fn has_focus(&self) -> bool { - self.isKeyWindow() - } - - pub fn set_theme(&self, theme: Option) { - let mtm = MainThreadMarker::from(self); - set_ns_theme(theme, mtm); - self.ivars() - .current_theme - .set(theme.or_else(|| Some(get_ns_theme(mtm)))); - } - - #[inline] - pub fn set_content_protected(&self, protected: bool) { - self.setSharingType(if protected { - NSWindowSharingNone - } else { - NSWindowSharingReadOnly - }) - } - - pub fn title(&self) -> String { - let window: &NSWindow = self; - window.title().to_string() - } - - pub fn reset_dead_keys(&self) { - // (Artur) I couldn't find a way to implement this. - } } - -impl WindowExtMacOS for WinitWindow { - #[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(); - - // Do nothing if native fullscreen is active. - if is_native_fullscreen - || (fullscreen && is_simple_fullscreen) - || (!fullscreen && !is_simple_fullscreen) - { - return false; - } - - if fullscreen { - // Remember the original window's settings - // Exclude title bar - self.ivars() - .standard_frame - .set(Some(self.contentRectForFrameRect(self.frame()))); - self.ivars().saved_style.set(Some(self.styleMask())); - self.ivars() - .save_presentation_opts - .set(Some(app.presentationOptions())); - - // Tell our window's state that we're in fullscreen - self.ivars().is_simple_fullscreen.set(true); - - // Simulate pre-Lion fullscreen by hiding the dock and menu bar - let presentation_options = - NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar; - app.setPresentationOptions(presentation_options); - - // Hide the titlebar - self.toggle_style_mask(NSWindowStyleMaskTitled, false); - - // Set the window frame to the screen frame size - let screen = self.screen().expect("expected screen to be available"); - self.setFrame_display(screen.frame(), true); - - // Fullscreen windows can't be resized, minimized, or moved - self.toggle_style_mask(NSWindowStyleMaskMiniaturizable, false); - self.toggle_style_mask(NSWindowStyleMaskResizable, false); - self.setMovable(false); - - true - } else { - let new_mask = self.saved_style(); - self.set_style_mask(new_mask); - 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); - } - - self.setFrame_display(frame, true); - self.setMovable(true); - - true - } - } - - #[inline] - fn has_shadow(&self) -> bool { - self.hasShadow() - } - - #[inline] - fn set_has_shadow(&self, has_shadow: bool) { - self.setHasShadow(has_shadow) - } - - #[inline] - fn set_tabbing_identifier(&self, identifier: &str) { - self.setTabbingIdentifier(&NSString::from_str(identifier)) - } - - #[inline] - fn tabbing_identifier(&self) -> String { - self.tabbingIdentifier().to_string() - } - - #[inline] - fn select_next_tab(&self) { - self.selectNextTab(None) - } - - #[inline] - fn select_previous_tab(&self) { - unsafe { self.selectPreviousTab(None) } - } - - #[inline] - fn select_tab_at_index(&self, index: usize) { - if let Some(group) = self.tabGroup() { - if let Some(windows) = unsafe { self.tabbedWindows() } { - if index < windows.len() { - group.setSelectedWindow(Some(&windows[index])); - } - } - } - } - - #[inline] - fn num_tabs(&self) -> usize { - unsafe { self.tabbedWindows() } - .map(|windows| windows.len()) - .unwrap_or(1) - } - - fn is_document_edited(&self) -> bool { - self.isDocumentEdited() - } - - fn set_document_edited(&self, edited: bool) { - self.setDocumentEdited(edited) - } - - fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { - self.ivars().option_as_alt.set(option_as_alt); - } - - fn option_as_alt(&self) -> OptionAsAlt { - self.ivars().option_as_alt.get() - } -} - -fn mask_contains(mask: NSWindowStyleMask, value: NSWindowStyleMask) -> bool { - mask & value == value -} - -pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme { - let app = NSApplication::sharedApplication(mtm); - let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] }; - if !has_theme { - return Theme::Light; - } - let appearance = app.effectiveAppearance(); - let name = appearance - .bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ - NSString::from_str("NSAppearanceNameAqua"), - NSString::from_str("NSAppearanceNameDarkAqua"), - ])) - .unwrap(); - match &*name.to_string() { - "NSAppearanceNameDarkAqua" => Theme::Dark, - _ => Theme::Light, - } -} - -fn set_ns_theme(theme: Option, mtm: MainThreadMarker) { - let app = NSApplication::sharedApplication(mtm); - let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] }; - if has_theme { - let appearance = theme.map(|t| { - let name = match t { - Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"), - Theme::Light => NSString::from_str("NSAppearanceNameAqua"), - }; - NSAppearance::appearanceNamed(&name).unwrap() - }); - app.setAppearance(appearance.as_ref().map(|a| a.as_ref())); - } -} - -const DEFAULT_STANDARD_FRAME: NSRect = - NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index fd89a8bd..e70718b2 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -1,15 +1,27 @@ #![allow(clippy::unnecessary_cast)] -use std::cell::Cell; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; use std::ptr; +use core_graphics::display::{CGDisplay, CGPoint}; use icrate::AppKit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication, + NSApplicationPresentationAutoHideDock, NSApplicationPresentationAutoHideMenuBar, NSApplicationPresentationFullScreen, NSApplicationPresentationHideDock, - NSApplicationPresentationHideMenuBar, NSApplicationPresentationOptions, NSDraggingDestination, - NSFilenamesPboardType, NSPasteboard, NSWindowDelegate, NSWindowOcclusionStateVisible, + NSApplicationPresentationHideMenuBar, NSApplicationPresentationOptions, NSBackingStoreBuffered, + NSColor, NSCriticalRequest, NSDraggingDestination, NSFilenamesPboardType, + NSInformationalRequest, NSPasteboard, NSScreen, NSView, NSWindowAbove, NSWindowCloseButton, + NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel, NSWindowMiniaturizeButton, + NSWindowOcclusionStateVisible, NSWindowSharingNone, NSWindowSharingReadOnly, NSWindowStyleMask, + NSWindowStyleMaskBorderless, NSWindowStyleMaskClosable, NSWindowStyleMaskFullSizeContentView, + NSWindowStyleMaskMiniaturizable, NSWindowStyleMaskResizable, NSWindowStyleMaskTitled, + NSWindowTabbingModePreferred, NSWindowTitleHidden, NSWindowZoomButton, }; use icrate::Foundation::{ - MainThreadMarker, NSArray, NSObject, NSObjectProtocol, NSPoint, NSSize, NSString, + CGFloat, MainThreadMarker, NSArray, NSCopying, NSObject, NSObjectProtocol, NSPoint, NSRect, + NSSize, NSString, }; +use monitor::VideoModeHandle; use objc2::rc::{autoreleasepool, Id}; use objc2::runtime::{AnyObject, ProtocolObject}; use objc2::{ @@ -17,23 +29,59 @@ use objc2::{ }; use super::app_delegate::ApplicationDelegate; -use super::monitor::flip_window_screen_coordinates; -use super::{ - window::{get_ns_theme, WinitWindow}, - Fullscreen, -}; -use crate::{ - dpi::{LogicalPosition, LogicalSize}, - event::WindowEvent, +use super::cursor::cursor_from_icon; +use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; +use super::view::WinitView; +use super::window::WinitWindow; +use super::{ffi, Fullscreen, MonitorHandle, OsError, WindowId}; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::WindowEvent; +use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; +use crate::window::{ + Cursor, CursorGrabMode, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, }; +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub movable_by_window_background: bool, + pub titlebar_transparent: bool, + pub title_hidden: bool, + pub titlebar_hidden: bool, + pub titlebar_buttons_hidden: bool, + pub fullsize_content_view: bool, + pub disallow_hidpi: bool, + pub has_shadow: bool, + pub accepts_first_mouse: bool, + pub tabbing_identifier: Option, + pub option_as_alt: OptionAsAlt, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + #[inline] + fn default() -> Self { + Self { + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + disallow_hidpi: false, + has_shadow: true, + accepts_first_mouse: true, + tabbing_identifier: None, + option_as_alt: Default::default(), + } + } +} + #[derive(Debug)] pub(crate) struct State { window: Id, - // This is set when WindowBuilder::with_fullscreen was set, - // see comments of `window_did_fail_to_enter_fullscreen` - initial_fullscreen: Cell, + current_theme: Cell>, // During `windowDidResize`, we use this to only send Moved if the position changed. // @@ -42,24 +90,54 @@ pub(crate) struct State { // Used to prevent redundant events. previous_scale_factor: Cell, + + /// The current resize increments for the window content. + resize_increments: Cell, + /// Whether the window is showing decorations. + decorations: Cell, + resizable: Cell, + maximized: Cell, + + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it. Also used when transitioning from Borderless to + /// Exclusive fullscreen in `set_fullscreen` because we need to disable the menu + /// bar in exclusive fullscreen but want to restore the original options when + /// transitioning back to borderless fullscreen. + save_presentation_opts: Cell>, + // This is set when WindowBuilder::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: Cell, + /// This field tracks the current fullscreen state of the window + /// (as seen by `WindowDelegate`). + fullscreen: RefCell>, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + target_fullscreen: RefCell>>, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + in_fullscreen_transition: Cell, + standard_frame: Cell>, + is_simple_fullscreen: Cell, + saved_style: Cell>, } declare_class!( - pub(crate) struct WinitWindowDelegate; + pub(crate) struct WindowDelegate; - unsafe impl ClassType for WinitWindowDelegate { + unsafe impl ClassType for WindowDelegate { type Super = NSObject; type Mutability = mutability::MainThreadOnly; const NAME: &'static str = "WinitWindowDelegate"; } - impl DeclaredClass for WinitWindowDelegate { + impl DeclaredClass for WindowDelegate { type Ivars = State; } - unsafe impl NSObjectProtocol for WinitWindowDelegate {} + unsafe impl NSObjectProtocol for WindowDelegate {} - unsafe impl NSWindowDelegate for WinitWindowDelegate { + unsafe impl NSWindowDelegate for WindowDelegate { #[method(windowShouldClose:)] fn window_should_close(&self, _: Option<&AnyObject>) -> bool { trace_scope!("windowShouldClose:"); @@ -74,7 +152,7 @@ declare_class!( autoreleasepool(|_| { // Since El Capitan, we need to be careful that delegate methods can't // be called after the window closes. - self.ivars().window.setDelegate(None); + self.window().setDelegate(None); }); self.queue_event(WindowEvent::Destroyed); } @@ -90,16 +168,14 @@ declare_class!( fn window_will_start_live_resize(&self, _: Option<&AnyObject>) { trace_scope!("windowWillStartLiveResize:"); - let increments = self.ivars().window.ivars().resize_increments.get(); - self.ivars().window.set_resize_increments_inner(increments); + let increments = self.ivars().resize_increments.get(); + self.set_resize_increments_inner(increments); } #[method(windowDidEndLiveResize:)] fn window_did_end_live_resize(&self, _: Option<&AnyObject>) { trace_scope!("windowDidEndLiveResize:"); - self.ivars() - .window - .set_resize_increments_inner(NSSize::new(1., 1.)); + self.set_resize_increments_inner(NSSize::new(1., 1.)); } // This won't be triggered if the move was part of a resize. @@ -133,7 +209,7 @@ declare_class!( // NSWindowDelegate, and as a result a tracked modifiers state can quite // easily fall out of synchrony with reality. This requires us to emit // a synthetic ModifiersChanged event when we lose focus. - self.ivars().window.view().reset_modifiers(); + self.view().reset_modifiers(); self.queue_event(WindowEvent::Focused(false)); } @@ -143,9 +219,8 @@ declare_class!( fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowWillEnterFullScreen:"); - let window_ivars = self.ivars().window.ivars(); - window_ivars.maximized.set(self.ivars().window.is_zoomed()); - let mut fullscreen = window_ivars.fullscreen.borrow_mut(); + self.ivars().maximized.set(self.is_zoomed()); + let mut fullscreen = self.ivars().fullscreen.borrow_mut(); match &*fullscreen { // Exclusive mode sets the state in `set_fullscreen` as the user // can't enter exclusive mode by other means (like the @@ -158,11 +233,11 @@ declare_class!( // Otherwise, we must've reached fullscreen by the user clicking // on the green fullscreen button. Update state! None => { - let current_monitor = self.ivars().window.current_monitor_inner(); + let current_monitor = self.current_monitor_inner(); *fullscreen = Some(Fullscreen::Borderless(current_monitor)); } } - window_ivars.in_fullscreen_transition.set(true); + self.ivars().in_fullscreen_transition.set(true); } /// Invoked when before exit fullscreen @@ -170,11 +245,7 @@ declare_class!( fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowWillExitFullScreen:"); - self.ivars() - .window - .ivars() - .in_fullscreen_transition - .set(true); + self.ivars().in_fullscreen_transition.set(true); } #[method(window:willUseFullScreenPresentationOptions:)] @@ -193,7 +264,7 @@ declare_class!( // we don't, for consistency. If we do, it should be documented that the // user-provided options are ignored in exclusive fullscreen. let mut options = proposed_options; - let fullscreen = self.ivars().window.ivars().fullscreen.borrow(); + let fullscreen = self.ivars().fullscreen.borrow(); if let Some(Fullscreen::Exclusive(_)) = &*fullscreen { options = NSApplicationPresentationFullScreen | NSApplicationPresentationHideDock @@ -208,10 +279,9 @@ declare_class!( fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidEnterFullScreen:"); self.ivars().initial_fullscreen.set(false); - let window_ivars = self.ivars().window.ivars(); - window_ivars.in_fullscreen_transition.set(false); - if let Some(target_fullscreen) = window_ivars.target_fullscreen.take() { - self.ivars().window.set_fullscreen(target_fullscreen); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); } } @@ -220,11 +290,10 @@ declare_class!( fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidExitFullScreen:"); - self.ivars().window.restore_state_from_fullscreen(); - let window_ivars = self.ivars().window.ivars(); - window_ivars.in_fullscreen_transition.set(false); - if let Some(target_fullscreen) = window_ivars.target_fullscreen.take() { - self.ivars().window.set_fullscreen(target_fullscreen); + 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); } } @@ -247,21 +316,20 @@ declare_class!( #[method(windowDidFailToEnterFullScreen:)] fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidFailToEnterFullScreen:"); - let window_ivars = self.ivars().window.ivars(); - window_ivars.in_fullscreen_transition.set(false); - window_ivars.target_fullscreen.replace(None); + self.ivars().in_fullscreen_transition.set(false); + self.ivars().target_fullscreen.replace(None); if self.ivars().initial_fullscreen.get() { #[allow(clippy::let_unit_value)] unsafe { let _: () = msg_send![ - &*self.ivars().window, + self.window(), performSelector: sel!(toggleFullScreen:), withObject: ptr::null::(), afterDelay: 0.5, ]; }; } else { - self.ivars().window.restore_state_from_fullscreen(); + self.restore_state_from_fullscreen(); } } @@ -269,7 +337,7 @@ declare_class!( #[method(windowDidChangeOcclusionState:)] fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeOcclusionState:"); - let visible = self.ivars().window.occlusionState() & NSWindowOcclusionStateVisible + let visible = self.window().occlusionState() & NSWindowOcclusionStateVisible == NSWindowOcclusionStateVisible; self.queue_event(WindowEvent::Occluded(!visible)); } @@ -277,16 +345,16 @@ declare_class!( #[method(windowDidChangeScreen:)] fn window_did_change_screen(&self, _: Option<&AnyObject>) { trace_scope!("windowDidChangeScreen:"); - let is_simple_fullscreen = self.ivars().window.ivars().is_simple_fullscreen.get(); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); if is_simple_fullscreen { - if let Some(screen) = self.ivars().window.screen() { - self.ivars().window.setFrame_display(screen.frame(), true); + if let Some(screen) = self.window().screen() { + self.window().setFrame_display(screen.frame(), true); } } } } - unsafe impl NSDraggingDestination for WinitWindowDelegate { + unsafe impl NSDraggingDestination for WindowDelegate { /// Invoked when the dragged image enters destination bounds or frame #[method(draggingEntered:)] fn dragging_entered(&self, sender: &NSObject) -> bool { @@ -350,14 +418,14 @@ declare_class!( } } - unsafe impl WinitWindowDelegate { + unsafe impl WindowDelegate { // Observe theme change #[method(effectiveAppearanceDidChange:)] - fn effective_appearance_did_change(&self, sender: Option<&AnyObject>) { - trace_scope!("Triggered `effectiveAppearanceDidChange:`"); + fn effective_appearance_did_change(this: *mut Self, sender: Option<&AnyObject>) { + trace_scope!("effectiveAppearanceDidChange:"); unsafe { msg_send![ - self, + this, performSelectorOnMainThread: sel!(effectiveAppearanceDidChangedOnMainThread:), withObject: sender, waitUntilDone: false, @@ -369,12 +437,7 @@ declare_class!( fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&AnyObject>) { let mtm = MainThreadMarker::from(self); let theme = get_ns_theme(mtm); - let old_theme = self - .ivars() - .window - .ivars() - .current_theme - .replace(Some(theme)); + let old_theme = self.ivars().current_theme.replace(Some(theme)); if old_theme != Some(theme) { self.queue_event(WindowEvent::ThemeChanged(theme)); } @@ -382,22 +445,255 @@ declare_class!( } ); -impl WinitWindowDelegate { - pub fn new(window: &WinitWindow, initial_fullscreen: bool) -> Id { - let mtm = MainThreadMarker::from(window); - let scale_factor = window.scale_factor(); - let this = mtm.alloc().set_ivars(State { +fn new_window( + attrs: &WindowAttributes, + pl_attrs: &PlatformSpecificWindowBuilderAttributes, + mtm: MainThreadMarker, +) -> Option> { + autoreleasepool(|_| { + let screen = match attrs.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Borderless(Some(monitor))) + | Some(Fullscreen::Exclusive(VideoModeHandle { monitor, .. })) => { + monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm)) + } + Some(Fullscreen::Borderless(None)) => 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.inner_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 { + Some(position) => { + let position = position.to_logical(scale_factor); + flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + size, + )) + } + // This value is ignored by calling win.center() below + None => NSPoint::new(0.0, 0.0), + }; + NSRect::new(position, size) + } + }; + + let mut masks = if (!attrs.decorations && screen.is_none()) || pl_attrs.titlebar_hidden { + // Resizable without a titlebar or borders + // if decorations is set to false, ignore pl_attrs + // + // if the titlebar is hidden, ignore other pl_attrs + NSWindowStyleMaskBorderless + | NSWindowStyleMaskResizable + | NSWindowStyleMaskMiniaturizable + } else { + // default case, resizable window with titlebar and titlebar buttons + NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskTitled + }; + + if !attrs.resizable { + masks &= !NSWindowStyleMaskResizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) { + masks &= !NSWindowStyleMaskMiniaturizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) { + masks &= !NSWindowStyleMaskClosable; + } + + if pl_attrs.fullsize_content_view { + masks |= NSWindowStyleMaskFullSizeContentView; + } + + let window: Option> = unsafe { + msg_send_id![ + super(mtm.alloc().set_ivars(())), + initWithContentRect: frame, + styleMask: masks, + backing: NSBackingStoreBuffered, + defer: false, + ] + }; + let window = window?; + + // It is very important for correct memory management that we + // disable the extra release that would otherwise happen when + // calling `close` on the window. + unsafe { window.setReleasedWhenClosed(false) }; + + window.setTitle(&NSString::from_str(&attrs.title)); + window.setAcceptsMouseMovedEvents(true); + + if let Some(identifier) = &pl_attrs.tabbing_identifier { + window.setTabbingIdentifier(&NSString::from_str(identifier)); + window.setTabbingMode(NSWindowTabbingModePreferred); + } + + if attrs.content_protected { + window.setSharingType(NSWindowSharingNone); + } + + if pl_attrs.titlebar_transparent { + window.setTitlebarAppearsTransparent(true); + } + if pl_attrs.title_hidden { + window.setTitleVisibility(NSWindowTitleHidden); + } + if pl_attrs.titlebar_buttons_hidden { + for titlebar_button in &[ + #[allow(deprecated)] + NSWindowFullScreenButton, + NSWindowMiniaturizeButton, + NSWindowCloseButton, + NSWindowZoomButton, + ] { + if let Some(button) = window.standardWindowButton(*titlebar_button) { + button.setHidden(true); + } + } + } + if pl_attrs.movable_by_window_background { + window.setMovableByWindowBackground(true); + } + + if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { + if let Some(button) = window.standardWindowButton(NSWindowZoomButton) { + button.setEnabled(false); + } + } + + if !pl_attrs.has_shadow { + window.setHasShadow(false); + } + if attrs.position.is_none() { + window.center(); + } + + let view = WinitView::new( + &window, + pl_attrs.accepts_first_mouse, + pl_attrs.option_as_alt, + ); + + // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until + // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid + // always the default system value in favour of the user's code + #[allow(deprecated)] + view.setWantsBestResolutionOpenGLSurface(!pl_attrs.disallow_hidpi); + + // On Mojave, views automatically become layer-backed shortly after being added to + // a window. Changing the layer-backedness of a view breaks the association between + // the view and its associated OpenGL context. To work around this, on Mojave we + // explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 { + view.setWantsLayer(true); + } + + // Configure the new view as the "key view" for the window + window.setContentView(Some(&view)); + window.setInitialFirstResponder(Some(&view)); + + if attrs.transparent { + window.setOpaque(false); + window.setBackgroundColor(Some(unsafe { &NSColor::clearColor() })); + } + + // register for drag and drop operations. + window + .registerForDraggedTypes(&NSArray::from_id_slice(&[ + unsafe { NSFilenamesPboardType }.copy() + ])); + + Some(window) + }) +} + +impl WindowDelegate { + pub fn new( + attrs: WindowAttributes, + pl_attrs: PlatformSpecificWindowBuilderAttributes, + mtm: MainThreadMarker, + ) -> Result, RootOsError> { + let window = new_window(&attrs, &pl_attrs, mtm) + .ok_or_else(|| os_error!(OsError::CreationError("couldn't create `NSWindow`")))?; + + #[cfg(feature = "rwh_06")] + match attrs.parent_window.map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::AppKit(handle)) => { + // SAFETY: Caller ensures the pointer is valid or NULL + // Unwrap is fine, since the pointer comes from `NonNull`. + let parent_view: Id = + unsafe { Id::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); + let parent = parent_view.window().ok_or_else(|| { + os_error!(OsError::CreationError( + "parent view should be installed in a window" + )) + })?; + + // SAFETY: We know that there are no parent -> child -> parent cycles since the only place in `winit` + // where we allow making a window a child window is right here, just after it's been created. + unsafe { parent.addChildWindow_ordered(&window, NSWindowAbove) }; + } + Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), + None => (), + } + + let resize_increments = match attrs + .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 _; + + let current_theme = match attrs.preferred_theme { + Some(theme) => Some(theme), + None => Some(get_ns_theme(mtm)), + }; + + let delegate = mtm.alloc().set_ivars(State { window: window.retain(), - initial_fullscreen: Cell::new(initial_fullscreen), + current_theme: Cell::new(current_theme), previous_position: Cell::new(None), previous_scale_factor: Cell::new(scale_factor), + resize_increments: Cell::new(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), }); - let this: Id = unsafe { msg_send_id![super(this), init] }; + let delegate: Id = unsafe { msg_send_id![super(delegate), init] }; if scale_factor != 1.0 { - this.queue_static_scale_factor_changed_event(); + delegate.queue_static_scale_factor_changed_event(); } - window.setDelegate(Some(ProtocolObject::from_ref(&*this))); + window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); // Enable theme change event let notification_center: Id = @@ -406,43 +702,98 @@ impl WinitWindowDelegate { let _: () = unsafe { msg_send![ ¬ification_center, - addObserver: &*this - selector: sel!(effectiveAppearanceDidChange:) - name: &*notification_name - object: ptr::null::() + addObserver: &*delegate, + selector: sel!(effectiveAppearanceDidChange:), + name: &*notification_name, + object: ptr::null::(), ] }; - this + if attrs.blur { + delegate.set_blur(attrs.blur); + } + + if let Some(dim) = attrs.min_inner_size { + delegate.set_min_inner_size(Some(dim)); + } + if let Some(dim) = attrs.max_inner_size { + delegate.set_max_inner_size(Some(dim)); + } + + delegate.set_window_level(attrs.window_level); + + delegate.set_cursor(attrs.cursor); + + // XXX Send `Focused(false)` right after creating the window delegate, so we won't + // obscure the real focused events on the startup. + delegate.queue_event(WindowEvent::Focused(false)); + + // Set fullscreen mode after we setup everything + delegate.set_fullscreen(attrs.fullscreen.map(Into::into)); + + // Setting the window as key has to happen *after* we set the fullscreen + // state, since otherwise we'll briefly see the window at normal size + // before it transitions. + if attrs.visible { + if attrs.active { + // Tightly linked with `app_state::window_activation_hack` + window.makeKeyAndOrderFront(None); + } else { + window.orderFront(None); + } + } + + if attrs.maximized { + delegate.set_maximized(attrs.maximized); + } + + Ok(delegate) + } + + #[track_caller] + pub(super) fn view(&self) -> Id { + // SAFETY: The view inside WinitWindow is always `WinitView` + unsafe { Id::cast(self.window().contentView().unwrap()) } + } + + #[track_caller] + pub(super) fn window(&self) -> &WinitWindow { + &self.ivars().window + } + + #[track_caller] + pub(crate) fn id(&self) -> WindowId { + self.window().id() } pub(crate) fn queue_event(&self, event: WindowEvent) { let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); - app_delegate.queue_window_event(self.ivars().window.id(), event); + app_delegate.queue_window_event(self.window().id(), event); } fn queue_static_scale_factor_changed_event(&self) { - let window = &self.ivars().window; - let scale_factor = window.scale_factor(); + 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 content_size = window.contentRectForFrameRect(window.frame()).size; + let content_size = self + .window() + .contentRectForFrameRect(self.window().frame()) + .size; let content_size = LogicalSize::new(content_size.width, content_size.height); let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); app_delegate.queue_static_scale_factor_changed_event( - window.clone(), + self.window().retain(), content_size.to_physical(scale_factor), scale_factor, ); } fn emit_move_event(&self) { - let window = &self.ivars().window; - let frame = window.frame(); + let frame = self.window().frame(); if self.ivars().previous_position.get() == Some(frame.origin) { return; } @@ -450,7 +801,1040 @@ impl WinitWindowDelegate { let position = flip_window_screen_coordinates(frame); let position = - LogicalPosition::new(position.x, position.y).to_physical(window.scale_factor()); + 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); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + 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) + } + + pub fn set_blur(&self, blur: bool) { + // NOTE: in general we want to specify the blur radius, but the choice of 80 + // should be a reasonable default. + let radius = if blur { 80 } else { 0 }; + let window_number = unsafe { self.window().windowNumber() }; + unsafe { + ffi::CGSSetWindowBackgroundBlurRadius( + ffi::CGSMainConnectionID(), + window_number, + radius, + ); + } + } + + 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 { + Some(self.window().isVisible()) + } + + pub fn request_redraw(&self) { + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_redraw(self.window().id()); + } + + #[inline] + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let position = flip_window_screen_coordinates(self.window().frame()); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let position = flip_window_screen_coordinates(content_rect); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical(self.scale_factor()); + let point = flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + self.window().frame().size, + )); + unsafe { self.window().setFrameOrigin(point) }; + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let frame = self.window().frame(); + let logical = LogicalSize::new(frame.size.width, frame.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + 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_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: 0.0, + height: 0.0, + })); + let min_size = dimensions.to_logical::(self.scale_factor()); + + let min_size = NSSize::new(min_size.width, min_size.height); + unsafe { self.window().setContentMinSize(min_size) }; + + // If necessary, resize the window to match constraint + 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_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: std::f32::MAX as f64, + height: std::f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + let max_size = dimensions.to_logical::(scale_factor); + + let max_size = NSSize::new(max_size.width, max_size.height); + unsafe { self.window().setContentMaxSize(max_size) }; + + // If necessary, resize the window to match constraint + 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 resize_increments(&self) -> Option> { + let increments = self.ivars().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_resize_increments(&self, increments: Option) { + // XXX the resize increments are only used during live resizes. + self.ivars().resize_increments.set( + increments + .map(|increments| { + let logical = increments.to_logical::(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) { + // It was concluded (#2411) that there is never a use-case for + // "outer" resize increments, hence we set "inner" ones here. + // ("outer" in macOS being just resizeIncrements, and "inner" - contentResizeIncrements) + // This is consistent with X11 size hints behavior + 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 |= NSWindowStyleMaskResizable; + } else { + mask &= !NSWindowStyleMaskResizable; + } + self.set_style_mask(mask); + } + // Otherwise, we don't change the mask until we exit fullscreen. + } + + #[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 |= NSWindowStyleMaskClosable; + } else { + mask &= !NSWindowStyleMaskClosable; + } + + if buttons.contains(WindowButtons::MINIMIZE) { + mask |= NSWindowStyleMaskMiniaturizable; + } else { + mask &= !NSWindowStyleMaskMiniaturizable; + } + + // This must happen before the button's "enabled" status has been set, + // hence we do it synchronously. + self.set_style_mask(mask); + + // We edit the button directly instead of using `NSResizableWindowMask`, + // since that mask also affect the resizability of the window (which is + // controllable by other means in `winit`). + if let Some(button) = self.window().standardWindowButton(NSWindowZoomButton) { + 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(NSWindowZoomButton) + .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) => cursor.inner.0, + }; + + 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<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) + .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) + } + + #[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<(), ExternalError> { + let physical_window_position = self.inner_position().unwrap(); + let scale_factor = self.scale_factor(); + let window_position = physical_window_position.to_logical::(scale_factor); + let logical_cursor_position = cursor_position.to_logical::(scale_factor); + let point = CGPoint { + x: logical_cursor_position.x + window_position.x, + y: logical_cursor_position.y + window_position.y, + }; + CGDisplay::warp_mouse_cursor_position(point) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + CGDisplay::associate_mouse_and_mouse_cursor_position(true) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + + Ok(()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let mtm = MainThreadMarker::from(self); + let event = NSApplication::sharedApplication(mtm) + .currentEvent() + .unwrap(); + self.window().performWindowDragWithEvent(&event); + Ok(()) + } + + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.window().setIgnoresMouseEvents(!hittest); + Ok(()) + } + + pub(crate) fn is_zoomed(&self) -> bool { + // because `isZoomed` doesn't work if the window's borderless, + // we make it resizable temporalily. + let curr_mask = self.window().styleMask(); + + let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; + let needs_temp_mask = !mask_contains(curr_mask, required); + if needs_temp_mask { + self.set_style_mask(required); + } + + let is_zoomed = self.window().isZoomed(); + + // Roll back temp styles + 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 | NSWindowStyleMaskResizable + } else { + base_mask & !NSWindowStyleMaskResizable + } + } + + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` + 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 { + unsafe { self.window().deminiaturize(Some(self)) }; + } + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(self.window().isMiniaturized()) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let mtm = MainThreadMarker::from(self); + let is_zoomed = self.is_zoomed(); + if is_zoomed == maximized { + return; + }; + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + self.ivars().standard_frame.set(Some(self.window().frame())); + } + + self.ivars().maximized.set(maximized); + + if self.ivars().fullscreen.borrow().is_some() { + // Handle it in window_did_exit_fullscreen + return; + } + + if mask_contains(self.window().styleMask(), NSWindowStyleMaskResizable) { + // Just use the native zoom if resizable + self.window().zoom(None); + } else { + // if it's not resizable, we set the frame directly + 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 { + self.ivars().fullscreen.borrow().clone() + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if self.ivars().is_simple_fullscreen.get() { + return; + } + if self.ivars().in_fullscreen_transition.get() { + // We can't set fullscreen here. + // Set fullscreen after transition. + self.ivars().target_fullscreen.replace(Some(fullscreen)); + return; + } + let old_fullscreen = self.ivars().fullscreen.borrow().clone(); + if fullscreen == old_fullscreen { + return; + } + + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // does not take a screen parameter, but uses the current screen) + if let Some(ref fullscreen) = fullscreen { + let new_screen = match fullscreen { + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => { + if let Some(monitor) = self.current_monitor_inner() { + monitor + } else { + return; + } + } + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + } + .ns_screen(mtm) + .unwrap(); + + let old_screen = self.window().screen().unwrap(); + if old_screen != new_screen { + unsafe { self.window().setFrameOrigin(new_screen.frame().origin) }; + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { + self.ivars() + .save_presentation_opts + .replace(Some(app.presentationOptions())); + } + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) + == ffi::kCGErrorSuccess + { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + self.ivars().fullscreen.replace(fullscreen.clone()); + + fn toggle_fullscreen(window: &WinitWindow) { + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + window.setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + window.toggleFullScreen(None); + } + + match (old_fullscreen, fullscreen) { + (None, Some(_)) => { + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + let curr_mask = self.window().styleMask(); + let required = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; + if !mask_contains(curr_mask, required) { + self.set_style_mask(required); + self.ivars().saved_style.set(Some(curr_mask)); + } + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Borderless(_)), None) => { + // State is restored by `window_did_exit_fullscreen` + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Exclusive(ref video_mode)), None) => { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), + ffi::kCGErrorSuccess + ); + }; + toggle_fullscreen(self.window()); + } + (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + self.ivars() + .save_presentation_opts + .set(Some(app.presentationOptions())); + + let presentation_options = NSApplicationPresentationFullScreen + | NSApplicationPresentationHideDock + | NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + + let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1; + self.window().setLevel(window_level); + } + (Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => { + let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or( + NSApplicationPresentationFullScreen + | NSApplicationPresentationAutoHideDock + | NSApplicationPresentationAutoHideMenuBar, + ); + app.setPresentationOptions(presentation_options); + + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(video_mode.monitor().native_identifier()), + ffi::kCGErrorSuccess + ); + }; + + // Restore the normal window level following the Borderless fullscreen + // `CGShieldingWindowLevel() + 1` hack. + self.window() + .setLevel(ffi::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 we're in fullscreen mode, we wait to apply decoration changes + // until we're in `window_did_exit_fullscreen`. + if fullscreen { + return; + } + + let new_mask = { + let mut new_mask = if decorations { + NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskTitled + } else { + NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable + }; + if !resizable { + new_mask &= !NSWindowStyleMaskResizable; + } + 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 => ffi::kCGFloatingWindowLevel as NSWindowLevel, + WindowLevel::AlwaysOnBottom => (ffi::kCGNormalWindowLevel - 1) as NSWindowLevel, + WindowLevel::Normal => ffi::kCGNormalWindowLevel as NSWindowLevel, + }; + self.window().setLevel(level); + } + + #[inline] + pub fn set_window_icon(&self, _icon: Option) { + // macOS doesn't have window icons. Though, there is + // `setRepresentedFilename`, but that's semantically distinct and should + // only be used when the window is in some way representing a specific + // file/directory. For instance, Terminal.app uses this for the CWD. + // Anyway, that should eventually be implemented as + // `WindowBuilderExt::with_represented_file` or something, and doesn't + // have anything to do with `set_window_icon`. + // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + 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); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.view().set_ime_allowed(allowed); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[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) { + let mtm = MainThreadMarker::from(self); + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSCriticalRequest, + UserAttentionType::Informational => NSInformationalRequest, + }); + if let Some(ty) = ns_request_type { + NSApplication::sharedApplication(mtm).requestUserAttention(ty); + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> Option { + let display_id = get_display_id(&*self.window().screen()?); + Some(MonitorHandle::new(display_id)) + } + + #[inline] + pub fn current_monitor(&self) -> Option { + self.current_monitor_inner() + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::AppKitHandle::empty(); + window_handle.ns_window = self.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Id::as_ptr(&self.contentView().unwrap()) as *mut _; + rwh_04::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::AppKitWindowHandle::empty(); + window_handle.ns_window = self.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Id::as_ptr(&self.view()) as *mut _; + rwh_05::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = Id::as_ptr(&self.view()) as *mut _; + std::ptr::NonNull::new(ptr).expect("Id 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 theme(&self) -> Option { + self.ivars().current_theme.get() + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window().isKeyWindow() + } + + pub fn set_theme(&self, theme: Option) { + let mtm = MainThreadMarker::from(self); + set_ns_theme(theme, mtm); + self.ivars() + .current_theme + .set(theme.or_else(|| Some(get_ns_theme(mtm)))); + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + self.window().setSharingType(if protected { + NSWindowSharingNone + } else { + NSWindowSharingReadOnly + }) + } + + pub fn title(&self) -> String { + self.window().title().to_string() + } + + pub fn reset_dead_keys(&self) { + // (Artur) I couldn't find a way to implement this. + } +} + +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(); + + // Do nothing if native fullscreen is active. + if is_native_fullscreen + || (fullscreen && is_simple_fullscreen) + || (!fullscreen && !is_simple_fullscreen) + { + return false; + } + + if fullscreen { + // Remember the original window's settings + // Exclude title bar + 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())); + + // Tell our window's state that we're in fullscreen + self.ivars().is_simple_fullscreen.set(true); + + // Simulate pre-Lion fullscreen by hiding the dock and menu bar + let presentation_options = + NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar; + app.setPresentationOptions(presentation_options); + + // Hide the titlebar + self.toggle_style_mask(NSWindowStyleMaskTitled, false); + + // Set the window frame to the screen frame size + let screen = self + .window() + .screen() + .expect("expected screen to be available"); + self.window().setFrame_display(screen.frame(), true); + + // Fullscreen windows can't be resized, minimized, or moved + self.toggle_style_mask(NSWindowStyleMaskMiniaturizable, false); + self.toggle_style_mask(NSWindowStyleMaskResizable, false); + self.window().setMovable(false); + + true + } else { + let new_mask = self.saved_style(); + self.set_style_mask(new_mask); + 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); + } + + self.window().setFrame_display(frame, true); + self.window().setMovable(true); + + 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) { + unsafe { self.window().selectPreviousTab(None) } + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + if let Some(group) = self.window().tabGroup() { + if let Some(windows) = unsafe { self.window().tabbedWindows() } { + if index < windows.len() { + group.setSelectedWindow(Some(&windows[index])); + } + } + } + } + + #[inline] + fn num_tabs(&self) -> usize { + unsafe { 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 option_as_alt(&self) -> OptionAsAlt { + self.view().option_as_alt() + } +} + +fn mask_contains(mask: NSWindowStyleMask, value: NSWindowStyleMask) -> bool { + mask & value == value +} + +const DEFAULT_STANDARD_FRAME: NSRect = + NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); + +pub(super) fn get_ns_theme(mtm: MainThreadMarker) -> Theme { + let app = NSApplication::sharedApplication(mtm); + let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] }; + if !has_theme { + return Theme::Light; + } + let appearance = app.effectiveAppearance(); + let name = appearance + .bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ + NSString::from_str("NSAppearanceNameAqua"), + NSString::from_str("NSAppearanceNameDarkAqua"), + ])) + .unwrap(); + match &*name.to_string() { + "NSAppearanceNameDarkAqua" => Theme::Dark, + _ => Theme::Light, + } +} + +fn set_ns_theme(theme: Option, mtm: MainThreadMarker) { + let app = NSApplication::sharedApplication(mtm); + let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] }; + if has_theme { + let appearance = theme.map(|t| { + let name = match t { + Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"), + Theme::Light => NSString::from_str("NSAppearanceNameAqua"), + }; + NSAppearance::appearanceNamed(&name).unwrap() + }); + app.setAppearance(appearance.as_ref().map(|a| a.as_ref())); + } }