Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,7 @@ dependencies = [
"egui",
"log",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"objc2-ui-kit 0.3.2",
"profiling",
Expand Down
10 changes: 10 additions & 0 deletions crates/egui-winit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ document-features = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
webbrowser = { workspace = true, optional = true }

[target.'cfg(target_os = "macos")'.dependencies]
objc2.workspace = true
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSResponder",
"NSView",
"NSWindow",
] }

[target.'cfg(target_os = "ios")'.dependencies]
objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
Expand Down
23 changes: 23 additions & 0 deletions crates/egui-winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
pub use winit;

pub mod clipboard;
#[cfg(target_os = "macos")]
mod macos;
mod safe_area;
mod window_settings;

Expand Down Expand Up @@ -1826,10 +1828,16 @@ fn process_viewport_command(
info.maximized = Some(v);
}
ViewportCommand::Fullscreen(v) => {
#[cfg(target_os = "macos")]
if v {
macos::clear_fullscreen_auxiliary(window);
}
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
}
ViewportCommand::SetMonitor(idx) => {
if let Some(monitor) = window.available_monitors().nth(idx) {
#[cfg(target_os = "macos")]
macos::clear_fullscreen_auxiliary(window);
window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
} else {
log::warn!(
Expand Down Expand Up @@ -1972,6 +1980,9 @@ pub fn create_winit_window_attributes(
) -> winit::window::WindowAttributes {
profiling::function_scope!();

#[cfg(target_os = "macos")]
let fullscreen_auxiliary = macos::should_be_fullscreen_auxiliary(&viewport_builder);

let ViewportBuilder {
title,
position,
Expand All @@ -1998,6 +2009,7 @@ pub fn create_winit_window_attributes(
titlebar_buttons_shown: _titlebar_buttons_shown,
titlebar_shown: _titlebar_shown,
has_shadow: _has_shadow,
fullscreen_auxiliary: _, // decided by `macos::should_be_fullscreen_auxiliary` below

// Windows:
drag_and_drop: _drag_and_drop,
Expand Down Expand Up @@ -2156,6 +2168,14 @@ pub fn create_winit_window_attributes(
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
.with_has_shadow(_has_shadow.unwrap_or(true));

if fullscreen_auxiliary {
// The fullscreen-auxiliary collection behavior must be set before the window
// is first ordered on screen, and `winit` shows the window during creation.
// So create the window hidden;
// `apply_viewport_builder_to_window` will mark it and then show it.
window_attributes = window_attributes.with_visible(false);
}
}

window_attributes
Expand Down Expand Up @@ -2226,6 +2246,9 @@ pub fn apply_viewport_builder_to_window(
window.set_maximized(maximized);
}
}

#[cfg(target_os = "macos")]
macos::apply_fullscreen_auxiliary(window, builder);
}

// ---------------------------------------------------------------------------
Expand Down
110 changes: 110 additions & 0 deletions crates/egui-winit/src/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! macOS-specific handling of native viewport windows.
//!
//! `winit` 0.30 does not expose `NSWindowCollectionBehavior`, so we set it here via
//! `objc2-app-kit`. Once egui is on a `winit` version with fullscreen-auxiliary support
//! (proposed upstream in <https://github.com/rust-windowing/winit/pull/4614>),
//! most of this module can be replaced by
//! `WindowAttributesMacOS::with_fullscreen_auxiliary` etc., keeping only the default
//! policy in [`should_be_fullscreen_auxiliary`].

use egui::ViewportBuilder;
use objc2::{MainThreadMarker, rc::Retained};
use objc2_app_kit::{
NSApplication, NSView, NSWindow, NSWindowCollectionBehavior, NSWindowStyleMask,
};
use raw_window_handle::{HasWindowHandle as _, RawWindowHandle};
use winit::window::Window;

/// Should the window created for this viewport be marked as a
/// "fullscreen auxiliary" window (`NSWindowCollectionBehaviorFullScreenAuxiliary`)?
///
/// See [`ViewportBuilder::with_fullscreen_auxiliary`].
pub(crate) fn should_be_fullscreen_auxiliary(builder: &ViewportBuilder) -> bool {
if let Some(explicit) = builder.fullscreen_auxiliary {
return explicit;
}

// Default: if the app currently has a native fullscreen window on the active Space,
// showing a normal new window would make macOS renegotiate the Space,
// flickering and potentially aborting the fullscreen state or triggering
// a Split View (https://github.com/emilk/egui/issues/8259).
// So mark the new window as an auxiliary window that can share the fullscreen Space —
// unless it wants to become fullscreen itself,
// which requires the (mutually exclusive) primary fullscreen behavior.
let wants_fullscreen = builder.fullscreen.unwrap_or(false) || builder.monitor.is_some();
!wants_fullscreen && app_has_fullscreen_window_on_active_space()
}

fn app_has_fullscreen_window_on_active_space() -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false; // AppKit windows can only be inspected on the main thread
};
let app = NSApplication::sharedApplication(mtm);
app.windows().iter().any(|window| {
window.styleMask().contains(NSWindowStyleMask::FullScreen) && window.isOnActiveSpace()
})
}

/// Finish the initialization of a window that [`should_be_fullscreen_auxiliary`],
/// and was therefore created hidden (see `create_winit_window_attributes`):
/// mark it as a fullscreen-auxiliary window,
/// so that ordering it on screen won't disturb any active fullscreen Space.
pub(crate) fn apply_fullscreen_auxiliary(window: &Window, builder: &ViewportBuilder) {
if should_be_fullscreen_auxiliary(builder) {
let Some(ns_window) = ns_window_from_winit(window) else {
log::warn!("Failed to get NSWindow to mark the window as fullscreen-auxiliary");
return;
};
log::debug!(
"Marking new window {:?} as fullscreen-auxiliary",
builder.title
);
ns_window.setCollectionBehavior(
ns_window.collectionBehavior() | NSWindowCollectionBehavior::FullScreenAuxiliary,
);
}

// The window was created hidden so that the collection behavior above
// takes effect before the window is first ordered on screen.
// Show it now, if the builder asked for a visible window:
if builder.visible.unwrap_or(true) && window.is_visible() == Some(false) {
let Some(ns_window) = ns_window_from_winit(window) else {
log::warn!("Failed to get NSWindow to show the window");
return;
};
if builder.active.unwrap_or(true) {
ns_window.makeKeyAndOrderFront(None);
} else {
ns_window.orderFront(None);
}
}
}

/// A window marked as fullscreen-auxiliary cannot enter native fullscreen,
/// so clear the flag before any attempt to make the window fullscreen.
///
/// See [`ViewportBuilder::with_fullscreen_auxiliary`].
pub(crate) fn clear_fullscreen_auxiliary(window: &Window) {
let Some(ns_window) = ns_window_from_winit(window) else {
log::warn!("Failed to get NSWindow to clear the fullscreen-auxiliary state");
return;
};
let behavior = ns_window.collectionBehavior();
if behavior.contains(NSWindowCollectionBehavior::FullScreenAuxiliary) {
ns_window.setCollectionBehavior(behavior - NSWindowCollectionBehavior::FullScreenAuxiliary);
}
}

fn ns_window_from_winit(window: &Window) -> Option<Retained<NSWindow>> {
let handle = window.window_handle().ok()?.as_raw();
let RawWindowHandle::AppKit(handle) = handle else {
return None;
};
let ns_view = handle.ns_view.as_ptr().cast::<NSView>();

// SAFETY: the pointer comes from winit, and is valid for as long as `window` is
#[expect(unsafe_code)]
let ns_view = unsafe { ns_view.as_ref() }?;

ns_view.window()
}
34 changes: 34 additions & 0 deletions crates/egui/src/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ pub struct ViewportBuilder {
pub titlebar_buttons_shown: Option<bool>,
pub titlebar_shown: Option<bool>,
pub has_shadow: Option<bool>,
pub fullscreen_auxiliary: Option<bool>,

// windows:
pub drag_and_drop: Option<bool>,
Expand Down Expand Up @@ -515,6 +516,31 @@ impl ViewportBuilder {
self
}

/// macOS: Set to `true` to mark the window as a "fullscreen auxiliary" window
/// (`NSWindowCollectionBehaviorFullScreenAuxiliary`),
/// which can be shown on the same Space (virtual desktop) as a fullscreen window.
///
/// Without this, opening a new native window while another window of the app
/// is in native fullscreen on the active Space makes macOS renegotiate the Space:
/// the new window flickers, and the fullscreen state can be aborted or turned
/// into a Split View (see <https://github.com/emilk/egui/issues/8259>).
///
/// The trade-off: an auxiliary window cannot itself enter native fullscreen
/// (the green traffic-light button will zoom the window instead).
/// Sending [`ViewportCommand::Fullscreen`] `(true)` clears the flag again,
/// so requesting fullscreen at runtime still works.
///
/// When not set, the default is to mark the window as fullscreen-auxiliary
/// if and only if it is created while another window of the app is in native
/// fullscreen on the active Space, and this viewport does not itself request
/// fullscreen ([`Self::with_fullscreen`] / [`Self::with_monitor`]).
/// Set this to `false` to opt out of that behavior.
#[inline]
pub fn with_fullscreen_auxiliary(mut self, fullscreen_auxiliary: bool) -> Self {
self.fullscreen_auxiliary = Some(fullscreen_auxiliary);
self
}

/// windows: Whether show or hide the window icon in the taskbar.
#[inline]
pub fn with_taskbar(mut self, show: bool) -> Self {
Expand Down Expand Up @@ -736,6 +762,7 @@ impl ViewportBuilder {
titlebar_buttons_shown: new_titlebar_buttons_shown,
titlebar_shown: new_titlebar_shown,
has_shadow: new_has_shadow,
fullscreen_auxiliary: new_fullscreen_auxiliary,
close_button: new_close_button,
minimize_button: new_minimize_button,
maximize_button: new_maximize_button,
Expand Down Expand Up @@ -913,6 +940,13 @@ impl ViewportBuilder {
recreate_window = true;
}

if new_fullscreen_auxiliary.is_some()
&& self.fullscreen_auxiliary != new_fullscreen_auxiliary
{
self.fullscreen_auxiliary = new_fullscreen_auxiliary;
recreate_window = true;
}

if new_taskbar.is_some() && self.taskbar != new_taskbar {
self.taskbar = new_taskbar;
recreate_window = true;
Expand Down
Loading