mirror of
https://github.com/italicsjenga/winit-sonoma-fix.git
synced 2024-12-27 15:41:31 +11:00
d29f7f34aa
* Rename hidpi_factor to scale_factor * Deprecate WINIT_HIDPI_FACTOR environment variable in favor of WINIT_X11_SCALE_FACTOR * Rename HiDpiFactorChanged to DpiChanged and update docs I'm renaming it to DpiChanged instead of ScaleFactorChanged, since I'd like Winit to expose the raw DPI value at some point in the near future, and DpiChanged is a more apt name for that purpose. * Format * Fix macos and ios again * Fix bad macos rebase
1145 lines
43 KiB
Rust
1145 lines
43 KiB
Rust
use raw_window_handle::{macos::MacOSHandle, RawWindowHandle};
|
|
use std::{
|
|
collections::VecDeque,
|
|
f64,
|
|
os::raw::c_void,
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, Mutex, Weak,
|
|
},
|
|
};
|
|
|
|
use crate::{
|
|
dpi::{
|
|
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical,
|
|
},
|
|
error::{ExternalError, NotSupportedError, OsError as RootOsError},
|
|
icon::Icon,
|
|
monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode},
|
|
platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS},
|
|
platform_impl::platform::{
|
|
app_state::AppState,
|
|
ffi,
|
|
monitor::{self, MonitorHandle, VideoMode},
|
|
util::{self, IdRef},
|
|
view::CursorState,
|
|
view::{self, new_view},
|
|
window_delegate::new_delegate,
|
|
OsError,
|
|
},
|
|
window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId},
|
|
};
|
|
use cocoa::{
|
|
appkit::{
|
|
self, CGFloat, NSApp, NSApplication, NSApplicationActivationPolicy,
|
|
NSApplicationPresentationOptions, NSColor, NSRequestUserAttentionType, NSScreen, NSView,
|
|
NSWindow, NSWindowButton, NSWindowStyleMask,
|
|
},
|
|
base::{id, nil},
|
|
foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString},
|
|
};
|
|
use core_graphics::display::{CGDisplay, CGDisplayMode};
|
|
use objc::{
|
|
declare::ClassDecl,
|
|
runtime::{Class, Object, Sel, BOOL, NO, YES},
|
|
};
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct Id(pub usize);
|
|
|
|
impl Id {
|
|
pub unsafe fn dummy() -> Self {
|
|
Id(0)
|
|
}
|
|
}
|
|
|
|
// Convert the `cocoa::base::id` associated with a window to a usize to use as a unique identifier
|
|
// for the window.
|
|
pub fn get_window_id(window_cocoa_id: id) -> Id {
|
|
Id(window_cocoa_id as *const Object as _)
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct PlatformSpecificWindowBuilderAttributes {
|
|
pub activation_policy: ActivationPolicy,
|
|
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 resize_increments: Option<LogicalSize<f64>>,
|
|
pub disallow_hidpi: bool,
|
|
}
|
|
|
|
fn create_app(activation_policy: ActivationPolicy) -> Option<id> {
|
|
unsafe {
|
|
let ns_app = NSApp();
|
|
if ns_app == nil {
|
|
None
|
|
} else {
|
|
use self::NSApplicationActivationPolicy::*;
|
|
ns_app.setActivationPolicy_(match activation_policy {
|
|
ActivationPolicy::Regular => NSApplicationActivationPolicyRegular,
|
|
ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory,
|
|
ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited,
|
|
});
|
|
ns_app.finishLaunching();
|
|
Some(ns_app)
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe fn create_view(
|
|
ns_window: id,
|
|
pl_attribs: &PlatformSpecificWindowBuilderAttributes,
|
|
) -> Option<(IdRef, Weak<Mutex<CursorState>>)> {
|
|
let (ns_view, cursor_state) = new_view(ns_window);
|
|
ns_view.non_nil().map(|ns_view| {
|
|
if !pl_attribs.disallow_hidpi {
|
|
ns_view.setWantsBestResolutionOpenGLSurface_(YES);
|
|
}
|
|
|
|
// 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 f64::floor(appkit::NSAppKitVersionNumber) > appkit::NSAppKitVersionNumber10_12 {
|
|
ns_view.setWantsLayer(YES);
|
|
}
|
|
|
|
ns_window.setContentView_(*ns_view);
|
|
ns_window.makeFirstResponder_(*ns_view);
|
|
(ns_view, cursor_state)
|
|
})
|
|
}
|
|
|
|
fn create_window(
|
|
attrs: &WindowAttributes,
|
|
pl_attrs: &PlatformSpecificWindowBuilderAttributes,
|
|
) -> Option<IdRef> {
|
|
unsafe {
|
|
let pool = NSAutoreleasePool::new(nil);
|
|
let screen = match attrs.fullscreen {
|
|
Some(Fullscreen::Borderless(RootMonitorHandle { inner: ref monitor }))
|
|
| Some(Fullscreen::Exclusive(RootVideoMode {
|
|
video_mode: VideoMode { ref monitor, .. },
|
|
})) => {
|
|
let monitor_screen = monitor.ns_screen();
|
|
Some(monitor_screen.unwrap_or(appkit::NSScreen::mainScreen(nil)))
|
|
}
|
|
None => None,
|
|
};
|
|
let frame = match screen {
|
|
Some(screen) => NSScreen::frame(screen),
|
|
None => {
|
|
let screen = NSScreen::mainScreen(nil);
|
|
let scale_factor = NSScreen::backingScaleFactor(screen) as f64;
|
|
let (width, height) = match attrs.inner_size {
|
|
Some(size) => {
|
|
let logical = size.to_logical(scale_factor);
|
|
(logical.width, logical.height)
|
|
}
|
|
None => (800.0, 600.0),
|
|
};
|
|
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(width, height))
|
|
}
|
|
};
|
|
|
|
let mut masks = if !attrs.decorations && !screen.is_some() {
|
|
// Resizable UnownedWindow without a titlebar or borders
|
|
// if decorations is set to false, ignore pl_attrs
|
|
NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask
|
|
} else if pl_attrs.titlebar_hidden {
|
|
// if the titlebar is hidden, ignore other pl_attrs
|
|
NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask
|
|
} else {
|
|
// default case, resizable window with titlebar and titlebar buttons
|
|
NSWindowStyleMask::NSClosableWindowMask
|
|
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
|
| NSWindowStyleMask::NSResizableWindowMask
|
|
| NSWindowStyleMask::NSTitledWindowMask
|
|
};
|
|
|
|
if !attrs.resizable {
|
|
masks &= !NSWindowStyleMask::NSResizableWindowMask;
|
|
}
|
|
|
|
if pl_attrs.fullsize_content_view {
|
|
masks |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
|
}
|
|
|
|
let ns_window: id = msg_send![WINDOW_CLASS.0, alloc];
|
|
let ns_window = IdRef::new(ns_window.initWithContentRect_styleMask_backing_defer_(
|
|
frame,
|
|
masks,
|
|
appkit::NSBackingStoreBuffered,
|
|
NO,
|
|
));
|
|
let res = ns_window.non_nil().map(|ns_window| {
|
|
let title = IdRef::new(NSString::alloc(nil).init_str(&attrs.title));
|
|
ns_window.setReleasedWhenClosed_(NO);
|
|
ns_window.setTitle_(*title);
|
|
ns_window.setAcceptsMouseMovedEvents_(YES);
|
|
|
|
if pl_attrs.titlebar_transparent {
|
|
ns_window.setTitlebarAppearsTransparent_(YES);
|
|
}
|
|
if pl_attrs.title_hidden {
|
|
ns_window.setTitleVisibility_(appkit::NSWindowTitleVisibility::NSWindowTitleHidden);
|
|
}
|
|
if pl_attrs.titlebar_buttons_hidden {
|
|
for titlebar_button in &[
|
|
NSWindowButton::NSWindowFullScreenButton,
|
|
NSWindowButton::NSWindowMiniaturizeButton,
|
|
NSWindowButton::NSWindowCloseButton,
|
|
NSWindowButton::NSWindowZoomButton,
|
|
] {
|
|
let button = ns_window.standardWindowButton_(*titlebar_button);
|
|
let _: () = msg_send![button, setHidden: YES];
|
|
}
|
|
}
|
|
if pl_attrs.movable_by_window_background {
|
|
ns_window.setMovableByWindowBackground_(YES);
|
|
}
|
|
|
|
if attrs.always_on_top {
|
|
let _: () = msg_send![
|
|
*ns_window,
|
|
setLevel: ffi::NSWindowLevel::NSFloatingWindowLevel
|
|
];
|
|
}
|
|
|
|
if let Some(increments) = pl_attrs.resize_increments {
|
|
let (x, y) = (increments.width, increments.height);
|
|
if x >= 1.0 && y >= 1.0 {
|
|
let size = NSSize::new(x as CGFloat, y as CGFloat);
|
|
ns_window.setResizeIncrements_(size);
|
|
}
|
|
}
|
|
|
|
ns_window.center();
|
|
ns_window
|
|
});
|
|
pool.drain();
|
|
res
|
|
}
|
|
}
|
|
|
|
struct WindowClass(*const Class);
|
|
unsafe impl Send for WindowClass {}
|
|
unsafe impl Sync for WindowClass {}
|
|
|
|
lazy_static! {
|
|
static ref WINDOW_CLASS: WindowClass = unsafe {
|
|
let window_superclass = class!(NSWindow);
|
|
let mut decl = ClassDecl::new("WinitWindow", window_superclass).unwrap();
|
|
decl.add_method(
|
|
sel!(canBecomeMainWindow),
|
|
util::yes as extern "C" fn(&Object, Sel) -> BOOL,
|
|
);
|
|
decl.add_method(
|
|
sel!(canBecomeKeyWindow),
|
|
util::yes as extern "C" fn(&Object, Sel) -> BOOL,
|
|
);
|
|
WindowClass(decl.register())
|
|
};
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct SharedState {
|
|
pub resizable: bool,
|
|
pub fullscreen: Option<Fullscreen>,
|
|
// This is true between windowWillEnterFullScreen and windowDidEnterFullScreen
|
|
// or windowWillExitFullScreen and windowDidExitFullScreen.
|
|
// We must not toggle fullscreen when this is true.
|
|
pub in_fullscreen_transition: bool,
|
|
// If it is attempted to toggle fullscreen when in_fullscreen_transition is true,
|
|
// Set target_fullscreen and do after fullscreen transition is end.
|
|
pub target_fullscreen: Option<Option<Fullscreen>>,
|
|
pub maximized: bool,
|
|
pub standard_frame: Option<NSRect>,
|
|
is_simple_fullscreen: bool,
|
|
pub saved_style: Option<NSWindowStyleMask>,
|
|
/// Presentation options saved before entering `set_simple_fullscreen`, and
|
|
/// restored upon exiting it
|
|
save_presentation_opts: Option<NSApplicationPresentationOptions>,
|
|
pub saved_desktop_display_mode: Option<(CGDisplay, CGDisplayMode)>,
|
|
}
|
|
|
|
impl SharedState {
|
|
pub fn saved_standard_frame(&self) -> NSRect {
|
|
self.standard_frame
|
|
.unwrap_or_else(|| NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)))
|
|
}
|
|
}
|
|
|
|
impl From<WindowAttributes> for SharedState {
|
|
fn from(attribs: WindowAttributes) -> Self {
|
|
SharedState {
|
|
resizable: attribs.resizable,
|
|
// This fullscreen field tracks the current state of the window
|
|
// (as seen by `WindowDelegate`), and since the window hasn't
|
|
// actually been fullscreened yet, we can't set it yet. This is
|
|
// necessary for state transitions to work right, since otherwise
|
|
// the initial value and the first `set_fullscreen` call would be
|
|
// identical, resulting in a no-op.
|
|
fullscreen: None,
|
|
maximized: attribs.maximized,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct UnownedWindow {
|
|
pub ns_window: IdRef, // never changes
|
|
pub ns_view: IdRef, // never changes
|
|
input_context: IdRef, // never changes
|
|
pub shared_state: Arc<Mutex<SharedState>>,
|
|
decorations: AtomicBool,
|
|
cursor_state: Weak<Mutex<CursorState>>,
|
|
pub inner_rect: Option<PhysicalSize<u32>>,
|
|
}
|
|
|
|
unsafe impl Send for UnownedWindow {}
|
|
unsafe impl Sync for UnownedWindow {}
|
|
|
|
impl UnownedWindow {
|
|
pub fn new(
|
|
mut win_attribs: WindowAttributes,
|
|
pl_attribs: PlatformSpecificWindowBuilderAttributes,
|
|
) -> Result<(Arc<Self>, IdRef), RootOsError> {
|
|
unsafe {
|
|
if !msg_send![class!(NSThread), isMainThread] {
|
|
panic!("Windows can only be created on the main thread on macOS");
|
|
}
|
|
}
|
|
|
|
let pool = unsafe { NSAutoreleasePool::new(nil) };
|
|
|
|
let ns_app = create_app(pl_attribs.activation_policy).ok_or_else(|| {
|
|
unsafe { pool.drain() };
|
|
os_error!(OsError::CreationError("Couldn't create `NSApplication`"))
|
|
})?;
|
|
|
|
let ns_window = create_window(&win_attribs, &pl_attribs).ok_or_else(|| {
|
|
unsafe { pool.drain() };
|
|
os_error!(OsError::CreationError("Couldn't create `NSWindow`"))
|
|
})?;
|
|
|
|
let (ns_view, cursor_state) =
|
|
unsafe { create_view(*ns_window, &pl_attribs) }.ok_or_else(|| {
|
|
unsafe { pool.drain() };
|
|
os_error!(OsError::CreationError("Couldn't create `NSView`"))
|
|
})?;
|
|
|
|
let input_context = unsafe { util::create_input_context(*ns_view) };
|
|
|
|
let dpi_factor = unsafe { NSWindow::backingScaleFactor(*ns_window) as f64 };
|
|
|
|
unsafe {
|
|
if win_attribs.transparent {
|
|
ns_window.setOpaque_(NO);
|
|
ns_window.setBackgroundColor_(NSColor::clearColor(nil));
|
|
}
|
|
|
|
ns_app.activateIgnoringOtherApps_(YES);
|
|
win_attribs.min_inner_size.map(|dim| {
|
|
let logical_dim = dim.to_logical(dpi_factor);
|
|
set_min_inner_size(*ns_window, logical_dim)
|
|
});
|
|
win_attribs.max_inner_size.map(|dim| {
|
|
let logical_dim = dim.to_logical(dpi_factor);
|
|
set_max_inner_size(*ns_window, logical_dim)
|
|
});
|
|
|
|
use cocoa::foundation::NSArray;
|
|
// register for drag and drop operations.
|
|
let () = msg_send![
|
|
*ns_window,
|
|
registerForDraggedTypes:
|
|
NSArray::arrayWithObject(nil, appkit::NSFilenamesPboardType)
|
|
];
|
|
}
|
|
|
|
// Since `win_attribs` is put into a mutex below, we'll just copy these
|
|
// attributes now instead of bothering to lock it later.
|
|
// Also, `SharedState` doesn't carry `fullscreen` over; it's set
|
|
// indirectly by us calling `set_fullscreen` below, causing handlers in
|
|
// `WindowDelegate` to update the state.
|
|
let fullscreen = win_attribs.fullscreen.take();
|
|
let maximized = win_attribs.maximized;
|
|
let visible = win_attribs.visible;
|
|
let decorations = win_attribs.decorations;
|
|
let inner_rect = win_attribs
|
|
.inner_size
|
|
.map(|size| size.to_physical(dpi_factor));
|
|
|
|
let window = Arc::new(UnownedWindow {
|
|
ns_view,
|
|
ns_window,
|
|
input_context,
|
|
shared_state: Arc::new(Mutex::new(win_attribs.into())),
|
|
decorations: AtomicBool::new(decorations),
|
|
cursor_state,
|
|
inner_rect,
|
|
});
|
|
|
|
let delegate = new_delegate(&window, fullscreen.is_some());
|
|
|
|
// Set fullscreen mode after we setup everything
|
|
window.set_fullscreen(fullscreen);
|
|
|
|
// 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.
|
|
unsafe {
|
|
if visible {
|
|
window.ns_window.makeKeyAndOrderFront_(nil);
|
|
} else {
|
|
window.ns_window.makeKeyWindow();
|
|
}
|
|
}
|
|
|
|
if maximized {
|
|
window.set_maximized(maximized);
|
|
}
|
|
|
|
unsafe { pool.drain() };
|
|
|
|
Ok((window, delegate))
|
|
}
|
|
|
|
fn set_style_mask_async(&self, mask: NSWindowStyleMask) {
|
|
unsafe { util::set_style_mask_async(*self.ns_window, *self.ns_view, mask) };
|
|
}
|
|
|
|
fn set_style_mask_sync(&self, mask: NSWindowStyleMask) {
|
|
unsafe { util::set_style_mask_sync(*self.ns_window, *self.ns_view, mask) };
|
|
}
|
|
|
|
pub fn id(&self) -> Id {
|
|
get_window_id(*self.ns_window)
|
|
}
|
|
|
|
pub fn set_title(&self, title: &str) {
|
|
unsafe {
|
|
util::set_title_async(*self.ns_window, title.to_string());
|
|
}
|
|
}
|
|
|
|
pub fn set_visible(&self, visible: bool) {
|
|
match visible {
|
|
true => unsafe { util::make_key_and_order_front_async(*self.ns_window) },
|
|
false => unsafe { util::order_out_async(*self.ns_window) },
|
|
}
|
|
}
|
|
|
|
pub fn request_redraw(&self) {
|
|
AppState::queue_redraw(RootWindowId(self.id()));
|
|
}
|
|
|
|
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
|
let frame_rect = unsafe { NSWindow::frame(*self.ns_window) };
|
|
let position = LogicalPosition::new(
|
|
frame_rect.origin.x as f64,
|
|
util::bottom_left_to_top_left(frame_rect),
|
|
);
|
|
let dpi_factor = self.scale_factor();
|
|
Ok(position.to_physical(dpi_factor))
|
|
}
|
|
|
|
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
|
let content_rect = unsafe {
|
|
NSWindow::contentRectForFrameRect_(*self.ns_window, NSWindow::frame(*self.ns_window))
|
|
};
|
|
let position = LogicalPosition::new(
|
|
content_rect.origin.x as f64,
|
|
util::bottom_left_to_top_left(content_rect),
|
|
);
|
|
let dpi_factor = self.scale_factor();
|
|
Ok(position.to_physical(dpi_factor))
|
|
}
|
|
|
|
pub fn set_outer_position(&self, position: Position) {
|
|
let dpi_factor = self.scale_factor();
|
|
let position = position.to_logical(dpi_factor);
|
|
let dummy = NSRect::new(
|
|
NSPoint::new(
|
|
position.x,
|
|
// While it's true that we're setting the top-left position,
|
|
// it still needs to be in a bottom-left coordinate system.
|
|
CGDisplay::main().pixels_high() as f64 - position.y,
|
|
),
|
|
NSSize::new(0f64, 0f64),
|
|
);
|
|
unsafe {
|
|
util::set_frame_top_left_point_async(*self.ns_window, dummy.origin);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
|
let view_frame = unsafe { NSView::frame(*self.ns_view) };
|
|
let logical: LogicalSize<f64> =
|
|
(view_frame.size.width as f64, view_frame.size.height as f64).into();
|
|
let dpi_factor = self.scale_factor();
|
|
logical.to_physical(dpi_factor)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn outer_size(&self) -> PhysicalSize<u32> {
|
|
let view_frame = unsafe { NSWindow::frame(*self.ns_window) };
|
|
let logical: LogicalSize<f64> =
|
|
(view_frame.size.width as f64, view_frame.size.height as f64).into();
|
|
let dpi_factor = self.scale_factor();
|
|
logical.to_physical(dpi_factor)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_inner_size(&self, size: Size) {
|
|
unsafe {
|
|
let dpi_factor = self.scale_factor();
|
|
util::set_content_size_async(*self.ns_window, size.to_logical(dpi_factor));
|
|
}
|
|
}
|
|
|
|
pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
|
|
unsafe {
|
|
let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
|
|
width: 0.0,
|
|
height: 0.0,
|
|
}));
|
|
let dpi_factor = self.scale_factor();
|
|
set_min_inner_size(*self.ns_window, dimensions.to_logical(dpi_factor));
|
|
}
|
|
}
|
|
|
|
pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
|
|
unsafe {
|
|
let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
|
|
width: std::f32::MAX as f64,
|
|
height: std::f32::MAX as f64,
|
|
}));
|
|
let dpi_factor = self.scale_factor();
|
|
set_max_inner_size(*self.ns_window, dimensions.to_logical(dpi_factor));
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_resizable(&self, resizable: bool) {
|
|
let fullscreen = {
|
|
trace!("Locked shared state in `set_resizable`");
|
|
let mut shared_state_lock = self.shared_state.lock().unwrap();
|
|
shared_state_lock.resizable = resizable;
|
|
trace!("Unlocked shared state in `set_resizable`");
|
|
shared_state_lock.fullscreen.is_some()
|
|
};
|
|
if !fullscreen {
|
|
let mut mask = unsafe { self.ns_window.styleMask() };
|
|
if resizable {
|
|
mask |= NSWindowStyleMask::NSResizableWindowMask;
|
|
} else {
|
|
mask &= !NSWindowStyleMask::NSResizableWindowMask;
|
|
}
|
|
self.set_style_mask_async(mask);
|
|
} // Otherwise, we don't change the mask until we exit fullscreen.
|
|
}
|
|
|
|
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
|
|
let cursor = util::Cursor::from(cursor);
|
|
if let Some(cursor_access) = self.cursor_state.upgrade() {
|
|
cursor_access.lock().unwrap().cursor = cursor;
|
|
}
|
|
unsafe {
|
|
let _: () = msg_send![*self.ns_window,
|
|
invalidateCursorRectsForView:*self.ns_view
|
|
];
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> {
|
|
// TODO: Do this for real https://stackoverflow.com/a/40922095/5435443
|
|
CGDisplay::associate_mouse_and_mouse_cursor_position(!grab)
|
|
.map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status))))
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_cursor_visible(&self, visible: bool) {
|
|
if let Some(cursor_access) = self.cursor_state.upgrade() {
|
|
let mut cursor_state = cursor_access.lock().unwrap();
|
|
if visible != cursor_state.visible {
|
|
cursor_state.visible = visible;
|
|
drop(cursor_state);
|
|
unsafe {
|
|
let _: () = msg_send![*self.ns_window,
|
|
invalidateCursorRectsForView:*self.ns_view
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn scale_factor(&self) -> f64 {
|
|
unsafe { NSWindow::backingScaleFactor(*self.ns_window) as _ }
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), ExternalError> {
|
|
let physical_window_position = self.inner_position().unwrap();
|
|
let dpi_factor = self.scale_factor();
|
|
let window_position = physical_window_position.to_logical::<CGFloat>(dpi_factor);
|
|
let logical_cursor_position = cursor_position.to_logical::<CGFloat>(dpi_factor);
|
|
let point = appkit::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(())
|
|
}
|
|
|
|
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 = unsafe { self.ns_window.styleMask() };
|
|
|
|
let required =
|
|
NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
|
|
let needs_temp_mask = !curr_mask.contains(required);
|
|
if needs_temp_mask {
|
|
self.set_style_mask_sync(required);
|
|
}
|
|
|
|
let is_zoomed: BOOL = unsafe { msg_send![*self.ns_window, isZoomed] };
|
|
|
|
// Roll back temp styles
|
|
if needs_temp_mask {
|
|
self.set_style_mask_async(curr_mask);
|
|
}
|
|
|
|
is_zoomed != 0
|
|
}
|
|
|
|
fn saved_style(&self, shared_state: &mut SharedState) -> NSWindowStyleMask {
|
|
let base_mask = shared_state
|
|
.saved_style
|
|
.take()
|
|
.unwrap_or_else(|| unsafe { self.ns_window.styleMask() });
|
|
if shared_state.resizable {
|
|
base_mask | NSWindowStyleMask::NSResizableWindowMask
|
|
} else {
|
|
base_mask & !NSWindowStyleMask::NSResizableWindowMask
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
trace!("Locked shared state in `restore_state_from_fullscreen`");
|
|
let mut shared_state_lock = self.shared_state.lock().unwrap();
|
|
|
|
shared_state_lock.fullscreen = None;
|
|
|
|
let maximized = shared_state_lock.maximized;
|
|
let mask = self.saved_style(&mut *shared_state_lock);
|
|
|
|
drop(shared_state_lock);
|
|
trace!("Unocked shared state in `restore_state_from_fullscreen`");
|
|
|
|
self.set_style_mask_async(mask);
|
|
self.set_maximized(maximized);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_minimized(&self, minimized: bool) {
|
|
let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] };
|
|
let is_minimized: bool = is_minimized == YES;
|
|
if is_minimized == minimized {
|
|
return;
|
|
}
|
|
|
|
if minimized {
|
|
unsafe {
|
|
NSWindow::miniaturize_(*self.ns_window, *self.ns_window);
|
|
}
|
|
} else {
|
|
unsafe {
|
|
NSWindow::deminiaturize_(*self.ns_window, *self.ns_window);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_maximized(&self, maximized: bool) {
|
|
let is_zoomed = self.is_zoomed();
|
|
if is_zoomed == maximized {
|
|
return;
|
|
};
|
|
unsafe {
|
|
util::set_maximized_async(
|
|
*self.ns_window,
|
|
is_zoomed,
|
|
maximized,
|
|
Arc::downgrade(&self.shared_state),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn fullscreen(&self) -> Option<Fullscreen> {
|
|
let shared_state_lock = self.shared_state.lock().unwrap();
|
|
shared_state_lock.fullscreen.clone()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
|
|
trace!("Locked shared state in `set_fullscreen`");
|
|
let mut shared_state_lock = self.shared_state.lock().unwrap();
|
|
if shared_state_lock.is_simple_fullscreen {
|
|
trace!("Unlocked shared state in `set_fullscreen`");
|
|
return;
|
|
}
|
|
if shared_state_lock.in_fullscreen_transition {
|
|
// We can't set fullscreen here.
|
|
// Set fullscreen after transition.
|
|
shared_state_lock.target_fullscreen = Some(fullscreen);
|
|
trace!("Unlocked shared state in `set_fullscreen`");
|
|
return;
|
|
}
|
|
let old_fullscreen = shared_state_lock.fullscreen.clone();
|
|
if fullscreen == old_fullscreen {
|
|
trace!("Unlocked shared state in `set_fullscreen`");
|
|
return;
|
|
}
|
|
trace!("Unlocked shared state in `set_fullscreen`");
|
|
drop(shared_state_lock);
|
|
|
|
// 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(RootMonitorHandle { inner: ref monitor }) => monitor,
|
|
Fullscreen::Exclusive(RootVideoMode {
|
|
video_mode: VideoMode { ref monitor, .. },
|
|
}) => monitor,
|
|
}
|
|
.ns_screen()
|
|
.unwrap();
|
|
|
|
unsafe {
|
|
let old_screen = NSWindow::screen(*self.ns_window);
|
|
if old_screen != new_screen {
|
|
let mut screen_frame: NSRect = msg_send![new_screen, frame];
|
|
// The coordinate system here has its origin at bottom-left
|
|
// and Y goes up
|
|
screen_frame.origin.y += screen_frame.size.height;
|
|
util::set_frame_top_left_point_async(*self.ns_window, 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().inner.native_identifier();
|
|
|
|
let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken;
|
|
|
|
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.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
trace!("Locked shared state in `set_fullscreen`");
|
|
let mut shared_state_lock = self.shared_state.lock().unwrap();
|
|
shared_state_lock.fullscreen = fullscreen.clone();
|
|
trace!("Unlocked shared state in `set_fullscreen`");
|
|
|
|
match (&old_fullscreen, &fullscreen) {
|
|
(&None, &Some(_)) => unsafe {
|
|
util::toggle_full_screen_async(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
old_fullscreen.is_none(),
|
|
Arc::downgrade(&self.shared_state),
|
|
);
|
|
},
|
|
(&Some(Fullscreen::Borderless(_)), &None) => unsafe {
|
|
// State is restored by `window_did_exit_fullscreen`
|
|
util::toggle_full_screen_async(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
old_fullscreen.is_none(),
|
|
Arc::downgrade(&self.shared_state),
|
|
);
|
|
},
|
|
(&Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), &None) => unsafe {
|
|
util::restore_display_mode_async(video_mode.monitor().inner.native_identifier());
|
|
// Rest of the state is restored by `window_did_exit_fullscreen`
|
|
util::toggle_full_screen_async(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
old_fullscreen.is_none(),
|
|
Arc::downgrade(&self.shared_state),
|
|
);
|
|
},
|
|
(&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => unsafe {
|
|
// 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:`.
|
|
let () = msg_send![*self.ns_window, setLevel: ffi::CGShieldingWindowLevel() + 1];
|
|
},
|
|
(
|
|
&Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })),
|
|
&Some(Fullscreen::Borderless(_)),
|
|
) => unsafe {
|
|
util::restore_display_mode_async(video_mode.monitor().inner.native_identifier());
|
|
},
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_decorations(&self, decorations: bool) {
|
|
if decorations != self.decorations.load(Ordering::Acquire) {
|
|
self.decorations.store(decorations, Ordering::Release);
|
|
|
|
let (fullscreen, resizable) = {
|
|
trace!("Locked shared state in `set_decorations`");
|
|
let shared_state_lock = self.shared_state.lock().unwrap();
|
|
trace!("Unlocked shared state in `set_decorations`");
|
|
(
|
|
shared_state_lock.fullscreen.is_some(),
|
|
shared_state_lock.resizable,
|
|
)
|
|
};
|
|
|
|
// 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 {
|
|
NSWindowStyleMask::NSClosableWindowMask
|
|
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
|
| NSWindowStyleMask::NSResizableWindowMask
|
|
| NSWindowStyleMask::NSTitledWindowMask
|
|
} else {
|
|
NSWindowStyleMask::NSBorderlessWindowMask
|
|
| NSWindowStyleMask::NSResizableWindowMask
|
|
};
|
|
if !resizable {
|
|
new_mask &= !NSWindowStyleMask::NSResizableWindowMask;
|
|
}
|
|
new_mask
|
|
};
|
|
self.set_style_mask_async(new_mask);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_always_on_top(&self, always_on_top: bool) {
|
|
let level = if always_on_top {
|
|
ffi::NSWindowLevel::NSFloatingWindowLevel
|
|
} else {
|
|
ffi::NSWindowLevel::NSNormalWindowLevel
|
|
};
|
|
unsafe { util::set_level_async(*self.ns_window, level) };
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_window_icon(&self, _icon: Option<Icon>) {
|
|
// 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_position(&self, spot: Position) {
|
|
let dpi_factor = self.scale_factor();
|
|
let logical_spot = spot.to_logical(dpi_factor);
|
|
unsafe {
|
|
view::set_ime_position(
|
|
*self.ns_view,
|
|
*self.input_context,
|
|
logical_spot.x,
|
|
logical_spot.y,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn current_monitor(&self) -> RootMonitorHandle {
|
|
unsafe {
|
|
let screen: id = msg_send![*self.ns_window, screen];
|
|
let desc = NSScreen::deviceDescription(screen);
|
|
let key = IdRef::new(NSString::alloc(nil).init_str("NSScreenNumber"));
|
|
let value = NSDictionary::valueForKey_(desc, *key);
|
|
let display_id = msg_send![value, unsignedIntegerValue];
|
|
RootMonitorHandle {
|
|
inner: MonitorHandle::new(display_id),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
|
monitor::available_monitors()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn primary_monitor(&self) -> MonitorHandle {
|
|
monitor::primary_monitor()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn raw_window_handle(&self) -> RawWindowHandle {
|
|
let handle = MacOSHandle {
|
|
ns_window: *self.ns_window as *mut _,
|
|
ns_view: *self.ns_view as *mut _,
|
|
..MacOSHandle::empty()
|
|
};
|
|
RawWindowHandle::MacOS(handle)
|
|
}
|
|
}
|
|
|
|
impl WindowExtMacOS for UnownedWindow {
|
|
#[inline]
|
|
fn ns_window(&self) -> *mut c_void {
|
|
*self.ns_window as *mut _
|
|
}
|
|
|
|
#[inline]
|
|
fn ns_view(&self) -> *mut c_void {
|
|
*self.ns_view as *mut _
|
|
}
|
|
|
|
#[inline]
|
|
fn request_user_attention(&self, request_type: RequestUserAttentionType) {
|
|
unsafe {
|
|
NSApp().requestUserAttention_(match request_type {
|
|
RequestUserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest,
|
|
RequestUserAttentionType::Informational => {
|
|
NSRequestUserAttentionType::NSInformationalRequest
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn simple_fullscreen(&self) -> bool {
|
|
let shared_state_lock = self.shared_state.lock().unwrap();
|
|
shared_state_lock.is_simple_fullscreen
|
|
}
|
|
|
|
#[inline]
|
|
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
|
|
let mut shared_state_lock = self.shared_state.lock().unwrap();
|
|
|
|
unsafe {
|
|
let app = NSApp();
|
|
let is_native_fullscreen = shared_state_lock.fullscreen.is_some();
|
|
let is_simple_fullscreen = shared_state_lock.is_simple_fullscreen;
|
|
|
|
// 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
|
|
shared_state_lock.standard_frame = Some(NSWindow::frame(*self.ns_window));
|
|
shared_state_lock.saved_style = Some(self.ns_window.styleMask());
|
|
shared_state_lock.save_presentation_opts = Some(app.presentationOptions_());
|
|
|
|
// Tell our window's state that we're in fullscreen
|
|
shared_state_lock.is_simple_fullscreen = true;
|
|
|
|
// Simulate pre-Lion fullscreen by hiding the dock and menu bar
|
|
let presentation_options =
|
|
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock |
|
|
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar;
|
|
app.setPresentationOptions_(presentation_options);
|
|
|
|
// Hide the titlebar
|
|
util::toggle_style_mask(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
NSWindowStyleMask::NSTitledWindowMask,
|
|
false,
|
|
);
|
|
|
|
// Set the window frame to the screen frame size
|
|
let screen = self.ns_window.screen();
|
|
let screen_frame = NSScreen::frame(screen);
|
|
NSWindow::setFrame_display_(*self.ns_window, screen_frame, YES);
|
|
|
|
// Fullscreen windows can't be resized, minimized, or moved
|
|
util::toggle_style_mask(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
NSWindowStyleMask::NSMiniaturizableWindowMask,
|
|
false,
|
|
);
|
|
util::toggle_style_mask(
|
|
*self.ns_window,
|
|
*self.ns_view,
|
|
NSWindowStyleMask::NSResizableWindowMask,
|
|
false,
|
|
);
|
|
NSWindow::setMovable_(*self.ns_window, NO);
|
|
|
|
true
|
|
} else {
|
|
let new_mask = self.saved_style(&mut *shared_state_lock);
|
|
self.set_style_mask_async(new_mask);
|
|
shared_state_lock.is_simple_fullscreen = false;
|
|
|
|
if let Some(presentation_opts) = shared_state_lock.save_presentation_opts {
|
|
app.setPresentationOptions_(presentation_opts);
|
|
}
|
|
|
|
let frame = shared_state_lock.saved_standard_frame();
|
|
NSWindow::setFrame_display_(*self.ns_window, frame, YES);
|
|
NSWindow::setMovable_(*self.ns_window, YES);
|
|
|
|
true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for UnownedWindow {
|
|
fn drop(&mut self) {
|
|
trace!("Dropping `UnownedWindow` ({:?})", self as *mut _);
|
|
// Close the window if it has not yet been closed.
|
|
if *self.ns_window != nil {
|
|
unsafe { util::close_async(*self.ns_window) };
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe fn set_min_inner_size<V: NSWindow + Copy>(window: V, mut min_size: LogicalSize<f64>) {
|
|
let mut current_rect = NSWindow::frame(window);
|
|
let content_rect = NSWindow::contentRectForFrameRect_(window, NSWindow::frame(window));
|
|
// Convert from client area size to window size
|
|
min_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0
|
|
min_size.height += (current_rect.size.height - content_rect.size.height) as f64;
|
|
window.setMinSize_(NSSize {
|
|
width: min_size.width as CGFloat,
|
|
height: min_size.height as CGFloat,
|
|
});
|
|
// If necessary, resize the window to match constraint
|
|
if current_rect.size.width < min_size.width {
|
|
current_rect.size.width = min_size.width;
|
|
window.setFrame_display_(current_rect, 0)
|
|
}
|
|
if current_rect.size.height < min_size.height {
|
|
// The origin point of a rectangle is at its bottom left in Cocoa.
|
|
// To ensure the window's top-left point remains the same:
|
|
current_rect.origin.y += current_rect.size.height - min_size.height;
|
|
current_rect.size.height = min_size.height;
|
|
window.setFrame_display_(current_rect, 0)
|
|
}
|
|
}
|
|
|
|
unsafe fn set_max_inner_size<V: NSWindow + Copy>(window: V, mut max_size: LogicalSize<f64>) {
|
|
let mut current_rect = NSWindow::frame(window);
|
|
let content_rect = NSWindow::contentRectForFrameRect_(window, NSWindow::frame(window));
|
|
// Convert from client area size to window size
|
|
max_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0
|
|
max_size.height += (current_rect.size.height - content_rect.size.height) as f64;
|
|
window.setMaxSize_(NSSize {
|
|
width: max_size.width as CGFloat,
|
|
height: max_size.height as CGFloat,
|
|
});
|
|
// If necessary, resize the window to match constraint
|
|
if current_rect.size.width > max_size.width {
|
|
current_rect.size.width = max_size.width;
|
|
window.setFrame_display_(current_rect, 0)
|
|
}
|
|
if current_rect.size.height > max_size.height {
|
|
// The origin point of a rectangle is at its bottom left in Cocoa.
|
|
// To ensure the window's top-left point remains the same:
|
|
current_rect.origin.y += current_rect.size.height - max_size.height;
|
|
current_rect.size.height = max_size.height;
|
|
window.setFrame_display_(current_rect, 0)
|
|
}
|
|
}
|