
I was interested in potentially using gpui for a hobby project, but needed [layer shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) support for it. Turns out gpui's (excellent!) architecture made that super easy to implement, so I went ahead and did it. Layer shell is a window role used for notification windows, lock screens, docks, backgrounds, etc. Supporting it in gpui opens the door to implementing applications like that using the framework. If this turns out interesting enough to merge - I'm also happy to provide a follow-up PR (when I have the time to) to implement some of the desirable window options for layer shell surfaces, such as: - namespace (currently always `""`) - keyboard interactivity (currently always `OnDemand`, which mimics normal keyboard interactivity) - anchor, exclusive zone, margins - popups Release Notes: - Added support for wayland layer shell surfaces in gpui --------- Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2276 lines
83 KiB
Rust
2276 lines
83 KiB
Rust
use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
|
||
use crate::{
|
||
AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent,
|
||
ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
|
||
PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||
ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||
WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
|
||
};
|
||
use block::ConcreteBlock;
|
||
use cocoa::{
|
||
appkit::{
|
||
NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered,
|
||
NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen,
|
||
NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial,
|
||
NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton,
|
||
NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
|
||
NSWindowStyleMask, NSWindowTitleVisibility,
|
||
},
|
||
base::{id, nil},
|
||
foundation::{
|
||
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound,
|
||
NSOperatingSystemVersion, NSPoint, NSProcessInfo, NSRect, NSSize, NSString, NSUInteger,
|
||
NSUserDefaults,
|
||
},
|
||
};
|
||
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
|
||
use ctor::ctor;
|
||
use futures::channel::oneshot;
|
||
use objc::{
|
||
class,
|
||
declare::ClassDecl,
|
||
msg_send,
|
||
runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES},
|
||
sel, sel_impl,
|
||
};
|
||
use parking_lot::Mutex;
|
||
use raw_window_handle as rwh;
|
||
use smallvec::SmallVec;
|
||
use std::{
|
||
cell::Cell,
|
||
ffi::{CStr, c_void},
|
||
mem,
|
||
ops::Range,
|
||
path::PathBuf,
|
||
ptr::{self, NonNull},
|
||
rc::Rc,
|
||
sync::{Arc, Weak},
|
||
time::Duration,
|
||
};
|
||
use util::ResultExt;
|
||
|
||
const WINDOW_STATE_IVAR: &str = "windowState";
|
||
|
||
static mut WINDOW_CLASS: *const Class = ptr::null();
|
||
static mut PANEL_CLASS: *const Class = ptr::null();
|
||
static mut VIEW_CLASS: *const Class = ptr::null();
|
||
static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
|
||
|
||
#[allow(non_upper_case_globals)]
|
||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||
NSWindowStyleMask::from_bits_retain(1 << 7);
|
||
#[allow(non_upper_case_globals)]
|
||
const NSNormalWindowLevel: NSInteger = 0;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSPopUpWindowLevel: NSInteger = 101;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSTrackingMouseMoved: NSUInteger = 0x02;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSTrackingActiveAlways: NSUInteger = 0x80;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSTrackingInVisibleRect: NSUInteger = 0x200;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
|
||
// https://developer.apple.com/documentation/appkit/nsdragoperation
|
||
type NSDragOperation = NSUInteger;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSDragOperationNone: NSDragOperation = 0;
|
||
#[allow(non_upper_case_globals)]
|
||
const NSDragOperationCopy: NSDragOperation = 1;
|
||
|
||
#[link(name = "CoreGraphics", kind = "framework")]
|
||
unsafe extern "C" {
|
||
// Widely used private APIs; Apple uses them for their Terminal.app.
|
||
fn CGSMainConnectionID() -> id;
|
||
fn CGSSetWindowBackgroundBlurRadius(
|
||
connection_id: id,
|
||
window_id: NSInteger,
|
||
radius: i64,
|
||
) -> i32;
|
||
}
|
||
|
||
#[ctor]
|
||
unsafe fn build_classes() {
|
||
unsafe {
|
||
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
|
||
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
|
||
VIEW_CLASS = {
|
||
let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
|
||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||
unsafe {
|
||
decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
|
||
|
||
decl.add_method(
|
||
sel!(performKeyEquivalent:),
|
||
handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||
);
|
||
decl.add_method(
|
||
sel!(keyDown:),
|
||
handle_key_down as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(keyUp:),
|
||
handle_key_up as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(mouseDown:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(mouseUp:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(rightMouseDown:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(rightMouseUp:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(otherMouseDown:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(otherMouseUp:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(mouseMoved:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(mouseExited:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(mouseDragged:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(scrollWheel:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(swipeWithEvent:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(flagsChanged:),
|
||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
|
||
decl.add_method(
|
||
sel!(makeBackingLayer),
|
||
make_backing_layer as extern "C" fn(&Object, Sel) -> id,
|
||
);
|
||
|
||
decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
|
||
decl.add_method(
|
||
sel!(viewDidChangeBackingProperties),
|
||
view_did_change_backing_properties as extern "C" fn(&Object, Sel),
|
||
);
|
||
decl.add_method(
|
||
sel!(setFrameSize:),
|
||
set_frame_size as extern "C" fn(&Object, Sel, NSSize),
|
||
);
|
||
decl.add_method(
|
||
sel!(displayLayer:),
|
||
display_layer as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
|
||
decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
|
||
decl.add_method(
|
||
sel!(validAttributesForMarkedText),
|
||
valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id,
|
||
);
|
||
decl.add_method(
|
||
sel!(hasMarkedText),
|
||
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
|
||
);
|
||
decl.add_method(
|
||
sel!(markedRange),
|
||
marked_range as extern "C" fn(&Object, Sel) -> NSRange,
|
||
);
|
||
decl.add_method(
|
||
sel!(selectedRange),
|
||
selected_range as extern "C" fn(&Object, Sel) -> NSRange,
|
||
);
|
||
decl.add_method(
|
||
sel!(firstRectForCharacterRange:actualRange:),
|
||
first_rect_for_character_range
|
||
as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect,
|
||
);
|
||
decl.add_method(
|
||
sel!(insertText:replacementRange:),
|
||
insert_text as extern "C" fn(&Object, Sel, id, NSRange),
|
||
);
|
||
decl.add_method(
|
||
sel!(setMarkedText:selectedRange:replacementRange:),
|
||
set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
|
||
);
|
||
decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
|
||
decl.add_method(
|
||
sel!(attributedSubstringForProposedRange:actualRange:),
|
||
attributed_substring_for_proposed_range
|
||
as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
|
||
);
|
||
decl.add_method(
|
||
sel!(viewDidChangeEffectiveAppearance),
|
||
view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
|
||
);
|
||
|
||
// Suppress beep on keystrokes with modifier keys.
|
||
decl.add_method(
|
||
sel!(doCommandBySelector:),
|
||
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
|
||
);
|
||
|
||
decl.add_method(
|
||
sel!(acceptsFirstMouse:),
|
||
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||
);
|
||
|
||
decl.add_method(
|
||
sel!(characterIndexForPoint:),
|
||
character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64,
|
||
);
|
||
}
|
||
decl.register()
|
||
};
|
||
BLURRED_VIEW_CLASS = {
|
||
let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap();
|
||
unsafe {
|
||
decl.add_method(
|
||
sel!(initWithFrame:),
|
||
blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id,
|
||
);
|
||
decl.add_method(
|
||
sel!(updateLayer),
|
||
blurred_view_update_layer as extern "C" fn(&Object, Sel),
|
||
);
|
||
decl.register()
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
|
||
point(
|
||
px(position.x as f32),
|
||
// macOS screen coordinates are relative to bottom left
|
||
window_height - px(position.y as f32),
|
||
)
|
||
}
|
||
|
||
unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
|
||
unsafe {
|
||
let mut decl = ClassDecl::new(name, superclass).unwrap();
|
||
decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
|
||
decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
|
||
|
||
decl.add_method(
|
||
sel!(canBecomeMainWindow),
|
||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||
);
|
||
decl.add_method(
|
||
sel!(canBecomeKeyWindow),
|
||
yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidResize:),
|
||
window_did_resize as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidChangeOcclusionState:),
|
||
window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowWillEnterFullScreen:),
|
||
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowWillExitFullScreen:),
|
||
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidMove:),
|
||
window_did_move as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidChangeScreen:),
|
||
window_did_change_screen as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidBecomeKey:),
|
||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowDidResignKey:),
|
||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(windowShouldClose:),
|
||
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||
);
|
||
|
||
decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
|
||
|
||
decl.add_method(
|
||
sel!(draggingEntered:),
|
||
dragging_entered as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
|
||
);
|
||
decl.add_method(
|
||
sel!(draggingUpdated:),
|
||
dragging_updated as extern "C" fn(&Object, Sel, id) -> NSDragOperation,
|
||
);
|
||
decl.add_method(
|
||
sel!(draggingExited:),
|
||
dragging_exited as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
decl.add_method(
|
||
sel!(performDragOperation:),
|
||
perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||
);
|
||
decl.add_method(
|
||
sel!(concludeDragOperation:),
|
||
conclude_drag_operation as extern "C" fn(&Object, Sel, id),
|
||
);
|
||
|
||
decl.register()
|
||
}
|
||
}
|
||
|
||
struct MacWindowState {
|
||
handle: AnyWindowHandle,
|
||
executor: ForegroundExecutor,
|
||
native_window: id,
|
||
native_view: NonNull<Object>,
|
||
blurred_view: Option<id>,
|
||
display_link: Option<DisplayLink>,
|
||
renderer: renderer::Renderer,
|
||
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
||
event_callback: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
|
||
activate_callback: Option<Box<dyn FnMut(bool)>>,
|
||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||
moved_callback: Option<Box<dyn FnMut()>>,
|
||
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
|
||
close_callback: Option<Box<dyn FnOnce()>>,
|
||
appearance_changed_callback: Option<Box<dyn FnMut()>>,
|
||
input_handler: Option<PlatformInputHandler>,
|
||
last_key_equivalent: Option<KeyDownEvent>,
|
||
synthetic_drag_counter: usize,
|
||
traffic_light_position: Option<Point<Pixels>>,
|
||
transparent_titlebar: bool,
|
||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||
keystroke_for_do_command: Option<Keystroke>,
|
||
do_command_handled: Option<bool>,
|
||
external_files_dragged: bool,
|
||
// Whether the next left-mouse click is also the focusing click.
|
||
first_mouse: bool,
|
||
fullscreen_restore_bounds: Bounds<Pixels>,
|
||
}
|
||
|
||
impl MacWindowState {
|
||
fn move_traffic_light(&self) {
|
||
if let Some(traffic_light_position) = self.traffic_light_position {
|
||
if self.is_fullscreen() {
|
||
// Moving traffic lights while fullscreen doesn't work,
|
||
// see https://github.com/zed-industries/zed/issues/4712
|
||
return;
|
||
}
|
||
|
||
let titlebar_height = self.titlebar_height();
|
||
|
||
unsafe {
|
||
let close_button: id = msg_send![
|
||
self.native_window,
|
||
standardWindowButton: NSWindowButton::NSWindowCloseButton
|
||
];
|
||
let min_button: id = msg_send![
|
||
self.native_window,
|
||
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
|
||
];
|
||
let zoom_button: id = msg_send![
|
||
self.native_window,
|
||
standardWindowButton: NSWindowButton::NSWindowZoomButton
|
||
];
|
||
|
||
let mut close_button_frame: CGRect = msg_send![close_button, frame];
|
||
let mut min_button_frame: CGRect = msg_send![min_button, frame];
|
||
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
|
||
let mut origin = point(
|
||
traffic_light_position.x,
|
||
titlebar_height
|
||
- traffic_light_position.y
|
||
- px(close_button_frame.size.height as f32),
|
||
);
|
||
let button_spacing =
|
||
px((min_button_frame.origin.x - close_button_frame.origin.x) as f32);
|
||
|
||
close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
|
||
let _: () = msg_send![close_button, setFrame: close_button_frame];
|
||
origin.x += button_spacing;
|
||
|
||
min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
|
||
let _: () = msg_send![min_button, setFrame: min_button_frame];
|
||
origin.x += button_spacing;
|
||
|
||
zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
|
||
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
|
||
origin.x += button_spacing;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn start_display_link(&mut self) {
|
||
self.stop_display_link();
|
||
unsafe {
|
||
if !self
|
||
.native_window
|
||
.occlusionState()
|
||
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
let display_id = unsafe { display_id_for_screen(self.native_window.screen()) };
|
||
if let Some(mut display_link) =
|
||
DisplayLink::new(display_id, self.native_view.as_ptr() as *mut c_void, step).log_err()
|
||
{
|
||
display_link.start().log_err();
|
||
self.display_link = Some(display_link);
|
||
}
|
||
}
|
||
|
||
fn stop_display_link(&mut self) {
|
||
self.display_link = None;
|
||
}
|
||
|
||
fn is_maximized(&self) -> bool {
|
||
unsafe {
|
||
let bounds = self.bounds();
|
||
let screen_size = self.native_window.screen().visibleFrame().into();
|
||
bounds.size == screen_size
|
||
}
|
||
}
|
||
|
||
fn is_fullscreen(&self) -> bool {
|
||
unsafe {
|
||
let style_mask = self.native_window.styleMask();
|
||
style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask)
|
||
}
|
||
}
|
||
|
||
fn bounds(&self) -> Bounds<Pixels> {
|
||
let mut window_frame = unsafe { NSWindow::frame(self.native_window) };
|
||
let screen_frame = unsafe {
|
||
let screen = NSWindow::screen(self.native_window);
|
||
NSScreen::frame(screen)
|
||
};
|
||
|
||
// Flip the y coordinate to be top-left origin
|
||
window_frame.origin.y =
|
||
screen_frame.size.height - window_frame.origin.y - window_frame.size.height;
|
||
|
||
Bounds::new(
|
||
point(
|
||
px((window_frame.origin.x - screen_frame.origin.x) as f32),
|
||
px((window_frame.origin.y + screen_frame.origin.y) as f32),
|
||
),
|
||
size(
|
||
px(window_frame.size.width as f32),
|
||
px(window_frame.size.height as f32),
|
||
),
|
||
)
|
||
}
|
||
|
||
fn content_size(&self) -> Size<Pixels> {
|
||
let NSSize { width, height, .. } =
|
||
unsafe { NSView::frame(self.native_window.contentView()) }.size;
|
||
size(px(width as f32), px(height as f32))
|
||
}
|
||
|
||
fn scale_factor(&self) -> f32 {
|
||
get_scale_factor(self.native_window)
|
||
}
|
||
|
||
fn titlebar_height(&self) -> Pixels {
|
||
unsafe {
|
||
let frame = NSWindow::frame(self.native_window);
|
||
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
|
||
px((frame.size.height - content_layout_rect.size.height) as f32)
|
||
}
|
||
}
|
||
|
||
fn window_bounds(&self) -> WindowBounds {
|
||
if self.is_fullscreen() {
|
||
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
|
||
} else {
|
||
WindowBounds::Windowed(self.bounds())
|
||
}
|
||
}
|
||
}
|
||
|
||
unsafe impl Send for MacWindowState {}
|
||
|
||
pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>);
|
||
|
||
impl MacWindow {
|
||
pub fn open(
|
||
handle: AnyWindowHandle,
|
||
WindowParams {
|
||
bounds,
|
||
titlebar,
|
||
kind,
|
||
is_movable,
|
||
focus,
|
||
show,
|
||
display_id,
|
||
window_min_size,
|
||
}: WindowParams,
|
||
executor: ForegroundExecutor,
|
||
renderer_context: renderer::Context,
|
||
) -> Self {
|
||
unsafe {
|
||
let pool = NSAutoreleasePool::new(nil);
|
||
|
||
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
|
||
|
||
let mut style_mask;
|
||
if let Some(titlebar) = titlebar.as_ref() {
|
||
style_mask = NSWindowStyleMask::NSClosableWindowMask
|
||
| NSWindowStyleMask::NSMiniaturizableWindowMask
|
||
| NSWindowStyleMask::NSResizableWindowMask
|
||
| NSWindowStyleMask::NSTitledWindowMask;
|
||
|
||
if titlebar.appears_transparent {
|
||
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
||
}
|
||
} else {
|
||
style_mask = NSWindowStyleMask::NSTitledWindowMask
|
||
| NSWindowStyleMask::NSFullSizeContentViewWindowMask;
|
||
}
|
||
|
||
let native_window: id = match kind {
|
||
WindowKind::Normal | WindowKind::Overlay => msg_send![WINDOW_CLASS, alloc],
|
||
WindowKind::PopUp => {
|
||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||
msg_send![PANEL_CLASS, alloc]
|
||
}
|
||
};
|
||
|
||
let display = display_id
|
||
.and_then(MacDisplay::find_by_id)
|
||
.unwrap_or_else(MacDisplay::primary);
|
||
|
||
let mut target_screen = nil;
|
||
let mut screen_frame = None;
|
||
|
||
let screens = NSScreen::screens(nil);
|
||
let count: u64 = cocoa::foundation::NSArray::count(screens);
|
||
for i in 0..count {
|
||
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
|
||
let frame = NSScreen::frame(screen);
|
||
let display_id = display_id_for_screen(screen);
|
||
if display_id == display.0 {
|
||
screen_frame = Some(frame);
|
||
target_screen = screen;
|
||
}
|
||
}
|
||
|
||
let screen_frame = screen_frame.unwrap_or_else(|| {
|
||
let screen = NSScreen::mainScreen(nil);
|
||
target_screen = screen;
|
||
NSScreen::frame(screen)
|
||
});
|
||
|
||
let window_rect = NSRect::new(
|
||
NSPoint::new(
|
||
screen_frame.origin.x + bounds.origin.x.0 as f64,
|
||
screen_frame.origin.y
|
||
+ (display.bounds().size.height - bounds.origin.y).0 as f64,
|
||
),
|
||
NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64),
|
||
);
|
||
|
||
let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_(
|
||
window_rect,
|
||
style_mask,
|
||
NSBackingStoreBuffered,
|
||
NO,
|
||
target_screen,
|
||
);
|
||
assert!(!native_window.is_null());
|
||
let () = msg_send![
|
||
native_window,
|
||
registerForDraggedTypes:
|
||
NSArray::arrayWithObject(nil, NSFilenamesPboardType)
|
||
];
|
||
let () = msg_send![
|
||
native_window,
|
||
setReleasedWhenClosed: NO
|
||
];
|
||
|
||
let content_view = native_window.contentView();
|
||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||
let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view));
|
||
assert!(!native_view.is_null());
|
||
|
||
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
|
||
handle,
|
||
executor,
|
||
native_window,
|
||
native_view: NonNull::new_unchecked(native_view),
|
||
blurred_view: None,
|
||
display_link: None,
|
||
renderer: renderer::new_renderer(
|
||
renderer_context,
|
||
native_window as *mut _,
|
||
native_view as *mut _,
|
||
bounds.size.map(|pixels| pixels.0),
|
||
false,
|
||
),
|
||
request_frame_callback: None,
|
||
event_callback: None,
|
||
activate_callback: None,
|
||
resize_callback: None,
|
||
moved_callback: None,
|
||
should_close_callback: None,
|
||
close_callback: None,
|
||
appearance_changed_callback: None,
|
||
input_handler: None,
|
||
last_key_equivalent: None,
|
||
synthetic_drag_counter: 0,
|
||
traffic_light_position: titlebar
|
||
.as_ref()
|
||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||
transparent_titlebar: titlebar
|
||
.as_ref()
|
||
.map_or(true, |titlebar| titlebar.appears_transparent),
|
||
previous_modifiers_changed_event: None,
|
||
keystroke_for_do_command: None,
|
||
do_command_handled: None,
|
||
external_files_dragged: false,
|
||
first_mouse: false,
|
||
fullscreen_restore_bounds: Bounds::default(),
|
||
})));
|
||
|
||
(*native_window).set_ivar(
|
||
WINDOW_STATE_IVAR,
|
||
Arc::into_raw(window.0.clone()) as *const c_void,
|
||
);
|
||
native_window.setDelegate_(native_window);
|
||
(*native_view).set_ivar(
|
||
WINDOW_STATE_IVAR,
|
||
Arc::into_raw(window.0.clone()) as *const c_void,
|
||
);
|
||
|
||
if let Some(title) = titlebar
|
||
.as_ref()
|
||
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
|
||
{
|
||
window.set_title(title);
|
||
}
|
||
|
||
native_window.setMovable_(is_movable as BOOL);
|
||
|
||
if let Some(window_min_size) = window_min_size {
|
||
native_window.setContentMinSize_(NSSize {
|
||
width: window_min_size.width.to_f64(),
|
||
height: window_min_size.height.to_f64(),
|
||
});
|
||
}
|
||
|
||
if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
|
||
native_window.setTitlebarAppearsTransparent_(YES);
|
||
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
|
||
}
|
||
|
||
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
|
||
native_view.setWantsBestResolutionOpenGLSurface_(YES);
|
||
|
||
// From winit crate: On Mojave, views automatically become layer-backed shortly after
|
||
// being added to a native_window. Changing the layer-backedness of a view breaks the
|
||
// association between the view and its associated OpenGL context. To work around this,
|
||
// on we explicitly make the view layer-backed up front so that AppKit doesn't do it
|
||
// itself and break the association with its context.
|
||
native_view.setWantsLayer(YES);
|
||
let _: () = msg_send![
|
||
native_view,
|
||
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
||
];
|
||
|
||
content_view.addSubview_(native_view.autorelease());
|
||
native_window.makeFirstResponder_(native_view);
|
||
|
||
match kind {
|
||
WindowKind::Normal | WindowKind::Overlay => {
|
||
native_window.setLevel_(NSNormalWindowLevel);
|
||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||
}
|
||
WindowKind::PopUp => {
|
||
// Use a tracking area to allow receiving MouseMoved events even when
|
||
// the window or application aren't active, which is often the case
|
||
// e.g. for notification windows.
|
||
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
|
||
let _: () = msg_send![
|
||
tracking_area,
|
||
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
|
||
options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
|
||
owner: native_view
|
||
userInfo: nil
|
||
];
|
||
let _: () =
|
||
msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
|
||
|
||
native_window.setLevel_(NSPopUpWindowLevel);
|
||
let _: () = msg_send![
|
||
native_window,
|
||
setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
|
||
];
|
||
native_window.setCollectionBehavior_(
|
||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
|
||
);
|
||
}
|
||
}
|
||
|
||
if focus && show {
|
||
native_window.makeKeyAndOrderFront_(nil);
|
||
} else if show {
|
||
native_window.orderFront_(nil);
|
||
}
|
||
|
||
// Set the initial position of the window to the specified origin.
|
||
// Although we already specified the position using `initWithContentRect_styleMask_backing_defer_screen_`,
|
||
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
|
||
// is different from the primary screen.
|
||
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
|
||
window.0.lock().move_traffic_light();
|
||
|
||
pool.drain();
|
||
|
||
window
|
||
}
|
||
}
|
||
|
||
pub fn active_window() -> Option<AnyWindowHandle> {
|
||
unsafe {
|
||
let app = NSApplication::sharedApplication(nil);
|
||
let main_window: id = msg_send![app, mainWindow];
|
||
if main_window.is_null() {
|
||
return None;
|
||
}
|
||
|
||
if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
|
||
let handle = get_window_state(&*main_window).lock().handle;
|
||
Some(handle)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn ordered_windows() -> Vec<AnyWindowHandle> {
|
||
unsafe {
|
||
let app = NSApplication::sharedApplication(nil);
|
||
let windows: id = msg_send![app, orderedWindows];
|
||
let count: NSUInteger = msg_send![windows, count];
|
||
|
||
let mut window_handles = Vec::new();
|
||
for i in 0..count {
|
||
let window: id = msg_send![windows, objectAtIndex:i];
|
||
if msg_send![window, isKindOfClass: WINDOW_CLASS] {
|
||
let handle = get_window_state(&*window).lock().handle;
|
||
window_handles.push(handle);
|
||
}
|
||
}
|
||
|
||
window_handles
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Drop for MacWindow {
|
||
fn drop(&mut self) {
|
||
let mut this = self.0.lock();
|
||
this.renderer.destroy();
|
||
let window = this.native_window;
|
||
this.display_link.take();
|
||
unsafe {
|
||
this.native_window.setDelegate_(nil);
|
||
}
|
||
this.input_handler.take();
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
window.close();
|
||
window.autorelease();
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
}
|
||
|
||
impl PlatformWindow for MacWindow {
|
||
fn bounds(&self) -> Bounds<Pixels> {
|
||
self.0.as_ref().lock().bounds()
|
||
}
|
||
|
||
fn window_bounds(&self) -> WindowBounds {
|
||
self.0.as_ref().lock().window_bounds()
|
||
}
|
||
|
||
fn is_maximized(&self) -> bool {
|
||
self.0.as_ref().lock().is_maximized()
|
||
}
|
||
|
||
fn content_size(&self) -> Size<Pixels> {
|
||
self.0.as_ref().lock().content_size()
|
||
}
|
||
|
||
fn resize(&mut self, size: Size<Pixels>) {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
window.setContentSize_(NSSize {
|
||
width: size.width.0 as f64,
|
||
height: size.height.0 as f64,
|
||
});
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
fn scale_factor(&self) -> f32 {
|
||
self.0.as_ref().lock().scale_factor()
|
||
}
|
||
|
||
fn appearance(&self) -> WindowAppearance {
|
||
unsafe {
|
||
let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance];
|
||
WindowAppearance::from_native(appearance)
|
||
}
|
||
}
|
||
|
||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||
unsafe {
|
||
let screen = self.0.lock().native_window.screen();
|
||
if screen.is_null() {
|
||
return None;
|
||
}
|
||
let device_description: id = msg_send![screen, deviceDescription];
|
||
let screen_number: id = NSDictionary::valueForKey_(
|
||
device_description,
|
||
NSString::alloc(nil).init_str("NSScreenNumber"),
|
||
);
|
||
|
||
let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
|
||
|
||
Some(Rc::new(MacDisplay(screen_number)))
|
||
}
|
||
}
|
||
|
||
fn mouse_position(&self) -> Point<Pixels> {
|
||
let position = unsafe {
|
||
self.0
|
||
.lock()
|
||
.native_window
|
||
.mouseLocationOutsideOfEventStream()
|
||
};
|
||
convert_mouse_position(position, self.content_size().height)
|
||
}
|
||
|
||
fn modifiers(&self) -> Modifiers {
|
||
unsafe {
|
||
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
|
||
|
||
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
||
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
|
||
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
|
||
|
||
Modifiers {
|
||
control,
|
||
alt,
|
||
shift,
|
||
platform: command,
|
||
function,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn capslock(&self) -> Capslock {
|
||
unsafe {
|
||
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
|
||
|
||
Capslock {
|
||
on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||
self.0.as_ref().lock().input_handler = Some(input_handler);
|
||
}
|
||
|
||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||
self.0.as_ref().lock().input_handler.take()
|
||
}
|
||
|
||
fn prompt(
|
||
&self,
|
||
level: PromptLevel,
|
||
msg: &str,
|
||
detail: Option<&str>,
|
||
answers: &[PromptButton],
|
||
) -> Option<oneshot::Receiver<usize>> {
|
||
// macOs applies overrides to modal window buttons after they are added.
|
||
// Two most important for this logic are:
|
||
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
|
||
// * Last button added to the modal via `addButtonWithTitle` stays focused
|
||
// * Focused buttons react on "space"/" " keypresses
|
||
// * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus
|
||
//
|
||
// See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion
|
||
// ```
|
||
// By default, the first button has a key equivalent of Return,
|
||
// any button with a title of “Cancel” has a key equivalent of Escape,
|
||
// and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button).
|
||
// ```
|
||
//
|
||
// To avoid situations when the last element added is "Cancel" and it gets the focus
|
||
// (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button
|
||
// last, so it gets focus and a Space shortcut.
|
||
// This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key.
|
||
let latest_non_cancel_label = answers
|
||
.iter()
|
||
.enumerate()
|
||
.rev()
|
||
.find(|(_, label)| !label.is_cancel())
|
||
.filter(|&(label_index, _)| label_index > 0);
|
||
|
||
unsafe {
|
||
let alert: id = msg_send![class!(NSAlert), alloc];
|
||
let alert: id = msg_send![alert, init];
|
||
let alert_style = match level {
|
||
PromptLevel::Info => 1,
|
||
PromptLevel::Warning => 0,
|
||
PromptLevel::Critical => 2,
|
||
};
|
||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||
if let Some(detail) = detail {
|
||
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
|
||
}
|
||
|
||
for (ix, answer) in answers
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
|
||
{
|
||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
|
||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||
|
||
if answer.is_cancel() {
|
||
// Bind Escape Key to Cancel Button
|
||
if let Some(key) = std::char::from_u32(super::events::ESCAPE_KEY as u32) {
|
||
let _: () =
|
||
msg_send![button, setKeyEquivalent: ns_string(&key.to_string())];
|
||
}
|
||
}
|
||
}
|
||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
|
||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||
}
|
||
|
||
let (done_tx, done_rx) = oneshot::channel();
|
||
let done_tx = Cell::new(Some(done_tx));
|
||
let block = ConcreteBlock::new(move |answer: NSInteger| {
|
||
if let Some(done_tx) = done_tx.take() {
|
||
let _ = done_tx.send(answer.try_into().unwrap());
|
||
}
|
||
});
|
||
let block = block.copy();
|
||
let native_window = self.0.lock().native_window;
|
||
let executor = self.0.lock().executor.clone();
|
||
executor
|
||
.spawn(async move {
|
||
let _: () = msg_send![
|
||
alert,
|
||
beginSheetModalForWindow: native_window
|
||
completionHandler: block
|
||
];
|
||
})
|
||
.detach();
|
||
|
||
Some(done_rx)
|
||
}
|
||
}
|
||
|
||
fn activate(&self) {
|
||
let window = self.0.lock().native_window;
|
||
let executor = self.0.lock().executor.clone();
|
||
executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
let _: () = msg_send![window, makeKeyAndOrderFront: nil];
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
fn is_active(&self) -> bool {
|
||
unsafe { self.0.lock().native_window.isKeyWindow() == YES }
|
||
}
|
||
|
||
// is_hovered is unused on macOS. See Window::is_window_hovered.
|
||
fn is_hovered(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
fn set_title(&mut self, title: &str) {
|
||
unsafe {
|
||
let app = NSApplication::sharedApplication(nil);
|
||
let window = self.0.lock().native_window;
|
||
let title = ns_string(title);
|
||
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
|
||
let _: () = msg_send![window, setTitle: title];
|
||
self.0.lock().move_traffic_light();
|
||
}
|
||
}
|
||
|
||
fn set_app_id(&mut self, _app_id: &str) {}
|
||
|
||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||
let mut this = self.0.as_ref().lock();
|
||
|
||
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
|
||
this.renderer.update_transparency(!opaque);
|
||
|
||
unsafe {
|
||
this.native_window.setOpaque_(opaque as BOOL);
|
||
let background_color = if opaque {
|
||
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
|
||
} else {
|
||
// Not using `+[NSColor clearColor]` to avoid broken shadow.
|
||
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001)
|
||
};
|
||
this.native_window.setBackgroundColor_(background_color);
|
||
|
||
if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 {
|
||
// Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`.
|
||
// On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t own concrete sublayers
|
||
// but uses a `CAProxyLayer`. Use the legacy WindowServer API.
|
||
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
|
||
80
|
||
} else {
|
||
0
|
||
};
|
||
|
||
let window_number = this.native_window.windowNumber();
|
||
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
|
||
} else {
|
||
// On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it
|
||
// could have a better performance (it downsamples the backdrop) and more control
|
||
// over the effect layer.
|
||
if background_appearance != WindowBackgroundAppearance::Blurred {
|
||
if let Some(blur_view) = this.blurred_view {
|
||
NSView::removeFromSuperview(blur_view);
|
||
this.blurred_view = None;
|
||
}
|
||
} else if this.blurred_view == None {
|
||
let content_view = this.native_window.contentView();
|
||
let frame = NSView::bounds(content_view);
|
||
let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
|
||
blur_view = NSView::initWithFrame_(blur_view, frame);
|
||
blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
|
||
|
||
let _: () = msg_send![
|
||
content_view,
|
||
addSubview: blur_view
|
||
positioned: NSWindowOrderingMode::NSWindowBelow
|
||
relativeTo: nil
|
||
];
|
||
this.blurred_view = Some(blur_view.autorelease());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn set_edited(&mut self, edited: bool) {
|
||
unsafe {
|
||
let window = self.0.lock().native_window;
|
||
msg_send![window, setDocumentEdited: edited as BOOL]
|
||
}
|
||
|
||
// Changing the document edited state resets the traffic light position,
|
||
// so we have to move it again.
|
||
self.0.lock().move_traffic_light();
|
||
}
|
||
|
||
fn show_character_palette(&self) {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
let app = NSApplication::sharedApplication(nil);
|
||
let _: () = msg_send![app, orderFrontCharacterPalette: window];
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
fn minimize(&self) {
|
||
let window = self.0.lock().native_window;
|
||
unsafe {
|
||
window.miniaturize_(nil);
|
||
}
|
||
}
|
||
|
||
fn zoom(&self) {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
window.zoom_(nil);
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
fn toggle_fullscreen(&self) {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
window.toggleFullScreen_(nil);
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
fn is_fullscreen(&self) -> bool {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
|
||
unsafe {
|
||
window
|
||
.styleMask()
|
||
.contains(NSWindowStyleMask::NSFullScreenWindowMask)
|
||
}
|
||
}
|
||
|
||
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
|
||
self.0.as_ref().lock().request_frame_callback = Some(callback);
|
||
}
|
||
|
||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||
self.0.as_ref().lock().event_callback = Some(callback);
|
||
}
|
||
|
||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||
self.0.as_ref().lock().activate_callback = Some(callback);
|
||
}
|
||
|
||
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
|
||
|
||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||
self.0.as_ref().lock().resize_callback = Some(callback);
|
||
}
|
||
|
||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||
self.0.as_ref().lock().moved_callback = Some(callback);
|
||
}
|
||
|
||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||
self.0.as_ref().lock().should_close_callback = Some(callback);
|
||
}
|
||
|
||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||
self.0.as_ref().lock().close_callback = Some(callback);
|
||
}
|
||
|
||
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
|
||
}
|
||
|
||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||
self.0.lock().appearance_changed_callback = Some(callback);
|
||
}
|
||
|
||
fn draw(&self, scene: &crate::Scene) {
|
||
let mut this = self.0.lock();
|
||
this.renderer.draw(scene);
|
||
}
|
||
|
||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||
self.0.lock().renderer.sprite_atlas().clone()
|
||
}
|
||
|
||
fn gpu_specs(&self) -> Option<crate::GpuSpecs> {
|
||
None
|
||
}
|
||
|
||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
|
||
let executor = self.0.lock().executor.clone();
|
||
executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
let input_context: id =
|
||
msg_send![class!(NSTextInputContext), currentInputContext];
|
||
if input_context.is_null() {
|
||
return;
|
||
}
|
||
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
|
||
}
|
||
})
|
||
.detach()
|
||
}
|
||
|
||
fn titlebar_double_click(&self) {
|
||
let this = self.0.lock();
|
||
let window = this.native_window;
|
||
this.executor
|
||
.spawn(async move {
|
||
unsafe {
|
||
let defaults: id = NSUserDefaults::standardUserDefaults();
|
||
let domain = NSString::alloc(nil).init_str("NSGlobalDomain");
|
||
let key = NSString::alloc(nil).init_str("AppleActionOnDoubleClick");
|
||
|
||
let dict: id = msg_send![defaults, persistentDomainForName: domain];
|
||
let action: id = if !dict.is_null() {
|
||
msg_send![dict, objectForKey: key]
|
||
} else {
|
||
nil
|
||
};
|
||
|
||
let action_str = if !action.is_null() {
|
||
CStr::from_ptr(NSString::UTF8String(action)).to_string_lossy()
|
||
} else {
|
||
"".into()
|
||
};
|
||
|
||
match action_str.as_ref() {
|
||
"Minimize" => {
|
||
window.miniaturize_(nil);
|
||
}
|
||
"Maximize" => {
|
||
window.zoom_(nil);
|
||
}
|
||
"Fill" => {
|
||
// There is no documented API for "Fill" action, so we'll just zoom the window
|
||
window.zoom_(nil);
|
||
}
|
||
_ => {
|
||
window.zoom_(nil);
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.detach();
|
||
}
|
||
}
|
||
|
||
impl rwh::HasWindowHandle for MacWindow {
|
||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||
// SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView
|
||
unsafe {
|
||
Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit(
|
||
rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()),
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
|
||
impl rwh::HasDisplayHandle for MacWindow {
|
||
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||
// SAFETY: This is a no-op on macOS
|
||
unsafe {
|
||
Ok(rwh::DisplayHandle::borrow_raw(
|
||
rwh::AppKitDisplayHandle::new().into(),
|
||
))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn get_scale_factor(native_window: id) -> f32 {
|
||
let factor = unsafe {
|
||
let screen: id = msg_send![native_window, screen];
|
||
if screen.is_null() {
|
||
return 1.0;
|
||
}
|
||
NSScreen::backingScaleFactor(screen) as f32
|
||
};
|
||
|
||
// We are not certain what triggers this, but it seems that sometimes
|
||
// this method would return 0 (https://github.com/zed-industries/zed/issues/6412)
|
||
// It seems most likely that this would happen if the window has no screen
|
||
// (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before
|
||
// it was rendered for real.
|
||
// Regardless, attempt to avoid the issue here.
|
||
if factor == 0.0 { 2. } else { factor }
|
||
}
|
||
|
||
unsafe fn get_window_state(object: &Object) -> Arc<Mutex<MacWindowState>> {
|
||
unsafe {
|
||
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
||
let rc1 = Arc::from_raw(raw as *mut Mutex<MacWindowState>);
|
||
let rc2 = rc1.clone();
|
||
mem::forget(rc1);
|
||
rc2
|
||
}
|
||
}
|
||
|
||
unsafe fn drop_window_state(object: &Object) {
|
||
unsafe {
|
||
let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
||
Arc::from_raw(raw as *mut Mutex<MacWindowState>);
|
||
}
|
||
}
|
||
|
||
extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
|
||
YES
|
||
}
|
||
|
||
extern "C" fn dealloc_window(this: &Object, _: Sel) {
|
||
unsafe {
|
||
drop_window_state(this);
|
||
let _: () = msg_send![super(this, class!(NSWindow)), dealloc];
|
||
}
|
||
}
|
||
|
||
extern "C" fn dealloc_view(this: &Object, _: Sel) {
|
||
unsafe {
|
||
drop_window_state(this);
|
||
let _: () = msg_send![super(this, class!(NSView)), dealloc];
|
||
}
|
||
}
|
||
|
||
extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
|
||
handle_key_event(this, native_event, true)
|
||
}
|
||
|
||
extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
||
handle_key_event(this, native_event, false);
|
||
}
|
||
|
||
extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) {
|
||
handle_key_event(this, native_event, false);
|
||
}
|
||
|
||
// Things to test if you're modifying this method:
|
||
// U.S. layout:
|
||
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
|
||
// the terminal behave incorrectly by default. This behavior should be patched by our
|
||
// IME integration
|
||
// - `alt-t` should open the tasks menu
|
||
// - In vim mode, this keybinding should work:
|
||
// ```
|
||
// {
|
||
// "context": "Editor && vim_mode == insert",
|
||
// "bindings": {"j j": "vim::NormalBefore"}
|
||
// }
|
||
// ```
|
||
// and typing 'j k' in insert mode with this keybinding should insert the two characters
|
||
// Brazilian layout:
|
||
// - `" space` should create an unmarked quote
|
||
// - `" backspace` should delete the marked quote
|
||
// - `" "`should create an unmarked quote and a second marked quote
|
||
// - `" up` should insert a quote, unmark it, and move up one line
|
||
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
|
||
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||
// Czech (QWERTY) layout:
|
||
// - in vim mode `option-4` should go to end of line (same as $)
|
||
// Japanese (Romaji) layout:
|
||
// - type `a i left down up enter enter` should create an unmarked text "愛"
|
||
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
|
||
let window_height = lock.content_size().height;
|
||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||
|
||
let Some(event) = event else {
|
||
return NO;
|
||
};
|
||
|
||
let run_callback = |event: PlatformInput| -> BOOL {
|
||
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||
let handled: BOOL = if let Some(callback) = callback.as_mut() {
|
||
!callback(event).propagate as BOOL
|
||
} else {
|
||
NO
|
||
};
|
||
window_state.as_ref().lock().event_callback = callback;
|
||
handled
|
||
};
|
||
|
||
match event {
|
||
PlatformInput::KeyDown(mut key_down_event) => {
|
||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
|
||
// makes no distinction between these two types of events, so we need to ignore
|
||
// the "key down" event if we've already just processed its "key equivalent" version.
|
||
if key_equivalent {
|
||
lock.last_key_equivalent = Some(key_down_event.clone());
|
||
} else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_event) {
|
||
return NO;
|
||
}
|
||
|
||
drop(lock);
|
||
|
||
let is_composing =
|
||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||
.flatten()
|
||
.is_some();
|
||
|
||
// If we're composing, send the key to the input handler first;
|
||
// otherwise we only send to the input handler if we don't have a matching binding.
|
||
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
|
||
// a key. If it does so, it will return YES so we won't send the key twice.
|
||
// We also do this for non-printing keys (like arrow keys and escape) as the IME menu
|
||
// may need them even if there is no marked text;
|
||
// however we skip keys with control or the input handler adds control-characters to the buffer.
|
||
// and keys with function, as the input handler swallows them.
|
||
if is_composing
|
||
|| (key_down_event.keystroke.key_char.is_none()
|
||
&& !key_down_event.keystroke.modifiers.control
|
||
&& !key_down_event.keystroke.modifiers.function)
|
||
{
|
||
{
|
||
let mut lock = window_state.as_ref().lock();
|
||
lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone());
|
||
lock.do_command_handled.take();
|
||
drop(lock);
|
||
}
|
||
|
||
let handled: BOOL = unsafe {
|
||
let input_context: id = msg_send![this, inputContext];
|
||
msg_send![input_context, handleEvent: native_event]
|
||
};
|
||
window_state.as_ref().lock().keystroke_for_do_command.take();
|
||
if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() {
|
||
return handled as BOOL;
|
||
} else if handled == YES {
|
||
return YES;
|
||
}
|
||
|
||
let handled = run_callback(PlatformInput::KeyDown(key_down_event));
|
||
return handled;
|
||
}
|
||
|
||
let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone()));
|
||
if handled == YES {
|
||
return YES;
|
||
}
|
||
|
||
if key_down_event.is_held {
|
||
if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() {
|
||
let handled = with_input_handler(&this, |input_handler| {
|
||
if !input_handler.apple_press_and_hold_enabled() {
|
||
input_handler.replace_text_in_range(None, &key_char);
|
||
return YES;
|
||
}
|
||
NO
|
||
});
|
||
if handled == Some(YES) {
|
||
return YES;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Don't send key equivalents to the input handler,
|
||
// or macOS shortcuts like cmd-` will stop working.
|
||
if key_equivalent {
|
||
return NO;
|
||
}
|
||
|
||
unsafe {
|
||
let input_context: id = msg_send![this, inputContext];
|
||
msg_send![input_context, handleEvent: native_event]
|
||
}
|
||
}
|
||
|
||
PlatformInput::KeyUp(_) => {
|
||
drop(lock);
|
||
run_callback(event)
|
||
}
|
||
|
||
_ => NO,
|
||
}
|
||
}
|
||
|
||
extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let weak_window_state = Arc::downgrade(&window_state);
|
||
let mut lock = window_state.as_ref().lock();
|
||
let window_height = lock.content_size().height;
|
||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||
|
||
if let Some(mut event) = event {
|
||
match &mut event {
|
||
PlatformInput::MouseDown(
|
||
event @ MouseDownEvent {
|
||
button: MouseButton::Left,
|
||
modifiers: Modifiers { control: true, .. },
|
||
..
|
||
},
|
||
) => {
|
||
// On mac, a ctrl-left click should be handled as a right click.
|
||
*event = MouseDownEvent {
|
||
button: MouseButton::Right,
|
||
modifiers: Modifiers {
|
||
control: false,
|
||
..event.modifiers
|
||
},
|
||
click_count: 1,
|
||
..*event
|
||
};
|
||
}
|
||
|
||
// Handles focusing click.
|
||
PlatformInput::MouseDown(
|
||
event @ MouseDownEvent {
|
||
button: MouseButton::Left,
|
||
..
|
||
},
|
||
) if (lock.first_mouse) => {
|
||
*event = MouseDownEvent {
|
||
first_mouse: true,
|
||
..*event
|
||
};
|
||
lock.first_mouse = false;
|
||
}
|
||
|
||
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
|
||
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
|
||
// user is still holding ctrl when releasing the left mouse button
|
||
PlatformInput::MouseUp(
|
||
event @ MouseUpEvent {
|
||
button: MouseButton::Left,
|
||
modifiers: Modifiers { control: true, .. },
|
||
..
|
||
},
|
||
) => {
|
||
*event = MouseUpEvent {
|
||
button: MouseButton::Right,
|
||
modifiers: Modifiers {
|
||
control: false,
|
||
..event.modifiers
|
||
},
|
||
click_count: 1,
|
||
..*event
|
||
};
|
||
}
|
||
|
||
_ => {}
|
||
};
|
||
|
||
match &event {
|
||
PlatformInput::MouseDown(_) => {
|
||
drop(lock);
|
||
unsafe {
|
||
let input_context: id = msg_send![this, inputContext];
|
||
msg_send![input_context, handleEvent: native_event]
|
||
}
|
||
lock = window_state.as_ref().lock();
|
||
}
|
||
PlatformInput::MouseMove(
|
||
event @ MouseMoveEvent {
|
||
pressed_button: Some(_),
|
||
..
|
||
},
|
||
) => {
|
||
// Synthetic drag is used for selecting long buffer contents while buffer is being scrolled.
|
||
// External file drag and drop is able to emit its own synthetic mouse events which will conflict
|
||
// with these ones.
|
||
if !lock.external_files_dragged {
|
||
lock.synthetic_drag_counter += 1;
|
||
let executor = lock.executor.clone();
|
||
executor
|
||
.spawn(synthetic_drag(
|
||
weak_window_state,
|
||
lock.synthetic_drag_counter,
|
||
event.clone(),
|
||
))
|
||
.detach();
|
||
}
|
||
}
|
||
|
||
PlatformInput::MouseUp(MouseUpEvent { .. }) => {
|
||
lock.synthetic_drag_counter += 1;
|
||
}
|
||
|
||
PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||
modifiers,
|
||
capslock,
|
||
}) => {
|
||
// Only raise modifiers changed event when they have actually changed
|
||
if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
|
||
modifiers: prev_modifiers,
|
||
capslock: prev_capslock,
|
||
})) = &lock.previous_modifiers_changed_event
|
||
{
|
||
if prev_modifiers == modifiers && prev_capslock == capslock {
|
||
return;
|
||
}
|
||
}
|
||
|
||
lock.previous_modifiers_changed_event = Some(event.clone());
|
||
}
|
||
|
||
_ => {}
|
||
}
|
||
|
||
if let Some(mut callback) = lock.event_callback.take() {
|
||
drop(lock);
|
||
callback(event);
|
||
window_state.lock().event_callback = Some(callback);
|
||
}
|
||
}
|
||
}
|
||
|
||
extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let lock = &mut *window_state.lock();
|
||
unsafe {
|
||
if lock
|
||
.native_window
|
||
.occlusionState()
|
||
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
|
||
{
|
||
lock.start_display_link();
|
||
} else {
|
||
lock.stop_display_link();
|
||
}
|
||
}
|
||
}
|
||
|
||
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
window_state.as_ref().lock().move_traffic_light();
|
||
}
|
||
|
||
extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
lock.fullscreen_restore_bounds = lock.bounds();
|
||
|
||
let min_version = NSOperatingSystemVersion::new(15, 3, 0);
|
||
|
||
if is_macos_version_at_least(min_version) {
|
||
unsafe {
|
||
lock.native_window.setTitlebarAppearsTransparent_(NO);
|
||
}
|
||
}
|
||
}
|
||
|
||
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
|
||
let min_version = NSOperatingSystemVersion::new(15, 3, 0);
|
||
|
||
if is_macos_version_at_least(min_version) && lock.transparent_titlebar {
|
||
unsafe {
|
||
lock.native_window.setTitlebarAppearsTransparent_(YES);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn is_macos_version_at_least(version: NSOperatingSystemVersion) -> bool {
|
||
unsafe { NSProcessInfo::processInfo(nil).isOperatingSystemAtLeastVersion(version) }
|
||
}
|
||
|
||
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
if let Some(mut callback) = lock.moved_callback.take() {
|
||
drop(lock);
|
||
callback();
|
||
window_state.lock().moved_callback = Some(callback);
|
||
}
|
||
}
|
||
|
||
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
lock.start_display_link();
|
||
}
|
||
|
||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let lock = window_state.lock();
|
||
let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
|
||
|
||
// When opening a pop-up while the application isn't active, Cocoa sends a spurious
|
||
// `windowDidBecomeKey` message to the previous key window even though that window
|
||
// isn't actually key. This causes a bug if the application is later activated while
|
||
// the pop-up is still open, making it impossible to activate the previous key window
|
||
// even if the pop-up gets closed. The only way to activate it again is to de-activate
|
||
// the app and re-activate it, which is a pretty bad UX.
|
||
// The following code detects the spurious event and invokes `resignKeyWindow`:
|
||
// in theory, we're not supposed to invoke this method manually but it balances out
|
||
// the spurious `becomeKeyWindow` event and helps us work around that bug.
|
||
if selector == sel!(windowDidBecomeKey:) && !is_active {
|
||
unsafe {
|
||
let _: () = msg_send![lock.native_window, resignKeyWindow];
|
||
return;
|
||
}
|
||
}
|
||
|
||
let executor = lock.executor.clone();
|
||
drop(lock);
|
||
executor
|
||
.spawn(async move {
|
||
let mut lock = window_state.as_ref().lock();
|
||
if let Some(mut callback) = lock.activate_callback.take() {
|
||
drop(lock);
|
||
callback(is_active);
|
||
window_state.lock().activate_callback = Some(callback);
|
||
};
|
||
})
|
||
.detach();
|
||
}
|
||
|
||
extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
if let Some(mut callback) = lock.should_close_callback.take() {
|
||
drop(lock);
|
||
let should_close = callback();
|
||
window_state.lock().should_close_callback = Some(callback);
|
||
should_close as BOOL
|
||
} else {
|
||
YES
|
||
}
|
||
}
|
||
|
||
extern "C" fn close_window(this: &Object, _: Sel) {
|
||
unsafe {
|
||
let close_callback = {
|
||
let window_state = get_window_state(this);
|
||
let mut lock = window_state.as_ref().lock();
|
||
lock.close_callback.take()
|
||
};
|
||
|
||
if let Some(callback) = close_callback {
|
||
callback();
|
||
}
|
||
|
||
let _: () = msg_send![super(this, class!(NSWindow)), close];
|
||
}
|
||
}
|
||
|
||
extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let window_state = window_state.as_ref().lock();
|
||
window_state.renderer.layer_ptr() as id
|
||
}
|
||
|
||
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
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);
|
||
unsafe {
|
||
let _: () = msg_send![
|
||
lock.renderer.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 set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
|
||
let new_size = Size::<Pixels>::from(size);
|
||
let old_size = unsafe {
|
||
let old_frame: NSRect = msg_send![this, frame];
|
||
Size::<Pixels>::from(old_frame.size)
|
||
};
|
||
|
||
if old_size == new_size {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
|
||
}
|
||
|
||
let scale_factor = lock.scale_factor();
|
||
let drawable_size = new_size.to_device_pixels(scale_factor);
|
||
lock.renderer.update_drawable_size(drawable_size);
|
||
|
||
if let Some(mut callback) = lock.resize_callback.take() {
|
||
let content_size = lock.content_size();
|
||
let scale_factor = lock.scale_factor();
|
||
drop(lock);
|
||
callback(content_size, scale_factor);
|
||
window_state.lock().resize_callback = Some(callback);
|
||
};
|
||
}
|
||
|
||
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.lock();
|
||
if let Some(mut callback) = lock.request_frame_callback.take() {
|
||
#[cfg(not(feature = "macos-blade"))]
|
||
lock.renderer.set_presents_with_transaction(true);
|
||
lock.stop_display_link();
|
||
drop(lock);
|
||
callback(Default::default());
|
||
|
||
let mut lock = window_state.lock();
|
||
lock.request_frame_callback = Some(callback);
|
||
#[cfg(not(feature = "macos-blade"))]
|
||
lock.renderer.set_presents_with_transaction(false);
|
||
lock.start_display_link();
|
||
}
|
||
}
|
||
|
||
unsafe extern "C" fn step(view: *mut c_void) {
|
||
let view = view as id;
|
||
let window_state = unsafe { get_window_state(&*view) };
|
||
let mut lock = window_state.lock();
|
||
|
||
if let Some(mut callback) = lock.request_frame_callback.take() {
|
||
drop(lock);
|
||
callback(Default::default());
|
||
window_state.lock().request_frame_callback = Some(callback);
|
||
}
|
||
}
|
||
|
||
extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||
unsafe { msg_send![class!(NSArray), array] }
|
||
}
|
||
|
||
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
|
||
let has_marked_text_result =
|
||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||
|
||
has_marked_text_result.is_some() as BOOL
|
||
}
|
||
|
||
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
|
||
let marked_range_result =
|
||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||
|
||
marked_range_result.map_or(NSRange::invalid(), |range| range.into())
|
||
}
|
||
|
||
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
|
||
let selected_range_result = with_input_handler(this, |input_handler| {
|
||
input_handler.selected_text_range(false)
|
||
})
|
||
.flatten();
|
||
|
||
selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into())
|
||
}
|
||
|
||
extern "C" fn first_rect_for_character_range(
|
||
this: &Object,
|
||
_: Sel,
|
||
range: NSRange,
|
||
_: id,
|
||
) -> NSRect {
|
||
let frame = get_frame(this);
|
||
with_input_handler(this, |input_handler| {
|
||
input_handler.bounds_for_range(range.to_range()?)
|
||
})
|
||
.flatten()
|
||
.map_or(
|
||
NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)),
|
||
|bounds| {
|
||
NSRect::new(
|
||
NSPoint::new(
|
||
frame.origin.x + bounds.origin.x.0 as f64,
|
||
frame.origin.y + frame.size.height
|
||
- bounds.origin.y.0 as f64
|
||
- bounds.size.height.0 as f64,
|
||
),
|
||
NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64),
|
||
)
|
||
},
|
||
)
|
||
}
|
||
|
||
fn get_frame(this: &Object) -> NSRect {
|
||
unsafe {
|
||
let state = get_window_state(this);
|
||
let lock = state.lock();
|
||
let mut frame = NSWindow::frame(lock.native_window);
|
||
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
|
||
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
|
||
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
|
||
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
|
||
}
|
||
frame
|
||
}
|
||
}
|
||
|
||
extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
|
||
unsafe {
|
||
let is_attributed_string: BOOL =
|
||
msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
|
||
let text: id = if is_attributed_string == YES {
|
||
msg_send![text, string]
|
||
} else {
|
||
text
|
||
};
|
||
|
||
let text = text.to_str();
|
||
let replacement_range = replacement_range.to_range();
|
||
with_input_handler(this, |input_handler| {
|
||
input_handler.replace_text_in_range(replacement_range, &text)
|
||
});
|
||
}
|
||
}
|
||
|
||
extern "C" fn set_marked_text(
|
||
this: &Object,
|
||
_: Sel,
|
||
text: id,
|
||
selected_range: NSRange,
|
||
replacement_range: NSRange,
|
||
) {
|
||
unsafe {
|
||
let is_attributed_string: BOOL =
|
||
msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
|
||
let text: id = if is_attributed_string == YES {
|
||
msg_send![text, string]
|
||
} else {
|
||
text
|
||
};
|
||
let selected_range = selected_range.to_range();
|
||
let replacement_range = replacement_range.to_range();
|
||
let text = text.to_str();
|
||
with_input_handler(this, |input_handler| {
|
||
input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
|
||
});
|
||
}
|
||
}
|
||
extern "C" fn unmark_text(this: &Object, _: Sel) {
|
||
with_input_handler(this, |input_handler| input_handler.unmark_text());
|
||
}
|
||
|
||
extern "C" fn attributed_substring_for_proposed_range(
|
||
this: &Object,
|
||
_: Sel,
|
||
range: NSRange,
|
||
actual_range: *mut c_void,
|
||
) -> id {
|
||
with_input_handler(this, |input_handler| {
|
||
let range = range.to_range()?;
|
||
if range.is_empty() {
|
||
return None;
|
||
}
|
||
let mut adjusted: Option<Range<usize>> = None;
|
||
|
||
let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
|
||
if let Some(adjusted) = adjusted {
|
||
if adjusted != range {
|
||
unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
|
||
}
|
||
}
|
||
unsafe {
|
||
let string: id = msg_send![class!(NSAttributedString), alloc];
|
||
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
|
||
Some(string)
|
||
}
|
||
})
|
||
.flatten()
|
||
.unwrap_or(nil)
|
||
}
|
||
|
||
// We ignore which selector it asks us to do because the user may have
|
||
// bound the shortcut to something else.
|
||
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
|
||
let state = unsafe { get_window_state(this) };
|
||
let mut lock = state.as_ref().lock();
|
||
let keystroke = lock.keystroke_for_do_command.take();
|
||
let mut event_callback = lock.event_callback.take();
|
||
drop(lock);
|
||
|
||
if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) {
|
||
let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent {
|
||
keystroke,
|
||
is_held: false,
|
||
}));
|
||
state.as_ref().lock().do_command_handled = Some(!handled.propagate);
|
||
}
|
||
|
||
state.as_ref().lock().event_callback = event_callback;
|
||
}
|
||
|
||
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
|
||
unsafe {
|
||
let state = get_window_state(this);
|
||
let mut lock = state.as_ref().lock();
|
||
if let Some(mut callback) = lock.appearance_changed_callback.take() {
|
||
drop(lock);
|
||
callback();
|
||
state.lock().appearance_changed_callback = Some(callback);
|
||
}
|
||
}
|
||
}
|
||
|
||
extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
lock.first_mouse = true;
|
||
YES
|
||
}
|
||
|
||
extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 {
|
||
let position = screen_point_to_gpui_point(this, position);
|
||
with_input_handler(this, |input_handler| {
|
||
input_handler.character_index_for_point(position)
|
||
})
|
||
.flatten()
|
||
.map(|index| index as u64)
|
||
.unwrap_or(NSNotFound as u64)
|
||
}
|
||
|
||
fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> {
|
||
let frame = get_frame(this);
|
||
let window_x = position.x - frame.origin.x;
|
||
let window_y = frame.size.height - (position.y - frame.origin.y);
|
||
let position = point(px(window_x as f32), px(window_y as f32));
|
||
position
|
||
}
|
||
|
||
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let position = drag_event_position(&window_state, dragging_info);
|
||
let paths = external_paths_from_event(dragging_info);
|
||
if let Some(event) =
|
||
paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }))
|
||
{
|
||
if send_new_event(&window_state, event) {
|
||
window_state.lock().external_files_dragged = true;
|
||
return NSDragOperationCopy;
|
||
}
|
||
}
|
||
NSDragOperationNone
|
||
}
|
||
|
||
extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let position = drag_event_position(&window_state, dragging_info);
|
||
if send_new_event(
|
||
&window_state,
|
||
PlatformInput::FileDrop(FileDropEvent::Pending { position }),
|
||
) {
|
||
NSDragOperationCopy
|
||
} else {
|
||
NSDragOperationNone
|
||
}
|
||
}
|
||
|
||
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
send_new_event(
|
||
&window_state,
|
||
PlatformInput::FileDrop(FileDropEvent::Exited),
|
||
);
|
||
window_state.lock().external_files_dragged = false;
|
||
}
|
||
|
||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
let position = drag_event_position(&window_state, dragging_info);
|
||
send_new_event(
|
||
&window_state,
|
||
PlatformInput::FileDrop(FileDropEvent::Submit { position }),
|
||
)
|
||
.to_objc()
|
||
}
|
||
|
||
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {
|
||
let mut paths = SmallVec::new();
|
||
let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
|
||
let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) };
|
||
if filenames == nil {
|
||
return None;
|
||
}
|
||
for file in unsafe { filenames.iter() } {
|
||
let path = unsafe {
|
||
let f = NSString::UTF8String(file);
|
||
CStr::from_ptr(f).to_string_lossy().into_owned()
|
||
};
|
||
paths.push(PathBuf::from(path))
|
||
}
|
||
Some(ExternalPaths(paths))
|
||
}
|
||
|
||
extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
|
||
let window_state = unsafe { get_window_state(this) };
|
||
send_new_event(
|
||
&window_state,
|
||
PlatformInput::FileDrop(FileDropEvent::Exited),
|
||
);
|
||
}
|
||
|
||
async fn synthetic_drag(
|
||
window_state: Weak<Mutex<MacWindowState>>,
|
||
drag_id: usize,
|
||
event: MouseMoveEvent,
|
||
) {
|
||
loop {
|
||
Timer::after(Duration::from_millis(16)).await;
|
||
if let Some(window_state) = window_state.upgrade() {
|
||
let mut lock = window_state.lock();
|
||
if lock.synthetic_drag_counter == drag_id {
|
||
if let Some(mut callback) = lock.event_callback.take() {
|
||
drop(lock);
|
||
callback(PlatformInput::MouseMove(event.clone()));
|
||
window_state.lock().event_callback = Some(callback);
|
||
}
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn send_new_event(window_state_lock: &Mutex<MacWindowState>, e: PlatformInput) -> bool {
|
||
let window_state = window_state_lock.lock().event_callback.take();
|
||
if let Some(mut callback) = window_state {
|
||
callback(e);
|
||
window_state_lock.lock().event_callback = Some(callback);
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
fn drag_event_position(window_state: &Mutex<MacWindowState>, dragging_info: id) -> Point<Pixels> {
|
||
let drag_location: NSPoint = unsafe { msg_send![dragging_info, draggingLocation] };
|
||
convert_mouse_position(drag_location, window_state.lock().content_size().height)
|
||
}
|
||
|
||
fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
|
||
where
|
||
F: FnOnce(&mut PlatformInputHandler) -> R,
|
||
{
|
||
let window_state = unsafe { get_window_state(window) };
|
||
let mut lock = window_state.as_ref().lock();
|
||
if let Some(mut input_handler) = lock.input_handler.take() {
|
||
drop(lock);
|
||
let result = f(&mut input_handler);
|
||
window_state.lock().input_handler = Some(input_handler);
|
||
Some(result)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
|
||
unsafe {
|
||
let device_description = NSScreen::deviceDescription(screen);
|
||
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
|
||
let screen_number = device_description.objectForKey_(screen_number_key);
|
||
let screen_number: NSUInteger = msg_send![screen_number, unsignedIntegerValue];
|
||
screen_number as CGDirectDisplayID
|
||
}
|
||
}
|
||
|
||
extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id {
|
||
unsafe {
|
||
let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame];
|
||
// Use a colorless semantic material. The default value `AppearanceBased`, though not
|
||
// manually set, is deprecated.
|
||
NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection);
|
||
NSVisualEffectView::setState_(view, NSVisualEffectState::Active);
|
||
view
|
||
}
|
||
}
|
||
|
||
extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) {
|
||
unsafe {
|
||
let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer];
|
||
let layer: id = msg_send![this, layer];
|
||
if !layer.is_null() {
|
||
remove_layer_background(layer);
|
||
}
|
||
}
|
||
}
|
||
|
||
unsafe fn remove_layer_background(layer: id) {
|
||
unsafe {
|
||
let _: () = msg_send![layer, setBackgroundColor:nil];
|
||
|
||
let class_name: id = msg_send![layer, className];
|
||
if class_name.isEqualToString("CAChameleonLayer") {
|
||
// Remove the desktop tinting effect.
|
||
let _: () = msg_send![layer, setHidden: YES];
|
||
return;
|
||
}
|
||
|
||
let filters: id = msg_send![layer, filters];
|
||
if !filters.is_null() {
|
||
// Remove the increased saturation.
|
||
// The effect of a `CAFilter` or `CIFilter` is determined by its name, and the
|
||
// `description` reflects its name and some parameters. Currently `NSVisualEffectView`
|
||
// uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the
|
||
// `description` will still contain "Saturat" ("... inputSaturation = ...").
|
||
let test_string: id = NSString::alloc(nil).init_str("Saturat").autorelease();
|
||
let count = NSArray::count(filters);
|
||
for i in 0..count {
|
||
let description: id = msg_send![filters.objectAtIndex(i), description];
|
||
let hit: BOOL = msg_send![description, containsString: test_string];
|
||
if hit == NO {
|
||
continue;
|
||
}
|
||
|
||
let all_indices = NSRange {
|
||
location: 0,
|
||
length: count,
|
||
};
|
||
let indices: id = msg_send![class!(NSMutableIndexSet), indexSet];
|
||
let _: () = msg_send![indices, addIndexesInRange: all_indices];
|
||
let _: () = msg_send![indices, removeIndex:i];
|
||
let filtered: id = msg_send![filters, objectsAtIndexes: indices];
|
||
let _: () = msg_send![layer, setFilters: filtered];
|
||
break;
|
||
}
|
||
}
|
||
|
||
let sublayers: id = msg_send![layer, sublayers];
|
||
if !sublayers.is_null() {
|
||
let count = NSArray::count(sublayers);
|
||
for i in 0..count {
|
||
let sublayer = sublayers.objectAtIndex(i);
|
||
remove_layer_background(sublayer);
|
||
}
|
||
}
|
||
}
|
||
}
|