Skip to content

Commit bfbcab3

Browse files
acheronfailfrancesca64
authored andcommitted
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
1 parent 4b4c73c commit bfbcab3

File tree

5 files changed

+182
-33
lines changed

5 files changed

+182
-33
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- On X11 with a tiling WM, fixed high CPU usage when moving windows across monitors.
44
- On X11, fixed panic caused by dropping the window before running the event loop.
5+
- on macOS, added `WindowExt::set_simple_fullscreen` which does not require a separate space
56
- Introduce `WindowBuilderExt::with_app_id` to allow setting the application ID on Wayland.
67
- 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.
78
- On Windows, fix issue where resizing or moving window combined with grabbing the cursor would freeze program.

examples/fullscreen.rs

+60-18
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,46 @@ use winit::{ControlFlow, Event, WindowEvent};
66
fn main() {
77
let mut events_loop = winit::EventsLoop::new();
88

9-
// enumerating monitors
10-
let monitor = {
11-
for (num, monitor) in events_loop.get_available_monitors().enumerate() {
12-
println!("Monitor #{}: {:?}", num, monitor.get_name());
13-
}
9+
#[cfg(target_os = "macos")]
10+
let mut macos_use_simple_fullscreen = false;
1411

15-
print!("Please write the number of the monitor to use: ");
16-
io::stdout().flush().unwrap();
12+
let monitor = {
13+
// On macOS there are two fullscreen modes "native" and "simple"
14+
#[cfg(target_os = "macos")]
15+
{
16+
print!("Please choose the fullscreen mode: (1) native, (2) simple: ");
17+
io::stdout().flush().unwrap();
1718

18-
let mut num = String::new();
19-
io::stdin().read_line(&mut num).unwrap();
20-
let num = num.trim().parse().ok().expect("Please enter a number");
21-
let monitor = events_loop.get_available_monitors().nth(num).expect("Please enter a valid ID");
19+
let mut num = String::new();
20+
io::stdin().read_line(&mut num).unwrap();
21+
let num = num.trim().parse().ok().expect("Please enter a number");
22+
match num {
23+
2 => macos_use_simple_fullscreen = true,
24+
_ => {}
25+
}
2226

23-
println!("Using {:?}", monitor.get_name());
27+
// Prompt for monitor when using native fullscreen
28+
if !macos_use_simple_fullscreen {
29+
Some(prompt_for_monitor(&events_loop))
30+
} else {
31+
None
32+
}
33+
}
2434

25-
monitor
35+
#[cfg(not(target_os = "macos"))]
36+
Some(prompt_for_monitor(&events_loop))
2637
};
2738

39+
let mut is_fullscreen = monitor.is_some();
40+
let mut is_maximized = false;
41+
let mut decorations = true;
42+
2843
let window = winit::WindowBuilder::new()
2944
.with_title("Hello world!")
30-
.with_fullscreen(Some(monitor))
45+
.with_fullscreen(monitor)
3146
.build(&events_loop)
3247
.unwrap();
3348

34-
let mut is_fullscreen = true;
35-
let mut is_maximized = false;
36-
let mut decorations = true;
37-
3849
events_loop.run_forever(|event| {
3950
println!("{:?}", event);
4051

@@ -52,6 +63,18 @@ fn main() {
5263
} => match (virtual_code, state) {
5364
(winit::VirtualKeyCode::Escape, _) => return ControlFlow::Break,
5465
(winit::VirtualKeyCode::F, winit::ElementState::Pressed) => {
66+
#[cfg(target_os = "macos")]
67+
{
68+
if macos_use_simple_fullscreen {
69+
use winit::os::macos::WindowExt;
70+
if WindowExt::set_simple_fullscreen(&window, !is_fullscreen) {
71+
is_fullscreen = !is_fullscreen;
72+
}
73+
74+
return ControlFlow::Continue;
75+
}
76+
}
77+
5578
is_fullscreen = !is_fullscreen;
5679
if !is_fullscreen {
5780
window.set_fullscreen(None);
@@ -77,3 +100,22 @@ fn main() {
77100
ControlFlow::Continue
78101
});
79102
}
103+
104+
// Enumerate monitors and prompt user to choose one
105+
fn prompt_for_monitor(events_loop: &winit::EventsLoop) -> winit::MonitorId {
106+
for (num, monitor) in events_loop.get_available_monitors().enumerate() {
107+
println!("Monitor #{}: {:?}", num, monitor.get_name());
108+
}
109+
110+
print!("Please write the number of the monitor to use: ");
111+
io::stdout().flush().unwrap();
112+
113+
let mut num = String::new();
114+
io::stdin().read_line(&mut num).unwrap();
115+
let num = num.trim().parse().ok().expect("Please enter a number");
116+
let monitor = events_loop.get_available_monitors().nth(num).expect("Please enter a valid ID");
117+
118+
println!("Using {:?}", monitor.get_name());
119+
120+
monitor
121+
}

src/os/macos.rs

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ pub trait WindowExt {
2222
/// - `false`: the dock icon will only bounce once.
2323
/// - `true`: the dock icon will bounce until the application is focused.
2424
fn request_user_attention(&self, is_critical: bool);
25+
26+
/// Toggles a fullscreen mode that doesn't require a new macOS space.
27+
/// Returns a boolean indicating whether the transition was successful (this
28+
/// won't work if the window was already in the native fullscreen).
29+
///
30+
/// This is how fullscreen used to work on macOS in versions before Lion.
31+
/// And allows the user to have a fullscreen window without using another
32+
/// space or taking control over the entire monitor.
33+
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool;
2534
}
2635

2736
impl WindowExt for Window {
@@ -39,6 +48,11 @@ impl WindowExt for Window {
3948
fn request_user_attention(&self, is_critical: bool) {
4049
self.window.request_user_attention(is_critical)
4150
}
51+
52+
#[inline]
53+
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
54+
self.window.set_simple_fullscreen(fullscreen)
55+
}
4256
}
4357

4458
/// Corresponds to `NSApplicationActivationPolicy`.

src/platform/macos/util.rs

+14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ pub unsafe fn set_style_mask(window: id, view: id, mask: NSWindowStyleMask) {
2525
window.makeFirstResponder_(view);
2626
}
2727

28+
pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, on: bool) {
29+
use cocoa::appkit::NSWindow;
30+
31+
let current_style_mask = window.styleMask();
32+
if on {
33+
window.setStyleMask_(current_style_mask | mask);
34+
} else {
35+
window.setStyleMask_(current_style_mask & (!mask));
36+
}
37+
38+
// If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly!
39+
window.makeFirstResponder_(view);
40+
}
41+
2842
pub unsafe fn create_input_context(view: id) -> IdRef {
2943
let input_context: id = msg_send![class!(NSTextInputContext), alloc];
3044
let input_context: id = msg_send![input_context, initWithClient:view];

src/platform/macos/window.rs

+93-15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use cocoa::appkit::{
1919
NSWindowButton,
2020
NSWindowStyleMask,
2121
NSApplicationActivationPolicy,
22+
NSApplicationPresentationOptions,
2223
};
2324
use cocoa::base::{id, nil};
2425
use cocoa::foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString};
@@ -57,7 +58,9 @@ pub struct DelegateState {
5758

5859
win_attribs: RefCell<WindowAttributes>,
5960
standard_frame: Cell<Option<NSRect>>,
61+
is_simple_fullscreen: Cell<bool>,
6062
save_style_mask: Cell<Option<NSWindowStyleMask>>,
63+
save_presentation_opts: Cell<Option<NSApplicationPresentationOptions>>,
6164

6265
// This is set when WindowBuilder::with_fullscreen was set,
6366
// see comments of `window_did_fail_to_enter_fullscreen`
@@ -94,22 +97,30 @@ impl DelegateState {
9497
}
9598
}
9699

100+
unsafe fn saved_style_mask(&self, resizable: bool) -> NSWindowStyleMask {
101+
let base_mask = self.save_style_mask
102+
.take()
103+
.unwrap_or_else(|| self.window.styleMask());
104+
if resizable {
105+
base_mask | NSWindowStyleMask::NSResizableWindowMask
106+
} else {
107+
base_mask & !NSWindowStyleMask::NSResizableWindowMask
108+
}
109+
}
110+
111+
fn saved_standard_frame(&self) -> NSRect {
112+
self.standard_frame.get().unwrap_or_else(|| NSRect::new(
113+
NSPoint::new(50.0, 50.0),
114+
NSSize::new(800.0, 600.0),
115+
))
116+
}
117+
97118
fn restore_state_from_fullscreen(&mut self) {
98119
let maximized = unsafe {
99120
let mut win_attribs = self.win_attribs.borrow_mut();
100121
win_attribs.fullscreen = None;
101122

102-
let mask = {
103-
let base_mask = self.save_style_mask
104-
.take()
105-
.unwrap_or_else(|| self.window.styleMask());
106-
if win_attribs.resizable {
107-
base_mask | NSWindowStyleMask::NSResizableWindowMask
108-
} else {
109-
base_mask & !NSWindowStyleMask::NSResizableWindowMask
110-
}
111-
};
112-
123+
let mask = self.saved_style_mask(win_attribs.resizable);
113124
util::set_style_mask(*self.window, *self.view, mask);
114125

115126
win_attribs.maximized
@@ -151,10 +162,7 @@ impl DelegateState {
151162
let screen = NSScreen::mainScreen(nil);
152163
NSScreen::visibleFrame(screen)
153164
} else {
154-
self.standard_frame.get().unwrap_or(NSRect::new(
155-
NSPoint::new(50.0, 50.0),
156-
NSSize::new(800.0, 600.0),
157-
))
165+
self.saved_standard_frame()
158166
};
159167

160168
self.window.setFrame_display_(new_rect, 0);
@@ -600,6 +608,68 @@ impl WindowExt for Window2 {
600608
NSApp().requestUserAttention_(request_type);
601609
}
602610
}
611+
612+
#[inline]
613+
fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
614+
let state = &self.delegate.state;
615+
616+
unsafe {
617+
let app = NSApp();
618+
let win_attribs = state.win_attribs.borrow_mut();
619+
let is_native_fullscreen = win_attribs.fullscreen.is_some();
620+
let is_simple_fullscreen = state.is_simple_fullscreen.get();
621+
622+
// Do nothing if native fullscreen is active.
623+
if is_native_fullscreen || (fullscreen && is_simple_fullscreen) || (!fullscreen && !is_simple_fullscreen) {
624+
return false;
625+
}
626+
627+
if fullscreen {
628+
// Remember the original window's settings
629+
state.standard_frame.set(Some(NSWindow::frame(*self.window)));
630+
state.save_style_mask.set(Some(self.window.styleMask()));
631+
state.save_presentation_opts.set(Some(app.presentationOptions_()));
632+
633+
// Tell our window's state that we're in fullscreen
634+
state.is_simple_fullscreen.set(true);
635+
636+
// Simulate pre-Lion fullscreen by hiding the dock and menu bar
637+
let presentation_options =
638+
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock |
639+
NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar;
640+
app.setPresentationOptions_(presentation_options);
641+
642+
// Hide the titlebar
643+
util::toggle_style_mask(*self.window, *self.view, NSWindowStyleMask::NSTitledWindowMask, false);
644+
645+
// Set the window frame to the screen frame size
646+
let screen = self.window.screen();
647+
let screen_frame = NSScreen::frame(screen);
648+
NSWindow::setFrame_display_(*self.window, screen_frame, YES);
649+
650+
// Fullscreen windows can't be resized, minimized, or moved
651+
util::toggle_style_mask(*self.window, *self.view, NSWindowStyleMask::NSMiniaturizableWindowMask, false);
652+
util::toggle_style_mask(*self.window, *self.view, NSWindowStyleMask::NSResizableWindowMask, false);
653+
NSWindow::setMovable_(*self.window, NO);
654+
655+
true
656+
} else {
657+
let saved_style_mask = state.saved_style_mask(win_attribs.resizable);
658+
util::set_style_mask(*self.window, *self.view, saved_style_mask);
659+
state.is_simple_fullscreen.set(false);
660+
661+
if let Some(presentation_opts) = state.save_presentation_opts.get() {
662+
app.setPresentationOptions_(presentation_opts);
663+
}
664+
665+
let frame = state.saved_standard_frame();
666+
NSWindow::setFrame_display_(*self.window, frame, YES);
667+
NSWindow::setMovable_(*self.window, YES);
668+
669+
true
670+
}
671+
}
672+
}
603673
}
604674

605675
impl Window2 {
@@ -675,7 +745,9 @@ impl Window2 {
675745
shared,
676746
win_attribs: RefCell::new(win_attribs.clone()),
677747
standard_frame: Cell::new(None),
748+
is_simple_fullscreen: Cell::new(false),
678749
save_style_mask: Cell::new(None),
750+
save_presentation_opts: Cell::new(None),
679751
handle_with_fullscreen: win_attribs.fullscreen.is_some(),
680752
previous_position: None,
681753
previous_dpi_factor: dpi_factor,
@@ -1086,6 +1158,12 @@ impl Window2 {
10861158
/// in fullscreen mode
10871159
pub fn set_fullscreen(&self, monitor: Option<RootMonitorId>) {
10881160
let state = &self.delegate.state;
1161+
1162+
// Do nothing if simple fullscreen is active.
1163+
if state.is_simple_fullscreen.get() {
1164+
return
1165+
}
1166+
10891167
let current = {
10901168
let win_attribs = state.win_attribs.borrow_mut();
10911169

0 commit comments

Comments
 (0)