Skip to content

Commit

Permalink
Implement ability to make non-activating window on macOS (#4035)
Browse files Browse the repository at this point in the history
Fixes #3894
  • Loading branch information
Exidex authored Dec 12, 2024
1 parent 5835c91 commit 4d5e68c
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 37 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ objc2-app-kit = { version = "0.2.2", features = [
"NSMenu",
"NSMenuItem",
"NSOpenGLView",
"NSPanel",
"NSPasteboard",
"NSResponder",
"NSRunningApplication",
Expand Down
1 change: 1 addition & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ changelog entry.
- 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.
- 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

Expand Down
13 changes: 13 additions & 0 deletions src/platform/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ pub trait WindowAttributesExtMacOS {
fn with_borderless_game(self, borderless_game: bool) -> Self;
/// See [`WindowExtMacOS::set_unified_titlebar`] for details on what this means if set.
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 {
Expand Down Expand Up @@ -412,6 +419,12 @@ impl WindowAttributesExtMacOS for WindowAttributes {
self.platform_specific.unified_titlebar = unified_titlebar;
self
}

#[inline]
fn with_panel(mut self, panel: bool) -> Self {
self.platform_specific.panel = panel;
self
}
}

pub trait EventLoopBuilderExtMacOS {
Expand Down
21 changes: 7 additions & 14 deletions src/platform_impl/apple/appkit/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use objc2::runtime::{AnyObject, Sel};
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient,
NSTrackingRectTag, NSView,
NSTrackingRectTag, NSView, NSWindow,
};
use objc2_foundation::{
MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying,
Expand All @@ -23,7 +23,7 @@ use super::event::{
code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed,
scancode_to_physicalkey, KeyEventExtra,
};
use super::window::WinitWindow;
use super::window::window_id;
use crate::dpi::{LogicalPosition, LogicalSize};
use crate::event::{
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
Expand Down Expand Up @@ -201,7 +201,7 @@ declare_class!(
fn draw_rect(&self, _rect: NSRect) {
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:
}
Expand Down Expand Up @@ -426,7 +426,7 @@ declare_class!(
}

// 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| {
if let Some(handler) = app.macos_handler() {
handler.standard_key_binding(event_loop, window_id, command.name());
Expand Down Expand Up @@ -828,19 +828,12 @@ impl WinitView {
this
}

fn window(&self) -> Retained<WinitWindow> {
let 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 window(&self) -> Retained<NSWindow> {
(**self).window().expect("view must be installed in a window")
}

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| {
app.window_event(event_loop, window_id, event);
});
Expand Down
32 changes: 27 additions & 5 deletions src/platform_impl/apple/appkit/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use dpi::{Position, Size};
use objc2::rc::{autoreleasepool, Retained};
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 super::event_loop::ActiveEventLoop;
Expand All @@ -16,7 +16,7 @@ use crate::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.
delegate: MainThreadBound<Retained<WindowDelegate>>,
}
Expand Down Expand Up @@ -360,8 +360,30 @@ declare_class!(
}
);

impl WinitWindow {
pub(super) fn id(&self) -> WindowId {
WindowId::from_raw(self as *const Self as usize)
declare_class!(
#[derive(Debug)]
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)
}
55 changes: 37 additions & 18 deletions src/platform_impl/apple/appkit/window_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use objc2_app_kit::{
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard,
NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification,
NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel,
NSWindow, NSWindowButton, NSWindowDelegate, NSWindowFullScreenButton, NSWindowLevel,
NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle,
};
Expand All @@ -33,7 +33,7 @@ use super::cursor::cursor_from_icon;
use super::monitor::{self, flip_window_screen_coordinates, get_display_id};
use super::observer::RunLoop;
use super::view::WinitView;
use super::window::WinitWindow;
use super::window::{window_id, WinitPanel, WinitWindow};
use super::{ffi, Fullscreen, MonitorHandle};
use crate::dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
Expand Down Expand Up @@ -62,6 +62,7 @@ pub struct PlatformSpecificWindowAttributes {
pub option_as_alt: OptionAsAlt,
pub borderless_game: bool,
pub unified_titlebar: bool,
pub panel: bool,
}

impl Default for PlatformSpecificWindowAttributes {
Expand All @@ -81,6 +82,7 @@ impl Default for PlatformSpecificWindowAttributes {
option_as_alt: Default::default(),
borderless_game: false,
unified_titlebar: false,
panel: false,
}
}
}
Expand All @@ -90,7 +92,7 @@ pub(crate) struct State {
/// Strong reference to the global application state.
app_state: Rc<AppState>,

window: Retained<WinitWindow>,
window: Retained<NSWindow>,

// During `windowDidResize`, we use this to only send Moved if the position changed.
//
Expand Down Expand Up @@ -501,7 +503,7 @@ fn new_window(
app_state: &Rc<AppState>,
attrs: &WindowAttributes,
mtm: MainThreadMarker,
) -> Option<Retained<WinitWindow>> {
) -> Option<Retained<NSWindow>> {
autoreleasepool(|_| {
let screen = match attrs.fullscreen.clone().map(Into::into) {
Some(Fullscreen::Borderless(Some(monitor)))
Expand Down Expand Up @@ -584,16 +586,33 @@ fn new_window(
// confusing issues with the window not being properly activated.
//
// Winit ensures this by not allowing access to `ActiveEventLoop` before handling events.
let window: Option<Retained<WinitWindow>> = unsafe {
msg_send_id![
super(mtm.alloc().set_ivars(())),
initWithContentRect: frame,
styleMask: masks,
backing: NSBackingStoreType::NSBackingStoreBuffered,
defer: false,
]
let window: Retained<NSWindow> = if attrs.platform_specific.panel {
masks |= NSWindowStyleMask::NonactivatingPanel;

let window: Option<Retained<WinitPanel>> = unsafe {
msg_send_id![
super(mtm.alloc().set_ivars(())),
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
// disable the extra release that would otherwise happen when
Expand Down Expand Up @@ -841,17 +860,17 @@ impl WindowDelegate {
}

#[track_caller]
pub(super) fn window(&self) -> &WinitWindow {
pub(super) fn window(&self) -> &NSWindow {
&self.ivars().window
}

#[track_caller]
pub(crate) fn id(&self) -> WindowId {
self.window().id()
window_id(self.window())
}

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| {
app.window_event(event_loop, window_id, event);
});
Expand Down Expand Up @@ -950,7 +969,7 @@ impl WindowDelegate {
}

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]
Expand Down Expand Up @@ -1488,7 +1507,7 @@ impl WindowDelegate {

self.ivars().fullscreen.replace(fullscreen.clone());

fn toggle_fullscreen(window: &WinitWindow) {
fn toggle_fullscreen(window: &NSWindow) {
// Window level must be restored from `CGShieldingWindowLevel()
// + 1` back to normal in order for `toggleFullScreen` to do
// anything
Expand Down

0 comments on commit 4d5e68c

Please sign in to comment.