use crate::{
BoolExt, MacDisplay, MetalFrames, NSRange, NSStringExt, TISCopyCurrentKeyboardInputSource,
TISGetInputSourceProperty, WindowFrameSource, events::platform_input_from_native,
kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode,
ns_string, renderer,
};
use anyhow::Result;
use block2::RcBlock;
use dispatch2::DispatchQueue;
use gpui::{
AnyWindowHandle, BackgroundExecutor, Bounds, Capslock, CursorStyle, ExternalDragPayload,
ExternalPaths, FileDropEvent, ForegroundExecutor, FrameRequestSource, KeyDownEvent, Keystroke,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, PlatformAtlas, PlatformDisplay, PlatformFrameSignal, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
SharedString, Size, SystemWindowTab, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowKind, WindowParams, WindowVisibility, 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;
use ctor::ctor;
use futures::channel::oneshot;
use gpui_apple::display_link::MetalFrameSource;
use gpui_util::ResultExt;
use objc2::{
AnyThread, MainThreadMarker, Message, class, msg_send,
rc::{Retained, autoreleasepool},
runtime::{AnyClass, AnyObject, AnyProtocol, Bool, ClassBuilder, ProtocolObject, Sel},
sel,
};
#[expect(
deprecated,
reason = "File-drop decoding expects a property-list array of paths; replacing NSFilenamesPboardType requires migrating both registration and decoding to file URLs"
)]
use objc2_app_kit::NSFilenamesPboardType;
use objc2_app_kit::{
NSAlert, NSAlertStyle, NSAppearance, NSAutoresizingMaskOptions, NSBackingStoreType, NSBeep,
NSButton, NSColor, NSDragOperation, NSDraggingContext, NSDraggingImageComponent,
NSDraggingImageComponentIconKey, NSDraggingItem, NSEventModifierFlags, NSEventType,
NSPasteboardWriting, NSRequestUserAttentionType, NSScreen, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSViewLayerContentsRedrawPolicy, NSVisualEffectBlendingMode,
NSVisualEffectMaterial, NSVisualEffectState, NSWindow, NSWindowAnimationBehavior,
NSWindowButton, NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowStyleMask, NSWindowTitleVisibility, NSWorkspace,
};
use objc2_core_graphics::CGColor;
use objc2_foundation::{
NSArray, NSInteger, NSNotFound, NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect,
NSSize, NSString, NSUInteger, NSURL,
};
use objc2_uniform_type_identifiers::{UTType, UTTypeData, UTTypeFolder};
use parking_lot::Mutex;
use raw_window_handle as rwh;
use smallvec::SmallVec;
use std::{
cell::Cell,
ffi::{CStr, CString, c_void},
mem,
ops::Range,
os::unix::ffi::OsStrExt,
path::PathBuf,
ptr::{self, NonNull},
rc::{Rc, Weak},
sync::{
Arc, Once,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
const WINDOW_STATE_IVAR: &CStr = c"windowState";
static RESTORES_WORKSPACE_AT_LAUNCH_DEFAULT: Once = Once::new();
static mut WINDOW_CLASS: *const AnyClass = ptr::null();
static mut PANEL_CLASS: *const AnyClass = ptr::null();
static mut VIEW_CLASS: *const AnyClass = ptr::null();
static mut BLURRED_VIEW_CLASS: *const AnyClass = ptr::null();
static mut WINDOW_STATE_ARCHIVER_DELEGATE_CLASS: *const AnyClass = ptr::null();
static mut WINDOW_STATE_UNARCHIVER_CLASS: *const AnyClass = ptr::null();
#[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;
#[derive(PartialEq)]
pub enum UserTabbingPreference {
Never,
Always,
InFullScreen,
}
#[ctor(unsafe)]
unsafe fn build_classes() {
unsafe {
WINDOW_CLASS = build_window_class(c"GPUIWindow", class!(NSWindow));
PANEL_CLASS = build_window_class(c"GPUIPanel", class!(NSPanel));
VIEW_CLASS = {
let mut decl = ClassBuilder::new(c"GPUIView", class!(NSView)).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(_, Sel));
decl.add_method(
sel!(performKeyEquivalent:),
handle_key_equivalent as extern "C" fn(_, Sel, *mut AnyObject) -> Bool,
);
decl.add_method(
sel!(keyDown:),
handle_key_down as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(keyUp:),
handle_key_up as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mouseDown:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mouseUp:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(rightMouseDown:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(rightMouseUp:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(otherMouseDown:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(otherMouseUp:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mouseMoved:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(resetCursorRects),
reset_cursor_rects as extern "C" fn(_, Sel),
);
decl.add_method(
sel!(pressureChangeWithEvent:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mouseExited:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(magnifyWithEvent:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mouseDragged:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(rightMouseDragged:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(otherMouseDragged:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(scrollWheel:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(swipeWithEvent:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(flagsChanged:),
handle_view_event as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(makeBackingLayer),
make_backing_layer as extern "C" fn(_, Sel) -> *mut AnyObject,
);
decl.add_protocol(AnyProtocol::get(c"CALayerDelegate").unwrap());
decl.add_method(
sel!(viewDidChangeBackingProperties),
view_did_change_backing_properties as extern "C" fn(_, Sel),
);
decl.add_method(
sel!(setFrameSize:),
set_frame_size as extern "C" fn(_, Sel, NSSize),
);
decl.add_method(
sel!(displayLayer:),
display_layer as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_protocol(AnyProtocol::get(c"NSTextInputClient").unwrap());
decl.add_method(
sel!(validAttributesForMarkedText),
valid_attributes_for_marked_text as extern "C" fn(_, Sel) -> *mut AnyObject,
);
decl.add_method(
sel!(hasMarkedText),
has_marked_text as extern "C" fn(_, Sel) -> Bool,
);
decl.add_method(
sel!(markedRange),
marked_range as extern "C" fn(_, Sel) -> NSRange,
);
decl.add_method(
sel!(selectedRange),
selected_range as extern "C" fn(_, Sel) -> NSRange,
);
decl.add_method(
sel!(firstRectForCharacterRange:actualRange:),
first_rect_for_character_range
as extern "C" fn(_, Sel, NSRange, *mut NSRange) -> NSRect,
);
decl.add_method(
sel!(insertText:replacementRange:),
insert_text as extern "C" fn(_, Sel, *mut AnyObject, NSRange),
);
decl.add_method(
sel!(setMarkedText:selectedRange:replacementRange:),
set_marked_text as extern "C" fn(_, Sel, *mut AnyObject, NSRange, NSRange),
);
decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(_, Sel));
decl.add_method(
sel!(attributedSubstringForProposedRange:actualRange:),
attributed_substring_for_proposed_range
as extern "C" fn(_, Sel, NSRange, *mut NSRange) -> *mut AnyObject,
);
decl.add_method(
sel!(viewDidChangeEffectiveAppearance),
view_did_change_effective_appearance as extern "C" fn(_, Sel),
);
decl.add_method(
sel!(doCommandBySelector:),
do_command_by_selector as extern "C" fn(_, Sel, Sel),
);
decl.add_method(
sel!(acceptsFirstMouse:),
accepts_first_mouse as extern "C" fn(_, Sel, *mut AnyObject) -> Bool,
);
decl.add_method(
sel!(_opaqueRectForWindowMoveWhenInTitlebar),
opaque_rect_for_window_move_when_in_titlebar as extern "C" fn(_, Sel) -> NSRect,
);
decl.add_method(
sel!(characterIndexForPoint:),
character_index_for_point as extern "C" fn(_, Sel, NSPoint) -> NSUInteger,
);
decl.register()
};
BLURRED_VIEW_CLASS = {
let mut decl = ClassBuilder::new(c"BlurredView", class!(NSVisualEffectView)).unwrap();
decl.add_method(
sel!(initWithFrame:),
blurred_view_init_with_frame as extern "C" fn(_, Sel, NSRect) -> *mut AnyObject,
);
decl.add_method(
sel!(updateLayer),
blurred_view_update_layer as extern "C" fn(_, Sel),
);
decl.register()
};
WINDOW_STATE_ARCHIVER_DELEGATE_CLASS = {
let mut decl =
ClassBuilder::new(c"GPUIWindowStateArchiverDelegate", class!(NSObject)).unwrap();
decl.add_method(
sel!(archiver:willEncodeObject:),
window_state_archiver_will_encode_object
as extern "C" fn(_, Sel, *mut AnyObject, *mut AnyObject) -> *mut AnyObject,
);
decl.register()
};
WINDOW_STATE_UNARCHIVER_CLASS = {
let mut decl =
ClassBuilder::new(c"GPUIWindowStateKeyedUnarchiver", class!(NSKeyedUnarchiver))
.unwrap();
decl.add_method(
sel!(_windowRestorationOptions),
window_state_unarchiver_restoration_options
as extern "C" fn(_, Sel) -> *mut AnyObject,
);
decl.register()
};
}
}
extern "C" fn window_state_archiver_will_encode_object(
_this: &AnyObject,
_sel: Sel,
_archiver: *mut AnyObject,
object: *mut AnyObject,
) -> *mut AnyObject {
unsafe {
if object.is_null() {
return object;
}
let is_view: Bool = msg_send![object, isKindOfClass: class!(NSView)];
let is_window: Bool = msg_send![object, isKindOfClass: class!(NSWindow)];
if is_view == Bool::YES || is_window == Bool::YES {
ptr::null_mut()
} else {
object
}
}
}
extern "C" fn window_state_unarchiver_restoration_options(
_this: &AnyObject,
_sel: Sel,
) -> *mut AnyObject {
if !is_macos_version_at_least(NSOperatingSystemVersion {
majorVersion: 15,
minorVersion: 0,
patchVersion: 0,
}) {
return ptr::null_mut();
}
unsafe {
match AnyClass::get(c"NSWindowRestorationOptions") {
Some(class) => {
let options: *mut AnyObject = msg_send![class, alloc];
let options: *mut AnyObject = msg_send![options, init];
msg_send![options, autorelease]
}
None => ptr::null_mut(),
}
}
}
pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
point(
px(position.x as f32),
window_height - px(position.y as f32),
)
}
pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) {
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let key_window: *mut AnyObject = msg_send![app, keyWindow];
let main_window: *mut AnyObject = msg_send![app, mainWindow];
let active_window = if !key_window.is_null() && is_gpui_window(key_window) {
Some(key_window)
} else if !main_window.is_null() && is_gpui_window(main_window) {
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: &CStr, superclass: &AnyClass) -> *const AnyClass {
unsafe {
let mut decl = ClassBuilder::new(name, superclass).unwrap();
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(_, Sel));
decl.add_method(
sel!(canBecomeMainWindow),
yes as extern "C" fn(_, Sel) -> Bool,
);
decl.add_method(
sel!(canBecomeKeyWindow),
yes as extern "C" fn(_, Sel) -> Bool,
);
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidChangeOcclusionState:),
window_did_change_occlusion_state as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidFailToExitFullScreen:),
window_did_fail_to_exit_fullscreen as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidExitFullScreen:),
window_did_exit_fullscreen as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidChangeScreenProfile:),
window_did_change_color_profile as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidChangeScreen:),
window_did_change_screen as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowDidResignKey:),
window_did_change_key_status as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(windowShouldClose:),
window_should_close as extern "C" fn(_, Sel, *mut AnyObject) -> Bool,
);
decl.add_method(sel!(close), close_window as extern "C" fn(_, Sel));
decl.add_method(
sel!(draggingEntered:),
dragging_entered as extern "C" fn(_, Sel, *mut AnyObject) -> NSDragOperation,
);
decl.add_method(
sel!(draggingUpdated:),
dragging_updated as extern "C" fn(_, Sel, *mut AnyObject) -> NSDragOperation,
);
decl.add_method(
sel!(draggingExited:),
dragging_exited as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(performDragOperation:),
perform_drag_operation as extern "C" fn(_, Sel, *mut AnyObject) -> Bool,
);
decl.add_method(
sel!(concludeDragOperation:),
conclude_drag_operation as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_protocol(AnyProtocol::get(c"NSDraggingSource").unwrap());
decl.add_method(
sel!(draggingSession:sourceOperationMaskForDraggingContext:),
dragging_session_source_operation_mask
as extern "C" fn(_, Sel, *mut AnyObject, NSDraggingContext) -> NSDragOperation,
);
decl.add_method(
sel!(draggingSession:endedAtPoint:operation:),
dragging_session_ended
as extern "C" fn(_, Sel, *mut AnyObject, NSPoint, NSDragOperation),
);
decl.add_method(
sel!(addTitlebarAccessoryViewController:),
add_titlebar_accessory_view_controller as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(moveTabToNewWindow:),
move_tab_to_new_window as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(mergeAllWindows:),
merge_all_windows as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(selectNextTab:),
select_next_tab as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(selectPreviousTab:),
select_previous_tab as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.add_method(
sel!(toggleTabBar:),
toggle_tab_bar as extern "C" fn(_, Sel, *mut AnyObject),
);
decl.register()
}
}
#[derive(Clone, Copy)]
struct TrafficLightFrames {
titlebar: NSRect,
close: NSRect,
minimize: NSRect,
zoom: NSRect,
}
struct TrafficLightButtons {
close: Retained<NSButton>,
minimize: Retained<NSButton>,
zoom: Retained<NSButton>,
}
const NS_APPLICATION_PRESENTATION_AUTO_HIDE_DOCK: NSUInteger = 1 << 0;
const NS_APPLICATION_PRESENTATION_AUTO_HIDE_MENU_BAR: NSUInteger = 1 << 2;
struct SimpleFullscreenState {
frame: NSRect,
bounds: Bounds<Pixels>,
style_mask: NSWindowStyleMask,
}
enum SimpleFullscreenPlan {
Enter { screen_frame: NSRect },
Exit(SimpleFullscreenState),
}
struct SimpleFullscreenAppState {
window_count: usize,
saved_presentation_options: NSUInteger,
}
static SIMPLE_FULLSCREEN_APP_STATE: Mutex<Option<SimpleFullscreenAppState>> = Mutex::new(None);
unsafe fn push_simple_fullscreen_presentation_options() {
let mut app_state = SIMPLE_FULLSCREEN_APP_STATE.lock();
match app_state.as_mut() {
Some(app_state) => app_state.window_count += 1,
None => unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let saved_presentation_options: NSUInteger = msg_send![app, presentationOptions];
let _: () = msg_send![
app,
setPresentationOptions: NS_APPLICATION_PRESENTATION_AUTO_HIDE_DOCK
| NS_APPLICATION_PRESENTATION_AUTO_HIDE_MENU_BAR
];
*app_state = Some(SimpleFullscreenAppState {
window_count: 1,
saved_presentation_options,
});
},
}
}
unsafe fn pop_simple_fullscreen_presentation_options() {
let mut app_state = SIMPLE_FULLSCREEN_APP_STATE.lock();
if let Some(state) = app_state.as_mut() {
state.window_count = state.window_count.saturating_sub(1);
if state.window_count == 0 {
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let _: () = msg_send![
app,
setPresentationOptions: state.saved_presentation_options
];
}
*app_state = None;
}
}
}
unsafe fn apply_simple_fullscreen_plan(
native_window: *mut AnyObject,
native_view: *mut AnyObject,
plan: SimpleFullscreenPlan,
) {
unsafe {
match plan {
SimpleFullscreenPlan::Exit(saved) => {
pop_simple_fullscreen_presentation_options();
let _: () = msg_send![native_window, setStyleMask: saved.style_mask];
let _: () = msg_send![native_window, setFrame: saved.frame, display: Bool::YES];
}
SimpleFullscreenPlan::Enter { screen_frame } => {
push_simple_fullscreen_presentation_options();
let _: () = msg_send![native_window, setStyleMask: NSWindowStyleMask::Borderless];
let _: () = msg_send![native_window, setFrame: screen_frame, display: Bool::YES];
}
}
let _: () = msg_send![native_window, makeKeyAndOrderFront: ptr::null_mut::<AnyObject>()];
let _: Bool = msg_send![native_window, makeFirstResponder: native_view];
}
}
enum FrameSource {
Metal(Rc<MetalFrames>),
DisplayLink(Option<WindowFrameSource>),
}
struct MacWindowState {
handle: AnyWindowHandle,
foreground_executor: ForegroundExecutor,
background_executor: BackgroundExecutor,
native_window: *mut AnyObject,
native_view: NonNull<AnyObject>,
blurred_view: Option<*mut AnyObject>,
background_appearance: WindowBackgroundAppearance,
cursor_style: CursorStyle,
cursor_visible: Arc<AtomicBool>,
frame_source: FrameSource,
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)>>,
visibility_callback: Option<Box<dyn FnMut(WindowVisibility)>>,
last_visibility: Option<WindowVisibility>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
display_changed_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>,
last_left_mouse_down_event: Option<Retained<AnyObject>>,
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
traffic_light_frames: Option<TrafficLightFrames>,
pre_fullscreen_traffic_light_frames: Option<TrafficLightFrames>,
transparent_titlebar: bool,
previous_modifiers_changed_event: Option<PlatformInput>,
keystroke_for_do_command: Option<Keystroke>,
do_command_handled: Option<bool>,
external_files_dragged: bool,
first_mouse: bool,
app_owns_titlebar_drag: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
is_exiting_fullscreen: bool,
simple_fullscreen_state: Option<SimpleFullscreenState>,
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,
color_status: gpui::WindowColorStatus,
color_profile_dirty: bool,
closed: Arc<AtomicBool>,
accesskit_adapter: Option<accesskit_macos::SubclassingAdapter>,
sheet_parent: Option<*mut AnyObject>,
}
impl MacWindowState {
fn move_traffic_light(&mut self) {
if let Some(traffic_light_position) = self.traffic_light_position {
if self.is_fullscreen() && !self.is_exiting_fullscreen {
self.restore_traffic_light();
return;
}
if self.traffic_light_frames.is_none() {
self.traffic_light_frames = self.capture_traffic_light_frames();
}
let window_height = Pixels::from(self.native_window().frame().size.height);
if self.traffic_light_frames.is_some() {
let Some(buttons) = self.traffic_light_buttons() else {
return;
};
let Some(titlebar_container) = Self::titlebar_container(&buttons.close) else {
return;
};
let close_frame = buttons.close.frame();
let minimize_frame = buttons.minimize.frame();
let button_width = Pixels::from(close_frame.size.width);
let button_height = Pixels::from(close_frame.size.height);
let button_padding = Pixels::from(
minimize_frame.origin.x - close_frame.origin.x - close_frame.size.width,
);
let container_height =
button_height + traffic_light_position.y + traffic_light_position.y;
let mut titlebar_frame = titlebar_container.frame();
titlebar_frame.size.height = container_height.to_f64();
titlebar_frame.origin.y = (window_height - container_height).to_f64();
let minimize_x = traffic_light_position.x + button_width + button_padding;
let zoom_x = minimize_x + button_width + button_padding;
titlebar_container.setFrame(titlebar_frame);
buttons.close.setFrameOrigin(NSPoint::new(
traffic_light_position.x.to_f64(),
traffic_light_position.y.to_f64(),
));
buttons.minimize.setFrameOrigin(NSPoint::new(
minimize_x.to_f64(),
traffic_light_position.y.to_f64(),
));
buttons.zoom.setFrameOrigin(NSPoint::new(
zoom_x.to_f64(),
traffic_light_position.y.to_f64(),
));
titlebar_container.updateTrackingAreas();
buttons.close.updateTrackingAreas();
buttons.minimize.updateTrackingAreas();
buttons.zoom.updateTrackingAreas();
}
}
}
fn capture_traffic_light_frames(&self) -> Option<TrafficLightFrames> {
let buttons = self.traffic_light_buttons()?;
let titlebar_container = Self::titlebar_container(&buttons.close)?;
Some(TrafficLightFrames {
titlebar: titlebar_container.frame(),
close: buttons.close.frame(),
minimize: buttons.minimize.frame(),
zoom: buttons.zoom.frame(),
})
}
fn native_window(&self) -> &NSWindow {
unsafe { &*self.native_window.cast::<NSWindow>() }
}
fn traffic_light_buttons(&self) -> Option<TrafficLightButtons> {
let window = self.native_window();
Some(TrafficLightButtons {
close: window.standardWindowButton(NSWindowButton::CloseButton)?,
minimize: window.standardWindowButton(NSWindowButton::MiniaturizeButton)?,
zoom: window.standardWindowButton(NSWindowButton::ZoomButton)?,
})
}
fn titlebar_container(close_button: &NSButton) -> Option<Retained<NSView>> {
unsafe {
let button_container = close_button.superview()?;
button_container.superview()
}
}
fn restore_traffic_light(&mut self) {
if let Some(frames) = self.traffic_light_frames.take() {
let Some(buttons) = self.traffic_light_buttons() else {
return;
};
let Some(titlebar_container) = Self::titlebar_container(&buttons.close) else {
return;
};
buttons.close.setFrame(frames.close);
buttons.minimize.setFrame(frames.minimize);
buttons.zoom.setFrame(frames.zoom);
titlebar_container.setFrame(frames.titlebar);
titlebar_container.updateTrackingAreas();
buttons.close.updateTrackingAreas();
buttons.minimize.updateTrackingAreas();
buttons.zoom.updateTrackingAreas();
}
}
fn start_display_link(&mut self, window_state: &Rc<Mutex<MacWindowState>>) {
self.stop_display_link();
if !self
.native_window()
.occlusionState()
.contains(NSWindowOcclusionState::Visible)
{
return;
}
match &mut self.frame_source {
FrameSource::Metal(metal_frames) => {
let Some(layer) = self.renderer.layer() else {
return;
};
let main_thread =
crate::main_thread_marker().expect("macOS windows require the main thread");
let window_state = Rc::downgrade(window_state);
let source = MetalFrameSource::new(layer, main_thread, move |update| {
if let Some(window_state) = window_state.upgrade() {
metal_frame(&window_state, update);
}
});
if let Some(source) = source {
metal_frames.replace(Some(source));
self.renderer.set_display_link_attached(true);
}
}
FrameSource::DisplayLink(frame_source) => {
let screen: *mut AnyObject = unsafe { msg_send![self.native_window, screen] };
let Some(display_id) = display_id_for_screen(screen) else {
return;
};
let data = self.native_view.as_ptr() as *mut c_void;
frame_source
.get_or_insert_with(|| WindowFrameSource::new(data, step))
.start(display_id)
.log_err();
}
}
}
fn stop_display_link(&mut self) {
match &mut self.frame_source {
FrameSource::Metal(metal_frames) => {
if metal_frames.is_attached() {
metal_frames.replace(None);
self.renderer.set_display_link_attached(false);
}
}
FrameSource::DisplayLink(Some(frame_source)) => frame_source.stop(),
FrameSource::DisplayLink(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())
}
let bounds = self.bounds();
let screen_frame = self
.native_window()
.screen()
.map_or_default(|screen| screen.visibleFrame());
bounds.size == rect_to_size(screen_frame)
}
fn is_fullscreen(&self) -> bool {
self.native_window()
.styleMask()
.contains(NSWindowStyleMask::FullScreen)
}
fn toggle_simple_fullscreen(&mut self) -> Option<SimpleFullscreenPlan> {
if self.is_fullscreen() {
return None;
}
if let Some(saved) = self.simple_fullscreen_state.take() {
Some(SimpleFullscreenPlan::Exit(saved))
} else {
let screen = self.native_window().screen()?;
let screen_frame = screen.frame();
let bounds = self.bounds();
self.simple_fullscreen_state = Some(SimpleFullscreenState {
frame: self.native_window().frame(),
bounds,
style_mask: self.native_window().styleMask(),
});
Some(SimpleFullscreenPlan::Enter { screen_frame })
}
}
fn bounds(&self) -> Bounds<Pixels> {
let mut window_frame = self.native_window().frame();
let Some(screen) = self.native_window().screen() else {
return Bounds::new(point(px(0.), px(0.)), gpui::DEFAULT_WINDOW_SIZE);
};
let screen_frame = screen.frame();
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 } = self
.native_window()
.contentView()
.map_or_default(|view| view.frame().size);
size(px(width as f32), px(height as f32))
}
fn scale_factor(&self) -> f32 {
get_scale_factor(self.native_window)
}
fn window_bounds(&self) -> WindowBounds {
if self.is_fullscreen() {
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
} else if let Some(state) = &self.simple_fullscreen_state {
WindowBounds::Windowed(state.bounds)
} else {
WindowBounds::Windowed(self.bounds())
}
}
}
pub(crate) struct MacWindow(
Rc<Mutex<MacWindowState>>,
MainThreadMarker,
Option<Rc<MetalFrames>>,
);
impl MacWindow {
pub fn open(
handle: AnyWindowHandle,
WindowParams {
bounds,
titlebar,
kind,
is_movable,
app_owns_titlebar_drag,
is_resizable,
is_minimizable,
focus,
show,
display_id,
window_min_size,
tabbing_identifier,
use_display_p3,
..
}: WindowParams,
cursor_visible: Arc<AtomicBool>,
foreground_executor: ForegroundExecutor,
background_executor: BackgroundExecutor,
renderer_context: renderer::Context,
marker: MainThreadMarker,
) -> Result<Self> {
autoreleasepool(|_| unsafe {
let renderer = renderer::new_renderer(renderer_context, false, use_display_p3)?;
let allows_automatic_window_tabbing = tabbing_identifier.is_some();
if allows_automatic_window_tabbing {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: Bool::YES];
} else {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: Bool::NO];
}
let mut style_mask;
if let Some(titlebar) = titlebar.as_ref() {
style_mask = NSWindowStyleMask::Closable | NSWindowStyleMask::Titled;
if is_resizable {
style_mask |= NSWindowStyleMask::Resizable;
}
if is_minimizable {
style_mask |= NSWindowStyleMask::Miniaturizable;
}
if titlebar.appears_transparent {
style_mask |= NSWindowStyleMask::FullSizeContentView;
}
} else {
style_mask = NSWindowStyleMask::Titled | NSWindowStyleMask::FullSizeContentView;
}
let native_window: *mut AnyObject = match kind {
WindowKind::Normal => {
msg_send![WINDOW_CLASS, alloc]
}
WindowKind::PopUp | WindowKind::AnchoredPopup(_) => {
style_mask |= NSWindowStyleMask::NonactivatingPanel;
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 = None;
let screens = NSScreen::screens(marker);
for screen in screens.iter() {
if display_id_for_screen(Retained::as_ptr(&screen).cast_mut().cast())
== Some(display.0)
{
target_screen = Some(screen);
}
}
let target_screen = target_screen.or_else(|| NSScreen::mainScreen(marker));
let screen_frame = target_screen
.as_ref()
.map_or_default(|screen| screen.frame());
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: *mut AnyObject = msg_send![
native_window,
initWithContentRect: window_rect,
styleMask: style_mask,
backing: NSBackingStoreType::Buffered,
defer: Bool::NO,
screen: target_screen.as_deref()
];
assert!(!native_window.is_null());
#[expect(
deprecated,
reason = "File-drop decoding expects a property-list array of paths; replacing NSFilenamesPboardType requires migrating both registration and decoding to file URLs"
)]
let () = msg_send![
native_window,
registerForDraggedTypes:
&*NSArray::from_slice(&[NSFilenamesPboardType])
];
let () = msg_send![
native_window,
setReleasedWhenClosed: Bool::NO
];
let content_view: *mut AnyObject = msg_send![native_window, contentView];
let native_view: *mut AnyObject = msg_send![VIEW_CLASS, alloc];
let content_bounds: NSRect = msg_send![content_view, bounds];
let native_view: *mut AnyObject = msg_send![native_view, initWithFrame: content_bounds];
assert!(!native_view.is_null());
let metal_frames = MetalFrameSource::is_available().then(Rc::<MetalFrames>::default);
let state = Rc::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,
frame_source: match &metal_frames {
Some(metal_frames) => FrameSource::Metal(metal_frames.clone()),
None => FrameSource::DisplayLink(None),
},
renderer,
request_frame_callback: None,
event_callback: None,
activate_callback: None,
visibility_callback: None,
last_visibility: None,
resize_callback: None,
moved_callback: None,
display_changed_callback: None,
should_close_callback: None,
close_callback: None,
appearance_changed_callback: None,
input_handler: None,
last_key_equivalent: None,
last_left_mouse_down_event: None,
synthetic_drag_counter: 0,
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
traffic_light_frames: None,
pre_fullscreen_traffic_light_frames: None,
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,
app_owns_titlebar_drag,
fullscreen_restore_bounds: Bounds::default(),
is_exiting_fullscreen: false,
simple_fullscreen_state: None,
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,
color_status: gpui::WindowColorStatus::new(
gpui::WindowColorMode::Default,
gpui::WindowColorMode::Default,
true,
None,
None,
),
color_profile_dirty: true,
closed: Arc::new(AtomicBool::new(false)),
accesskit_adapter: None,
sheet_parent: None,
}));
let mut window = Self(state, marker, metal_frames);
(*native_window)
.class()
.instance_variable(WINDOW_STATE_IVAR)
.expect("GPUI window state ivar")
.load_ptr::<*mut c_void>(&*native_window)
.write(Rc::into_raw(window.0.clone()).cast_mut().cast());
let _: () = msg_send![native_window, setDelegate: native_window];
(*native_view)
.class()
.instance_variable(WINDOW_STATE_IVAR)
.expect("GPUI view state ivar")
.load_ptr::<*mut c_void>(&*native_view)
.write(Rc::into_raw(window.0.clone()).cast_mut().cast());
if let Some(title) = titlebar
.as_ref()
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
{
window.set_title(title);
}
let _: () = msg_send![native_window, setMovable: is_movable.to_objc()];
if let Some(window_min_size) = window_min_size {
let _: () = msg_send![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) {
let _: () = msg_send![native_window, setTitlebarAppearsTransparent: Bool::YES];
let _: () =
msg_send![native_window, setTitleVisibility: NSWindowTitleVisibility::Hidden];
}
let _: () = msg_send![native_view, setAutoresizingMask: NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable];
let _: () = msg_send![native_view, setWantsBestResolutionOpenGLSurface: Bool::YES];
let _: () = msg_send![native_view, setWantsLayer: Bool::YES];
let _: () = msg_send![
native_view,
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawPolicy::DuringViewResize
];
let native_view: *mut AnyObject = msg_send![native_view, autorelease];
let _: () = msg_send![content_view, addSubview: native_view];
let _: Bool = msg_send![native_window, makeFirstResponder: native_view];
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let main_window: *mut AnyObject = msg_send![app, mainWindow];
let mut sheet_parent = None;
let tracking_view = &*native_view.cast::<NSView>();
match kind {
WindowKind::Normal | WindowKind::Floating => {
if kind == WindowKind::Floating {
let _: () = msg_send![native_window, setLevel: NSFloatingWindowLevel];
let _: () = msg_send![native_window, setAcceptsMouseMovedEvents: Bool::YES];
} else {
let _: () = msg_send![native_window, setLevel: NSNormalWindowLevel];
let _: () = msg_send![native_window, setAcceptsMouseMovedEvents: Bool::NO];
add_mouse_tracking_area(tracking_view);
}
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:ptr::null_mut::<AnyObject>()];
}
}
WindowKind::PopUp | WindowKind::AnchoredPopup(_) => {
add_mouse_tracking_area(tracking_view);
let _: () = msg_send![native_window, setLevel: NSPopUpWindowLevel];
let _: () = msg_send![
native_window,
setAnimationBehavior: NSWindowAnimationBehavior::UtilityWindow
];
let _: () = msg_send![native_window, setCollectionBehavior: NSWindowCollectionBehavior::CanJoinAllSpaces
| NSWindowCollectionBehavior::FullScreenAuxiliary];
}
WindowKind::Dialog => {
if !main_window.is_null() {
let parent = {
let active_sheet: *mut AnyObject =
msg_send![main_window, attachedSheet];
if active_sheet.is_null() {
main_window
} else {
active_sheet
}
};
let _: () = msg_send![parent, beginSheet: native_window, completionHandler: None::<&block2::Block<dyn Fn(NSInteger)>>];
sheet_parent = Some(parent);
}
}
}
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
{
let main_window_is_fullscreen = (&*main_window.cast::<NSWindow>())
.styleMask()
.contains(NSWindowStyleMask::FullScreen);
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 == Bool::YES && main_window_visible == Bool::YES {
let _: () = msg_send![main_window, addTabbedWindow: native_window, ordered: NSWindowOrderingMode::Above];
if !main_window_is_fullscreen {
let _: () =
msg_send![native_window, orderFront: ptr::null_mut::<AnyObject>()];
}
}
}
}
if focus && show {
let _: () =
msg_send![native_window, makeKeyAndOrderFront: ptr::null_mut::<AnyObject>()];
} else if show {
let _: () = msg_send![native_window, orderFront: ptr::null_mut::<AnyObject>()];
}
let _: () = msg_send![native_window, setFrameTopLeftPoint: window_rect.origin];
{
let mut window_state = window.0.lock();
window_state.move_traffic_light();
window_state.sheet_parent = sheet_parent;
}
Ok(window)
})
}
pub fn active_window() -> Option<AnyWindowHandle> {
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let main_window: *mut AnyObject = 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: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let windows: *mut AnyObject = msg_send![app, orderedWindows];
let count: NSUInteger = msg_send![windows, count];
let mut window_handles = Vec::new();
for i in 0..count {
let window: *mut AnyObject = 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: *mut AnyObject = msg_send![class!(NSUserDefaults), standardUserDefaults];
let domain = ns_string("NSGlobalDomain");
let key = ns_string("AppleWindowTabbingMode");
let dict: *mut AnyObject = msg_send![defaults, persistentDomainForName: domain];
let value: *mut AnyObject = if !dict.is_null() {
msg_send![dict, objectForKey: key]
} else {
ptr::null_mut::<AnyObject>()
};
let value_str = if !value.is_null() {
CStr::from_ptr(msg_send![value, UTF8String]).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();
drop(this.accesskit_adapter.take());
this.renderer.destroy();
let window = this.native_window;
let sheet_parent = this.sheet_parent.take();
match &mut this.frame_source {
FrameSource::Metal(metal_frames) => metal_frames.replace(None),
FrameSource::DisplayLink(frame_source) => drop(frame_source.take()),
}
unsafe {
let _: () = msg_send![this.native_window, setDelegate: ptr::null_mut::<AnyObject>()];
}
this.input_handler.take();
this.visibility_callback.take();
this.foreground_executor
.spawn(async move {
unsafe {
if let Some(parent) = sheet_parent {
let _: () = msg_send![parent, endSheet: window];
}
let _: () = msg_send![window, close];
let _: *mut AnyObject = msg_send![window, autorelease];
}
})
.detach();
}
}
fn if_window_not_closed(closed: Arc<AtomicBool>, f: impl FnOnce()) {
if !closed.load(Ordering::Acquire) {
f();
}
}
fn color_status_for_window(state: &MacWindowState) -> gpui::WindowColorStatus {
let renderer = state.renderer.color_status();
let evidence = state
.native_window()
.screen()
.and_then(|screen| screen.colorSpace())
.and_then(|space| space.CGColorSpace())
.map(|space| space.is_wide_gamut_rgb());
gpui::WindowColorStatus::new(
renderer.requested(),
renderer.effective(),
renderer.supports_wide_gamut_sdr(),
evidence,
renderer.fallback(),
)
}
impl PlatformWindow for MacWindow {
fn set_color_mode(
&mut self,
mode: gpui::WindowColorMode,
) -> anyhow::Result<gpui::WindowColorStatus> {
let mut state = self.0.lock();
state.renderer.set_color_mode(mode)?;
state.color_status = color_status_for_window(&state);
state.color_profile_dirty = false;
Ok(state.color_status)
}
fn color_status(&self) -> gpui::WindowColorStatus {
self.0.lock().color_status
}
fn prepare_frame(&self) -> bool {
let mut state = self.0.lock();
if !state.color_profile_dirty {
return false;
}
state.color_status = color_status_for_window(&state);
state.color_profile_dirty = false;
true
}
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 {
let _: () = msg_send![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 *mut AnyObject;
let _: () = msg_send![native_window, mergeAllWindows:ptr::null_mut::<AnyObject>()];
}
}
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 *mut AnyObject;
let _: () =
msg_send![native_window, moveTabToNewWindow:ptr::null_mut::<AnyObject>()];
let _: () =
msg_send![native_window, makeKeyAndOrderFront: ptr::null_mut::<AnyObject>()];
}
}
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:ptr::null_mut::<AnyObject>()];
}
}
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: Bool::YES];
} else {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: Bool::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:ptr::null_mut::<AnyObject>()];
}
}
}
fn native_window_state(&self) -> Option<Vec<u8>> {
let native_window = {
let state = self.0.lock();
if state.is_fullscreen() || state.simple_fullscreen_state.is_some() {
return None;
}
state.native_window
};
unsafe {
let archiver: *mut AnyObject = msg_send![class!(NSKeyedArchiver), alloc];
let archiver: *mut AnyObject =
msg_send![archiver, initRequiringSecureCoding: Bool::YES];
if archiver.is_null() {
log::warn!("failed to create an archiver for the native window state");
return None;
}
let delegate: *mut AnyObject = msg_send![WINDOW_STATE_ARCHIVER_DELEGATE_CLASS, new];
let _: () = msg_send![archiver, setDelegate: delegate];
let _: () = msg_send![native_window, encodeRestorableStateWithCoder: archiver];
let _: () = msg_send![archiver, finishEncoding];
let _: () = msg_send![archiver, setDelegate: ptr::null_mut::<AnyObject>()];
let data: *mut AnyObject = msg_send![archiver, encodedData];
let bytes: *const c_void = if data.is_null() {
ptr::null()
} else {
msg_send![data, bytes]
};
let state = if bytes.is_null() {
log::warn!("the archiver produced no data for the native window state");
None
} else {
let length: NSUInteger = msg_send![data, length];
Some(std::slice::from_raw_parts(bytes.cast::<u8>(), length).to_vec())
};
let _: () = msg_send![delegate, release];
let _: () = msg_send![archiver, release];
state
}
}
fn restore_native_window_state(&self, state: &[u8]) {
if state.is_empty() {
return;
}
let native_window = self.0.lock().native_window;
unsafe {
let data: *mut AnyObject = msg_send![
class!(NSData),
dataWithBytes: state.as_ptr().cast::<c_void>(),
length: state.len()
];
if data.is_null() {
log::warn!(
"failed to wrap {} bytes of native window state",
state.len()
);
return;
}
if !is_macos_version_at_least(NSOperatingSystemVersion {
majorVersion: 15,
minorVersion: 0,
patchVersion: 0,
}) {
RESTORES_WORKSPACE_AT_LAUNCH_DEFAULT.call_once(|| {
let defaults: *mut AnyObject =
msg_send![class!(NSUserDefaults), standardUserDefaults];
let key = ns_string("NSWindowRestoresWorkspaceAtLaunch");
let yes_value: *mut AnyObject =
msg_send![class!(NSNumber), numberWithBool: Bool::YES];
let dict: *mut AnyObject = msg_send![
class!(NSDictionary),
dictionaryWithObject: yes_value,
forKey: key
];
let _: () = msg_send![defaults, registerDefaults: dict];
});
}
let unarchiver: *mut AnyObject = msg_send![WINDOW_STATE_UNARCHIVER_CLASS, alloc];
let mut error: *mut AnyObject = ptr::null_mut::<AnyObject>();
let unarchiver: *mut AnyObject =
msg_send![unarchiver, initForReadingFromData: data, error: &mut error];
if unarchiver.is_null() {
log::warn!(
"failed to unarchive the native window state: {}",
ns_error_description(error)
);
return;
}
let _: () = msg_send![native_window, restoreStateWithCoder: unarchiver];
let error: *mut AnyObject = msg_send![unarchiver, error];
if !error.is_null() {
log::warn!(
"failed to restore the native window state: {}",
ns_error_description(error)
);
}
let _: () = msg_send![unarchiver, release];
}
}
fn set_traffic_light_position(&self, position: Point<Pixels>) {
let mut state = self.0.lock();
state.traffic_light_position = Some(position);
state.move_traffic_light();
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().lock().scale_factor()
}
fn appearance(&self) -> WindowAppearance {
unsafe {
let appearance: Option<Retained<NSAppearance>> =
msg_send![self.0.lock().native_window, effectiveAppearance];
crate::window_appearance::window_appearance_from_native(appearance.as_deref())
}
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
let screen: *mut AnyObject = unsafe { msg_send![self.0.lock().native_window, screen] };
let display_id = display_id_for_screen(screen)?;
Some(Rc::new(MacDisplay(display_id)))
}
fn mouse_position(&self) -> Point<Pixels> {
let position = 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::Control);
let alt = modifiers.contains(NSEventModifierFlags::Option);
let shift = modifiers.contains(NSEventModifierFlags::Shift);
let command = modifiers.contains(NSEventModifierFlags::Command);
let function = modifiers.contains(NSEventModifierFlags::Function);
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::CapsLock),
}
}
}
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>> {
let initial_focus_ix = answers
.iter()
.enumerate()
.rev()
.find(|(_, label)| !label.is_cancel())
.map(|(ix, _)| ix)
.filter(|&ix| ix > 0);
let alert = NSAlert::new(self.1);
alert.setAlertStyle(match level {
PromptLevel::Critical => NSAlertStyle::Critical,
PromptLevel::Warning => NSAlertStyle::Warning,
PromptLevel::Info => NSAlertStyle::Informational,
});
let message = NSString::from_str(msg);
alert.setMessageText(message.as_ref());
if let Some(detail) = detail {
let detail_text = NSString::from_str(detail);
alert.setInformativeText(detail_text.as_ref());
}
let mut initial_focus_button: Option<Retained<NSButton>> = None;
for (ix, answer) in answers.iter().enumerate() {
let title = NSString::from_str(answer.label());
let button = alert.addButtonWithTitle(&title);
button.setTag(ix as NSInteger);
if answer.is_cancel() {
if let Some(key) = core::char::from_u32(crate::events::ESCAPE_KEY as u32) {
let key = NSString::from_str(&key.to_string());
button.setKeyEquivalent(&key);
}
} else if Some(ix) == initial_focus_ix {
initial_focus_button = Some(button);
}
}
if let Some(button) = initial_focus_button {
alert.window().setInitialFirstResponder(Some(&button));
}
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Cell::new(Some(done_tx));
let block = RcBlock::new(move |answer: NSInteger| {
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(answer.try_into().unwrap());
}
});
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 sheet_window: &NSWindow = unsafe { &*(native_window as *const NSWindow) };
alert.beginSheetModalForWindow_completionHandler(sheet_window, Some(&block));
}
})
.detach();
Some(done_rx)
}
fn activate(&self) {
let lock = self.0.lock();
let window = lock.native_window;
let view = lock.native_view.as_ptr();
let closed = lock.closed.clone();
let executor = lock.foreground_executor.clone();
executor
.spawn(async move {
if !closed.load(Ordering::Acquire) {
let window = unsafe { &*window.cast::<NSWindow>() };
if !window.isVisible() {
let view = unsafe { &*view.cast::<NSView>() };
view.setNeedsDisplay(true);
}
window.makeKeyAndOrderFront(None);
}
})
.detach();
}
fn request_attention(&self) {
if self.is_active() {
return;
}
let executor = self.0.lock().foreground_executor.clone();
executor
.spawn(async move {
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let _: NSInteger = msg_send![app, requestUserAttention: NSRequestUserAttentionType::InformationalRequest];
}
})
.detach();
}
fn is_active(&self) -> bool {
self.0.lock().native_window().isKeyWindow()
}
fn visibility(&self) -> WindowVisibility {
visibility(&self.0.lock())
}
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, title: &str) {
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
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: *mut AnyObject = 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();
if background_appearance != WindowBackgroundAppearance::Opaque
&& this.renderer.color_status().requested() == gpui::WindowColorMode::WideGamutSdr
{
log::warn!("disable wide-gamut SDR before enabling a transparent window");
return;
}
this.background_appearance = background_appearance;
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
this.renderer.update_transparency(!opaque);
unsafe {
let _: () = msg_send![this.native_window, setOpaque: opaque.to_objc()];
let background_color = if opaque {
NSColor::colorWithSRGBRed_green_blue_alpha(0f64, 0f64, 0f64, 1f64)
} else {
NSColor::colorWithSRGBRed_green_blue_alpha(0f64, 0f64, 0f64, 0.0001)
};
let _: () = msg_send![this.native_window, setBackgroundColor: &*background_color];
if background_appearance != WindowBackgroundAppearance::Blurred {
if let Some(blur_view) = this.blurred_view {
let _: () = msg_send![blur_view, removeFromSuperview];
this.blurred_view = None;
}
} else if this.blurred_view.is_none() {
let content_view: *mut AnyObject = msg_send![this.native_window, contentView];
let frame: NSRect = msg_send![content_view, bounds];
let blur_view: *mut AnyObject = msg_send![BLURRED_VIEW_CLASS, alloc];
let blur_view: *mut AnyObject = msg_send![blur_view, initWithFrame: frame];
let _: () = msg_send![blur_view, setAutoresizingMask: NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable];
let _: () = msg_send![
content_view,
addSubview: blur_view,
positioned: NSWindowOrderingMode::Below,
relativeTo: ptr::null_mut::<AnyObject>()
];
this.blurred_view = Some(msg_send![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.to_objc()]
}
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];
}
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: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let _: () = msg_send![app, orderFrontCharacterPalette: window];
}
})
.detach();
}
fn minimize(&self) {
let window = self.0.lock().native_window;
unsafe {
let _: () = msg_send![window, miniaturize: ptr::null_mut::<AnyObject>()];
}
}
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 {
let _: () = msg_send![window, zoom: ptr::null_mut::<AnyObject>()];
})
})
.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 {
let _: () = msg_send![window, toggleFullScreen: ptr::null_mut::<AnyObject>()];
})
})
.detach();
}
fn toggle_simple_fullscreen(&self) {
let state = self.0.clone();
let (foreground_executor, closed) = {
let this = self.0.lock();
(this.foreground_executor.clone(), this.closed.clone())
};
foreground_executor
.spawn(async move {
if_window_not_closed(closed, move || {
let (native_window, native_view, plan) = {
let mut lock = state.lock();
(
lock.native_window,
lock.native_view.as_ptr(),
lock.toggle_simple_fullscreen(),
)
};
if let Some(plan) = plan {
unsafe { apply_simple_fullscreen_plan(native_window, native_view, plan) };
}
})
})
.detach();
}
fn is_simple_fullscreen(&self) -> bool {
self.0.lock().simple_fullscreen_state.is_some()
}
fn is_fullscreen(&self) -> bool {
let this = self.0.lock();
let window = this.native_window;
unsafe {
(&*window.cast::<NSWindow>())
.styleMask()
.contains(NSWindowStyleMask::FullScreen)
}
}
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_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
let mut state = self.0.lock();
state.last_visibility = Some(visibility(&state));
state.visibility_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_display_changed(&self, callback: Box<dyn FnMut()>) {
self.0.as_ref().lock().display_changed_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: *mut AnyObject = 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: *mut AnyObject = msg_send![windows, objectAtIndex:i];
if msg_send![window, isKindOfClass: WINDOW_CLASS] {
let handle = get_window_state(&*window).lock().handle;
let title: *mut AnyObject = 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: *mut AnyObject = 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 == Bool::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);
}
#[cfg(any(test, feature = "test-support"))]
fn submit_scene_for_validation(&self, scene: &gpui::Scene) -> anyhow::Result<()> {
let mut this = self.0.lock();
let drawable = this
.renderer
.layer()
.ok_or_else(|| anyhow::anyhow!("window has no native layer"))?
.drawableSize();
let extent = gpui::size(
gpui::DevicePixels(drawable.width.ceil() as i32),
gpui::DevicePixels(drawable.height.ceil() as i32),
);
this.renderer.render_scene(scene, extent)
}
fn schedule_frame(&self) {
if let Some(metal_frames) = &self.2 {
metal_frames.wake();
}
}
fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
let metal_frames = Rc::downgrade(self.2.as_ref()?);
Some(Rc::new(move || {
if let Some(metal_frames) = metal_frames.upgrade() {
metal_frames.wake();
}
}))
}
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: *mut AnyObject =
msg_send![class!(NSTextInputContext), currentInputContext];
if input_context.is_null() {
return;
}
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
}
})
.detach()
}
fn titlebar_double_click(&self, is_resizable: bool, is_minimizable: bool) {
let this = self.0.lock();
if this.simple_fullscreen_state.is_some() {
return;
}
let window = this.native_window;
let closed = this.closed.clone();
this.foreground_executor
.spawn(async move {
if_window_not_closed(closed, || {
unsafe {
let defaults: *mut AnyObject = msg_send![class!(NSUserDefaults), standardUserDefaults];
let domain = ns_string("NSGlobalDomain");
let key = ns_string("AppleActionOnDoubleClick");
let dict: *mut AnyObject = msg_send![defaults, persistentDomainForName: domain];
let action: *mut AnyObject = if !dict.is_null() {
msg_send![dict, objectForKey: key]
} else {
ptr::null_mut::<AnyObject>()
};
let action_str = if !action.is_null() {
CStr::from_ptr(msg_send![action, UTF8String]).to_string_lossy()
} else {
"".into()
};
match action_str.as_ref() {
"None" => {
}
"Minimize" => {
if is_minimizable {
let _: () = msg_send![window, miniaturize: ptr::null_mut::<AnyObject>()];
}
}
"Maximize" => {
if is_resizable {
let _: () = msg_send![window, zoom: ptr::null_mut::<AnyObject>()];
}
}
"Fill" => {
if is_resizable {
let responds_to_zoom_fill: Bool =
msg_send![window, respondsToSelector: sel!(_zoomFill:)];
if responds_to_zoom_fill == Bool::YES {
let _: () = msg_send![window, _zoomFill: ptr::null_mut::<AnyObject>()];
} else {
let _: () = msg_send![window, zoom: ptr::null_mut::<AnyObject>()];
}
}
}
_ => {
if is_resizable {
let _: () = msg_send![window, zoom: ptr::null_mut::<AnyObject>()];
}
}
}
}
})
})
.detach();
}
fn start_window_move(&self) {
let this = self.0.lock();
if this.simple_fullscreen_state.is_some() {
return;
}
let window = this.native_window;
unsafe {
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let event: *mut AnyObject = msg_send![app, currentEvent];
let _: () = msg_send![window, performWindowDragWithEvent: event];
}
}
fn can_start_external_drag(&self) -> bool {
true
}
fn start_external_drag(&self, payload: &ExternalDragPayload) -> bool {
let ExternalDragPayload::Files(paths) = payload;
if paths.entries().is_empty() {
log::warn!("start_external_drag declined: no paths");
return false;
}
let (native_view, native_window, last_left_mouse_down_event) = {
let state = self.0.lock();
(
state.native_view.as_ptr(),
state.native_window,
state.last_left_mouse_down_event.clone(),
)
};
let Some(last_left_mouse_down_event) = last_left_mouse_down_event else {
log::warn!("start_external_drag declined: no retained left mouse down event");
return false;
};
unsafe {
let event: *mut AnyObject = Retained::as_ptr(&last_left_mouse_down_event)
.cast_mut()
.cast();
let dragging_items: *mut AnyObject = msg_send![class!(NSMutableArray), array];
let location: NSPoint = msg_send![event, locationInWindow];
let frame = NSRect::new(
NSPoint::new(location.x - 16., location.y - 16.),
NSSize::new(32., 32.),
);
for (path, is_directory) in paths.entries() {
let Ok(path_bytes) = CString::new(path.as_os_str().as_bytes()) else {
log::warn!("start_external_drag skipped path containing an interior nul byte");
continue;
};
let path_bytes = NonNull::new_unchecked(path_bytes.as_ptr().cast_mut());
let url = NSURL::fileURLWithFileSystemRepresentation_isDirectory_relativeToURL(
path_bytes,
*is_directory,
None,
);
let pasteboard_writer = ProtocolObject::<dyn NSPasteboardWriting>::from_ref(&*url);
let item = NSDraggingItem::initWithPasteboardWriter(
NSDraggingItem::alloc(),
pasteboard_writer,
);
let is_directory = *is_directory;
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.map(String::from);
let provider = RcBlock::new(move || {
let component = NSDraggingImageComponent::draggingImageComponentWithKey(
NSDraggingImageComponentIconKey,
);
let workspace = NSWorkspace::sharedWorkspace();
let content_type = if is_directory {
UTTypeFolder.retain()
} else {
extension
.as_ref()
.and_then(|extension| {
UTType::typeWithFilenameExtension(&NSString::from_str(extension))
})
.unwrap_or_else(|| UTTypeData.retain())
};
let icon = workspace.iconForContentType(&content_type);
component.setContents(Some(&icon));
component.setFrame(NSRect::new(NSPoint::new(0., 0.), NSSize::new(32., 32.)));
let components = NSArray::from_slice(&[&*component]);
NonNull::new_unchecked(Retained::autorelease_return(components))
});
item.setDraggingFrame(frame);
item.setImageComponentsProvider(Some(&provider));
let _: () = msg_send![dragging_items, addObject: Retained::as_ptr(&item)];
}
let count: NSUInteger = msg_send![dragging_items, count];
if count == 0 {
log::warn!("start_external_drag declined: no dragging items");
return false;
}
let session: *mut AnyObject = msg_send![
native_view,
beginDraggingSessionWithItems: dragging_items,
event: event,
source: native_window
];
let started = !session.is_null();
if started {
self.0.lock().synthetic_drag_counter += 1;
}
log::debug!(
"start_external_drag completed: started={}, item_count={}",
started,
count
);
started
}
}
fn play_system_bell(&self) {
NSBeep()
}
#[cfg(any(test, feature = "test-support"))]
fn render_to_linear_p3(&self, scene: &gpui::Scene) -> Result<gpui::LinearP3Image> {
self.0.lock().renderer.render_to_linear_p3(scene)
}
#[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) {
}
}
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> {
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: *mut AnyObject) -> f32 {
let factor = unsafe {
let screen: *mut AnyObject = msg_send![native_window, screen];
if screen.is_null() {
return 2.0;
}
(&*screen.cast::<NSScreen>()).backingScaleFactor() as f32
};
if factor == 0.0 { 2. } else { factor }
}
unsafe fn is_gpui_window(window: *mut AnyObject) -> bool {
unsafe {
msg_send![window, isKindOfClass: WINDOW_CLASS]
|| msg_send![window, isKindOfClass: PANEL_CLASS]
}
}
unsafe fn get_window_state(object: &AnyObject) -> Rc<Mutex<MacWindowState>> {
unsafe {
let raw = *object
.class()
.instance_variable(WINDOW_STATE_IVAR)
.expect("GPUI window state ivar")
.load::<*mut c_void>(object);
let rc1 = Rc::from_raw(raw as *mut Mutex<MacWindowState>);
let rc2 = rc1.clone();
mem::forget(rc1);
rc2
}
}
unsafe fn drop_window_state(object: &AnyObject) {
unsafe {
let raw = *object
.class()
.instance_variable(WINDOW_STATE_IVAR)
.expect("GPUI window state ivar")
.load::<*mut c_void>(object);
Rc::from_raw(raw as *mut Mutex<MacWindowState>);
}
}
extern "C" fn yes(_: &AnyObject, _: Sel) -> Bool {
Bool::YES
}
extern "C" fn dealloc_window(this: &AnyObject, _: Sel) {
unsafe {
drop_window_state(this);
let _: () = msg_send![super(this, class!(NSWindow)), dealloc];
}
}
extern "C" fn dealloc_view(this: &AnyObject, _: Sel) {
unsafe {
drop_window_state(this);
let _: () = msg_send![super(this, class!(NSView)), dealloc];
}
}
fn add_mouse_tracking_area(native_view: &NSView) {
let options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect;
let tracking_area = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
NSRect::ZERO,
options,
Some(native_view),
None,
)
};
native_view.addTrackingArea(&tracking_area);
}
extern "C" fn reset_cursor_rects(this: &AnyObject, _: Sel) {
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: *mut AnyObject = 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],
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: NSRect = msg_send![this, bounds];
let _: () = msg_send![this, addCursorRect: bounds, cursor: cursor];
}
}
extern "C" fn handle_key_equivalent(
this: &AnyObject,
_: Sel,
native_event: *mut AnyObject,
) -> Bool {
handle_key_event(this, native_event, true)
}
extern "C" fn handle_key_down(this: &AnyObject, _: Sel, native_event: *mut AnyObject) {
handle_key_event(this, native_event, false);
}
extern "C" fn handle_key_up(this: &AnyObject, _: Sel, native_event: *mut AnyObject) {
handle_key_event(this, native_event, false);
}
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: &AnyObject,
native_event: *mut AnyObject,
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 Bool::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).to_objc()
} else {
Bool::NO
};
window_state.as_ref().lock().event_callback = callback;
handled
};
match event {
PlatformInput::KeyDown(key_down_event) => {
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 Bool::NO;
}
drop(lock);
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
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: *mut AnyObject = msg_send![this, inputContext];
if input_context.is_null() {
Bool::NO
} else {
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.to_objc();
} else if handled == Bool::YES {
return Bool::YES;
}
let handled = run_callback(PlatformInput::KeyDown(key_down_event));
return handled;
}
let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone()));
if handled == Bool::YES {
return Bool::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 Bool::YES;
}
Bool::NO
});
if handled == Some(Bool::YES) {
return Bool::YES;
}
}
if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() {
return Bool::NO;
}
unsafe {
let input_context: *mut AnyObject = msg_send![this, inputContext];
if input_context.is_null() {
Bool::NO
} else {
msg_send![input_context, handleEvent: native_event]
}
}
}
PlatformInput::KeyUp(_) => {
drop(lock);
run_callback(event)
}
_ => Bool::NO,
}
}
extern "C" fn handle_view_event(this: &AnyObject, _: Sel, native_event: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let weak_window_state = Rc::downgrade(&window_state);
let mut lock = window_state.as_ref().lock();
let window_height = lock.content_size().height;
let native_event_type: NSEventType = unsafe { msg_send![native_event, type] };
match native_event_type {
NSEventType::LeftMouseDown => {
lock.last_left_mouse_down_event = unsafe { Retained::retain(native_event) };
}
NSEventType::LeftMouseUp => {
lock.last_left_mouse_down_event = None;
}
_ => {}
}
let event = unsafe { platform_input_from_native(native_event, Some(window_height)) };
if let Some(mut event) = event {
if matches!(
event,
PlatformInput::MouseMove(_)
| PlatformInput::MouseDown(_)
| PlatformInput::MouseUp(_)
| PlatformInput::MousePressure(_)
| PlatformInput::MouseExited(_)
| PlatformInput::Scroll(_)
| PlatformInput::Pinch(_)
) {
lock.cursor_visible.store(true, Ordering::Relaxed);
}
match &mut event {
PlatformInput::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
modifiers: Modifiers { control: true, .. },
..
},
) => {
*event = MouseDownEvent {
button: MouseButton::Right,
modifiers: Modifiers {
control: false,
..event.modifiers
},
click_count: 1,
..*event
};
}
PlatformInput::MouseDown(
event @ MouseDownEvent {
button: MouseButton::Left,
..
},
) if (lock.first_mouse) => {
*event = MouseDownEvent {
first_mouse: true,
..*event
};
lock.first_mouse = false;
}
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: *mut AnyObject = msg_send![this, inputContext];
if !input_context.is_null() {
let _: Bool = msg_send![input_context, handleEvent: native_event];
}
}
lock = window_state.as_ref().lock();
}
PlatformInput::MouseMove(
event @ MouseMoveEvent {
pressed_button: Some(_),
..
},
) => {
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,
}) => {
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);
}
}
}
fn visibility(state: &MacWindowState) -> WindowVisibility {
let is_visible = state
.native_window()
.occlusionState()
.contains(NSWindowOcclusionState::Visible);
if is_visible {
WindowVisibility::Visible
} else {
WindowVisibility::Hidden
}
}
fn report_visibility(window_state: &Rc<Mutex<MacWindowState>>) {
let state = window_state.lock();
if state.last_visibility.is_none() {
return;
}
let executor = state.foreground_executor.clone();
drop(state);
executor
.spawn({
let window_state = window_state.clone();
async move {
let mut state = window_state.lock();
let visibility = visibility(&state);
if state.last_visibility == Some(visibility) {
return;
}
state.last_visibility = Some(visibility);
if let Some(mut callback) = state.visibility_callback.take() {
drop(state);
callback(visibility);
window_state.lock().visibility_callback = Some(callback);
}
}
})
.detach();
}
extern "C" fn window_did_change_occlusion_state(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if lock
.native_window()
.occlusionState()
.contains(NSWindowOcclusionState::Visible)
{
lock.move_traffic_light();
lock.start_display_link(&window_state);
} else {
lock.stop_display_link();
}
drop(lock);
report_visibility(&window_state);
}
extern "C" fn window_did_resize(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().lock().move_traffic_light();
}
extern "C" fn window_will_enter_fullscreen(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.fullscreen_restore_bounds = lock.bounds();
lock.is_exiting_fullscreen = false;
lock.pre_fullscreen_traffic_light_frames = lock.traffic_light_frames;
lock.restore_traffic_light();
let min_version = NSOperatingSystemVersion {
majorVersion: 15,
minorVersion: 3,
patchVersion: 0,
};
if is_macos_version_at_least(min_version) {
unsafe {
let _: () = msg_send![lock.native_window, setTitlebarAppearsTransparent: Bool::NO];
}
}
}
extern "C" fn window_will_exit_fullscreen(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.is_exiting_fullscreen = true;
let min_version = NSOperatingSystemVersion {
majorVersion: 15,
minorVersion: 3,
patchVersion: 0,
};
if is_macos_version_at_least(min_version) && lock.transparent_titlebar {
unsafe {
let _: () = msg_send![lock.native_window, setTitlebarAppearsTransparent: Bool::YES];
}
}
lock.move_traffic_light();
}
extern "C" fn window_did_fail_to_exit_fullscreen(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.is_exiting_fullscreen = false;
lock.restore_traffic_light();
}
extern "C" fn window_did_exit_fullscreen(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.is_exiting_fullscreen = false;
lock.traffic_light_frames = lock.pre_fullscreen_traffic_light_frames.take();
lock.move_traffic_light();
}
pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool {
NSProcessInfo::processInfo().isOperatingSystemAtLeastVersion(version)
}
fn ns_error_description(error: *mut AnyObject) -> String {
if error.is_null() {
return "unknown error".to_owned();
}
unsafe {
let description: *mut AnyObject = msg_send![error, localizedDescription];
description.to_str().to_owned()
}
}
extern "C" fn window_did_move(this: &AnyObject, _: Sel, _: *mut AnyObject) {
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);
}
}
fn update_window_scale_factor(window_state: &Rc<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);
let layer = lock.renderer.layer_ptr().cast::<AnyObject>();
if !layer.is_null() {
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_color_profile(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let state = unsafe { get_window_state(this) };
let mut state = state.lock();
state.color_profile_dirty = true;
unsafe {
(&*state.native_view.as_ptr().cast::<NSView>()).setNeedsDisplay(true);
}
}
extern "C" fn window_did_change_screen(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.start_display_link(&window_state);
lock.color_profile_dirty = true;
unsafe {
(&*lock.native_view.as_ptr().cast::<NSView>()).setNeedsDisplay(true);
}
drop(lock);
update_window_scale_factor(&window_state);
report_display_change(&window_state);
}
fn report_display_change(window_state: &Rc<Mutex<MacWindowState>>) {
let executor = window_state.lock().foreground_executor.clone();
executor
.spawn({
let window_state = window_state.clone();
async move {
let callback = window_state.lock().display_changed_callback.take();
if let Some(mut callback) = callback {
callback();
window_state.lock().display_changed_callback = Some(callback);
}
}
})
.detach();
}
extern "C" fn window_did_change_key_status(this: &AnyObject, selector: Sel, _: *mut AnyObject) {
let signal_at = PlatformFrameSignal::capture(scheduler::Instant::now);
let window_state = unsafe { get_window_state(this) };
let lock = window_state.lock();
let is_active = lock.native_window().isKeyWindow();
lock.cursor_visible.store(true, Ordering::Relaxed);
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();
}
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(RequestFrameOptions {
signal_at,
signal_source: FrameRequestSource::NativeCallback,
..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(&window_state);
}
} 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: &AnyObject, _: Sel, _: *mut AnyObject) -> 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.to_objc()
} else {
Bool::YES
}
}
extern "C" fn close_window(this: &AnyObject, _: Sel) {
unsafe {
let (close_callback, simple_fullscreen_state) = {
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(),
lock.simple_fullscreen_state.take(),
)
};
if simple_fullscreen_state.is_some() {
pop_simple_fullscreen_presentation_options();
}
if let Some(callback) = close_callback {
callback();
}
let _: () = msg_send![super(this, class!(NSWindow)), close];
}
}
extern "C" fn make_backing_layer(this: &AnyObject, _: Sel) -> *mut AnyObject {
let window_state = unsafe { get_window_state(this) };
let window_state = window_state.as_ref().lock();
window_state.renderer.layer_ptr() as *mut AnyObject
}
extern "C" fn view_did_change_backing_properties(this: &AnyObject, _: Sel) {
let window_state = unsafe { get_window_state(this) };
update_window_scale_factor(&window_state);
}
extern "C" fn set_frame_size(this: &AnyObject, _: 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: &AnyObject, _: Sel, _: *mut AnyObject) {
let signal_at = PlatformFrameSignal::capture(scheduler::Instant::now);
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(RequestFrameOptions {
signal_at,
signal_source: FrameRequestSource::NativeCallback,
..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(&window_state);
}
}
extern "C" fn step(view: *mut c_void) {
let view = view as *mut AnyObject;
let window_state = unsafe { get_window_state(&*view) };
let mut lock = window_state.lock();
if let Some(mut callback) = lock.request_frame_callback.take() {
let signal = match &lock.frame_source {
FrameSource::DisplayLink(Some(frame_source)) => frame_source.take_signal(),
_ => None,
};
let (signal_at, signal_source) = signal.map_or(
(None, FrameRequestSource::NativeCallback),
|(at, source)| (Some(at), source),
);
drop(lock);
callback(RequestFrameOptions {
signal_at,
signal_source,
..Default::default()
});
window_state.lock().request_frame_callback = Some(callback);
}
}
fn metal_frame(
window_state: &Rc<Mutex<MacWindowState>>,
update: &objc2_quartz_core::CAMetalDisplayLinkUpdate,
) {
let signal_at = PlatformFrameSignal::capture(scheduler::Instant::now);
let mut lock = window_state.lock();
let FrameSource::Metal(metal_frames) = &lock.frame_source else {
return;
};
let metal_frames = metal_frames.clone();
let Some(mut callback) = lock.request_frame_callback.take() else {
return;
};
lock.renderer.set_frame_drawable(Some(update.drawable()));
drop(lock);
metal_frames.begin_frame();
callback(RequestFrameOptions {
signal_at,
signal_source: FrameRequestSource::NativeCallback,
..Default::default()
});
let mut lock = window_state.lock();
lock.request_frame_callback = Some(callback);
let used_drawable = lock.renderer.set_frame_drawable(None).is_none();
metal_frames.end_frame(used_drawable);
}
extern "C" fn valid_attributes_for_marked_text(_: &AnyObject, _: Sel) -> *mut AnyObject {
unsafe { msg_send![class!(NSArray), array] }
}
extern "C" fn has_marked_text(this: &AnyObject, _: 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().to_objc()
}
extern "C" fn marked_range(this: &AnyObject, _: 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: &AnyObject, _: 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: &AnyObject,
_: Sel,
range: NSRange,
_: *mut NSRange,
) -> 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: &AnyObject) -> NSRect {
unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = lock.native_window().frame();
let content_layout_rect: NSRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::FullSizeContentView) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
}
}
extern "C" fn insert_text(
this: &AnyObject,
_: Sel,
text: *mut AnyObject,
replacement_range: NSRange,
) {
unsafe {
let is_attributed_string: Bool = msg_send![text, isKindOfClass: class!(NSAttributedString)];
let text: *mut AnyObject = if is_attributed_string == Bool::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: &AnyObject,
_: Sel,
text: *mut AnyObject,
selected_range: NSRange,
replacement_range: NSRange,
) {
unsafe {
let is_attributed_string: Bool = msg_send![text, isKindOfClass: class!(NSAttributedString)];
let text: *mut AnyObject = if is_attributed_string == Bool::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: &AnyObject, _: Sel) {
with_input_handler(this, |input_handler| input_handler.unmark_text());
}
extern "C" fn attributed_substring_for_proposed_range(
this: &AnyObject,
_: Sel,
range: NSRange,
actual_range: *mut NSRange,
) -> *mut AnyObject {
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)?;
unsafe { write_actual_text_range(actual_range, adjusted.unwrap_or(range)) };
unsafe {
let string: *mut AnyObject = msg_send![class!(NSAttributedString), alloc];
let string: *mut AnyObject =
msg_send![string, initWithString: ns_string(&selected_text)];
let string: *mut AnyObject = msg_send![string, autorelease];
Some(string)
}
})
.flatten()
.unwrap_or(ptr::null_mut::<AnyObject>())
}
unsafe fn write_actual_text_range(actual_range: *mut NSRange, range: Range<usize>) {
if !actual_range.is_null() {
unsafe { actual_range.write(NSRange::from(range)) };
}
}
extern "C" fn do_command_by_selector(this: &AnyObject, _: 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: &AnyObject, _: Sel) {
unsafe {
let state = get_window_state(this);
let appearance_changed_callback = {
let mut lock = state.as_ref().lock();
lock.appearance_changed_callback.take()
};
if let Some(mut callback) = appearance_changed_callback {
callback();
state.lock().appearance_changed_callback = Some(callback);
}
state.lock().move_traffic_light();
}
}
extern "C" fn accepts_first_mouse(this: &AnyObject, _: Sel, _: *mut AnyObject) -> Bool {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.first_mouse = true;
Bool::YES
}
extern "C" fn opaque_rect_for_window_move_when_in_titlebar(this: &AnyObject, _: Sel) -> NSRect {
let zero_rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
let window_state = unsafe { get_window_state(this) };
let app_owns_titlebar_drag = window_state.as_ref().lock().app_owns_titlebar_drag;
if app_owns_titlebar_drag {
unsafe { msg_send![this, bounds] }
} else {
zero_rect
}
}
extern "C" fn character_index_for_point(this: &AnyObject, _: Sel, position: NSPoint) -> NSUInteger {
let position = screen_point_to_gpui_point(this, position);
with_input_handler(this, |input_handler| {
input_handler.character_index_for_point(position)
})
.flatten()
.unwrap_or(NSNotFound as NSUInteger)
}
fn screen_point_to_gpui_point(this: &AnyObject, 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))
}
fn is_drag_from_this_window(this: &AnyObject, dragging_info: *mut AnyObject) -> bool {
let source: *mut AnyObject = unsafe { msg_send![dragging_info, draggingSource] };
std::ptr::eq(source as *const AnyObject, this as *const AnyObject)
}
extern "C" fn dragging_entered(
this: &AnyObject,
_: Sel,
dragging_info: *mut AnyObject,
) -> NSDragOperation {
let is_source_window = is_drag_from_this_window(this, dragging_info);
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)
{
if is_source_window {
return NSDragOperation::Move;
}
return NSDragOperation::Copy;
}
NSDragOperation::None
}
extern "C" fn dragging_updated(
this: &AnyObject,
_: Sel,
dragging_info: *mut AnyObject,
) -> NSDragOperation {
let is_source_window = is_drag_from_this_window(this, dragging_info);
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 }) {
if is_source_window {
NSDragOperation::Move
} else {
NSDragOperation::Copy
}
} else {
NSDragOperation::None
}
}
extern "C" fn dragging_exited(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
send_file_drop_event(window_state, FileDropEvent::Exited);
}
extern "C" fn perform_drag_operation(
this: &AnyObject,
_: Sel,
dragging_info: *mut AnyObject,
) -> 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 AnyObject) -> Option<ExternalPaths> {
let mut paths = SmallVec::new();
let pasteboard: *mut AnyObject = unsafe { msg_send![dragging_info, draggingPasteboard] };
if pasteboard.is_null() {
return None;
}
#[expect(
deprecated,
reason = "File-drop decoding expects a property-list array of paths; replacing NSFilenamesPboardType requires migrating both registration and decoding to file URLs"
)]
let filenames: *mut NSArray<NSString> =
unsafe { msg_send![pasteboard, propertyListForType: NSFilenamesPboardType] };
let filenames = unsafe { filenames.as_ref() }?;
for file in filenames {
let path = unsafe {
let f = file.UTF8String();
CStr::from_ptr(f).to_string_lossy().into_owned()
};
paths.push(PathBuf::from(path))
}
Some(ExternalPaths(paths))
}
extern "C" fn conclude_drag_operation(this: &AnyObject, _: Sel, _: *mut AnyObject) {
let window_state = unsafe { get_window_state(this) };
send_file_drop_event(window_state, FileDropEvent::Exited);
}
extern "C" fn dragging_session_source_operation_mask(
_: &AnyObject,
_: Sel,
_: *mut AnyObject,
context: NSDraggingContext,
) -> NSDragOperation {
let operation = match context {
NSDraggingContext::OutsideApplication => NSDragOperation::Copy,
NSDraggingContext::WithinApplication => NSDragOperation::Copy | NSDragOperation::Move,
_ => NSDragOperation::Copy | NSDragOperation::Move,
};
log::debug!(
"dragging_session_source_operation_mask: context={}, operation={}",
context.0,
operation.bits()
);
operation
}
extern "C" fn dragging_session_ended(
this: &AnyObject,
_: Sel,
_: *mut AnyObject,
_: NSPoint,
operation: NSDragOperation,
) {
log::debug!("dragging_session_ended operation={}", operation.bits());
let window_state = unsafe { get_window_state(this) };
{
let mut lock = window_state.lock();
lock.synthetic_drag_counter += 1;
lock.last_left_mouse_down_event = None;
}
send_file_drop_event(window_state, FileDropEvent::Ended);
}
fn synthetic_drag_button_is_pressed(button: Option<MouseButton>, pressed: NSUInteger) -> bool {
let bit = match button {
Some(MouseButton::Left) => 0,
Some(MouseButton::Right) => 1,
Some(MouseButton::Middle) => 2,
Some(MouseButton::Navigate(gpui::NavigationDirection::Back)) => 3,
Some(MouseButton::Navigate(gpui::NavigationDirection::Forward)) => 4,
None => return false,
};
pressed & (1 << bit) != 0
}
async fn synthetic_drag(
window_state: Weak<Mutex<MacWindowState>>,
drag_id: usize,
event: MouseMoveEvent,
executor: BackgroundExecutor,
) {
loop {
executor.timer(Duration::from_millis(16)).await;
let Some(window_state) = window_state.upgrade() else {
break;
};
let mut lock = window_state.lock();
if lock.synthetic_drag_counter != drag_id {
break;
}
let pressed: NSUInteger = unsafe { msg_send![class!(NSEvent), pressedMouseButtons] };
if !synthetic_drag_button_is_pressed(event.pressed_button, pressed) {
lock.synthetic_drag_counter += 1;
break;
}
if let Some(mut callback) = lock.event_callback.take() {
drop(lock);
callback(PlatformInput::MouseMove(event.clone()));
window_state.lock().event_callback = Some(callback);
}
}
}
fn send_file_drop_event(
window_state: Rc<Mutex<MacWindowState>>,
file_drop_event: FileDropEvent,
) -> bool {
let external_files_dragged = match file_drop_event {
FileDropEvent::Entered { .. } => Some(true),
FileDropEvent::Exited | FileDropEvent::Ended => 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: *mut AnyObject,
) -> 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: &AnyObject, 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
}
}
fn display_id_for_screen(screen: *mut AnyObject) -> Option<CGDirectDisplayID> {
if screen.is_null() {
return None;
}
unsafe {
let device_description: *mut AnyObject = msg_send![screen, deviceDescription];
if device_description.is_null() {
return None;
}
let screen_number_key: *mut AnyObject = ns_string("NSScreenNumber");
let screen_number: *mut AnyObject =
msg_send![device_description, objectForKey: screen_number_key];
if screen_number.is_null() {
return None;
}
let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
Some(screen_number as CGDirectDisplayID)
}
}
extern "C" fn blurred_view_init_with_frame(
this: &AnyObject,
_: Sel,
frame: NSRect,
) -> *mut AnyObject {
unsafe {
let view: *mut AnyObject =
msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame];
let _: () = msg_send![view, setMaterial: NSVisualEffectMaterial::UnderWindowBackground];
let _: () = msg_send![view, setBlendingMode: NSVisualEffectBlendingMode::BehindWindow];
let _: () = msg_send![view, setState: NSVisualEffectState::Active];
view
}
}
extern "C" fn blurred_view_update_layer(this: &AnyObject, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer];
let layer: *mut AnyObject = msg_send![this, layer];
if !layer.is_null() {
remove_layer_background(layer);
}
}
}
unsafe fn remove_layer_background(layer: *mut AnyObject) {
unsafe {
let _: () = msg_send![layer, setBackgroundColor: None::<&CGColor>];
let class_name: *mut AnyObject = msg_send![layer, className];
if msg_send![class_name, isEqualToString: ns_string("CAChameleonLayer")] {
let _: () = msg_send![layer, setHidden: Bool::YES];
return;
}
let filters: *mut AnyObject = msg_send![layer, filters];
if !filters.is_null() {
let test_string: *mut AnyObject = ns_string("Saturat");
let count: NSUInteger = msg_send![filters, count];
for i in 0..count {
let filter: *mut AnyObject = msg_send![filters, objectAtIndex: i];
let description: *mut AnyObject = msg_send![filter, description];
let hit: Bool = msg_send![description, containsString: test_string];
if hit == Bool::NO {
continue;
}
let all_indices = NSRange {
location: 0,
length: count,
};
let indices: *mut AnyObject = msg_send![class!(NSMutableIndexSet), indexSet];
let _: () = msg_send![indices, addIndexesInRange: all_indices];
let _: () = msg_send![indices, removeIndex:i];
let filtered: *mut AnyObject = msg_send![filters, objectsAtIndexes: indices];
let _: () = msg_send![layer, setFilters: filtered];
break;
}
}
let sublayers: *mut AnyObject = msg_send![layer, sublayers];
if !sublayers.is_null() {
let count: NSUInteger = msg_send![sublayers, count];
for i in 0..count {
let sublayer: *mut AnyObject = msg_send![sublayers, objectAtIndex: i];
remove_layer_background(sublayer);
}
}
}
}
extern "C" fn add_titlebar_accessory_view_controller(
this: &AnyObject,
_: Sel,
view_controller: *mut AnyObject,
) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller];
let accessory_view: *mut AnyObject = msg_send![view_controller, view];
let _: () = msg_send![accessory_view, setHidden: Bool::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: &AnyObject, _: Sel, _: *mut AnyObject) {
unsafe {
let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:ptr::null_mut::<AnyObject>()];
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: &AnyObject, _: Sel, _: *mut AnyObject) {
unsafe {
let _: () =
msg_send![super(this, class!(NSWindow)), mergeAllWindows:ptr::null_mut::<AnyObject>()];
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: &AnyObject, _sel: Sel, _id: *mut AnyObject) {
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: &AnyObject, _sel: Sel, _id: *mut AnyObject) {
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: &AnyObject, _sel: Sel, _id: *mut AnyObject) {
unsafe {
let _: () =
msg_send![super(this, class!(NSWindow)), toggleTabBar:ptr::null_mut::<AnyObject>()];
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);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use objc2::runtime::{AnyClass, AnyProtocol};
#[test]
fn registered_callbacks_match_system_protocols() {
use objc2::encode::EncodingBox;
fn encodings(mut types: &str) -> Vec<EncodingBox> {
let mut result = Vec::new();
while !types.is_empty() {
result.push(EncodingBox::from_start_of_str(&mut types).unwrap());
types = types.trim_start_matches(|c: char| c.is_ascii_digit() || c == '-');
}
result
}
for core in [c"NSWindowDelegate", c"NSTextInputClient"] {
assert!(
AnyProtocol::get(core).is_some(),
"{core:?} must be registered to check window callbacks"
);
}
let mut checked = 0;
for name in [
c"GPUIView",
c"GPUIWindow",
c"GPUIPanel",
c"GPUIApplicationDelegate",
c"GPUIWindowStateArchiverDelegate",
] {
let class = AnyClass::get(name).unwrap();
for method in class.instance_methods().iter() {
for protocol_name in [
c"NSTextInputClient",
c"NSWindowDelegate",
c"NSDraggingSource",
c"NSDraggingDestination",
c"NSApplicationDelegate",
c"NSMenuDelegate",
c"NSMenuItemValidation",
c"CALayerDelegate",
c"NSKeyedArchiverDelegate",
] {
let Some(protocol) = AnyProtocol::get(protocol_name) else {
continue;
};
for required in [Bool::YES, Bool::NO] {
let description = unsafe {
objc2::ffi::protocol_getMethodDescription(
protocol,
method.name(),
required,
Bool::YES,
)
};
if description.types.is_null() {
continue;
}
let expected = encodings(
unsafe { CStr::from_ptr(description.types) }
.to_str()
.unwrap(),
);
let actual = encodings(method.return_type().to_str().unwrap());
assert_eq!(
actual[0],
expected[0],
"{name:?} {:?}: return encoding",
method.name()
);
assert_eq!(method.arguments_count(), expected.len() - 1);
for (index, expected) in expected[1..].iter().enumerate() {
let argument = method.argument_type(index).unwrap();
let actual = encodings(argument.to_str().unwrap());
assert_eq!(
&actual[0],
expected,
"{name:?} {:?}: argument {index}",
method.name()
);
}
checked += 1;
}
}
}
}
assert!(checked >= 30, "only checked {checked} callback signatures");
}
#[test]
fn native_attention_signature_matches_appkit() {
class!(NSApplication)
.verify_sel::<(NSRequestUserAttentionType,), NSInteger>(sel!(requestUserAttention:))
.unwrap();
}
#[test]
fn text_input_range_pointer_signatures_match_appkit() {
let view = unsafe { &*VIEW_CLASS };
view.verify_sel::<(NSRange, *mut NSRange), NSRect>(
sel!(firstRectForCharacterRange:actualRange:),
)
.unwrap();
view.verify_sel::<(NSRange, *mut NSRange), *mut AnyObject>(
sel!(attributedSubstringForProposedRange:actualRange:),
)
.unwrap();
}
#[test]
fn optional_actual_text_range_accepts_null_and_reports_range() {
unsafe { write_actual_text_range(ptr::null_mut(), 2..5) };
let mut actual = NSRange::invalid();
unsafe { write_actual_text_range(&mut actual, 2..5) };
assert_eq!(actual.to_range(), Some(2..5));
}
#[test]
fn layer_background_can_be_cleared_with_objc_checks_enabled() {
objc2::rc::autoreleasepool(|_| {
let layer = objc2_quartz_core::CALayer::new();
unsafe {
remove_layer_background(Retained::as_ptr(&layer).cast_mut().cast());
}
assert!(layer.backgroundColor().is_none());
});
}
#[test]
fn text_input_class_checks_accept_plain_and_attributed_strings() {
objc2::rc::autoreleasepool(|_| unsafe {
let plain = objc2_foundation::NSString::from_str("text");
let attributed = objc2_foundation::NSAttributedString::initWithString(
objc2_foundation::NSAttributedString::alloc(),
&plain,
);
let plain_result: Bool = msg_send![&*plain, isKindOfClass: class!(NSAttributedString)];
let attributed_result: Bool =
msg_send![&*attributed, isKindOfClass: class!(NSAttributedString)];
assert!(!plain_result.as_bool());
assert!(attributed_result.as_bool());
});
}
#[test]
fn display_id_for_screen_handles_missing_metadata() {
assert_eq!(display_id_for_screen(ptr::null_mut::<AnyObject>()), None);
autoreleasepool(|_| unsafe {
let mut builder = ClassBuilder::new(c"GPUIDisplayIdTestScreen", class!(NSObject))
.expect("test screen class");
builder.add_ivar::<*mut AnyObject>(c"deviceDescription");
builder.add_method(
sel!(deviceDescription),
test_screen_device_description as extern "C" fn(_, Sel) -> *mut AnyObject,
);
let screen: Retained<AnyObject> = msg_send![builder.register(), new];
let description = screen
.class()
.instance_variable(c"deviceDescription")
.expect("test screen description ivar")
.load_ptr::<*mut AnyObject>(&screen);
let screen = Retained::as_ptr(&screen).cast_mut();
description.write(ptr::null_mut());
assert_eq!(display_id_for_screen(screen), None);
let dictionary: Retained<AnyObject> = msg_send![class!(NSMutableDictionary), new];
description.write(Retained::as_ptr(&dictionary).cast_mut());
assert_eq!(display_id_for_screen(screen), None);
for display_id in [0u32, 42] {
let number: Retained<AnyObject> =
msg_send![class!(NSNumber), numberWithUnsignedInt: display_id];
let _: () = msg_send![
&*dictionary,
setObject: &*number,
forKey: ns_string("NSScreenNumber")
];
assert_eq!(display_id_for_screen(screen), Some(display_id));
}
});
}
extern "C" fn test_screen_device_description(screen: &AnyObject, _: Sel) -> *mut AnyObject {
unsafe {
*screen
.class()
.instance_variable(c"deviceDescription")
.expect("test screen description ivar")
.load::<*mut AnyObject>(screen)
}
}
}