Files
oak-gpui/crates/gpui_macos/src/window.rs
T
2026-06-04 21:41:59 -04:00

3046 lines
113 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::{
BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, TISCopyCurrentKeyboardInputSource,
TISGetInputSourceProperty, events::platform_input_from_native,
kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode,
ns_string, renderer,
};
#[cfg(any(test, feature = "test-support"))]
use anyhow::Result;
use block::ConcreteBlock;
use cocoa::{
appkit::{
NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered,
NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen,
NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial,
NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton,
NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowStyleMask, NSWindowTitleVisibility,
},
base::{id, nil},
foundation::{
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound,
NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUInteger,
NSUserDefaults,
},
};
use dispatch2::DispatchQueue;
use gpui::{
AnyWindowHandle, BackgroundExecutor, Bounds, Capslock, CursorStyle, ExternalPaths,
FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton,
PromptLevel, RequestFrameOptions, SharedString, Size, SystemWindowTab, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams, point,
px, size,
};
#[cfg(any(test, feature = "test-support"))]
use image::RgbaImage;
use core_foundation::base::{CFRelease, CFTypeRef};
use core_foundation_sys::base::CFEqual;
use core_foundation_sys::number::{CFBooleanGetValue, CFBooleanRef};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
use ctor::ctor;
use futures::channel::oneshot;
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES},
sel, sel_impl,
};
use objc2_app_kit::NSBeep;
use parking_lot::Mutex;
use raw_window_handle as rwh;
use smallvec::SmallVec;
use std::{
cell::Cell,
ffi::{CStr, c_void},
mem,
ops::Range,
path::PathBuf,
ptr::{self, NonNull},
rc::Rc,
sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use util::ResultExt;
const WINDOW_STATE_IVAR: &str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null();
static mut PANEL_CLASS: *const Class = ptr::null();
static mut VIEW_CLASS: *const Class = ptr::null();
static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
NSWindowStyleMask::from_bits_retain(1 << 7);
// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
#[allow(non_upper_case_globals)]
const NSNormalWindowLevel: NSInteger = 0;
#[allow(non_upper_case_globals)]
const NSFloatingWindowLevel: NSInteger = 3;
#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
#[allow(non_upper_case_globals)]
const NSTrackingMouseMoved: NSUInteger = 0x02;
#[allow(non_upper_case_globals)]
const NSTrackingActiveAlways: NSUInteger = 0x80;
#[allow(non_upper_case_globals)]
const NSTrackingInVisibleRect: NSUInteger = 0x200;
#[allow(non_upper_case_globals)]
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
#[allow(non_upper_case_globals)]
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
// https://developer.apple.com/documentation/appkit/nsdragoperation
type NSDragOperation = NSUInteger;
#[allow(non_upper_case_globals)]
const NSDragOperationNone: NSDragOperation = 0;
#[allow(non_upper_case_globals)]
const NSDragOperationCopy: NSDragOperation = 1;
#[derive(PartialEq)]
pub enum UserTabbingPreference {
Never,
Always,
InFullScreen,
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
// Widely used private APIs; Apple uses them for their Terminal.app.
fn CGSMainConnectionID() -> id;
fn CGSSetWindowBackgroundBlurRadius(
connection_id: id,
window_id: NSInteger,
radius: i64,
) -> i32;
}
#[ctor(unsafe)]
unsafe fn build_classes() {
unsafe {
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
VIEW_CLASS = {
let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(performKeyEquivalent:),
handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(keyDown:),
handle_key_down as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(keyUp:),
handle_key_up as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(rightMouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(rightMouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(otherMouseDown:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(otherMouseUp:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(resetCursorRects),
reset_cursor_rects as extern "C" fn(&Object, Sel),
);
decl.add_method(
sel!(pressureChangeWithEvent:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseExited:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(magnifyWithEvent:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(rightMouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(otherMouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(scrollWheel:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(swipeWithEvent:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(flagsChanged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(makeBackingLayer),
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
);
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
decl.add_method(
sel!(viewDidChangeBackingProperties),
view_did_change_backing_properties as extern "C" fn(&Object, Sel),
);
decl.add_method(
sel!(setFrameSize:),
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
);
decl.add_method(
sel!(displayLayer:),
display_layer as extern "C" fn(&Object, Sel, id),
);
decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
decl.add_method(
sel!(validAttributesForMarkedText),
valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id,
);
decl.add_method(
sel!(hasMarkedText),
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(markedRange),
marked_range as extern "C" fn(&Object, Sel) -> NSRange,
);
decl.add_method(
sel!(selectedRange),
selected_range as extern "C" fn(&Object, Sel) -> NSRange,
);
decl.add_method(
sel!(firstRectForCharacterRange:actualRange:),
first_rect_for_character_range
as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect,
);
decl.add_method(
sel!(insertText:replacementRange:),
insert_text as extern "C" fn(&Object, Sel, id, NSRange),
);
decl.add_method(
sel!(setMarkedText:selectedRange:replacementRange:),
set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
);
decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(attributedSubstringForProposedRange:actualRange:),
attributed_substring_for_proposed_range
as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
);
decl.add_method(
sel!(viewDidChangeEffectiveAppearance),
view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
);
// Suppress beep on keystrokes with modifier keys.
decl.add_method(
sel!(doCommandBySelector:),
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
);
decl.add_method(
sel!(acceptsFirstMouse:),
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(characterIndexForPoint:),
character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64,
);
decl.register()
};
BLURRED_VIEW_CLASS = {
let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap();
decl.add_method(
sel!(initWithFrame:),
blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id,
);
decl.add_method(
sel!(updateLayer),
blurred_view_update_layer as extern "C" fn(&Object, Sel),
);
decl.register()
};
}
}
pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
point(
px(position.x as f32),
// macOS screen coordinates are relative to bottom left
window_height - px(position.y as f32),
)
}
/// Stores the cursor style on the active GPUI window and invalidates its cursor rects.
///
/// # Safety
///
/// This function is not thread safe. Callers must ensure this is called on the AppKit main
/// thread because it reads the active AppKit window and updates GPUI window state associated
/// with Objective-C objects.
pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) {
// SAFETY: The caller guarantees AppKit main-thread access. The class check ensures the
// window has our WINDOW_STATE_IVAR before reading it.
unsafe {
let app = NSApplication::sharedApplication(nil);
let key_window: id = msg_send![app, keyWindow];
let main_window: id = msg_send![app, mainWindow];
let active_window = if !key_window.is_null()
&& msg_send![key_window, isKindOfClass: WINDOW_CLASS]
{
Some(key_window)
} else if !main_window.is_null() && msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
Some(main_window)
} else {
None
};
let Some(active_window) = active_window else {
return;
};
let window_state = get_window_state(&*active_window);
let mut window_state = window_state.lock();
if window_state.cursor_style != style {
window_state.cursor_style = style;
let _: () = msg_send![
window_state.native_window,
invalidateCursorRectsForView: window_state.native_view.as_ptr()
];
}
}
}
unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
unsafe {
let mut decl = ClassDecl::new(name, superclass).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(canBecomeMainWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(canBecomeKeyWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeOcclusionState:),
window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeScreen:),
window_did_change_screen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidResignKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowShouldClose:),
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
decl.add_method(
sel!(draggingEntered:),
dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
);
decl.add_method(
sel!(draggingUpdated:),
dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
);
decl.add_method(
sel!(draggingExited:),
dragging_exited as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(performDragOperation:),
perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(concludeDragOperation:),
conclude_drag_operation as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(addTitlebarAccessoryViewController:),
add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(moveTabToNewWindow:),
move_tab_to_new_window as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mergeAllWindows:),
merge_all_windows as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(selectNextTab:),
select_next_tab as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(selectPreviousTab:),
select_previous_tab as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(toggleTabBar:),
toggle_tab_bar as extern "C" fn(&Object, Sel, id),
);
decl.register()
}
}
struct MacWindowState {
handle: AnyWindowHandle,
foreground_executor: ForegroundExecutor,
background_executor: BackgroundExecutor,
native_window: id,
native_view: NonNull<Object>,
blurred_view: Option<id>,
background_appearance: WindowBackgroundAppearance,
cursor_style: CursorStyle,
cursor_visible: Arc<AtomicBool>,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
event_callback: Option<Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<PlatformInputHandler>,
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
transparent_titlebar: bool,
previous_modifiers_changed_event: Option<PlatformInput>,
keystroke_for_do_command: Option<Keystroke>,
do_command_handled: Option<bool>,
external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
merge_all_windows_callback: Option<Box<dyn FnMut()>>,
select_next_tab_callback: Option<Box<dyn FnMut()>>,
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
activated_least_once: bool,
closed: Arc<AtomicBool>,
accesskit_adapter: Option<accesskit_macos::SubclassingAdapter>,
// The parent window if this window is a sheet (Dialog kind)
sheet_parent: Option<id>,
}
impl MacWindowState {
fn move_traffic_light(&self) {
if let Some(traffic_light_position) = self.traffic_light_position {
if self.is_fullscreen() {
// Moving traffic lights while fullscreen doesn't work,
// see https://github.com/zed-industries/zed/issues/4712
return;
}
let titlebar_height = self.titlebar_height();
unsafe {
let close_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowCloseButton
];
let min_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
];
let zoom_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowZoomButton
];
let mut close_button_frame: CGRect = msg_send![close_button, frame];
let mut min_button_frame: CGRect = msg_send![min_button, frame];
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
let mut origin = point(
traffic_light_position.x,
titlebar_height
- traffic_light_position.y
- px(close_button_frame.size.height as f32),
);
let button_spacing =
px((min_button_frame.origin.x - close_button_frame.origin.x) as f32);
close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
let _: () = msg_send![close_button, setFrame: close_button_frame];
origin.x += button_spacing;
min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
let _: () = msg_send![min_button, setFrame: min_button_frame];
origin.x += button_spacing;
zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
origin.x += button_spacing;
}
}
}
fn start_display_link(&mut self) {
self.stop_display_link();
unsafe {
if !self
.native_window
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
{
return;
}
}
let display_id = unsafe { display_id_for_screen(self.native_window.screen()) };
if let Some(mut display_link) =
DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err()
{
display_link.start().log_err();
self.display_link = Some(display_link);
}
}
fn stop_display_link(&mut self) {
self.display_link = None;
}
fn is_maximized(&self) -> bool {
fn rect_to_size(rect: NSRect) -> Size<Pixels> {
let NSSize { width, height } = rect.size;
size(width.into(), height.into())
}
unsafe {
let bounds = self.bounds();
let screen_size = rect_to_size(self.native_window.screen().visibleFrame());
bounds.size == screen_size
}
}
fn is_fullscreen(&self) -> bool {
unsafe {
let style_mask = self.native_window.styleMask();
style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask)
}
}
fn bounds(&self) -> Bounds<Pixels> {
let mut window_frame = unsafe { NSWindow::frame(self.native_window) };
let screen = unsafe { NSWindow::screen(self.native_window) };
if screen == nil {
return Bounds::new(point(px(0.), px(0.)), gpui::DEFAULT_WINDOW_SIZE);
}
let screen_frame = unsafe { NSScreen::frame(screen) };
// Flip the y coordinate to be top-left origin
window_frame.origin.y =
screen_frame.size.height - window_frame.origin.y - window_frame.size.height;
Bounds::new(
point(
px((window_frame.origin.x - screen_frame.origin.x) as f32),
px((window_frame.origin.y + screen_frame.origin.y) as f32),
),
size(
px(window_frame.size.width as f32),
px(window_frame.size.height as f32),
),
)
}
fn content_size(&self) -> Size<Pixels> {
let NSSize { width, height, .. } =
unsafe { NSView::frame(self.native_window.contentView()) }.size;
size(px(width as f32), px(height as f32))
}
fn scale_factor(&self) -> f32 {
get_scale_factor(self.native_window)
}
fn titlebar_height(&self) -> Pixels {
unsafe {
let frame = NSWindow::frame(self.native_window);
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
px((frame.size.height - content_layout_rect.size.height) as f32)
}
}
fn window_bounds(&self) -> WindowBounds {
if self.is_fullscreen() {
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
} else {
WindowBounds::Windowed(self.bounds())
}
}
}
unsafe impl Send for MacWindowState {}
pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>);
impl MacWindow {
pub fn open(
handle: AnyWindowHandle,
WindowParams {
bounds,
titlebar,
kind,
is_movable,
is_resizable,
is_minimizable,
focus,
show,
display_id,
window_min_size,
tabbing_identifier,
..
}: WindowParams,
cursor_visible: Arc<AtomicBool>,
foreground_executor: ForegroundExecutor,
background_executor: BackgroundExecutor,
renderer_context: renderer::Context,
) -> Self {
unsafe {
let pool = NSAutoreleasePool::new(nil);
let allows_automatic_window_tabbing = tabbing_identifier.is_some();
if allows_automatic_window_tabbing {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES];
} else {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
}
let mut style_mask;
if let Some(titlebar) = titlebar.as_ref() {
style_mask =
NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask;
if is_resizable {
style_mask |= NSWindowStyleMask::NSResizableWindowMask;
}
if is_minimizable {
style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask;
}
if titlebar.appears_transparent {
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
}
} else {
style_mask = NSWindowStyleMask::NSTitledWindowMask
| NSWindowStyleMask::NSFullSizeContentViewWindowMask;
}
let native_window: id = match kind {
WindowKind::Normal => {
msg_send![WINDOW_CLASS, alloc]
}
WindowKind::PopUp => {
style_mask |= NSWindowStyleMaskNonactivatingPanel;
msg_send![PANEL_CLASS, alloc]
}
WindowKind::Floating | WindowKind::Dialog => {
msg_send![PANEL_CLASS, alloc]
}
};
let display = display_id
.and_then(MacDisplay::find_by_id)
.unwrap_or_else(MacDisplay::primary);
let mut target_screen = nil;
let mut screen_frame = None;
let screens = NSScreen::screens(nil);
let count: u64 = cocoa::foundation::NSArray::count(screens);
for i in 0..count {
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
let frame = NSScreen::frame(screen);
let display_id = display_id_for_screen(screen);
if display_id == display.0 {
screen_frame = Some(frame);
target_screen = screen;
}
}
let screen_frame = screen_frame.unwrap_or_else(|| {
let screen = NSScreen::mainScreen(nil);
target_screen = screen;
NSScreen::frame(screen)
});
let window_rect = NSRect::new(
NSPoint::new(
screen_frame.origin.x + bounds.origin.x.as_f32() as f64,
screen_frame.origin.y
+ (display.bounds().size.height - bounds.origin.y).as_f32() as f64,
),
NSSize::new(
bounds.size.width.as_f32() as f64,
bounds.size.height.as_f32() as f64,
),
);
let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_(
window_rect,
style_mask,
NSBackingStoreBuffered,
NO,
target_screen,
);
assert!(!native_window.is_null());
let () = msg_send![
native_window,
registerForDraggedTypes:
NSArray::arrayWithObject(nil, NSFilenamesPboardType)
];
let () = msg_send![
native_window,
setReleasedWhenClosed: NO
];
let content_view = native_window.contentView();
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view));
assert!(!native_view.is_null());
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
foreground_executor,
background_executor,
native_window,
native_view: NonNull::new_unchecked(native_view),
blurred_view: None,
background_appearance: WindowBackgroundAppearance::Opaque,
cursor_style: CursorStyle::Arrow,
cursor_visible,
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.as_f32()),
false,
),
request_frame_callback: None,
event_callback: None,
activate_callback: None,
resize_callback: None,
moved_callback: None,
should_close_callback: None,
close_callback: None,
appearance_changed_callback: None,
input_handler: None,
last_key_equivalent: None,
synthetic_drag_counter: 0,
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
.is_none_or(|titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
move_tab_to_new_window_callback: None,
merge_all_windows_callback: None,
select_next_tab_callback: None,
select_previous_tab_callback: None,
toggle_tab_bar_callback: None,
activated_least_once: false,
closed: Arc::new(AtomicBool::new(false)),
accesskit_adapter: None,
sheet_parent: None,
})));
(*native_window).set_ivar(
WINDOW_STATE_IVAR,
Arc::into_raw(window.0.clone()) as *const c_void,
);
native_window.setDelegate_(native_window);
(*native_view).set_ivar(
WINDOW_STATE_IVAR,
Arc::into_raw(window.0.clone()) as *const c_void,
);
if let Some(title) = titlebar
.as_ref()
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
{
window.set_title(title);
}
native_window.setMovable_(is_movable as BOOL);
if let Some(window_min_size) = window_min_size {
native_window.setContentMinSize_(NSSize {
width: window_min_size.width.to_f64(),
height: window_min_size.height.to_f64(),
});
}
if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) {
native_window.setTitlebarAppearsTransparent_(YES);
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
}
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
native_view.setWantsBestResolutionOpenGLSurface_(YES);
// From winit crate: On Mojave, views automatically become layer-backed shortly after
// being added to a native_window. Changing the layer-backedness of a view breaks the
// association between the view and its associated OpenGL context. To work around this,
// on we explicitly make the view layer-backed up front so that AppKit doesn't do it
// itself and break the association with its context.
native_view.setWantsLayer(YES);
let _: () = msg_send![
native_view,
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
];
content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
let app: id = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
let mut sheet_parent = None;
match kind {
WindowKind::Normal | WindowKind::Floating => {
if kind == WindowKind::Floating {
// Let the window float keep above normal windows.
native_window.setLevel_(NSFloatingWindowLevel);
} else {
native_window.setLevel_(NSNormalWindowLevel);
}
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
let tabbing_id = ns_string(tabbing_identifier.as_str());
let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id];
} else {
let _: () = msg_send![native_window, setTabbingIdentifier:nil];
}
}
WindowKind::PopUp => {
// Use a tracking area to allow receiving MouseMoved events even when
// the window or application aren't active, which is often the case
// e.g. for notification windows.
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
let _: () = msg_send![
tracking_area,
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
owner: native_view
userInfo: nil
];
let _: () =
msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
native_window.setLevel_(NSPopUpWindowLevel);
let _: () = msg_send![
native_window,
setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
];
native_window.setCollectionBehavior_(
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
);
}
WindowKind::Dialog => {
if !main_window.is_null() {
let parent = {
let active_sheet: id = msg_send![main_window, attachedSheet];
if active_sheet.is_null() {
main_window
} else {
active_sheet
}
};
let _: () =
msg_send![parent, beginSheet: native_window completionHandler: nil];
sheet_parent = Some(parent);
}
}
}
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
{
let main_window_is_fullscreen = main_window
.styleMask()
.contains(NSWindowStyleMask::NSFullScreenWindowMask);
let user_tabbing_preference = Self::get_user_tabbing_preference()
.unwrap_or(UserTabbingPreference::InFullScreen);
let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always
|| user_tabbing_preference == UserTabbingPreference::InFullScreen
&& main_window_is_fullscreen;
if should_add_as_tab {
let main_window_can_tab: BOOL =
msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)];
let main_window_visible: BOOL = msg_send![main_window, isVisible];
if main_window_can_tab == YES && main_window_visible == YES {
let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove];
// Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point.
// Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen.
if !main_window_is_fullscreen {
let _: () = msg_send![native_window, orderFront: nil];
}
}
}
}
if focus && show {
native_window.makeKeyAndOrderFront_(nil);
} else if show {
native_window.orderFront_(nil);
}
// Set the initial position of the window to the specified origin.
// Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`,
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
// is different from the primary screen.
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
{
let mut window_state = window.0.lock();
window_state.move_traffic_light();
window_state.sheet_parent = sheet_parent;
}
pool.drain();
window
}
}
pub fn active_window() -> Option<AnyWindowHandle> {
unsafe {
let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
if main_window.is_null() {
return None;
}
if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
let handle = get_window_state(&*main_window).lock().handle;
Some(handle)
} else {
None
}
}
}
pub fn ordered_windows() -> Vec<AnyWindowHandle> {
unsafe {
let app = NSApplication::sharedApplication(nil);
let windows: id = msg_send![app, orderedWindows];
let count: NSUInteger = msg_send![windows, count];
let mut window_handles = Vec::new();
for i in 0..count {
let window: id = msg_send![windows, objectAtIndex:i];
if msg_send![window, isKindOfClass: WINDOW_CLASS] {
let handle = get_window_state(&*window).lock().handle;
window_handles.push(handle);
}
}
window_handles
}
}
pub fn get_user_tabbing_preference() -> Option<UserTabbingPreference> {
unsafe {
let defaults: id = NSUserDefaults::standardUserDefaults();
let domain = ns_string("NSGlobalDomain");
let key = ns_string("AppleWindowTabbingMode");
let dict: id = msg_send![defaults, persistentDomainForName: domain];
let value: id = if !dict.is_null() {
msg_send![dict, objectForKey: key]
} else {
nil
};
let value_str = if !value.is_null() {
CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy()
} else {
"".into()
};
match value_str.as_ref() {
"manual" => Some(UserTabbingPreference::Never),
"always" => Some(UserTabbingPreference::Always),
_ => Some(UserTabbingPreference::InFullScreen),
}
}
}
}
impl Drop for MacWindow {
fn drop(&mut self) {
let mut this = self.0.lock();
this.renderer.destroy();
let window = this.native_window;
let sheet_parent = this.sheet_parent.take();
this.display_link.take();
unsafe {
this.native_window.setDelegate_(nil);
}
this.input_handler.take();
this.foreground_executor
.spawn(async move {
unsafe {
if let Some(parent) = sheet_parent {
let _: () = msg_send![parent, endSheet: window];
}
window.close();
window.autorelease();
}
})
.detach();
}
}
/// Calls `f` if the window is not closed.
///
/// This should be used when spawning foreground tasks interacting with the
/// window, as some messages will end hard faulting if dispatched to no longer
/// valid window handles.
fn if_window_not_closed(closed: Arc<AtomicBool>, f: impl FnOnce()) {
if !closed.load(Ordering::Acquire) {
f();
}
}
impl PlatformWindow for MacWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.0.as_ref().lock().bounds()
}
fn window_bounds(&self) -> WindowBounds {
self.0.as_ref().lock().window_bounds()
}
fn is_maximized(&self) -> bool {
self.0.as_ref().lock().is_maximized()
}
fn content_size(&self) -> Size<Pixels> {
self.0.as_ref().lock().content_size()
}
fn resize(&mut self, size: Size<Pixels>) {
let this = self.0.lock();
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
.spawn(async move {
if_window_not_closed(closed, || unsafe {
window.setContentSize_(NSSize {
width: size.width.as_f32() as f64,
height: size.height.as_f32() as f64,
});
})
})
.detach();
}
fn merge_all_windows(&self) {
let native_window = self.0.lock().native_window;
extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) {
unsafe {
let native_window = context as id;
let _: () = msg_send![native_window, mergeAllWindows:nil];
}
}
unsafe {
DispatchQueue::main()
.exec_async_f(native_window as *mut std::ffi::c_void, merge_windows_async);
}
}
fn move_tab_to_new_window(&self) {
let native_window = self.0.lock().native_window;
extern "C" fn move_tab_async(context: *mut std::ffi::c_void) {
unsafe {
let native_window = context as id;
let _: () = msg_send![native_window, moveTabToNewWindow:nil];
let _: () = msg_send![native_window, makeKeyAndOrderFront: nil];
}
}
unsafe {
DispatchQueue::main()
.exec_async_f(native_window as *mut std::ffi::c_void, move_tab_async);
}
}
fn toggle_window_tab_overview(&self) {
let native_window = self.0.lock().native_window;
unsafe {
let _: () = msg_send![native_window, toggleTabOverview:nil];
}
}
fn set_tabbing_identifier(&self, tabbing_identifier: Option<String>) {
let native_window = self.0.lock().native_window;
unsafe {
let allows_automatic_window_tabbing = tabbing_identifier.is_some();
if allows_automatic_window_tabbing {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES];
} else {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
}
if let Some(tabbing_identifier) = tabbing_identifier {
let tabbing_id = ns_string(tabbing_identifier.as_str());
let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id];
} else {
let _: () = msg_send![native_window, setTabbingIdentifier:nil];
}
}
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().lock().scale_factor()
}
fn appearance(&self) -> WindowAppearance {
unsafe {
let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance];
crate::window_appearance::window_appearance_from_native(appearance)
}
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
unsafe {
let screen = self.0.lock().native_window.screen();
if screen.is_null() {
return None;
}
let device_description: id = msg_send![screen, deviceDescription];
let screen_number: id =
NSDictionary::valueForKey_(device_description, ns_string("NSScreenNumber"));
let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
Some(Rc::new(MacDisplay(screen_number)))
}
}
fn mouse_position(&self) -> Point<Pixels> {
let position = unsafe {
self.0
.lock()
.native_window
.mouseLocationOutsideOfEventStream()
};
convert_mouse_position(position, self.content_size().height)
}
fn modifiers(&self) -> Modifiers {
unsafe {
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
Modifiers {
control,
alt,
shift,
platform: command,
function,
}
}
}
fn capslock(&self) -> Capslock {
unsafe {
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
Capslock {
on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
}
}
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.0.as_ref().lock().input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.0.as_ref().lock().input_handler.take()
}
fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[PromptButton],
) -> Option<oneshot::Receiver<usize>> {
// NSAlert's first button keeps Return and Cancel keeps Escape, but the keyboard
// focus (and therefore Space) defaults to Cancel, leaving the middle button of
// prompts like "Save / Don't Save / Cancel" unreachable from the keyboard. Move
// the initial focus onto the last non-cancel, non-default button instead.
let initial_focus_ix = answers
.iter()
.enumerate()
.rev()
.find(|(_, label)| !label.is_cancel())
.map(|(ix, _)| ix)
.filter(|&ix| ix > 0);
unsafe {
let alert: id = msg_send![class!(NSAlert), alloc];
let alert: id = msg_send![alert, init];
let alert_style = match level {
PromptLevel::Info => 1,
PromptLevel::Warning => 0,
PromptLevel::Critical => 2,
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
if let Some(detail) = detail {
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
}
let mut initial_focus_button: Option<id> = None;
for (ix, answer) in answers.iter().enumerate() {
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
let _: () = msg_send![button, setTag: ix as NSInteger];
if answer.is_cancel() {
if let Some(key) = std::char::from_u32(crate::events::ESCAPE_KEY as u32) {
let _: () =
msg_send![button, setKeyEquivalent: ns_string(&key.to_string())];
}
} else if Some(ix) == initial_focus_ix {
initial_focus_button = Some(button);
}
}
if let Some(button) = initial_focus_button {
let alert_window: id = msg_send![alert, window];
let _: () = msg_send![alert_window, setInitialFirstResponder: button];
}
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |answer: NSInteger| {
let _: () = msg_send![alert, release];
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(answer.try_into().unwrap());
}
});
let block = block.copy();
let lock = self.0.lock();
let native_window = lock.native_window;
let closed = lock.closed.clone();
let executor = lock.foreground_executor.clone();
executor
.spawn(async move {
if !closed.load(Ordering::Acquire) {
let _: () = msg_send![
alert,
beginSheetModalForWindow: native_window
completionHandler: block
];
} else {
let _: () = msg_send![alert, release];
}
})
.detach();
Some(done_rx)
}
}
fn activate(&self) {
let lock = self.0.lock();
let window = lock.native_window;
let closed = lock.closed.clone();
let executor = lock.foreground_executor.clone();
executor
.spawn(async move {
if !closed.load(Ordering::Acquire) {
unsafe {
let _: () = msg_send![window, makeKeyAndOrderFront: nil];
}
}
})
.detach();
}
fn is_active(&self) -> bool {
unsafe { self.0.lock().native_window.isKeyWindow() == YES }
}
// is_hovered is unused on macOS. See Window::is_window_hovered.
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, title: &str) {
unsafe {
let app = NSApplication::sharedApplication(nil);
let window = self.0.lock().native_window;
let title = ns_string(title);
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
let _: () = msg_send![window, setTitle: title];
self.0.lock().move_traffic_light();
}
}
fn get_title(&self) -> String {
unsafe {
let title: id = msg_send![self.0.lock().native_window, title];
if title.is_null() {
"".to_string()
} else {
title.to_str().to_string()
}
}
}
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
this.background_appearance = background_appearance;
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
this.renderer.update_transparency(!opaque);
unsafe {
this.native_window.setOpaque_(opaque as BOOL);
let background_color = if opaque {
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
} else {
// Not using `+[NSColor clearColor]` to avoid broken shadow.
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001)
};
this.native_window.setBackgroundColor_(background_color);
if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 {
// Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`.
// On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers
// but uses a `CAProxyLayer`. Use the legacy WindowServer API.
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
80
} else {
0
};
let window_number = this.native_window.windowNumber();
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
} else {
// On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it
// could have a better performance (it downsamples the backdrop) and more control
// over the effect layer.
if background_appearance != WindowBackgroundAppearance::Blurred {
if let Some(blur_view) = this.blurred_view {
NSView::removeFromSuperview(blur_view);
this.blurred_view = None;
}
} else if this.blurred_view.is_none() {
let content_view = this.native_window.contentView();
let frame = NSView::bounds(content_view);
let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
blur_view = NSView::initWithFrame_(blur_view, frame);
blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
let _: () = msg_send![
content_view,
addSubview: blur_view
positioned: NSWindowOrderingMode::NSWindowBelow
relativeTo: nil
];
this.blurred_view = Some(blur_view.autorelease());
}
}
}
}
fn background_appearance(&self) -> WindowBackgroundAppearance {
self.0.as_ref().lock().background_appearance
}
fn is_subpixel_rendering_supported(&self) -> bool {
false
}
fn set_edited(&mut self, edited: bool) {
unsafe {
let window = self.0.lock().native_window;
msg_send![window, setDocumentEdited: edited as BOOL]
}
// Changing the document edited state resets the traffic light position,
// so we have to move it again.
self.0.lock().move_traffic_light();
}
fn set_document_path(&self, path: Option<&std::path::Path>) {
unsafe {
let window = self.0.lock().native_window;
let filename = path.map_or(ns_string(""), |p| ns_string(&p.to_string_lossy()));
let _: () = msg_send![window, setRepresentedFilename: filename];
}
// Changing the document path state resets the traffic light position,
// so we have to move it again.
self.0.lock().move_traffic_light();
}
fn show_character_palette(&self) {
let this = self.0.lock();
let window = this.native_window;
this.foreground_executor
.spawn(async move {
unsafe {
let app = NSApplication::sharedApplication(nil);
let _: () = msg_send![app, orderFrontCharacterPalette: window];
}
})
.detach();
}
fn minimize(&self) {
let window = self.0.lock().native_window;
unsafe {
window.miniaturize_(nil);
}
}
fn zoom(&self) {
let this = self.0.lock();
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
.spawn(async move {
if_window_not_closed(closed, || unsafe {
window.zoom_(nil);
})
})
.detach();
}
fn toggle_fullscreen(&self) {
let this = self.0.lock();
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
.spawn(async move {
if_window_not_closed(closed, || unsafe {
window.toggleFullScreen_(nil);
})
})
.detach();
}
fn is_fullscreen(&self) -> bool {
let this = self.0.lock();
let window = this.native_window;
unsafe {
window
.styleMask()
.contains(NSWindowStyleMask::NSFullScreenWindowMask)
}
}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
self.0.as_ref().lock().request_frame_callback = Some(callback);
}
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> gpui::DispatchEventResult>) {
self.0.as_ref().lock().event_callback = Some(callback);
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().lock().activate_callback = Some(callback);
}
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.as_ref().lock().resize_callback = Some(callback);
}
fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().moved_callback = Some(callback);
}
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
self.0.as_ref().lock().should_close_callback = Some(callback);
}
fn on_close(&self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().lock().close_callback = Some(callback);
}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.0.lock().appearance_changed_callback = Some(callback);
}
fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
unsafe {
let windows: id = msg_send![self.0.lock().native_window, tabbedWindows];
if windows.is_null() {
return None;
}
let count: NSUInteger = msg_send![windows, count];
let mut result = Vec::new();
for i in 0..count {
let window: id = msg_send![windows, objectAtIndex:i];
if msg_send![window, isKindOfClass: WINDOW_CLASS] {
let handle = get_window_state(&*window).lock().handle;
let title: id = msg_send![window, title];
let title = SharedString::from(title.to_str().to_string());
result.push(SystemWindowTab::new(title, handle));
}
}
Some(result)
}
}
fn tab_bar_visible(&self) -> bool {
unsafe {
let tab_group: id = msg_send![self.0.lock().native_window, tabGroup];
if tab_group.is_null() {
false
} else {
let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible];
tab_bar_visible == YES
}
}
}
fn on_move_tab_to_new_window(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback);
}
fn on_merge_all_windows(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().merge_all_windows_callback = Some(callback);
}
fn on_select_next_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_next_tab_callback = Some(callback);
}
fn on_select_previous_tab(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().select_previous_tab_callback = Some(callback);
}
fn on_toggle_tab_bar(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback);
}
fn draw(&self, scene: &gpui::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone()
}
fn gpu_specs(&self) -> Option<gpui::GpuSpecs> {
None
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
let executor = self.0.lock().foreground_executor.clone();
executor
.spawn(async move {
unsafe {
let input_context: id =
msg_send![class!(NSTextInputContext), currentInputContext];
if input_context.is_null() {
return;
}
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
}
})
.detach()
}
fn titlebar_double_click(&self) {
let this = self.0.lock();
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
.spawn(async move {
if_window_not_closed(closed, || {
unsafe {
let defaults: id = NSUserDefaults::standardUserDefaults();
let domain = ns_string("NSGlobalDomain");
let key = ns_string("AppleActionOnDoubleClick");
let dict: id = msg_send![defaults, persistentDomainForName: domain];
let action: id = if !dict.is_null() {
msg_send![dict, objectForKey: key]
} else {
nil
};
let action_str = if !action.is_null() {
CStr::from_ptr(NSString::UTF8String(action)).to_string_lossy()
} else {
"".into()
};
match action_str.as_ref() {
"None" => {
// "Do Nothing" selected, so do no action
}
"Minimize" => {
window.miniaturize_(nil);
}
"Maximize" => {
window.zoom_(nil);
}
"Fill" => {
// There is no documented API for "Fill" action, so we'll just zoom the window
window.zoom_(nil);
}
_ => {
window.zoom_(nil);
}
}
}
})
})
.detach();
}
fn start_window_move(&self) {
let this = self.0.lock();
let window = this.native_window;
unsafe {
let app = NSApplication::sharedApplication(nil);
let event: id = msg_send![app, currentEvent];
let _: () = msg_send![window, performWindowDragWithEvent: event];
}
}
fn play_system_bell(&self) {
NSBeep()
}
#[cfg(any(test, feature = "test-support"))]
fn render_to_image(&self, scene: &gpui::Scene) -> Result<RgbaImage> {
let mut this = self.0.lock();
this.renderer.render_to_image(scene)
}
fn a11y_init(&self, callbacks: gpui::A11yCallbacks) {
let mut lock = self.0.lock();
let activation_handler = A11yActivationHandler {
callback: callbacks.activation,
};
let action_handler = A11yActionHandler(callbacks.action);
let adapter = unsafe {
accesskit_macos::SubclassingAdapter::for_window(
lock.native_window as *mut c_void,
activation_handler,
action_handler,
)
};
lock.accesskit_adapter = Some(adapter);
}
fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) {
let events = {
let mut lock = self.0.lock();
lock.accesskit_adapter
.as_mut()
.and_then(|adapter| adapter.update_if_active(|| tree_update))
};
if let Some(events) = events {
events.raise();
}
}
fn a11y_update_window_bounds(&self) {
// macOS handles window bounds tracking automatically via NSAccessibility.
}
}
struct A11yActivationHandler {
callback: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
}
impl accesskit::ActivationHandler for A11yActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
(self.callback)()
}
}
struct A11yActionHandler(Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>);
impl accesskit::ActionHandler for A11yActionHandler {
fn do_action(&mut self, request: accesskit::ActionRequest) {
(self.0)(request);
}
}
impl rwh::HasWindowHandle for MacWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
// SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView
unsafe {
Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit(
rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()),
)))
}
}
}
impl rwh::HasDisplayHandle for MacWindow {
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
Ok(rwh::DisplayHandle::appkit())
}
}
fn get_scale_factor(native_window: id) -> f32 {
let factor = unsafe {
let screen: id = msg_send![native_window, screen];
if screen.is_null() {
return 2.0;
}
NSScreen::backingScaleFactor(screen) as f32
};
// We are not certain what triggers this, but it seems that sometimes
// this method would return 0 (https://github.com/zed-industries/zed/issues/6412)
// It seems most likely that this would happen if the window has no screen
// (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before
// it was rendered for real.
// Regardless, attempt to avoid the issue here.
if factor == 0.0 { 2. } else { factor }
}
unsafe fn get_window_state(object: &Object) -> Arc<Mutex<MacWindowState>> {
unsafe {
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
let rc1 = Arc::from_raw(raw as *mut Mutex<MacWindowState>);
let rc2 = rc1.clone();
mem::forget(rc1);
rc2
}
}
unsafe fn drop_window_state(object: &Object) {
unsafe {
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
Arc::from_raw(raw as *mut Mutex<MacWindowState>);
}
}
extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
YES
}
extern "C" fn dealloc_window(this: &Object, _: Sel) {
unsafe {
drop_window_state(this);
let _: () = msg_send![super(this, class!(NSWindow)), dealloc];
}
}
extern "C" fn dealloc_view(this: &Object, _: Sel) {
unsafe {
drop_window_state(this);
let _: () = msg_send![super(this, class!(NSView)), dealloc];
}
}
extern "C" fn reset_cursor_rects(this: &Object, _: Sel) {
// SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances,
// whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered
// below is a valid NSCursor.
unsafe {
let _: () = msg_send![super(this, class!(NSView)), resetCursorRects];
let window_state = get_window_state(this);
let cursor_style = window_state.lock().cursor_style;
let cursor: id = match cursor_style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
// Undocumented, private class methods:
// https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor
CursorStyle::ResizeUpLeftDownRight => {
msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor]
}
CursorStyle::ResizeUpRightDownLeft => {
msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor]
}
CursorStyle::IBeamCursorForVerticalLayout => {
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
}
CursorStyle::OperationNotAllowed => {
msg_send![class!(NSCursor), operationNotAllowedCursor]
}
CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
};
let bounds = NSView::bounds(this as *const Object as id);
let _: () = msg_send![this, addCursorRect: bounds cursor: cursor];
}
}
extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
handle_key_event(this, native_event, true)
}
extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
handle_key_event(this, native_event, false);
}
extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) {
handle_key_event(this, native_event, false);
}
// Things to test if you're modifying this method:
// U.S. layout:
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
// the terminal behave incorrectly by default. This behavior should be patched by our
// IME integration
// - `alt-t` should open the tasks menu
// - In vim mode, this keybinding should work:
// ```
// {
// "context": "Editor && vim_mode == insert",
// "bindings": {"j j": "vim::NormalBefore"}
// }
// ```
// and typing 'j k' in insert mode with this keybinding should insert the two characters
// Brazilian layout:
// - `" space` should create an unmarked quote
// - `" backspace` should delete the marked quote
// - `" "`should create an unmarked quote and a second marked quote
// - `" up` should insert a quote, unmark it, and move up one line
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
// - `cmd-ctrl-space` and clicking on an emoji should type it
// Czech (QWERTY) layout:
// - in vim mode `option-4` should go to end of line (same as $)
// Japanese (Romaji) layout:
// - type `a i left down up enter enter` should create an unmarked text "愛"
// - In vim mode with `jj` bound to `vim::NormalBefore` in insert mode, typing 'j i' with
// Japanese IME should produce "じ" (ji), not "jい"
/// Returns true if the current keyboard input source is a composition-based IME
/// (e.g. Japanese Hiragana, Korean, Chinese Pinyin) that produces non-ASCII output.
///
/// This checks two properties:
/// 1. The source type is `kTISTypeKeyboardInputMode` (an IME input mode, not a plain
/// keyboard layout). This excludes non-ASCII layouts like Armenian and Ukrainian
/// that map keys directly without composition.
/// 2. The source is not ASCII-capable, which excludes modes like Japanese Romaji that
/// produce ASCII characters and should allow multi-stroke keybindings like `jj`.
unsafe fn is_ime_input_source_active() -> bool {
unsafe {
let source = TISCopyCurrentKeyboardInputSource();
if source.is_null() {
return false;
}
let source_type =
TISGetInputSourceProperty(source, kTISPropertyInputSourceType as *const c_void);
let is_input_mode = !source_type.is_null()
&& CFEqual(
source_type as CFTypeRef,
kTISTypeKeyboardInputMode as CFTypeRef,
) != 0;
let is_ascii = TISGetInputSourceProperty(
source,
kTISPropertyInputSourceIsASCIICapable as *const c_void,
);
let is_ascii_capable = !is_ascii.is_null() && CFBooleanGetValue(is_ascii as CFBooleanRef);
CFRelease(source as CFTypeRef);
is_input_mode && !is_ascii_capable
}
}
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
let window_height = lock.content_size().height;
let event = unsafe { platform_input_from_native(native_event, Some(window_height)) };
let Some(event) = event else {
return NO;
};
let run_callback = |event: PlatformInput| -> BOOL {
let mut callback = window_state.as_ref().lock().event_callback.take();
let handled: BOOL = if let Some(callback) = callback.as_mut() {
!callback(event).propagate as BOOL
} else {
NO
};
window_state.as_ref().lock().event_callback = callback;
handled
};
match event {
PlatformInput::KeyDown(key_down_event) => {
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
// makes no distinction between these two types of events, so we need to ignore
// the "key down" event if we've already just processed its "key equivalent" version.
if key_equivalent {
lock.last_key_equivalent = Some(key_down_event.clone());
} else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_event) {
return NO;
}
drop(lock);
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
// If we're composing, send the key to the input handler first;
// otherwise we only send to the input handler if we don't have a matching binding.
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
// a key. If it does so, it will return YES so we won't send the key twice.
// We also do this for non-printing keys (like arrow keys and escape) as the IME menu
// may need them even if there is no marked text;
// however we skip keys with control or the input handler adds control-characters to the buffer.
// and keys with function, as the input handler swallows them.
// and keys with platform (Cmd), so that Cmd+key events (e.g. Cmd+`) are not
// consumed by the IME on non-QWERTY / dead-key layouts.
// We also send printable keys to the IME first when an IME input source (e.g. Japanese,
// Korean, Chinese) is active and the input handler accepts text input. This prevents
// multi-stroke keybindings like `jj` from intercepting keys that the IME should compose
// (e.g. typing 'ji' should produce 'じ', not 'jい'). If the IME doesn't handle the key,
// it calls `doCommandBySelector:` which routes it back to keybinding matching.
let is_ime_printable_key = !is_composing
&& key_down_event
.keystroke
.key_char
.as_ref()
.is_some_and(|key_char| key_char.chars().all(|c| !c.is_control()))
&& !key_down_event.keystroke.modifiers.control
&& !key_down_event.keystroke.modifiers.function
&& !key_down_event.keystroke.modifiers.platform
&& unsafe { is_ime_input_source_active() }
&& with_input_handler(this, |input_handler| {
input_handler.query_prefers_ime_for_printable_keys()
})
.unwrap_or(false);
if is_composing
|| is_ime_printable_key
|| (key_down_event.keystroke.key_char.is_none()
&& !key_down_event.keystroke.modifiers.control
&& !key_down_event.keystroke.modifiers.function
&& !key_down_event.keystroke.modifiers.platform)
{
{
let mut lock = window_state.as_ref().lock();
lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone());
lock.do_command_handled.take();
drop(lock);
}
let handled: BOOL = unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
};
window_state.as_ref().lock().keystroke_for_do_command.take();
if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() {
return handled as BOOL;
} else if handled == YES {
return YES;
}
let handled = run_callback(PlatformInput::KeyDown(key_down_event));
return handled;
}
let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone()));
if handled == YES {
return YES;
}
if key_down_event.is_held
&& let Some(key_char) = key_down_event.keystroke.key_char.as_ref()
{
let handled = with_input_handler(this, |input_handler| {
if !input_handler.apple_press_and_hold_enabled() {
input_handler.replace_text_in_range(None, key_char);
return YES;
}
NO
});
if handled == Some(YES) {
return YES;
}
}
// Don't send key equivalents to the input handler if there are key modifiers other
// than Function key, or macOS shortcuts like cmd-` will stop working.
if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() {
return NO;
}
unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
}
}
PlatformInput::KeyUp(_) => {
drop(lock);
run_callback(event)
}
_ => NO,
}
}
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let window_state = unsafe { get_window_state(this) };
let weak_window_state = Arc::downgrade(&window_state);
let mut lock = window_state.as_ref().lock();
let window_height = lock.content_size().height;
let event = unsafe { platform_input_from_native(native_event, Some(window_height)) };
if let Some(mut event) = event {
// AppKit unhides the cursor on the next mouse movement; mirror that here.
if matches!(
event,
PlatformInput::MouseMove(_)
| PlatformInput::MouseDown(_)
| PlatformInput::MouseUp(_)
| PlatformInput::MousePressure(_)
| PlatformInput::MouseExited(_)
| PlatformInput::ScrollWheel(_)
| PlatformInput::Pinch(_)
) {
lock.cursor_visible.store(true, Ordering::Relaxed);
}
match &mut event {
PlatformInput::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
modifiers: Modifiers { control: true, .. },
..
},
) => {
// On mac, a ctrl-left click should be handled as a right click.
*event = MouseDownEvent {
button: MouseButton::Right,
modifiers: Modifiers {
control: false,
..event.modifiers
},
click_count: 1,
..*event
};
}
// Handles focusing click.
PlatformInput::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
..
},
) if (lock.first_mouse) => {
*event = MouseDownEvent {
first_mouse: true,
..*event
};
lock.first_mouse = false;
}
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
// user is still holding ctrl when releasing the left mouse button
PlatformInput::MouseUp(
event @ MouseUpEvent {
button: MouseButton::Left,
modifiers: Modifiers { control: true, .. },
..
},
) => {
*event = MouseUpEvent {
button: MouseButton::Right,
modifiers: Modifiers {
control: false,
..event.modifiers
},
click_count: 1,
..*event
};
}
_ => {}
};
match &event {
PlatformInput::MouseDown(_) => {
drop(lock);
unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
}
lock = window_state.as_ref().lock();
}
PlatformInput::MouseMove(
event @ MouseMoveEvent {
pressed_button: Some(_),
..
},
) => {
// Synthetic drag is used for selecting long buffer contents while buffer is being scrolled.
// External file drag and drop is able to emit its own synthetic mouse events which will conflict
// with these ones.
if !lock.external_files_dragged {
lock.synthetic_drag_counter += 1;
let executor = lock.foreground_executor.clone();
executor
.spawn(synthetic_drag(
weak_window_state,
lock.synthetic_drag_counter,
event.clone(),
lock.background_executor.clone(),
))
.detach();
}
}
PlatformInput::MouseUp(MouseUpEvent { .. }) => {
lock.synthetic_drag_counter += 1;
}
PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers,
capslock,
}) => {
// Only raise modifiers changed event when they have actually changed
if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers: prev_modifiers,
capslock: prev_capslock,
})) = &lock.previous_modifiers_changed_event
&& prev_modifiers == modifiers
&& prev_capslock == capslock
{
return;
}
lock.previous_modifiers_changed_event = Some(event.clone());
}
_ => {}
}
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(event);
window_state.lock().event_callback = Some(callback);
}
}
}
extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = &mut *window_state.lock();
unsafe {
if lock
.native_window
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
{
lock.move_traffic_light();
lock.start_display_link();
} else {
lock.stop_display_link();
}
}
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().lock().move_traffic_light();
}
extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.fullscreen_restore_bounds = lock.bounds();
let min_version = NSOperatingSystemVersion::new(15, 3, 0);
if is_macos_version_at_least(min_version) {
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(NO);
}
}
}
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = window_state.as_ref().lock();
let min_version = NSOperatingSystemVersion::new(15, 3, 0);
if is_macos_version_at_least(min_version) && lock.transparent_titlebar {
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(YES);
}
}
}
pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool {
unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) }
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.moved_callback.take() {
drop(lock);
callback();
window_state.lock().moved_callback = Some(callback);
}
}
// Update the window scale factor and drawable size, and call the resize callback if any.
fn update_window_scale_factor(window_state: &Arc<Mutex<MacWindowState>>) {
let mut lock = window_state.as_ref().lock();
let scale_factor = lock.scale_factor();
let size = lock.content_size();
let drawable_size = size.to_device_pixels(scale_factor);
if let Some(layer) = lock.renderer.layer() {
unsafe {
let _: () = msg_send![
layer,
setContentsScale: scale_factor as f64
];
}
}
lock.renderer.update_drawable_size(drawable_size);
if let Some(mut callback) = lock.resize_callback.take() {
let content_size = lock.content_size();
let scale_factor = lock.scale_factor();
drop(lock);
callback(content_size, scale_factor);
window_state.as_ref().lock().resize_callback = Some(callback);
};
}
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.start_display_link();
drop(lock);
update_window_scale_factor(&window_state);
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = window_state.lock();
let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
// AppKit also unhides the cursor on activation changes, so mirror that here.
lock.cursor_visible.store(true, Ordering::Relaxed);
// When opening a pop-up while the application isn't active, Cocoa sends a spurious
// `windowDidBecomeKey` message to the previous key window even though that window
// isn't actually key. This causes a bug if the application is later activated while
// the pop-up is still open, making it impossible to activate the previous key window
// even if the pop-up gets closed. The only way to activate it again is to de-activate
// the app and re-activate it, which is a pretty bad UX.
// The following code detects the spurious event and invokes `resignKeyWindow`:
// in theory, we're not supposed to invoke this method manually but it balances out
// the spurious `becomeKeyWindow` event and helps us work around that bug.
if selector == sel!(windowDidBecomeKey:) && !is_active {
let native_window = lock.native_window;
drop(lock);
unsafe {
let _: () = msg_send![native_window, resignKeyWindow];
}
return;
}
let executor = lock.foreground_executor.clone();
drop(lock);
let a11y_events = {
let mut lock = window_state.lock();
lock.accesskit_adapter
.as_mut()
.and_then(|adapter| adapter.update_view_focus_state(is_active))
};
if let Some(events) = a11y_events {
events.raise();
}
// When a window becomes active, trigger an immediate synchronous frame request to prevent
// tab flicker when switching between windows in native tabs mode.
//
// This is only done on subsequent activations (not the first) to ensure the initial focus
// path is properly established. Without this guard, the focus state would remain unset until
// the first mouse click, causing keybindings to be non-functional.
if selector == sel!(windowDidBecomeKey:) && is_active {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if lock.activated_least_once {
if let Some(mut callback) = lock.request_frame_callback.take() {
lock.renderer.set_presents_with_transaction(true);
lock.stop_display_link();
drop(lock);
callback(Default::default());
let mut lock = window_state.lock();
lock.request_frame_callback = Some(callback);
lock.renderer.set_presents_with_transaction(false);
lock.start_display_link();
}
} else {
lock.activated_least_once = true;
}
}
executor
.spawn(async move {
let mut lock = window_state.as_ref().lock();
if is_active {
lock.move_traffic_light();
}
if let Some(mut callback) = lock.activate_callback.take() {
drop(lock);
callback(is_active);
window_state.lock().activate_callback = Some(callback);
};
})
.detach();
}
extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.should_close_callback.take() {
drop(lock);
let should_close = callback();
window_state.lock().should_close_callback = Some(callback);
should_close as BOOL
} else {
YES
}
}
extern "C" fn close_window(this: &Object, _: Sel) {
unsafe {
let close_callback = {
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
lock.closed.store(true, Ordering::Release);
lock.close_callback.take()
};
if let Some(callback) = close_callback {
callback();
}
let _: () = msg_send![super(this, class!(NSWindow)), close];
}
}
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
let window_state = unsafe { get_window_state(this) };
let window_state = window_state.as_ref().lock();
window_state.renderer.layer_ptr() as id
}
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
let window_state = unsafe { get_window_state(this) };
update_window_scale_factor(&window_state);
}
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
fn convert(value: NSSize) -> Size<Pixels> {
Size {
width: px(value.width as f32),
height: px(value.height as f32),
}
}
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
let new_size = convert(size);
let old_size = unsafe {
let old_frame: NSRect = msg_send![this, frame];
convert(old_frame.size)
};
if old_size == new_size {
return;
}
unsafe {
let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
}
let scale_factor = lock.scale_factor();
let drawable_size = new_size.to_device_pixels(scale_factor);
lock.renderer.update_drawable_size(drawable_size);
if let Some(mut callback) = lock.resize_callback.take() {
let content_size = lock.content_size();
let scale_factor = lock.scale_factor();
drop(lock);
callback(content_size, scale_factor);
window_state.lock().resize_callback = Some(callback);
};
}
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
lock.renderer.set_presents_with_transaction(true);
lock.stop_display_link();
drop(lock);
callback(Default::default());
let mut lock = window_state.lock();
lock.request_frame_callback = Some(callback);
lock.renderer.set_presents_with_transaction(false);
lock.start_display_link();
}
}
extern "C" fn step(view: *mut c_void) {
let view = view as id;
let window_state = unsafe { get_window_state(&*view) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
drop(lock);
callback(Default::default());
window_state.lock().request_frame_callback = Some(callback);
}
}
extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
unsafe { msg_send![class!(NSArray), array] }
}
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
let has_marked_text_result =
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
has_marked_text_result.is_some() as BOOL
}
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
let marked_range_result =
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
marked_range_result.map_or(NSRange::invalid(), |range| range.into())
}
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
let selected_range_result = with_input_handler(this, |input_handler| {
input_handler.selected_text_range(false)
})
.flatten();
selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into())
}
extern "C" fn first_rect_for_character_range(
this: &Object,
_: Sel,
range: NSRange,
_: id,
) -> NSRect {
let frame = get_frame(this);
with_input_handler(this, |input_handler| {
input_handler.bounds_for_range(range.to_range()?)
})
.flatten()
.map_or(
NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)),
|bounds| {
NSRect::new(
NSPoint::new(
frame.origin.x + bounds.origin.x.as_f32() as f64,
frame.origin.y + frame.size.height
- bounds.origin.y.as_f32() as f64
- bounds.size.height.as_f32() as f64,
),
NSSize::new(
bounds.size.width.as_f32() as f64,
bounds.size.height.as_f32() as f64,
),
)
},
)
}
fn get_frame(this: &Object) -> NSRect {
unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
}
}
extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
unsafe {
let is_attributed_string: BOOL =
msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
let text: id = if is_attributed_string == YES {
msg_send![text, string]
} else {
text
};
let text = text.to_str();
let replacement_range = replacement_range.to_range();
with_input_handler(this, |input_handler| {
input_handler.replace_text_in_range(replacement_range, text)
});
}
}
extern "C" fn set_marked_text(
this: &Object,
_: Sel,
text: id,
selected_range: NSRange,
replacement_range: NSRange,
) {
unsafe {
let is_attributed_string: BOOL =
msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
let text: id = if is_attributed_string == YES {
msg_send![text, string]
} else {
text
};
let selected_range = selected_range.to_range();
let replacement_range = replacement_range.to_range();
let text = text.to_str();
with_input_handler(this, |input_handler| {
input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range)
});
}
}
extern "C" fn unmark_text(this: &Object, _: Sel) {
with_input_handler(this, |input_handler| input_handler.unmark_text());
}
extern "C" fn attributed_substring_for_proposed_range(
this: &Object,
_: Sel,
range: NSRange,
actual_range: *mut c_void,
) -> id {
with_input_handler(this, |input_handler| {
let range = range.to_range()?;
if range.is_empty() {
return None;
}
let mut adjusted: Option<Range<usize>> = None;
let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
if let Some(adjusted) = adjusted
&& adjusted != range
{
unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
}
unsafe {
let string: id = msg_send![class!(NSAttributedString), alloc];
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
Some(string)
}
})
.flatten()
.unwrap_or(nil)
}
// We ignore which selector it asks us to do because the user may have
// bound the shortcut to something else.
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
let state = unsafe { get_window_state(this) };
let mut lock = state.as_ref().lock();
let keystroke = lock.keystroke_for_do_command.take();
let mut event_callback = lock.event_callback.take();
drop(lock);
if let Some((keystroke, callback)) = keystroke.zip(event_callback.as_mut()) {
let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: false,
prefer_character_input: false,
}));
state.as_ref().lock().do_command_handled = Some(!handled.propagate);
}
state.as_ref().lock().event_callback = event_callback;
}
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
unsafe {
let state = get_window_state(this);
let mut lock = state.as_ref().lock();
if let Some(mut callback) = lock.appearance_changed_callback.take() {
drop(lock);
callback();
state.lock().appearance_changed_callback = Some(callback);
}
}
}
extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.first_mouse = true;
YES
}
extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 {
let position = screen_point_to_gpui_point(this, position);
with_input_handler(this, |input_handler| {
input_handler.character_index_for_point(position)
})
.flatten()
.map(|index| index as u64)
.unwrap_or(NSNotFound as u64)
}
fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> {
let frame = get_frame(this);
let window_x = position.x - frame.origin.x;
let window_y = frame.size.height - (position.y - frame.origin.y);
point(px(window_x as f32), px(window_y as f32))
}
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);
let paths = external_paths_from_event(dragging_info);
if let Some(event) = paths.map(|paths| FileDropEvent::Entered { position, paths })
&& send_file_drop_event(window_state, event)
{
return NSDragOperationCopy;
}
NSDragOperationNone
}
extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);
if send_file_drop_event(window_state, FileDropEvent::Pending { position }) {
NSDragOperationCopy
} else {
NSDragOperationNone
}
}
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
send_file_drop_event(window_state, FileDropEvent::Exited);
}
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);
send_file_drop_event(window_state, FileDropEvent::Submit { position }).to_objc()
}
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {
let mut paths = SmallVec::new();
let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) };
if filenames == nil {
return None;
}
for file in unsafe { filenames.iter() } {
let path = unsafe {
let f = NSString::UTF8String(file);
CStr::from_ptr(f).to_string_lossy().into_owned()
};
paths.push(PathBuf::from(path))
}
Some(ExternalPaths(paths))
}
extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
send_file_drop_event(window_state, FileDropEvent::Exited);
}
async fn synthetic_drag(
window_state: Weak<Mutex<MacWindowState>>,
drag_id: usize,
event: MouseMoveEvent,
executor: BackgroundExecutor,
) {
loop {
executor.timer(Duration::from_millis(16)).await;
if let Some(window_state) = window_state.upgrade() {
let mut lock = window_state.lock();
if lock.synthetic_drag_counter == drag_id {
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(PlatformInput::MouseMove(event.clone()));
window_state.lock().event_callback = Some(callback);
}
} else {
break;
}
}
}
}
/// Sends the specified FileDropEvent using `PlatformInput::FileDrop` to the window
/// state and updates the window state according to the event passed.
fn send_file_drop_event(
window_state: Arc<Mutex<MacWindowState>>,
file_drop_event: FileDropEvent,
) -> bool {
let external_files_dragged = match file_drop_event {
FileDropEvent::Entered { .. } => Some(true),
FileDropEvent::Exited => Some(false),
_ => None,
};
let mut lock = window_state.lock();
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(PlatformInput::FileDrop(file_drop_event));
let mut lock = window_state.lock();
lock.event_callback = Some(callback);
if let Some(external_files_dragged) = external_files_dragged {
lock.external_files_dragged = external_files_dragged;
}
true
} else {
false
}
}
fn drag_event_position(window_state: &Mutex<MacWindowState>, dragging_info: id) -> Point<Pixels> {
let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] };
convert_mouse_position(drag_location, window_state.lock().content_size().height)
}
fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
where
F: FnOnce(&mut PlatformInputHandler) -> R,
{
let window_state = unsafe { get_window_state(window) };
let mut lock = window_state.as_ref().lock();
if let Some(mut input_handler) = lock.input_handler.take() {
drop(lock);
let result = f(&mut input_handler);
window_state.lock().input_handler = Some(input_handler);
Some(result)
} else {
None
}
}
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
unsafe {
let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = ns_string("NSScreenNumber");
let screen_number = device_description.objectForKey_(screen_number_key);
let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
screen_number as CGDirectDisplayID
}
}
extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id {
unsafe {
let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame];
// Use a colorless semantic material. The default value `AppearanceBased`, though not
// manually set, is deprecated.
NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection);
NSVisualEffectView::setState_(view, NSVisualEffectState::Active);
view
}
}
extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer];
let layer: id = msg_send![this, layer];
if !layer.is_null() {
remove_layer_background(layer);
}
}
}
unsafe fn remove_layer_background(layer: id) {
unsafe {
let _: () = msg_send![layer, setBackgroundColor:nil];
let class_name: id = msg_send![layer, className];
if class_name.isEqualToString("CAChameleonLayer") {
// Remove the desktop tinting effect.
let _: () = msg_send![layer, setHidden: YES];
return;
}
let filters: id = msg_send![layer, filters];
if !filters.is_null() {
// Remove the increased saturation.
// The effect of a `CAFilter` or `CIFilter` is determined by its name, and the
// `description` reflects its name and some parameters. Currently `NSVisualEffectView`
// uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the
// `description` will still contain "Saturat" ("... inputSaturation = ...").
let test_string: id = ns_string("Saturat");
let count = NSArray::count(filters);
for i in 0..count {
let description: id = msg_send![filters.objectAtIndex(i), description];
let hit: BOOL = msg_send![description, containsString: test_string];
if hit == NO {
continue;
}
let all_indices = NSRange {
location: 0,
length: count,
};
let indices: id = msg_send![class!(NSMutableIndexSet), indexSet];
let _: () = msg_send![indices, addIndexesInRange: all_indices];
let _: () = msg_send![indices, removeIndex:i];
let filtered: id = msg_send![filters, objectsAtIndexes: indices];
let _: () = msg_send![layer, setFilters: filtered];
break;
}
}
let sublayers: id = msg_send![layer, sublayers];
if !sublayers.is_null() {
let count = NSArray::count(sublayers);
for i in 0..count {
let sublayer = sublayers.objectAtIndex(i);
remove_layer_background(sublayer);
}
}
}
}
extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller];
// Hide the native tab bar and set its height to 0, since we render our own.
let accessory_view: id = msg_send![view_controller, view];
let _: () = msg_send![accessory_view, setHidden: YES];
let mut frame: NSRect = msg_send![accessory_view, frame];
frame.size.height = 0.0;
let _: () = msg_send![accessory_view, setFrame: frame];
}
}
extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() {
drop(lock);
callback();
window_state.lock().move_tab_to_new_window_callback = Some(callback);
}
}
}
extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.merge_all_windows_callback.take() {
drop(lock);
callback();
window_state.lock().merge_all_windows_callback = Some(callback);
}
}
}
extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.select_next_tab_callback.take() {
drop(lock);
callback();
window_state.lock().select_next_tab_callback = Some(callback);
}
}
extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if let Some(mut callback) = lock.select_previous_tab_callback.take() {
drop(lock);
callback();
window_state.lock().select_previous_tab_callback = Some(callback);
}
}
extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil];
let window_state = get_window_state(this);
let mut lock = window_state.as_ref().lock();
lock.move_traffic_light();
if let Some(mut callback) = lock.toggle_tab_bar_callback.take() {
drop(lock);
callback();
window_state.lock().toggle_tab_bar_callback = Some(callback);
}
}
}