From 92b643c3f177ad2fabe1a88256ab91f2fb8080f3 Mon Sep 17 00:00:00 2001 From: Steffen Moeller Date: Fri, 3 Jul 2026 23:20:14 +0200 Subject: [PATCH] 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 --- winit-appkit/src/lib.rs | 44 +++++++++++++++++++++++++++ winit-appkit/src/window_delegate.rs | 46 +++++++++++++++++++++++++++-- winit/src/changelog/unreleased.md | 5 ++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index 8548f8f73..3ff9c82f4 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -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::().unwrap(); window.maybe_wait_on_main(|w| w.unified_titlebar()) } + + #[inline] + fn set_fullscreen_auxiliary(&self, fullscreen_auxiliary: bool) { + let window = self.cast_ref::().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::().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, } } } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 9a6515016..8bc14e3df 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -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 = diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 420859434..e2f7326ea 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -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