Implement ability to make non-activating window on macOS (#4035)

Fixes #3894
This commit is contained in:
Exidex 2024-12-13 00:56:39 +01:00 committed by GitHub
parent 5835c9102e
commit 4d5e68c6e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 86 additions and 37 deletions

View file

@ -128,6 +128,7 @@ objc2-app-kit = { version = "0.2.2", features = [
"NSMenu", "NSMenu",
"NSMenuItem", "NSMenuItem",
"NSOpenGLView", "NSOpenGLView",
"NSPanel",
"NSPasteboard", "NSPasteboard",
"NSResponder", "NSResponder",
"NSRunningApplication", "NSRunningApplication",

View file

@ -76,6 +76,7 @@ changelog entry.
- Added `Window::surface_position`, which is the position of the surface inside the window. - Added `Window::surface_position`, which is the position of the surface inside the window.
- Added `Window::safe_area`, which describes the area of the surface that is unobstructed. - Added `Window::safe_area`, which describes the area of the surface that is unobstructed.
- On X11, Wayland, Windows and macOS, improved scancode conversions for more obscure key codes. - On X11, Wayland, Windows and macOS, improved scancode conversions for more obscure key codes.
- Add ability to make non-activating window on macOS using `NSPanel` with `NSWindowStyleMask::NonactivatingPanel`.
### Changed ### Changed

View file

@ -332,6 +332,13 @@ pub trait WindowAttributesExtMacOS {
fn with_borderless_game(self, borderless_game: bool) -> Self; fn with_borderless_game(self, borderless_game: bool) -> Self;
/// See [`WindowExtMacOS::set_unified_titlebar`] for details on what this means if set. /// See [`WindowExtMacOS::set_unified_titlebar`] for details on what this means if set.
fn with_unified_titlebar(self, unified_titlebar: bool) -> Self; fn with_unified_titlebar(self, unified_titlebar: bool) -> Self;
/// Use [`NSPanel`] window with [`NonactivatingPanel`] window style mask instead of
/// [`NSWindow`].
///
/// [`NSWindow`]: https://developer.apple.com/documentation/appkit/NSWindow?language=objc
/// [`NSPanel`]: https://developer.apple.com/documentation/appkit/NSPanel?language=objc
/// [`NonactivatingPanel`]: https://developer.apple.com/documentation/appkit/nswindow/stylemask-swift.struct/nonactivatingpanel?language=objc
fn with_panel(self, panel: bool) -> Self;
} }
impl WindowAttributesExtMacOS for WindowAttributes { impl WindowAttributesExtMacOS for WindowAttributes {
@ -412,6 +419,12 @@ impl WindowAttributesExtMacOS for WindowAttributes {
self.platform_specific.unified_titlebar = unified_titlebar; self.platform_specific.unified_titlebar = unified_titlebar;
self self
} }
#[inline]
fn with_panel(mut self, panel: bool) -> Self {
self.platform_specific.panel = panel;
self
}
} }
pub trait EventLoopBuilderExtMacOS { pub trait EventLoopBuilderExtMacOS {

View file

@ -9,7 +9,7 @@ use objc2::runtime::{AnyObject, Sel};
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{ use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient,
NSTrackingRectTag, NSView, NSTrackingRectTag, NSView, NSWindow,
}; };
use objc2_foundation::{ use objc2_foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying,
@ -23,7 +23,7 @@ use super::event::{
code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed, code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed,
scancode_to_physicalkey, KeyEventExtra, scancode_to_physicalkey, KeyEventExtra,
}; };
use super::window::WinitWindow; use super::window::window_id;
use crate::dpi::{LogicalPosition, LogicalSize}; use crate::dpi::{LogicalPosition, LogicalSize};
use crate::event::{ use crate::event::{
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
@ -201,7 +201,7 @@ declare_class!(
fn draw_rect(&self, _rect: NSRect) { fn draw_rect(&self, _rect: NSRect) {
trace_scope!("drawRect:"); trace_scope!("drawRect:");
self.ivars().app_state.handle_redraw(self.window().id()); self.ivars().app_state.handle_redraw(window_id(&self.window()));
// This is a direct subclass of NSView, no need to call superclass' drawRect: // This is a direct subclass of NSView, no need to call superclass' drawRect:
} }
@ -426,7 +426,7 @@ declare_class!(
} }
// Send command action to user if they requested it. // Send command action to user if they requested it.
let window_id = self.window().id(); let window_id = window_id(&self.window());
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
if let Some(handler) = app.macos_handler() { if let Some(handler) = app.macos_handler() {
handler.standard_key_binding(event_loop, window_id, command.name()); handler.standard_key_binding(event_loop, window_id, command.name());
@ -828,19 +828,12 @@ impl WinitView {
this this
} }
fn window(&self) -> Retained<WinitWindow> { fn window(&self) -> Retained<NSWindow> {
let window = (**self).window().expect("view must be installed in a window"); (**self).window().expect("view must be installed in a window")
if !window.isKindOfClass(WinitWindow::class()) {
unreachable!("view installed in non-WinitWindow");
}
// SAFETY: Just checked that the window is `WinitWindow`
unsafe { Retained::cast(window) }
} }
fn queue_event(&self, event: WindowEvent) { fn queue_event(&self, event: WindowEvent) {
let window_id = self.window().id(); let window_id = window_id(&self.window());
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, event); app.window_event(event_loop, window_id, event);
}); });

View file

@ -3,7 +3,7 @@
use dpi::{Position, Size}; use dpi::{Position, Size};
use objc2::rc::{autoreleasepool, Retained}; use objc2::rc::{autoreleasepool, Retained};
use objc2::{declare_class, mutability, ClassType, DeclaredClass}; use objc2::{declare_class, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{NSResponder, NSWindow}; use objc2_app_kit::{NSPanel, NSResponder, NSWindow};
use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject}; use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject};
use super::event_loop::ActiveEventLoop; use super::event_loop::ActiveEventLoop;
@ -16,7 +16,7 @@ use crate::window::{
}; };
pub(crate) struct Window { pub(crate) struct Window {
window: MainThreadBound<Retained<WinitWindow>>, window: MainThreadBound<Retained<NSWindow>>,
/// The window only keeps a weak reference to this, so we must keep it around here. /// The window only keeps a weak reference to this, so we must keep it around here.
delegate: MainThreadBound<Retained<WindowDelegate>>, delegate: MainThreadBound<Retained<WindowDelegate>>,
} }
@ -360,8 +360,30 @@ declare_class!(
} }
); );
impl WinitWindow { declare_class!(
pub(super) fn id(&self) -> WindowId { #[derive(Debug)]
WindowId::from_raw(self as *const Self as usize) pub struct WinitPanel;
unsafe impl ClassType for WinitPanel {
#[inherits(NSWindow, NSResponder, NSObject)]
type Super = NSPanel;
type Mutability = mutability::MainThreadOnly;
const NAME: &'static str = "WinitPanel";
} }
impl DeclaredClass for WinitPanel {}
unsafe impl WinitPanel {
// although NSPanel can become key window
// it doesn't if window doesn't have NSWindowStyleMask::Titled
#[method(canBecomeKeyWindow)]
fn can_become_key_window(&self) -> bool {
trace_scope!("canBecomeKeyWindow");
true
}
}
);
pub(super) fn window_id(window: &NSWindow) -> WindowId {
WindowId::from_raw(window as *const _ as usize)
} }

View file

@ -16,7 +16,7 @@ use objc2_app_kit::{
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard, NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard,
NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification, NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification,
NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel, NSWindow, NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel,
NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle,
}; };
@ -33,7 +33,7 @@ use super::cursor::cursor_from_icon;
use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; use super::monitor::{self, flip_window_screen_coordinates, get_display_id};
use super::observer::RunLoop; use super::observer::RunLoop;
use super::view::WinitView; use super::view::WinitView;
use super::window::WinitWindow; use super::window::{window_id, WinitPanel, WinitWindow};
use super::{ffi, Fullscreen, MonitorHandle}; use super::{ffi, Fullscreen, MonitorHandle};
use crate::dpi::{ use crate::dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
@ -62,6 +62,7 @@ pub struct PlatformSpecificWindowAttributes {
pub option_as_alt: OptionAsAlt, pub option_as_alt: OptionAsAlt,
pub borderless_game: bool, pub borderless_game: bool,
pub unified_titlebar: bool, pub unified_titlebar: bool,
pub panel: bool,
} }
impl Default for PlatformSpecificWindowAttributes { impl Default for PlatformSpecificWindowAttributes {
@ -81,6 +82,7 @@ impl Default for PlatformSpecificWindowAttributes {
option_as_alt: Default::default(), option_as_alt: Default::default(),
borderless_game: false, borderless_game: false,
unified_titlebar: false, unified_titlebar: false,
panel: false,
} }
} }
} }
@ -90,7 +92,7 @@ pub(crate) struct State {
/// Strong reference to the global application state. /// Strong reference to the global application state.
app_state: Rc<AppState>, app_state: Rc<AppState>,
window: Retained<WinitWindow>, window: Retained<NSWindow>,
// During `windowDidResize`, we use this to only send Moved if the position changed. // During `windowDidResize`, we use this to only send Moved if the position changed.
// //
@ -501,7 +503,7 @@ fn new_window(
app_state: &Rc<AppState>, app_state: &Rc<AppState>,
attrs: &WindowAttributes, attrs: &WindowAttributes,
mtm: MainThreadMarker, mtm: MainThreadMarker,
) -> Option<Retained<WinitWindow>> { ) -> Option<Retained<NSWindow>> {
autoreleasepool(|_| { autoreleasepool(|_| {
let screen = match attrs.fullscreen.clone().map(Into::into) { let screen = match attrs.fullscreen.clone().map(Into::into) {
Some(Fullscreen::Borderless(Some(monitor))) Some(Fullscreen::Borderless(Some(monitor)))
@ -584,16 +586,33 @@ fn new_window(
// confusing issues with the window not being properly activated. // confusing issues with the window not being properly activated.
// //
// Winit ensures this by not allowing access to `ActiveEventLoop` before handling events. // Winit ensures this by not allowing access to `ActiveEventLoop` before handling events.
let window: Option<Retained<WinitWindow>> = unsafe { let window: Retained<NSWindow> = if attrs.platform_specific.panel {
msg_send_id![ masks |= NSWindowStyleMask::NonactivatingPanel;
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame, let window: Option<Retained<WinitPanel>> = unsafe {
styleMask: masks, msg_send_id![
backing: NSBackingStoreType::NSBackingStoreBuffered, super(mtm.alloc().set_ivars(())),
defer: false, initWithContentRect: frame,
] styleMask: masks,
backing: NSBackingStoreType::NSBackingStoreBuffered,
defer: false,
]
};
window?.as_super().as_super().retain()
} else {
let window: Option<Retained<WinitWindow>> = unsafe {
msg_send_id![
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame,
styleMask: masks,
backing: NSBackingStoreType::NSBackingStoreBuffered,
defer: false,
]
};
window?.as_super().retain()
}; };
let window = window?;
// It is very important for correct memory management that we // It is very important for correct memory management that we
// disable the extra release that would otherwise happen when // disable the extra release that would otherwise happen when
@ -841,17 +860,17 @@ impl WindowDelegate {
} }
#[track_caller] #[track_caller]
pub(super) fn window(&self) -> &WinitWindow { pub(super) fn window(&self) -> &NSWindow {
&self.ivars().window &self.ivars().window
} }
#[track_caller] #[track_caller]
pub(crate) fn id(&self) -> WindowId { pub(crate) fn id(&self) -> WindowId {
self.window().id() window_id(self.window())
} }
pub(crate) fn queue_event(&self, event: WindowEvent) { pub(crate) fn queue_event(&self, event: WindowEvent) {
let window_id = self.window().id(); let window_id = window_id(self.window());
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, event); app.window_event(event_loop, window_id, event);
}); });
@ -950,7 +969,7 @@ impl WindowDelegate {
} }
pub fn request_redraw(&self) { pub fn request_redraw(&self) {
self.ivars().app_state.queue_redraw(self.window().id()); self.ivars().app_state.queue_redraw(window_id(self.window()));
} }
#[inline] #[inline]
@ -1488,7 +1507,7 @@ impl WindowDelegate {
self.ivars().fullscreen.replace(fullscreen.clone()); self.ivars().fullscreen.replace(fullscreen.clone());
fn toggle_fullscreen(window: &WinitWindow) { fn toggle_fullscreen(window: &NSWindow) {
// Window level must be restored from `CGShieldingWindowLevel() // Window level must be restored from `CGShieldingWindowLevel()
// + 1` back to normal in order for `toggleFullScreen` to do // + 1` back to normal in order for `toggleFullScreen` to do
// anything // anything