From bfbcab3a010667e74fa3cc07ac85bd4c85491e65 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 19 Dec 2018 15:07:33 +1100 Subject: [PATCH] feat: add macos simple fullscreen (#692) * feat: add macos simple fullscreen * move impl to WindowExt * feedback: remove warning, unused file and rename param * feedback: combine fullscreen examples into one example * fix: ensure decorations and maximize do not toggle while in fullscreen * fix: prevent warning on non-macos platforms * feedback: make changelog more explicit * fix: prevent unconditional construction of NSRect * fix: don't try to set_simple_fullscreen if already using native fullscreen * fix: ensure set_simple_fullscreen plays nicely with set_fullscreen * fix: do not enter native fullscreen if simple fullscreen is active --- CHANGELOG.md | 1 + examples/fullscreen.rs | 80 ++++++++++++++++++++------ src/os/macos.rs | 14 +++++ src/platform/macos/util.rs | 14 +++++ src/platform/macos/window.rs | 108 ++++++++++++++++++++++++++++++----- 5 files changed, 183 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9506aee3..1a913a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - On X11 with a tiling WM, fixed high CPU usage when moving windows across monitors. - On X11, fixed panic caused by dropping the window before running the event loop. +- on macOS, added `WindowExt::set_simple_fullscreen` which does not require a separate space - Introduce `WindowBuilderExt::with_app_id` to allow setting the application ID on Wayland. - On Windows, catch panics in event loop child thread and forward them to the parent thread. This prevents an invocation of undefined behavior due to unwinding into foreign code. - On Windows, fix issue where resizing or moving window combined with grabbing the cursor would freeze program. diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index a6cb5b68..c29013c4 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -6,35 +6,46 @@ use winit::{ControlFlow, Event, WindowEvent}; fn main() { let mut events_loop = winit::EventsLoop::new(); - // enumerating monitors + #[cfg(target_os = "macos")] + let mut macos_use_simple_fullscreen = false; + let monitor = { - for (num, monitor) in events_loop.get_available_monitors().enumerate() { - println!("Monitor #{}: {:?}", num, monitor.get_name()); + // On macOS there are two fullscreen modes "native" and "simple" + #[cfg(target_os = "macos")] + { + print!("Please choose the fullscreen mode: (1) native, (2) simple: "); + io::stdout().flush().unwrap(); + + let mut num = String::new(); + io::stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); + match num { + 2 => macos_use_simple_fullscreen = true, + _ => {} + } + + // Prompt for monitor when using native fullscreen + if !macos_use_simple_fullscreen { + Some(prompt_for_monitor(&events_loop)) + } else { + None + } } - print!("Please write the number of the monitor to use: "); - io::stdout().flush().unwrap(); - - let mut num = String::new(); - io::stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().ok().expect("Please enter a number"); - let monitor = events_loop.get_available_monitors().nth(num).expect("Please enter a valid ID"); - - println!("Using {:?}", monitor.get_name()); - - monitor + #[cfg(not(target_os = "macos"))] + Some(prompt_for_monitor(&events_loop)) }; + let mut is_fullscreen = monitor.is_some(); + let mut is_maximized = false; + let mut decorations = true; + let window = winit::WindowBuilder::new() .with_title("Hello world!") - .with_fullscreen(Some(monitor)) + .with_fullscreen(monitor) .build(&events_loop) .unwrap(); - let mut is_fullscreen = true; - let mut is_maximized = false; - let mut decorations = true; - events_loop.run_forever(|event| { println!("{:?}", event); @@ -52,6 +63,18 @@ fn main() { } => match (virtual_code, state) { (winit::VirtualKeyCode::Escape, _) => return ControlFlow::Break, (winit::VirtualKeyCode::F, winit::ElementState::Pressed) => { + #[cfg(target_os = "macos")] + { + if macos_use_simple_fullscreen { + use winit::os::macos::WindowExt; + if WindowExt::set_simple_fullscreen(&window, !is_fullscreen) { + is_fullscreen = !is_fullscreen; + } + + return ControlFlow::Continue; + } + } + is_fullscreen = !is_fullscreen; if !is_fullscreen { window.set_fullscreen(None); @@ -77,3 +100,22 @@ fn main() { ControlFlow::Continue }); } + +// Enumerate monitors and prompt user to choose one +fn prompt_for_monitor(events_loop: &winit::EventsLoop) -> winit::MonitorId { + for (num, monitor) in events_loop.get_available_monitors().enumerate() { + println!("Monitor #{}: {:?}", num, monitor.get_name()); + } + + print!("Please write the number of the monitor to use: "); + io::stdout().flush().unwrap(); + + let mut num = String::new(); + io::stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); + let monitor = events_loop.get_available_monitors().nth(num).expect("Please enter a valid ID"); + + println!("Using {:?}", monitor.get_name()); + + monitor +} diff --git a/src/os/macos.rs b/src/os/macos.rs index 7118eeb9..b9ea993b 100644 --- a/src/os/macos.rs +++ b/src/os/macos.rs @@ -22,6 +22,15 @@ pub trait WindowExt { /// - `false`: the dock icon will only bounce once. /// - `true`: the dock icon will bounce until the application is focused. fn request_user_attention(&self, is_critical: bool); + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this + /// won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another + /// space or taking control over the entire monitor. + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; } impl WindowExt for Window { @@ -39,6 +48,11 @@ impl WindowExt for Window { fn request_user_attention(&self, is_critical: bool) { self.window.request_user_attention(is_critical) } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window.set_simple_fullscreen(fullscreen) + } } /// Corresponds to `NSApplicationActivationPolicy`. diff --git a/src/platform/macos/util.rs b/src/platform/macos/util.rs index 2e5d6b49..2d8e4f34 100644 --- a/src/platform/macos/util.rs +++ b/src/platform/macos/util.rs @@ -25,6 +25,20 @@ pub unsafe fn set_style_mask(window: id, view: id, mask: NSWindowStyleMask) { window.makeFirstResponder_(view); } +pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, on: bool) { + use cocoa::appkit::NSWindow; + + let current_style_mask = window.styleMask(); + if on { + window.setStyleMask_(current_style_mask | mask); + } else { + window.setStyleMask_(current_style_mask & (!mask)); + } + + // If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly! + window.makeFirstResponder_(view); +} + pub unsafe fn create_input_context(view: id) -> IdRef { let input_context: id = msg_send![class!(NSTextInputContext), alloc]; let input_context: id = msg_send![input_context, initWithClient:view]; diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 881002c3..90a258df 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -19,6 +19,7 @@ use cocoa::appkit::{ NSWindowButton, NSWindowStyleMask, NSApplicationActivationPolicy, + NSApplicationPresentationOptions, }; use cocoa::base::{id, nil}; use cocoa::foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString}; @@ -57,7 +58,9 @@ pub struct DelegateState { win_attribs: RefCell, standard_frame: Cell>, + is_simple_fullscreen: Cell, save_style_mask: Cell>, + save_presentation_opts: Cell>, // This is set when WindowBuilder::with_fullscreen was set, // see comments of `window_did_fail_to_enter_fullscreen` @@ -94,22 +97,30 @@ impl DelegateState { } } + unsafe fn saved_style_mask(&self, resizable: bool) -> NSWindowStyleMask { + let base_mask = self.save_style_mask + .take() + .unwrap_or_else(|| self.window.styleMask()); + if resizable { + base_mask | NSWindowStyleMask::NSResizableWindowMask + } else { + base_mask & !NSWindowStyleMask::NSResizableWindowMask + } + } + + fn saved_standard_frame(&self) -> NSRect { + self.standard_frame.get().unwrap_or_else(|| NSRect::new( + NSPoint::new(50.0, 50.0), + NSSize::new(800.0, 600.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 mask = { - let base_mask = self.save_style_mask - .take() - .unwrap_or_else(|| self.window.styleMask()); - if win_attribs.resizable { - base_mask | NSWindowStyleMask::NSResizableWindowMask - } else { - base_mask & !NSWindowStyleMask::NSResizableWindowMask - } - }; - + let mask = self.saved_style_mask(win_attribs.resizable); util::set_style_mask(*self.window, *self.view, mask); win_attribs.maximized @@ -151,10 +162,7 @@ impl DelegateState { 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.saved_standard_frame() }; self.window.setFrame_display_(new_rect, 0); @@ -600,6 +608,68 @@ impl WindowExt for Window2 { NSApp().requestUserAttention_(request_type); } } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + let state = &self.delegate.state; + + unsafe { + let app = NSApp(); + let win_attribs = state.win_attribs.borrow_mut(); + let is_native_fullscreen = win_attribs.fullscreen.is_some(); + let is_simple_fullscreen = state.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 + state.standard_frame.set(Some(NSWindow::frame(*self.window))); + state.save_style_mask.set(Some(self.window.styleMask())); + state.save_presentation_opts.set(Some(app.presentationOptions_())); + + // Tell our window's state that we're in fullscreen + state.is_simple_fullscreen.set(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.window, *self.view, NSWindowStyleMask::NSTitledWindowMask, false); + + // Set the window frame to the screen frame size + let screen = self.window.screen(); + let screen_frame = NSScreen::frame(screen); + NSWindow::setFrame_display_(*self.window, screen_frame, YES); + + // Fullscreen windows can't be resized, minimized, or moved + util::toggle_style_mask(*self.window, *self.view, NSWindowStyleMask::NSMiniaturizableWindowMask, false); + util::toggle_style_mask(*self.window, *self.view, NSWindowStyleMask::NSResizableWindowMask, false); + NSWindow::setMovable_(*self.window, NO); + + true + } else { + let saved_style_mask = state.saved_style_mask(win_attribs.resizable); + util::set_style_mask(*self.window, *self.view, saved_style_mask); + state.is_simple_fullscreen.set(false); + + if let Some(presentation_opts) = state.save_presentation_opts.get() { + app.setPresentationOptions_(presentation_opts); + } + + let frame = state.saved_standard_frame(); + NSWindow::setFrame_display_(*self.window, frame, YES); + NSWindow::setMovable_(*self.window, YES); + + true + } + } + } } impl Window2 { @@ -675,7 +745,9 @@ impl Window2 { shared, win_attribs: RefCell::new(win_attribs.clone()), standard_frame: Cell::new(None), + is_simple_fullscreen: Cell::new(false), save_style_mask: Cell::new(None), + save_presentation_opts: Cell::new(None), handle_with_fullscreen: win_attribs.fullscreen.is_some(), previous_position: None, previous_dpi_factor: dpi_factor, @@ -1086,6 +1158,12 @@ impl Window2 { /// in fullscreen mode pub fn set_fullscreen(&self, monitor: Option) { let state = &self.delegate.state; + + // Do nothing if simple fullscreen is active. + if state.is_simple_fullscreen.get() { + return + } + let current = { let win_attribs = state.win_attribs.borrow_mut();