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
This commit is contained in:
acheronfail 2018-12-19 15:07:33 +11:00 committed by Francesca Plebani
parent 4b4c73cee4
commit bfbcab3a01
5 changed files with 183 additions and 34 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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`.

View file

@ -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];

View file

@ -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<WindowAttributes>,
standard_frame: Cell<Option<NSRect>>,
is_simple_fullscreen: Cell<bool>,
save_style_mask: Cell<Option<NSWindowStyleMask>>,
save_presentation_opts: Cell<Option<NSApplicationPresentationOptions>>,
// 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<RootMonitorId>) {
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();