From 0474dc9861cdc7e4a71fa231064b551426ad3bb2 Mon Sep 17 00:00:00 2001 From: Edwin Cheng Date: Wed, 18 Apr 2018 02:07:54 +0800 Subject: [PATCH] Implement set_maximized, get_current_monitor, set_fullscreen and set_decorations for MacOS (#465) * Added helper function for make monitor from display. * Implement get_current_monitor for macos * Implemented with_fullscreen and set_fullscreen for macos * Implemented set_decorations for macos * Implement set_maximized and with_maximized for macos * Changed fullscreen example fullscreen keypress from F11 to F * Update CHANGELOG.md * Add and fixed some comments * Reformat and add more comments * Better handling window and maximized state * Reformat and typo fix --- CHANGELOG.md | 1 + examples/fullscreen.rs | 2 +- src/platform/macos/monitor.rs | 7 +- src/platform/macos/window.rs | 353 +++++++++++++++++++++++++++++----- 4 files changed, 318 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b9b20d..816986ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for MacOS. - Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for Windows. - On Windows, `WindowBuilder::with_dimensions` no longer changing monitor display resolution. - Overhauled X11 window geometry calculations. `get_position` and `set_position` are more universally accurate across different window managers, and `get_outer_size` actually works now. diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index 481ba223..067b3bae 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -51,7 +51,7 @@ fn main() { .. } => match (virtual_code, state) { (winit::VirtualKeyCode::Escape, _) => return ControlFlow::Break, - (winit::VirtualKeyCode::F11, winit::ElementState::Pressed) => { + (winit::VirtualKeyCode::F, winit::ElementState::Pressed) => { is_fullscreen = !is_fullscreen; if !is_fullscreen { window.set_fullscreen(None); diff --git a/src/platform/macos/monitor.rs b/src/platform/macos/monitor.rs index ea37e7d8..b01ca75d 100644 --- a/src/platform/macos/monitor.rs +++ b/src/platform/macos/monitor.rs @@ -6,7 +6,7 @@ use std::collections::VecDeque; use super::EventsLoop; use super::window::IdRef; -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct MonitorId(CGDirectDisplayID); impl EventsLoop { @@ -25,6 +25,11 @@ impl EventsLoop { let id = MonitorId(CGDisplay::main().id); id } + + pub fn make_monitor_from_display(id: CGDirectDisplayID) -> MonitorId { + let id = MonitorId(id); + id + } } impl MonitorId { diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 2b4dba5d..0ab8eb0f 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -11,9 +11,10 @@ use objc::runtime::{Class, Object, Sel, BOOL, YES, NO}; use objc::declare::ClassDecl; use cocoa; +use cocoa::appkit::{self, NSApplication, NSColor, NSScreen, NSView, NSWindow, NSWindowButton, + NSWindowStyleMask}; use cocoa::base::{id, nil}; -use cocoa::foundation::{NSPoint, NSRect, NSSize, NSString}; -use cocoa::appkit::{self, NSApplication, NSColor, NSView, NSWindow, NSWindowStyleMask, NSWindowButton}; +use cocoa::foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString}; use core_graphics::display::CGDisplay; @@ -21,8 +22,9 @@ use std; use std::ops::Deref; use std::os::raw::c_void; use std::sync::Weak; +use std::cell::{Cell,RefCell}; -use super::events_loop::Shared; +use super::events_loop::{EventsLoop, Shared}; use window::MonitorId as RootMonitorId; @@ -33,6 +35,98 @@ struct DelegateState { view: IdRef, window: IdRef, shared: Weak, + + win_attribs: RefCell, + standard_frame: Cell>, + save_style_mask: Cell>, + + // This is set when WindowBuilder::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + handle_with_fullscreen: bool, +} + +impl DelegateState { + fn is_zoomed(&self) -> bool { + unsafe { + // Because isZoomed do not work in Borderless mode, we set it + // resizable temporality + let curr_mask = self.window.styleMask(); + + if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { + self.window + .setStyleMask_(NSWindowStyleMask::NSResizableWindowMask); + } + + let is_zoomed: BOOL = msg_send![*self.window, isZoomed]; + + // Roll back temp styles + if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { + self.window.setStyleMask_(curr_mask); + } + + is_zoomed != 0 + } + } + + fn restore_state_from_fullscreen(&mut self) { + let maximized = unsafe { + let mut win_attribs = self.win_attribs.borrow_mut(); + + win_attribs.fullscreen = None; + let save_style_opt = self.save_style_mask.take(); + + if let Some(save_style) = save_style_opt { + self.window.setStyleMask_(save_style); + } + + win_attribs.maximized + }; + + self.perform_maximized(maximized); + } + + fn perform_maximized(&self, maximized: bool) { + let is_zoomed = self.is_zoomed(); + + if is_zoomed == maximized { + return; + } + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + unsafe { + self.standard_frame.set(Some(NSWindow::frame(*self.window))); + } + } + + let mut win_attribs = self.win_attribs.borrow_mut(); + win_attribs.maximized = maximized; + + if win_attribs.fullscreen.is_some() { + // Handle it in window_did_exit_fullscreen + return; + } else if win_attribs.decorations { + // Just use the native zoom if not borderless + unsafe { + self.window.zoom_(nil); + } + } else { + // if it is borderless, we set the frame directly + unsafe { + let new_rect = if maximized { + let screen = NSScreen::mainScreen(nil); + NSScreen::visibleFrame(screen) + } else { + self.standard_frame.get().unwrap_or(NSRect::new( + NSPoint::new(50.0, 50.0), + NSSize::new(800.0, 600.0), + )) + }; + + self.window.setFrame_display_(new_rect, 0); + } + } + } } pub struct WindowDelegate { @@ -199,6 +293,71 @@ impl WindowDelegate { } } + /// Invoked when entered fullscreen + extern fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id){ + unsafe { + let state: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state as *mut DelegateState); + state.win_attribs.borrow_mut().fullscreen = Some(get_current_monitor()); + + state.handle_with_fullscreen = false; + } + } + + /// Invoked when before enter fullscreen + extern fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + unsafe { + let state: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state as *mut DelegateState); + let is_zoomed = state.is_zoomed(); + + state.win_attribs.borrow_mut().maximized = is_zoomed; + } + } + + /// Invoked when exited fullscreen + extern fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id){ + let state = unsafe { + let state: *mut c_void = *this.get_ivar("winitState"); + &mut *(state as *mut DelegateState) + }; + + state.restore_state_from_fullscreen(); + } + + /// Invoked when fail to enter fullscreen + /// + /// When this window launch from a fullscreen app (e.g. launch from VS Code + /// terminal), it creates a new virtual destkop and a transition animation. + /// This animation takes one second and cannot be disable without + /// elevated privileges. In this animation time, all toggleFullscreen events + /// will be failed. In this implementation, we will try again by using + /// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. + /// It should be fine as we only do this at initialzation (i.e with_fullscreen + /// was set). + /// + /// From Apple doc: + /// In some cases, the transition to enter full-screen mode can fail, + /// due to being in the midst of handling some other animation or user gesture. + /// This method indicates that there was an error, and you should clean up any + /// work you may have done to prepare to enter full-screen mode. + extern fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { + unsafe { + let state: *mut c_void = *this.get_ivar("winitState"); + let state = &mut *(state as *mut DelegateState); + + if state.handle_with_fullscreen { + let _: () = msg_send![*state.window, + performSelector:sel!(toggleFullScreen:) + withObject:nil + afterDelay: 0.5 + ]; + } else { + state.restore_state_from_fullscreen(); + } + } + } + static mut DELEGATE_CLASS: *const Class = 0 as *const Class; static INIT: std::sync::Once = std::sync::ONCE_INIT; @@ -233,6 +392,16 @@ impl WindowDelegate { decl.add_method(sel!(draggingExited:), dragging_exited as extern fn(&Object, Sel, id)); + // callbacks for fullscreen events + decl.add_method(sel!(windowDidEnterFullScreen:), + window_did_enter_fullscreen as extern fn(&Object, Sel, id)); + decl.add_method(sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidExitFullScreen:), + window_did_exit_fullscreen as extern fn(&Object, Sel, id)); + decl.add_method(sel!(windowDidFailToEnterFullScreen:), + window_did_fail_to_enter_fullscreen as extern fn(&Object, Sel, id)); + // Store internal state as user data decl.add_ivar::<*mut c_void>("winitState"); @@ -288,6 +457,20 @@ pub struct Window2 { unsafe impl Send for Window2 {} unsafe impl Sync for Window2 {} +/// Helpper funciton to convert NSScreen::mainScreen to MonitorId +unsafe fn get_current_monitor() -> RootMonitorId { + let screen = NSScreen::mainScreen(nil); + 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]; + + RootMonitorId { + inner: EventsLoop::make_monitor_from_display(display_id), + } +} + impl Drop for Window2 { fn drop(&mut self) { // Remove this window from the `EventLoop`s list of windows. @@ -319,11 +502,11 @@ impl WindowExt for Window2 { } impl Window2 { - pub fn new(shared: Weak, - win_attribs: &WindowAttributes, - pl_attribs: &PlatformSpecificWindowBuilderAttributes) - -> Result - { + pub fn new( + shared: Weak, + win_attribs: &WindowAttributes, + pl_attribs: &PlatformSpecificWindowBuilderAttributes, + ) -> Result { unsafe { if !msg_send![cocoa::base::class("NSThread"), isMainThread] { panic!("Windows can only be created on the main thread on macOS"); @@ -352,11 +535,6 @@ impl Window2 { } app.activateIgnoringOtherApps_(YES); - if win_attribs.visible { - window.makeKeyAndOrderFront_(nil); - } else { - window.makeKeyWindow(); - } if let Some((width, height)) = win_attribs.min_dimensions { nswindow_set_min_dimensions(window.0, width.into(), height.into()); @@ -375,8 +553,13 @@ impl Window2 { let ds = DelegateState { view: view.clone(), window: window.clone(), + win_attribs: RefCell::new(win_attribs.clone()), + standard_frame: Cell::new(None), + save_style_mask: Cell::new(None), + handle_with_fullscreen: win_attribs.fullscreen.is_some(), shared: shared, }; + ds.win_attribs.borrow_mut().fullscreen = None; let window = Window2 { view: view, @@ -384,6 +567,30 @@ impl Window2 { delegate: WindowDelegate::new(ds), }; + // Set fullscreen mode after we setup everything + if let Some(ref monitor) = win_attribs.fullscreen { + unsafe { + if monitor.inner != get_current_monitor().inner { + unimplemented!(); + } + } + window.set_fullscreen(Some(monitor.clone())); + } + + // Make key have to be after set fullscreen + // to prevent normal size window brefly appears + unsafe { + if win_attribs.visible { + window.window.makeKeyAndOrderFront_(nil); + } else { + window.window.makeKeyWindow(); + } + } + + if win_attribs.maximized { + window.delegate.state.perform_maximized(win_attribs.maximized); + } + Ok(window) } @@ -424,19 +631,11 @@ impl Window2 { } }; - let masks = if screen.is_some() { - // Fullscreen window - NSWindowStyleMask::NSBorderlessWindowMask | - NSWindowStyleMask::NSResizableWindowMask | - NSWindowStyleMask::NSTitledWindowMask - } else if !attrs.decorations { - // Window2 without a titlebar - NSWindowStyleMask::NSBorderlessWindowMask - } else if pl_attrs.titlebar_hidden { + let masks = if pl_attrs.titlebar_hidden { NSWindowStyleMask::NSBorderlessWindowMask | NSWindowStyleMask::NSResizableWindowMask - } else if !pl_attrs.titlebar_transparent { - // Window2 with a titlebar + } else if pl_attrs.titlebar_transparent { + // Window2 with a transparent titlebar and regular content view NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask | NSWindowStyleMask::NSResizableWindowMask | @@ -449,11 +648,16 @@ impl Window2 { NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSFullSizeContentViewWindowMask } else { - // Window2 with a transparent titlebar and regular content view - NSWindowStyleMask::NSClosableWindowMask | - NSWindowStyleMask::NSMiniaturizableWindowMask | - NSWindowStyleMask::NSResizableWindowMask | - NSWindowStyleMask::NSTitledWindowMask + if !attrs.decorations && !screen.is_some() { + // Window2 without a titlebar + NSWindowStyleMask::NSBorderlessWindowMask + } else { + // Window2 with a titlebar + NSWindowStyleMask::NSClosableWindowMask | + NSWindowStyleMask::NSMiniaturizableWindowMask | + NSWindowStyleMask::NSResizableWindowMask | + NSWindowStyleMask::NSTitledWindowMask + } }; let winit_window = Class::get("WinitWindow").unwrap_or_else(|| { @@ -504,12 +708,7 @@ impl Window2 { window.setTitlebarAppearsTransparent_(YES); } - if screen.is_some() { - window.setLevel_(appkit::NSMainMenuWindowLevel as i64 + 1); - } - else { - window.center(); - } + window.center(); window }) } @@ -517,7 +716,7 @@ impl Window2 { fn create_view(window: id) -> Option { unsafe { - let view = IdRef::new(NSView::alloc(nil).init()); + let view = IdRef::new(NSView::init(NSView::alloc(nil))); view.non_nil().map(|view| { view.setWantsBestResolutionOpenGLSurface_(YES); window.setContentView_(*view); @@ -719,23 +918,91 @@ impl Window2 { } #[inline] - pub fn set_maximized(&self, _maximized: bool) { - unimplemented!() + pub fn set_maximized(&self, maximized: bool) { + self.delegate.state.perform_maximized(maximized) } #[inline] - pub fn set_fullscreen(&self, _monitor: Option) { - unimplemented!() + /// TODO: Right now set_fullscreen do not work on switching monitors + /// in fullscreen mode + pub fn set_fullscreen(&self, monitor: Option) { + let state = &self.delegate.state; + let current = { + let win_attribs = state.win_attribs.borrow_mut(); + + let current = win_attribs.fullscreen.clone(); + match (¤t, monitor) { + (&None, None) => { + return; + } + (&Some(ref a), Some(ref b)) if a.inner != b.inner => { + unimplemented!(); + } + (&Some(_), Some(_)) => { + return; + } + _ => (), + } + + current + }; + + unsafe { + // Because toggleFullScreen will not work if the StyleMask is none, + // We set a normal style to it temporary. + // It will clean up at window_did_exit_fullscreen. + if current.is_none() { + let curr_mask = state.window.styleMask(); + + if !curr_mask.contains(NSWindowStyleMask::NSTitledWindowMask) { + state.window.setStyleMask_( + NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSResizableWindowMask, + ); + state.save_style_mask.set(Some(curr_mask)); + } + } + + self.window.toggleFullScreen_(nil); + } } #[inline] - pub fn set_decorations(&self, _decorations: bool) { - unimplemented!() + pub fn set_decorations(&self, decorations: bool) { + let state = &self.delegate.state; + let mut win_attribs = state.win_attribs.borrow_mut(); + + if win_attribs.decorations == decorations { + return; + } + + win_attribs.decorations = decorations; + + // Skip modifiy if we are in fullscreen mode, + // window_did_exit_fullscreen will handle it + if win_attribs.fullscreen.is_some() { + return; + } + + unsafe { + let new_mask = if decorations { + NSWindowStyleMask::NSClosableWindowMask + | NSWindowStyleMask::NSMiniaturizableWindowMask + | NSWindowStyleMask::NSResizableWindowMask + | NSWindowStyleMask::NSTitledWindowMask + } else { + NSWindowStyleMask::NSBorderlessWindowMask + }; + + state.window.setStyleMask_(new_mask); + } } #[inline] pub fn get_current_monitor(&self) -> RootMonitorId { - unimplemented!() + unsafe { + self::get_current_monitor() + } } }