On macOS, add fullscreen auxiliary window support

Add `WindowAttributesMacOS::with_fullscreen_auxiliary` and
`WindowExtMacOS::set_fullscreen_auxiliary` / `fullscreen_auxiliary`,
exposing `NSWindowCollectionBehaviorFullScreenAuxiliary`.

An auxiliary window can be shown on the same Space as a fullscreen
window. Without this, ordering a new window on screen while another
window of the application is fullscreen on the active Space makes macOS
switch Spaces or attempt Split View tiling, which flickers and can
abort the fullscreen state. This affects any application that opens
secondary windows (palettes, inspectors, tool windows) from a
fullscreen main window; see e.g.
https://github.com/emilk/egui/issues/8259 for a downstream bug caused
by this.

The attribute is applied in `new_window` so the collection behavior is
already in place when the window is first ordered on screen, which is
required for the fullscreen Space to remain undisturbed (and is not
achievable through the runtime setter, since winit shows the window
during creation).

Since `toggleFullScreen:` is silently ignored by AppKit on windows with
the auxiliary collection behavior, `set_fullscreen` now warns and bails
out early on such windows instead of recording a fullscreen state that
never materializes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Steffen Moeller
2026-07-03 23:20:14 +02:00
committed by Simon Hausmann
parent 9674d8ceef
commit 92b643c3f1
3 changed files with 92 additions and 3 deletions

View File

@@ -188,6 +188,25 @@ pub trait WindowExtMacOS {
/// Getter for the [`WindowExtMacOS::set_unified_titlebar`].
fn unified_titlebar(&self) -> bool;
/// Sets whether the window can be shown on the same Space as a fullscreen window.
///
/// This corresponds to [`NSWindowCollectionBehaviorFullScreenAuxiliary`], and is useful
/// for floating palettes, inspectors and other secondary windows accompanying a fullscreen
/// window. Without it, ordering a new window on screen while another window of the
/// application is fullscreen on the active Space makes macOS switch Spaces or attempt
/// Split View tiling.
///
/// A window marked as fullscreen auxiliary cannot itself enter (native) fullscreen;
/// [`Window::set_fullscreen`] will warn and do nothing. Call
/// `set_fullscreen_auxiliary(false)` first if you want to make the window fullscreen.
///
/// [`NSWindowCollectionBehaviorFullScreenAuxiliary`]: https://developer.apple.com/documentation/appkit/nswindow/collectionbehavior-swift.struct/fullscreenauxiliary?language=objc
/// [`Window::set_fullscreen`]: winit_core::window::Window::set_fullscreen
fn set_fullscreen_auxiliary(&self, fullscreen_auxiliary: bool);
/// Getter for the [`WindowExtMacOS::set_fullscreen_auxiliary`].
fn fullscreen_auxiliary(&self) -> bool;
}
impl WindowExtMacOS for dyn Window + '_ {
@@ -298,6 +317,18 @@ impl WindowExtMacOS for dyn Window + '_ {
let window = self.cast_ref::<AppKitWindow>().unwrap();
window.maybe_wait_on_main(|w| w.unified_titlebar())
}
#[inline]
fn set_fullscreen_auxiliary(&self, fullscreen_auxiliary: bool) {
let window = self.cast_ref::<AppKitWindow>().unwrap();
window.maybe_wait_on_main(move |w| w.set_fullscreen_auxiliary(fullscreen_auxiliary))
}
#[inline]
fn fullscreen_auxiliary(&self) -> bool {
let window = self.cast_ref::<AppKitWindow>().unwrap();
window.maybe_wait_on_main(|w| w.fullscreen_auxiliary())
}
}
/// Corresponds to `NSApplicationActivationPolicy`.
@@ -342,6 +373,7 @@ pub struct WindowAttributesMacOS {
pub(crate) borderless_game: bool,
pub(crate) unified_titlebar: bool,
pub(crate) panel: bool,
pub(crate) fullscreen_auxiliary: bool,
}
impl WindowAttributesMacOS {
@@ -449,6 +481,17 @@ impl WindowAttributesMacOS {
self.panel = panel;
self
}
/// See [`WindowExtMacOS::set_fullscreen_auxiliary`] for details on what this means if set.
///
/// Contrary to the runtime setter, setting this attribute guarantees that the collection
/// behavior is already in place when the window is first ordered on screen, which is
/// required to avoid disturbing an active fullscreen Space.
#[inline]
pub fn with_fullscreen_auxiliary(mut self, fullscreen_auxiliary: bool) -> Self {
self.fullscreen_auxiliary = fullscreen_auxiliary;
self
}
}
impl Default for WindowAttributesMacOS {
@@ -469,6 +512,7 @@ impl Default for WindowAttributesMacOS {
borderless_game: false,
unified_titlebar: false,
panel: false,
fullscreen_auxiliary: false,
}
}
}

View File

@@ -24,9 +24,9 @@ use objc2_app_kit::{
NSDraggingSession, NSDraggingSource, NSPasteboardTypeFileURL, NSPasteboardTypeHTML,
NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF,
NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification,
NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState,
NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode,
NSWindowTitleVisibility, NSWindowToolbarStyle,
NSWindow, NSWindowButton, NSWindowCollectionBehavior, NSWindowDelegate, NSWindowLevel,
NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle,
};
use objc2_core_foundation::{CGFloat, CGPoint};
use objc2_core_graphics::{
@@ -829,6 +829,14 @@ fn new_window(
if !macos_attrs.has_shadow {
window.setHasShadow(false);
}
if macos_attrs.fullscreen_auxiliary {
// This must happen before the window is ordered on screen (below, and in
// `WindowDelegate::new`), so that showing the window doesn't make macOS
// switch away from an active fullscreen Space or attempt Split View tiling.
window.setCollectionBehavior(
window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary,
);
}
// Popups are positioned relative to their parent in `WindowDelegate::new`.
if attrs.position.is_none() && !is_popup {
window.center();
@@ -1646,6 +1654,20 @@ impl WindowDelegate {
if self.ivars().is_simple_fullscreen.get() {
return;
}
if fullscreen.is_some()
&& self
.window()
.collectionBehavior()
.contains(NSWindowCollectionBehavior::FullScreenAuxiliary)
{
// `toggleFullScreen:` is silently ignored on such windows, which would leave our
// internal fullscreen state out of sync, so bail out early instead.
warn!(
"cannot fullscreen a window marked as fullscreen auxiliary; call \
`set_fullscreen_auxiliary(false)` first"
);
return;
}
if self.ivars().in_fullscreen_transition.get() {
// We can't set fullscreen here.
// Set fullscreen after transition.
@@ -2245,6 +2267,24 @@ impl WindowExtMacOS for WindowDelegate {
window.toolbar().is_some() && window.toolbarStyle() == NSWindowToolbarStyle::Unified
}
#[inline]
fn set_fullscreen_auxiliary(&self, fullscreen_auxiliary: bool) {
let window = self.window();
let behavior = window.collectionBehavior();
if fullscreen_auxiliary {
window
.setCollectionBehavior(behavior | NSWindowCollectionBehavior::FullScreenAuxiliary);
} else {
window
.setCollectionBehavior(behavior - NSWindowCollectionBehavior::FullScreenAuxiliary);
}
}
#[inline]
fn fullscreen_auxiliary(&self) -> bool {
self.window().collectionBehavior().contains(NSWindowCollectionBehavior::FullScreenAuxiliary)
}
}
const DEFAULT_STANDARD_FRAME: NSRect =

View File

@@ -52,6 +52,11 @@ changelog entry.
- On Wayland, added `HoldGesture` event for multi-finger hold gestures
- On Wayland, added ext-background-effect-v1 support.
- On Wayland, Windows and macOS, added native popups (`WindowType::Popup`).
- On macOS, add `WindowAttributesMacOS::with_fullscreen_auxiliary` and
`WindowExtMacOS::set_fullscreen_auxiliary` / `WindowExtMacOS::fullscreen_auxiliary`, allowing a
window to be shown on the same Space as a fullscreen window
(`NSWindowCollectionBehaviorFullScreenAuxiliary`) instead of triggering a Space switch or Split
View tiling.
### Changed