1672 lines
56 KiB
Rust
1672 lines
56 KiB
Rust
use super::delegate::DispatchDelegate;
|
|
use super::{
|
|
app, make_nsstring,
|
|
rendering::{is_integrated_gpu, Device, RendererManager},
|
|
RectFExt as _,
|
|
};
|
|
use anyhow::{anyhow, Result};
|
|
use cocoa::appkit::{NSWindowButton, NSWindowStyleMask};
|
|
use cocoa::foundation::{NSArray, NSPoint, NSRange, NSString, NSUInteger};
|
|
use cocoa::{
|
|
appkit::{NSApp, NSView, NSWindow},
|
|
base::{id, nil},
|
|
foundation::{NSAutoreleasePool, NSInteger, NSRect, NSSize},
|
|
};
|
|
use foreign_types::ForeignType;
|
|
use itertools::Itertools;
|
|
use num_traits::FromPrimitive;
|
|
use objc::class;
|
|
use objc::{
|
|
msg_send,
|
|
runtime::{Object, BOOL, NO, YES},
|
|
sel, sel_impl,
|
|
};
|
|
use pathfinder_geometry::{
|
|
rect::RectF,
|
|
vector::{vec2f, Vector2F},
|
|
};
|
|
use warpui_core::event::ModifiersState;
|
|
use warpui_core::platform::{
|
|
file_picker, FilePickerCallback, FilePickerConfiguration, FullscreenState, GraphicsBackend,
|
|
TerminationMode, WindowFocusBehavior,
|
|
};
|
|
use warpui_core::r#async::Timer;
|
|
use warpui_core::rendering::GPUPowerPreference;
|
|
use warpui_core::windowing::WindowCallbacks;
|
|
use warpui_core::{
|
|
accessibility::AccessibilityContent,
|
|
actions::StandardAction,
|
|
platform::{self, WindowBounds, WindowOptions, WindowStyle},
|
|
r#async::executor,
|
|
Event, OptionalPlatformWindow, Scene, WindowId,
|
|
};
|
|
use warpui_core::{DisplayId, DisplayIdx};
|
|
|
|
use instant::Instant;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use std::{cell::Cell, os::raw::c_uchar, panic, path::Path, ptr};
|
|
use std::{cell::RefCell, ffi::c_void, rc::Rc};
|
|
use std::{slice, str};
|
|
|
|
extern "C" {
|
|
fn screenFrame() -> NSRect;
|
|
fn activeScreenId() -> NSUInteger;
|
|
}
|
|
|
|
pub const WINDOW_STATE_IVAR: &str = "windowState";
|
|
const INITIAL_WINDOW_WIDTH: f32 = 1280.;
|
|
const INITIAL_WINDOW_HEIGHT: f32 = 800.;
|
|
const DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS: u8 = 1;
|
|
|
|
// A mac::window::Window holds a reference to a WindowState.
|
|
// The NSWindow also holds a reference, so that either may be deallocated first.
|
|
pub struct Window(Rc<WindowState>);
|
|
|
|
pub(crate) struct WindowManager {
|
|
windows: HashMap<WindowId, Rc<Window>>,
|
|
renderer_manager: Rc<RefCell<RendererManager>>,
|
|
}
|
|
|
|
impl WindowManager {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
windows: Default::default(),
|
|
renderer_manager: Rc::new(RefCell::new(RendererManager::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl platform::WindowManager for WindowManager {
|
|
fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow {
|
|
self.windows
|
|
.get(&window_id)
|
|
.map(Rc::clone)
|
|
.map(|inner| inner as Rc<dyn crate::platform::Window>)
|
|
}
|
|
|
|
fn open_window(
|
|
&mut self,
|
|
window_id: WindowId,
|
|
options: WindowOptions,
|
|
callbacks: WindowCallbacks,
|
|
) -> Result<()> {
|
|
let executor = Rc::new(executor::Foreground::platform(Arc::new(DispatchDelegate))?);
|
|
let window = Rc::new(Window::open(
|
|
options,
|
|
window_id,
|
|
callbacks,
|
|
executor,
|
|
Rc::clone(&self.renderer_manager),
|
|
)?);
|
|
self.windows.insert(window_id, Rc::clone(&window));
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_window(&mut self, window_id: WindowId) {
|
|
self.windows.remove(&window_id);
|
|
}
|
|
|
|
fn active_window_id(&self) -> Option<WindowId> {
|
|
Window::active_window_id()
|
|
}
|
|
|
|
fn key_window_is_modal_panel(&self) -> bool {
|
|
Window::key_window_is_modal_panel()
|
|
}
|
|
|
|
fn app_is_active(&self) -> bool {
|
|
let res: BOOL = unsafe { msg_send![app::get_warp_app(), isActive] };
|
|
res == YES
|
|
}
|
|
|
|
fn hide_app(&self) {
|
|
unsafe {
|
|
hide_app();
|
|
}
|
|
}
|
|
|
|
fn activate_app(&self, _last_active_window: Option<WindowId>) -> Option<WindowId> {
|
|
unsafe {
|
|
activate_app();
|
|
}
|
|
Window::frontmost_window_id()
|
|
}
|
|
|
|
fn show_window_and_focus_app(&self, window_id: WindowId, behavior: WindowFocusBehavior) {
|
|
if matches!(behavior, WindowFocusBehavior::BringToFront) {
|
|
Window::show_window_and_focus_app(window_id, true)
|
|
} else {
|
|
Window::show_window_and_focus_app(window_id, false)
|
|
}
|
|
}
|
|
|
|
fn hide_window(&self, window_id: WindowId) {
|
|
Window::hide_window(window_id)
|
|
}
|
|
|
|
fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
|
|
Window::set_window_bounds(window_id, bound)
|
|
}
|
|
|
|
fn set_all_windows_background_blur_radius(&self, blur_radius_pixels: u8) {
|
|
Window::set_all_windows_background_blur_radius(blur_radius_pixels)
|
|
}
|
|
|
|
fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
|
|
// no-op on MacOS. This is only available on Windows.
|
|
}
|
|
|
|
fn set_window_title(&self, window_id: WindowId, title: &str) {
|
|
Window::set_window_title(window_id, title)
|
|
}
|
|
|
|
fn close_window_async(&self, window_id: WindowId, termination_mode: TerminationMode) {
|
|
Window::close_window_async(window_id, termination_mode);
|
|
}
|
|
|
|
fn display_count(&self) -> usize {
|
|
unsafe {
|
|
let screens: id = msg_send![class!(NSScreen), screens];
|
|
screens.count() as usize
|
|
}
|
|
}
|
|
|
|
fn active_display_bounds(&self) -> RectF {
|
|
let rect = unsafe { screenFrame() };
|
|
let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
|
|
let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
|
|
RectF::new(
|
|
transform_origin_from_frame_coord_to_rect_coord(point, size),
|
|
size,
|
|
)
|
|
}
|
|
|
|
fn active_display_id(&self) -> DisplayId {
|
|
let id = unsafe { activeScreenId() };
|
|
(id as usize).into()
|
|
}
|
|
|
|
fn bounds_for_display_idx(&self, display_idx: DisplayIdx) -> Option<RectF> {
|
|
let rect: NSRect = unsafe {
|
|
let screens: id = msg_send![class!(NSScreen), screens];
|
|
|
|
let idx: u64 = match display_idx {
|
|
DisplayIdx::Primary => 0,
|
|
DisplayIdx::External(idx) => (idx + 1) as u64,
|
|
};
|
|
|
|
if idx >= screens.count() {
|
|
return None;
|
|
}
|
|
|
|
let screen: id = screens.objectAtIndex(idx);
|
|
msg_send![screen, frame]
|
|
};
|
|
|
|
let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
|
|
let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
|
|
|
|
Some(RectF::new(
|
|
transform_origin_from_frame_coord_to_rect_coord(point, size),
|
|
size,
|
|
))
|
|
}
|
|
|
|
fn active_cursor_position_updated(&self) {
|
|
// no-op on macOS
|
|
}
|
|
|
|
fn windowing_system(&self) -> Option<crate::windowing::System> {
|
|
Some(crate::windowing::System::AppKit)
|
|
}
|
|
|
|
fn os_window_manager_name(&self) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
fn is_tiling_window_manager(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn ordered_window_ids(&self) -> Vec<WindowId> {
|
|
unsafe {
|
|
let ordered_windows: id = msg_send![NSApp(), orderedWindows];
|
|
let count = ordered_windows.count();
|
|
let mut result = Vec::with_capacity(count as usize);
|
|
for i in 0..count {
|
|
let window: id = ordered_windows.objectAtIndex(i);
|
|
if is_warp_window(window) == YES {
|
|
result.push(get_window_state(&*window).window_id);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
/// Cancels any in-flight synthetic mouse-drag event loop for the given window.
|
|
///
|
|
/// During tab drag, we run a synthetic drag loop (pumping mouse-moved events)
|
|
/// to track the cursor across windows. When a tab is handed off to another
|
|
/// window or the drag is finalized, we need to stop the old loop so stale
|
|
/// events don't continue driving the source window's drag state.
|
|
/// Incrementing the drag ID causes the running loop to see a mismatched ID
|
|
/// and exit on its next iteration.
|
|
fn cancel_synthetic_drag(&self, window_id: WindowId) {
|
|
if let Some(window) = self.windows.get(&window_id) {
|
|
window.0.next_synthetic_drag_id();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct IntegrationTestWindowManager {
|
|
window_manager: WindowManager,
|
|
}
|
|
|
|
impl IntegrationTestWindowManager {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
window_manager: WindowManager::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl platform::WindowManager for IntegrationTestWindowManager {
|
|
fn open_window(
|
|
&mut self,
|
|
window_id: WindowId,
|
|
window_options: WindowOptions,
|
|
callbacks: WindowCallbacks,
|
|
) -> Result<()> {
|
|
self.window_manager
|
|
.open_window(window_id, window_options, callbacks)
|
|
}
|
|
|
|
fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow {
|
|
self.window_manager.platform_window(window_id)
|
|
}
|
|
|
|
fn remove_window(&mut self, window_id: WindowId) {
|
|
self.window_manager.remove_window(window_id)
|
|
}
|
|
|
|
fn active_window_id(&self) -> Option<WindowId> {
|
|
// Pretend that the frontmost window is the active one. This aligns with forcing
|
|
// `app_is_active()` to always return `true`.
|
|
Window::frontmost_window_id()
|
|
}
|
|
|
|
fn key_window_is_modal_panel(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn app_is_active(&self) -> bool {
|
|
// always assume active for tests.
|
|
true
|
|
}
|
|
|
|
fn activate_app(&self, last_active_window: Option<WindowId>) -> Option<WindowId> {
|
|
self.window_manager.activate_app(last_active_window)
|
|
}
|
|
|
|
fn show_window_and_focus_app(&self, window_id: WindowId, behavior: WindowFocusBehavior) {
|
|
self.window_manager
|
|
.show_window_and_focus_app(window_id, behavior)
|
|
}
|
|
|
|
fn hide_app(&self) {
|
|
self.window_manager.hide_app()
|
|
}
|
|
|
|
fn hide_window(&self, window_id: WindowId) {
|
|
self.window_manager.hide_window(window_id)
|
|
}
|
|
|
|
fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
|
|
self.window_manager.set_window_bounds(window_id, bound)
|
|
}
|
|
|
|
fn set_all_windows_background_blur_radius(&self, _blur_radius_pixels: u8) {
|
|
// no-op for tests
|
|
}
|
|
|
|
fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
|
|
// no-op for tests
|
|
}
|
|
|
|
fn set_window_title(&self, window_id: WindowId, title: &str) {
|
|
self.window_manager.set_window_title(window_id, title)
|
|
}
|
|
|
|
fn close_window_async(&self, window_id: WindowId, termination_mode: TerminationMode) {
|
|
self.window_manager
|
|
.close_window_async(window_id, termination_mode)
|
|
}
|
|
|
|
fn active_display_bounds(&self) -> RectF {
|
|
self.window_manager.active_display_bounds()
|
|
}
|
|
|
|
fn active_display_id(&self) -> DisplayId {
|
|
self.window_manager.active_display_id()
|
|
}
|
|
|
|
fn display_count(&self) -> usize {
|
|
1
|
|
}
|
|
|
|
fn bounds_for_display_idx(&self, idx: DisplayIdx) -> Option<RectF> {
|
|
self.window_manager.bounds_for_display_idx(idx)
|
|
}
|
|
|
|
fn active_cursor_position_updated(&self) {
|
|
// no-op on macOS
|
|
}
|
|
|
|
fn windowing_system(&self) -> Option<crate::windowing::System> {
|
|
None
|
|
}
|
|
|
|
fn os_window_manager_name(&self) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
fn is_tiling_window_manager(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn ordered_window_ids(&self) -> Vec<WindowId> {
|
|
self.window_manager.ordered_window_ids()
|
|
}
|
|
|
|
fn cancel_synthetic_drag(&self, window_id: WindowId) {
|
|
self.window_manager.cancel_synthetic_drag(window_id)
|
|
}
|
|
}
|
|
|
|
// We put a pointer type into NSWindow and sometimes NSView ivars.
|
|
// The pointer type is a Box<Rc<WindowState>>, into raw.
|
|
// Note that the ivar holds a strong reference to the window state.
|
|
#[allow(non_snake_case)]
|
|
mod Ivar {
|
|
use super::*;
|
|
type WindowStatePtr = *mut Rc<WindowState>;
|
|
|
|
// Convert an Rc<WindowState> into our ivar pointer type.
|
|
// This increments the reference count.
|
|
pub fn from_state(ws: &Rc<WindowState>) -> *const c_void {
|
|
// Note this increments the reference count.
|
|
let reference = ws.clone();
|
|
Box::into_raw(Box::new(reference)) as *const c_void
|
|
}
|
|
|
|
// Get a reference to the window state from an ivar pointer.
|
|
// Note the lifetime here is suspicious: the caller must ensure that the object
|
|
// is not deallocated while the reference is alive.
|
|
pub fn get_state<'a>(ptr: *mut c_void) -> &'a Rc<WindowState> {
|
|
unsafe { (ptr as WindowStatePtr).as_ref().expect("Pointer was null") }
|
|
}
|
|
|
|
// Extract the window state from an ivar pointer.
|
|
// After calling this, the pointer is now dangling, and the ivar should be cleared.
|
|
pub fn take_state(ptr: *mut c_void) -> Rc<WindowState> {
|
|
// Note dereferencing a box consumes it, because Rust is confused.
|
|
unsafe { *Box::from_raw(ptr as WindowStatePtr) }
|
|
}
|
|
}
|
|
|
|
// Declarations of functions implemented in ObjC files.
|
|
// These signatures must be manually synced - there's no type checking here.
|
|
extern "C" {
|
|
fn create_warp_nswindow(
|
|
contentRect: NSRect,
|
|
metalDevice: id,
|
|
hideTitleBar: BOOL,
|
|
backgroundBlurRadiusPixels: u8,
|
|
testMode: BOOL,
|
|
) -> id;
|
|
fn create_warp_nspanel(
|
|
contentRect: NSRect,
|
|
metalDevice: id,
|
|
hideTitleBar: BOOL,
|
|
backgroundBlurRadiusPixels: u8,
|
|
testMode: BOOL,
|
|
) -> id;
|
|
fn is_warp_window(window: id) -> BOOL;
|
|
fn get_frontmost_window() -> id;
|
|
fn set_accessibility_contents(
|
|
window: id,
|
|
value: id, /* NSString */
|
|
help: id, /* NSString */
|
|
warpRole: id, /* NSString */
|
|
setFrame: BOOL,
|
|
frame: NSRect,
|
|
);
|
|
|
|
fn hide_app();
|
|
fn activate_app();
|
|
fn show_window_and_focus_app(window: id, bringToFront: BOOL);
|
|
fn hide_window(window: id);
|
|
fn position_and_order_front(window: id);
|
|
fn position_at_given_location(window: id, origin: NSPoint);
|
|
fn order_front_without_focus(window: id, origin: NSPoint);
|
|
fn set_window_title(window: id, title: id);
|
|
fn set_window_bounds(window: id, bound: NSRect);
|
|
fn set_window_background_blur_radius(window: id, blurRadiusPixels: u8);
|
|
fn open_file_path(pathString: id);
|
|
fn open_file_path_in_explorer(pathString: id);
|
|
fn open_file_picker(
|
|
callback: *mut c_void,
|
|
file_types: id,
|
|
allow_files: BOOL,
|
|
allow_folders: BOOL,
|
|
allow_multi_selection: BOOL,
|
|
);
|
|
fn open_save_file_picker(callback: *mut c_void, default_filename: id, default_directory: id);
|
|
fn open_url(urlString: id);
|
|
fn set_titlebar_height(window: id, height: f64);
|
|
}
|
|
|
|
pub type FrameCaptureCallback = Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>;
|
|
|
|
pub struct WindowState {
|
|
native_window: id,
|
|
window_id: WindowId,
|
|
callbacks: WindowCallbacks,
|
|
next_scene: RefCell<Option<Rc<Scene>>>,
|
|
device: Option<Device>,
|
|
renderer_manager: Option<Rc<RefCell<RendererManager>>>,
|
|
synthetic_drag_counter: Cell<usize>,
|
|
executor: Rc<executor::Foreground>,
|
|
ime_active: Cell<bool>,
|
|
pub(super) capture_callback: RefCell<Option<FrameCaptureCallback>>,
|
|
}
|
|
|
|
impl Window {
|
|
pub fn open(
|
|
options: WindowOptions,
|
|
window_id: WindowId,
|
|
callbacks: WindowCallbacks,
|
|
executor: Rc<executor::Foreground>,
|
|
renderer_manager: Rc<RefCell<RendererManager>>,
|
|
) -> Result<Self> {
|
|
log::info!("Opening window with id {window_id}");
|
|
unsafe {
|
|
let pool = NSAutoreleasePool::new(nil);
|
|
let frame = match options.bounds {
|
|
WindowBounds::ExactPosition(bounds) => RectF::new(
|
|
transform_origin_from_rect_coord_to_frame_coord(bounds.origin(), bounds.size()),
|
|
bounds.size(),
|
|
)
|
|
.to_ns_rect(),
|
|
WindowBounds::ExactSize(size) => RectF::new(Vector2F::zero(), size).to_ns_rect(),
|
|
WindowBounds::Default => RectF::new(
|
|
Vector2F::zero(),
|
|
Vector2F::new(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT),
|
|
)
|
|
.to_ns_rect(),
|
|
};
|
|
|
|
let test_mode = cfg!(feature = "integration_tests")
|
|
&& std::env::var("WARPUI_USE_REAL_DISPLAY_IN_INTEGRATION_TESTS").is_err();
|
|
|
|
let (metal_device, metal_device_ptr) = if test_mode {
|
|
(None, nil)
|
|
} else {
|
|
// TODO: device appears to be leaked here.
|
|
let metal_device = match options.gpu_power_preference {
|
|
GPUPowerPreference::LowPower => metal::Device::all()
|
|
.into_iter()
|
|
.find(is_integrated_gpu)
|
|
.or_else(metal::Device::system_default),
|
|
_ => metal::Device::system_default(),
|
|
}
|
|
.ok_or_else(|| anyhow!("could not obtain metal device"))?;
|
|
log::info!(
|
|
"Using {} GPU for rendering new window.",
|
|
if is_integrated_gpu(&metal_device) {
|
|
"integrated"
|
|
} else {
|
|
"discrete"
|
|
}
|
|
);
|
|
|
|
let ptr = metal_device.as_ptr() as id;
|
|
(Some(metal_device), ptr)
|
|
};
|
|
|
|
let native_window: id = match options.style {
|
|
WindowStyle::Pin => {
|
|
let panel: id = create_warp_nspanel(
|
|
frame,
|
|
metal_device_ptr,
|
|
options.hide_title_bar as BOOL,
|
|
options
|
|
.background_blur_radius_pixels
|
|
.unwrap_or(DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS),
|
|
test_mode as BOOL,
|
|
);
|
|
|
|
let _: () = msg_send![panel, positionPinnedPanel];
|
|
panel
|
|
}
|
|
_ => create_warp_nswindow(
|
|
frame,
|
|
metal_device_ptr,
|
|
options.hide_title_bar as BOOL,
|
|
options
|
|
.background_blur_radius_pixels
|
|
.unwrap_or(DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS),
|
|
test_mode as BOOL,
|
|
),
|
|
};
|
|
if native_window == nil {
|
|
return Err(anyhow!("WarpWindow returned nil from initializer"));
|
|
}
|
|
|
|
if options.fullscreen_state == FullscreenState::Fullscreen {
|
|
// Instead of directly calling toggleFullScreen, we call a wrapper method that
|
|
// ensures MacOS window animations don't overlap.
|
|
let _: () = msg_send![native_window, enqueueFullscreenTransition];
|
|
}
|
|
|
|
let native_view: id = msg_send![native_window, contentView];
|
|
|
|
let device = metal_device.map(move |metal_device| {
|
|
Device::new(
|
|
metal_device,
|
|
native_view as id,
|
|
native_window as id,
|
|
options.gpu_power_preference,
|
|
options.on_gpu_device_info_reported,
|
|
)
|
|
});
|
|
|
|
let window_state = Rc::new(WindowState {
|
|
native_window,
|
|
window_id,
|
|
callbacks,
|
|
next_scene: Default::default(),
|
|
renderer_manager: Some(renderer_manager),
|
|
device,
|
|
synthetic_drag_counter: Cell::new(0),
|
|
executor,
|
|
ime_active: Cell::new(false),
|
|
capture_callback: RefCell::new(None),
|
|
});
|
|
|
|
(*native_window).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
|
|
(*native_view).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
|
|
|
|
let native_window_delegate: id = msg_send![native_window, delegate];
|
|
(*native_window_delegate).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
|
|
|
|
// Set the initial scale properly.
|
|
warp_view_did_change_backing_properties(&*native_view, true);
|
|
|
|
match options.style {
|
|
WindowStyle::Normal | WindowStyle::Pin => {
|
|
match options.bounds {
|
|
WindowBounds::ExactPosition(_) => {
|
|
// If specfied, we should set the window to the exact position.
|
|
// Note that the final position could be different from the set one as the original
|
|
// frame may no longer exist (e.g. user unplugs the monitor).
|
|
position_at_given_location(native_window, frame.origin)
|
|
}
|
|
WindowBounds::ExactSize(_) | WindowBounds::Default => {
|
|
// Otherwise we put it in the center of the window or cascade from the previous window.
|
|
position_and_order_front(native_window)
|
|
}
|
|
}
|
|
}
|
|
WindowStyle::Cascade => position_and_order_front(native_window),
|
|
WindowStyle::NotStealFocus => (),
|
|
WindowStyle::PositionedNoFocus => {
|
|
order_front_without_focus(native_window, frame.origin)
|
|
}
|
|
}
|
|
|
|
pool.drain();
|
|
|
|
Ok(Self(window_state))
|
|
}
|
|
}
|
|
|
|
/// Returns the key window, if any.
|
|
pub fn key_window() -> Option<id> {
|
|
unsafe {
|
|
let native_window: id = msg_send![NSApp(), keyWindow];
|
|
if native_window == nil {
|
|
None
|
|
} else {
|
|
Some(native_window)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn active_window_id() -> Option<WindowId> {
|
|
unsafe {
|
|
let native_window = Self::key_window()?;
|
|
let is_ours: bool = is_warp_window(native_window) == YES;
|
|
if is_ours {
|
|
Some(get_window_state(&*native_window).window_id)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn frontmost_window_id() -> Option<WindowId> {
|
|
unsafe {
|
|
let native_window: id = get_frontmost_window();
|
|
let is_ours: bool = is_warp_window(native_window) == YES;
|
|
if is_ours {
|
|
Some(get_window_state(&*native_window).window_id)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn key_window_is_modal_panel() -> bool {
|
|
unsafe {
|
|
let Some(native_window) = Self::key_window() else {
|
|
return false;
|
|
};
|
|
let is_modal_panel: BOOL = msg_send![native_window, isModalPanel];
|
|
is_modal_panel == YES
|
|
}
|
|
}
|
|
|
|
pub fn close_ime_on_active_window() {
|
|
unsafe {
|
|
let Some(native_window) = Self::key_window() else {
|
|
return;
|
|
};
|
|
let is_ours: bool = is_warp_window(native_window) == YES;
|
|
if is_ours {
|
|
Self::send_close_ime_msg(&*native_window);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn send_close_ime_msg(native_window: &Object) {
|
|
unsafe {
|
|
let view = get_window_state(native_window).native_window.contentView();
|
|
let _: () = msg_send![view, closeIMEAsync];
|
|
}
|
|
}
|
|
|
|
pub fn close_ime_async(window_id: WindowId) {
|
|
unsafe {
|
|
if let Some(native_window) = Self::find_window_with_id(window_id) {
|
|
Self::send_close_ime_msg(&*native_window);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn open_url(url: &str) {
|
|
unsafe {
|
|
open_url(make_nsstring(url));
|
|
}
|
|
}
|
|
|
|
pub fn open_file_path(path: &Path) {
|
|
unsafe {
|
|
if let Some(path_string) = path.to_str() {
|
|
open_file_path(make_nsstring(path_string));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn open_file_path_in_explorer(path: &Path) {
|
|
unsafe {
|
|
if let Some(path_string) = path.to_str() {
|
|
open_file_path_in_explorer(make_nsstring(path_string));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn open_file_picker(callback: FilePickerCallback, config: FilePickerConfiguration) {
|
|
let file_types = config
|
|
.file_types()
|
|
.iter()
|
|
.map(|file_type| make_nsstring(file_type.to_string()))
|
|
.collect_vec();
|
|
unsafe {
|
|
open_file_picker(
|
|
Box::into_raw(Box::new(callback)) as *mut c_void,
|
|
NSArray::arrayWithObjects(nil, &file_types),
|
|
config.allows_files() as BOOL,
|
|
config.allows_folder() as BOOL,
|
|
config.allows_multi_select() as BOOL,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn open_save_file_picker(
|
|
callback: file_picker::SaveFilePickerCallback,
|
|
config: file_picker::SaveFilePickerConfiguration,
|
|
) {
|
|
let default_directory = config
|
|
.default_directory
|
|
.map(|path| make_nsstring(path.display().to_string()))
|
|
.unwrap_or_else(|| make_nsstring(""));
|
|
let default_filename = config
|
|
.default_filename
|
|
.map(make_nsstring)
|
|
.unwrap_or_else(|| make_nsstring(""));
|
|
unsafe {
|
|
open_save_file_picker(
|
|
Box::into_raw(Box::new(callback)) as *mut c_void,
|
|
default_filename,
|
|
default_directory,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn is_ime_open() -> bool {
|
|
unsafe {
|
|
let Some(native_window) = Self::key_window() else {
|
|
return false;
|
|
};
|
|
let is_ours: bool = is_warp_window(native_window) == YES;
|
|
if is_ours {
|
|
get_window_state(&*native_window).ime_active.get()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_accessibility_contents(content: AccessibilityContent) {
|
|
unsafe {
|
|
let Some(native_window) = Self::key_window() else {
|
|
return;
|
|
};
|
|
if is_warp_window(native_window) == YES {
|
|
let frame = if let Some(frame) = content.frame {
|
|
RectF::new(
|
|
transform_origin_from_rect_coord_to_frame_coord(
|
|
frame.origin(),
|
|
frame.size(),
|
|
),
|
|
frame.size(),
|
|
)
|
|
.to_ns_rect()
|
|
} else {
|
|
RectF::default().to_ns_rect()
|
|
};
|
|
// Wrap in a local autorelease pool: under VoiceOver this is invoked
|
|
// from `AppContext::handle_action` on every user action, so it's a hot
|
|
// path. The ObjC `set_accessibility_contents` callee retains the strings,
|
|
// so draining after the call safely bounds peak memory.
|
|
let pool = NSAutoreleasePool::new(nil);
|
|
set_accessibility_contents(
|
|
native_window,
|
|
make_nsstring(&content.value),
|
|
make_nsstring(content.help.unwrap_or_default()),
|
|
make_nsstring(content.role.to_string()),
|
|
content.frame.is_some() as BOOL,
|
|
frame,
|
|
);
|
|
pool.drain();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets the background blur radius for all active windows to `blur_radius_pixels`.
|
|
pub fn set_all_windows_background_blur_radius(blur_radius_pixels: u8) {
|
|
unsafe {
|
|
let windows: id = msg_send![NSApp(), windows];
|
|
for i in 0..windows.count() {
|
|
let window: id = windows.objectAtIndex(i);
|
|
if is_warp_window(window) == YES {
|
|
set_window_background_blur_radius(window, blur_radius_pixels)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn show_window_and_focus_app(window_id: WindowId, bring_to_front: bool) {
|
|
unsafe {
|
|
if let Some(window) = Self::find_window_with_id(window_id) {
|
|
show_window_and_focus_app(window, bring_to_front as BOOL);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn hide_window(window_id: WindowId) {
|
|
unsafe {
|
|
if let Some(window) = Self::find_window_with_id(window_id) {
|
|
hide_window(window)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a reference to a `WarpWindow` identified by `window_id`, if any.
|
|
///
|
|
/// # Safety
|
|
/// This code is unsafe since it requires interfacing with platform code.
|
|
unsafe fn find_window_with_id(window_id: WindowId) -> Option<id> {
|
|
let windows: id = msg_send![NSApp(), windows];
|
|
(0..windows.count())
|
|
.find(|i| {
|
|
let window: id = windows.objectAtIndex(*i);
|
|
let is_ours: bool = is_warp_window(window) == YES;
|
|
|
|
is_ours && get_window_state(&*window).window_id == window_id
|
|
})
|
|
.map(|idx| windows.objectAtIndex(idx))
|
|
}
|
|
|
|
pub fn close_window_async(window_id: WindowId, termination_mode: TerminationMode) {
|
|
let force_terminate = match termination_mode {
|
|
TerminationMode::Cancellable => NO,
|
|
TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => YES,
|
|
};
|
|
unsafe {
|
|
if let Some(window) = Self::find_window_with_id(window_id) {
|
|
let _: id = msg_send![window, closeWindowAsync: force_terminate];
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_window_bounds(window_id: WindowId, bound: RectF) {
|
|
unsafe {
|
|
if let Some(window) = Self::find_window_with_id(window_id) {
|
|
set_window_bounds(
|
|
window,
|
|
RectF::new(
|
|
// Transform the bound from the UI internal coordinate system
|
|
// to Cocoa's coordinate system.
|
|
transform_origin_from_rect_coord_to_frame_coord(
|
|
bound.origin(),
|
|
bound.size(),
|
|
),
|
|
bound.size(),
|
|
)
|
|
.to_ns_rect(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_window_title(window_id: WindowId, title: &str) {
|
|
unsafe {
|
|
if let Some(window) = Self::find_window_with_id(window_id) {
|
|
set_window_title(window, make_nsstring(title));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl platform::Window for Window {
|
|
fn minimize(&self) {
|
|
let native_window = self.0.native_window;
|
|
unsafe {
|
|
let _: () = msg_send![native_window, miniaturize: nil];
|
|
}
|
|
}
|
|
|
|
fn fullscreen_state(&self) -> FullscreenState {
|
|
let native_window = self.0.native_window;
|
|
let zoomed: BOOL = unsafe { msg_send![native_window, isZoomed] };
|
|
if unsafe {
|
|
NSWindow::styleMask(native_window).contains(NSWindowStyleMask::NSFullScreenWindowMask)
|
|
} {
|
|
FullscreenState::Fullscreen
|
|
} else if zoomed == YES {
|
|
FullscreenState::Maximized
|
|
} else {
|
|
FullscreenState::Normal
|
|
}
|
|
}
|
|
|
|
fn toggle_fullscreen(&self) {
|
|
let native_window = self.0.native_window;
|
|
unsafe {
|
|
let _: () = msg_send![native_window, enqueueFullscreenTransition];
|
|
}
|
|
}
|
|
|
|
fn toggle_maximized(&self) {
|
|
let native_window = self.0.native_window;
|
|
unsafe {
|
|
let _: () = msg_send![native_window, zoomAsync: nil];
|
|
}
|
|
}
|
|
|
|
fn as_ctx(&self) -> &dyn platform::WindowContext {
|
|
self
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn callbacks(&self) -> &WindowCallbacks {
|
|
&self.0.callbacks
|
|
}
|
|
|
|
fn supports_transparency(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn graphics_backend(&self) -> GraphicsBackend {
|
|
GraphicsBackend::Metal
|
|
}
|
|
|
|
fn supported_backends(&self) -> Vec<GraphicsBackend> {
|
|
vec![GraphicsBackend::Metal]
|
|
}
|
|
|
|
/// We never use the MacOS native window frame.
|
|
fn uses_native_window_decorations(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn set_titlebar_height(&self, height: f64) {
|
|
self.0.set_titlebar_height(height);
|
|
}
|
|
}
|
|
|
|
impl platform::WindowContext for Window {
|
|
fn size(&self) -> Vector2F {
|
|
self.0.logical_size()
|
|
}
|
|
|
|
fn origin(&self) -> Vector2F {
|
|
self.0.origin()
|
|
}
|
|
|
|
fn backing_scale_factor(&self) -> f32 {
|
|
self.0.backing_scale_factor() as f32
|
|
}
|
|
|
|
fn max_texture_dimension_2d(&self) -> Option<u32> {
|
|
self.0.max_texture_dimension_2d()
|
|
}
|
|
|
|
fn render_scene(&self, scene: Rc<Scene>) {
|
|
self.0.render_scene(scene)
|
|
}
|
|
|
|
fn request_redraw(&self) {
|
|
self.0.request_redraw();
|
|
}
|
|
|
|
fn request_frame_capture(
|
|
&self,
|
|
callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
|
|
) {
|
|
self.0.request_frame_capture(callback);
|
|
}
|
|
}
|
|
|
|
impl WindowState {
|
|
pub fn id(&self) -> WindowId {
|
|
self.window_id
|
|
}
|
|
|
|
/// Returns the logical (resolution-agnostic) size of the current window.
|
|
pub fn logical_size(&self) -> Vector2F {
|
|
let view_frame = unsafe { NSView::frame(self.native_window.contentView()) };
|
|
vec2f(view_frame.size.width as f32, view_frame.size.height as f32)
|
|
}
|
|
|
|
/// Returns the physical (resolution-aware) size of the current window.
|
|
pub fn physical_size(&self) -> Vector2F {
|
|
self.logical_size() * self.backing_scale_factor() as f32
|
|
}
|
|
|
|
pub fn backing_scale_factor(&self) -> f64 {
|
|
unsafe { NSWindow::backingScaleFactor(self.native_window) }
|
|
}
|
|
|
|
fn next_synthetic_drag_id(&self) -> usize {
|
|
let next_id = self.synthetic_drag_counter.get() + 1;
|
|
self.synthetic_drag_counter.set(next_id);
|
|
next_id
|
|
}
|
|
|
|
/// Attempts to resize the renderer with the new physical size of the window. Noops if there is
|
|
/// no device or the renderer manager is unset.
|
|
fn resize_renderer(&self) {
|
|
if let Some((renderer_manager, device)) = self.renderer_manager.as_ref().zip(self.device())
|
|
{
|
|
let mut renderer_manager = renderer_manager.borrow_mut();
|
|
let renderer = renderer_manager.renderer_for_device(device, self.physical_size());
|
|
|
|
renderer.resize(self);
|
|
}
|
|
}
|
|
|
|
/// Returns an `id` to the current `NSView` of the window.
|
|
pub(super) fn native_view(&self) -> id {
|
|
unsafe { msg_send![self.native_window, contentView] }
|
|
}
|
|
|
|
/// Returns the current [`Device`] for rendering. `None` if the window was configured with no
|
|
/// display.
|
|
pub fn device(&self) -> Option<&Device> {
|
|
self.device.as_ref()
|
|
}
|
|
|
|
fn has_window_buttons(&self) -> bool {
|
|
unsafe {
|
|
// Use the close button as a proxy, since we modify all standard buttons together.
|
|
let button = NSWindow::standardWindowButton_(
|
|
self.native_window,
|
|
NSWindowButton::NSWindowCloseButton,
|
|
);
|
|
let is_hidden: BOOL = msg_send![button, isHidden];
|
|
is_hidden == NO
|
|
}
|
|
}
|
|
|
|
fn set_window_buttons(&self, show_window_buttons: bool) {
|
|
let hide_buttons = !show_window_buttons as BOOL;
|
|
unsafe {
|
|
let close_button = NSWindow::standardWindowButton_(
|
|
self.native_window,
|
|
NSWindowButton::NSWindowCloseButton,
|
|
);
|
|
let _: () = msg_send![close_button, setHidden: hide_buttons];
|
|
let miniaturize_button = NSWindow::standardWindowButton_(
|
|
self.native_window,
|
|
NSWindowButton::NSWindowMiniaturizeButton,
|
|
);
|
|
let _: () = msg_send![miniaturize_button, setHidden: hide_buttons];
|
|
let zoom_button = NSWindow::standardWindowButton_(
|
|
self.native_window,
|
|
NSWindowButton::NSWindowZoomButton,
|
|
);
|
|
let _: () = msg_send![zoom_button, setHidden: hide_buttons];
|
|
}
|
|
}
|
|
|
|
fn set_titlebar_height(&self, height: f64) {
|
|
unsafe {
|
|
set_titlebar_height(self.native_window, height);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl platform::WindowContext for WindowState {
|
|
fn size(&self) -> Vector2F {
|
|
let view_frame = unsafe { NSView::frame(self.native_window.contentView()) };
|
|
vec2f(view_frame.size.width as f32, view_frame.size.height as f32)
|
|
}
|
|
|
|
fn origin(&self) -> Vector2F {
|
|
let view_frame = unsafe { NSWindow::frame(self.native_window) };
|
|
transform_origin_from_frame_coord_to_rect_coord(
|
|
vec2f(view_frame.origin.x as f32, view_frame.origin.y as f32),
|
|
vec2f(view_frame.size.width as f32, view_frame.size.height as f32),
|
|
)
|
|
}
|
|
|
|
fn backing_scale_factor(&self) -> f32 {
|
|
unsafe { NSWindow::backingScaleFactor(self.native_window) as f32 }
|
|
}
|
|
|
|
fn max_texture_dimension_2d(&self) -> Option<u32> {
|
|
// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
|
|
Some(8192)
|
|
}
|
|
|
|
fn render_scene(&self, scene: Rc<Scene>) {
|
|
*self.next_scene.borrow_mut() = Some(scene);
|
|
unsafe {
|
|
let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
|
|
}
|
|
}
|
|
|
|
fn request_redraw(&self) {
|
|
let _ = self.next_scene.borrow_mut().take();
|
|
unsafe {
|
|
let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
|
|
}
|
|
}
|
|
|
|
fn request_frame_capture(
|
|
&self,
|
|
callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
|
|
) {
|
|
*self.capture_callback.borrow_mut() = Some(callback);
|
|
unsafe {
|
|
let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An extension trait defining additional window-management options when running on macOS.
|
|
pub trait WindowExt {
|
|
/// Returns whether or not the native macOS window buttons are visible.
|
|
fn has_window_buttons(&self) -> bool;
|
|
|
|
/// Sets whether or not to show the native macOS window buttons (traffic lights).
|
|
fn set_window_buttons(&self, window_buttons: bool);
|
|
}
|
|
|
|
/// Utility for interacting with the native [`Window`] implementation. The native window is always
|
|
/// available if the app is running, but not in unit tests.
|
|
fn native_window(window: &dyn platform::Window) -> Option<&Window> {
|
|
let native_window = window.as_any().downcast_ref::<Window>();
|
|
if native_window.is_none() {
|
|
let is_headless_window = window.as_any().is::<crate::platform::headless::Window>();
|
|
let is_test_window = cfg!(any(test, feature = "test-util"));
|
|
assert!(
|
|
is_headless_window || is_test_window,
|
|
"Should not fail to downcast the platform window to its concrete type"
|
|
);
|
|
}
|
|
native_window
|
|
}
|
|
|
|
impl WindowExt for &dyn platform::Window {
|
|
fn has_window_buttons(&self) -> bool {
|
|
native_window(*self).is_none_or(|window| window.0.has_window_buttons())
|
|
}
|
|
|
|
fn set_window_buttons(&self, window_buttons: bool) {
|
|
if let Some(window) = native_window(*self) {
|
|
window.0.set_window_buttons(window_buttons)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_view_did_change_backing_properties(this: &Object, async_callback: bool) {
|
|
let window;
|
|
unsafe {
|
|
window = get_window_state(this);
|
|
let layer: id = msg_send![this, layer];
|
|
let _: () = msg_send![layer, setContentsScale: window.backing_scale_factor()];
|
|
let size = window.logical_size();
|
|
|
|
let scale_factor = window.backing_scale_factor();
|
|
|
|
if !size.is_zero() {
|
|
// Manually convert the size into the drawable size by multiplying by the scale factor. For
|
|
// some reason using `convertSizeToBacking` incorrectly upscales the drawable size in some
|
|
// cases even when the backing scale factor is 1.0.
|
|
let drawable_size: NSSize = NSSize::new(
|
|
size.x() as f64 * scale_factor,
|
|
size.y() as f64 * scale_factor,
|
|
);
|
|
let _: () = msg_send![layer, setDrawableSize: drawable_size];
|
|
}
|
|
}
|
|
|
|
window.resize_renderer();
|
|
|
|
if async_callback {
|
|
// Dispatch the callback asynchronously if async_callback is true.
|
|
let weak_window_state = Rc::downgrade(window);
|
|
window
|
|
.executor
|
|
.spawn(async move {
|
|
if let Some(window_state) = weak_window_state.upgrade() {
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window_state.clone()))
|
|
.window_resized(window_state.as_ref());
|
|
}
|
|
})
|
|
.detach();
|
|
} else {
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.window_resized(window.as_ref());
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C-unwind" fn warp_get_accessibility_contents(object: &mut Object) -> id {
|
|
let state = unsafe { get_window_state(object) };
|
|
let window_id = state.window_id;
|
|
let accessibility_data = app::callback_dispatcher()
|
|
.with_mutable_app_context(|app| app.focused_view_accessibility_data(window_id));
|
|
|
|
let accessibility_contents = accessibility_data
|
|
.map(|data| data.content)
|
|
.unwrap_or_default();
|
|
make_nsstring(accessibility_contents.as_str())
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C-unwind" fn warp_ime_position(object: &mut Object, content_rect: NSRect) -> NSRect {
|
|
let state = unsafe { get_window_state(object) };
|
|
|
|
let cursor_info = app::callback_dispatcher()
|
|
.for_window(&Window(state.clone()))
|
|
.get_active_cursor_position();
|
|
|
|
let size = Vector2F::new(
|
|
content_rect.size.width as f32,
|
|
content_rect.size.height as f32,
|
|
);
|
|
|
|
NSRect {
|
|
origin: match cursor_info {
|
|
Some(cursor_info) => NSPoint {
|
|
x: content_rect.origin.x + cursor_info.position.origin_x() as f64,
|
|
y: content_rect.origin.y + (size.y() - cursor_info.position.origin_y()) as f64
|
|
- (1.2 * cursor_info.font_size) as f64,
|
|
},
|
|
None => NSPoint {
|
|
x: content_rect.origin.x,
|
|
y: content_rect.origin.y + size.y() as f64,
|
|
},
|
|
},
|
|
size: NSSize::new(0., 0.),
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_view_set_frame_size(this: &Object, size: NSSize, async_callback: bool) {
|
|
let window;
|
|
unsafe {
|
|
window = get_window_state(this);
|
|
// Manually convert the size into the drawable size by multiplying by the scale factor. For
|
|
// some reason using `convertSizeToBacking` incorrectly upscales the drawable size in some
|
|
// cases even when the backing scale factor is 1.0.
|
|
let scale_factor = window.backing_scale_factor();
|
|
let drawable_size: NSSize = NSSize {
|
|
width: size.width * scale_factor,
|
|
height: size.height * scale_factor,
|
|
};
|
|
let layer: id = msg_send![this, layer];
|
|
let _: () = msg_send![layer, setDrawableSize: drawable_size];
|
|
}
|
|
|
|
window.resize_renderer();
|
|
|
|
if async_callback {
|
|
// Dispatch the callback asynchronously if async_callback is true.
|
|
let weak_window_state = Rc::downgrade(window);
|
|
window
|
|
.executor
|
|
.spawn(async move {
|
|
if let Some(window_state) = weak_window_state.upgrade() {
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window_state.clone()))
|
|
.window_resized(window_state.as_ref());
|
|
}
|
|
})
|
|
.detach();
|
|
} else {
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.window_resized(window.as_ref());
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_update_layer(this: &Object) {
|
|
if !app::callback_dispatcher().can_borrow_mut() {
|
|
#[cfg(debug_assertions)]
|
|
log::warn!(
|
|
"Tried to update window's backing CAMetalDrawable but app was already mutably borrowed!\nStack trace:\n{:#}",
|
|
std::backtrace::Backtrace::force_capture()
|
|
);
|
|
return;
|
|
}
|
|
|
|
unsafe {
|
|
let window = get_window_state(this);
|
|
|
|
let scene = {
|
|
if window.next_scene.borrow().is_none() {
|
|
// Do this without holding a mutable borrow on
|
|
// window.next_scene, to ensure that we don't hit BorrowMut
|
|
// errors if `build_scene()` ends up invoking `request_redraw`.
|
|
let scene = app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.build_scene(window.as_ref());
|
|
*window.next_scene.borrow_mut() = Some(scene);
|
|
}
|
|
|
|
let Some(scene) = window.next_scene.borrow().clone() else {
|
|
return;
|
|
};
|
|
|
|
scene
|
|
};
|
|
debug_assert!(
|
|
window.next_scene.try_borrow_mut().is_ok(),
|
|
"Should not be holding a borrow of the scene RefCell before beginning to render."
|
|
);
|
|
|
|
// SAFETY: warp_update_layer should only be invoked for windows
|
|
// created via Window::open(), which always sets a non-None device.
|
|
let device = window
|
|
.device
|
|
.as_ref()
|
|
.expect("warp_update_layer should not be called for a window that has no real display");
|
|
// SAFETY: warp_update_layer is only invoked by the event loop,
|
|
// which should never attempt to draw a window while it is already
|
|
// being drawn.
|
|
let mut renderer_manager = window
|
|
.renderer_manager
|
|
.as_ref()
|
|
.expect("warp_update_layer should never be called twice in parallel")
|
|
.borrow_mut();
|
|
let renderer = renderer_manager.renderer_for_device(device, window.physical_size());
|
|
|
|
app::callback_dispatcher().with_mutable_app_context(|ctx| {
|
|
renderer.render(&scene, window.as_ref(), ctx.font_cache());
|
|
});
|
|
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.frame_drawn();
|
|
}
|
|
}
|
|
|
|
/// Returns whether this event was handled.
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_view_event(
|
|
this: &Object,
|
|
native_event: id,
|
|
composing_state: bool,
|
|
) -> bool {
|
|
let window = unsafe { get_window_state(this) };
|
|
let event = unsafe {
|
|
super::event::from_native(
|
|
native_event,
|
|
Some(window.logical_size().y()),
|
|
false, /* is_first_mouse */
|
|
)
|
|
};
|
|
if let Some(mut event) = event {
|
|
match event {
|
|
Event::LeftMouseDragged {
|
|
position,
|
|
modifiers,
|
|
..
|
|
} => schedule_synthetic_drag(window, position, modifiers),
|
|
Event::LeftMouseUp { .. } => {
|
|
window.next_synthetic_drag_id();
|
|
}
|
|
Event::KeyDown {
|
|
ref mut is_composing,
|
|
..
|
|
} => {
|
|
*is_composing = composing_state;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
return app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(event)
|
|
.handled;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Handles the "first mouse event" - the first mouse event fired that causes an unfocused window to
|
|
/// gain focus.
|
|
/// Returns whether this event was handled.
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_first_mouse_event(this: &Object, native_event: id) -> bool {
|
|
let window = unsafe { get_window_state(this) };
|
|
let event =
|
|
unsafe { super::event::from_native(native_event, Some(window.logical_size().y()), true) };
|
|
if let Some(event) = event {
|
|
return app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(event)
|
|
.handled;
|
|
}
|
|
false
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_insert_text(this: &Object, characters: id) {
|
|
let string = unsafe { to_string(characters) };
|
|
let window = unsafe { get_window_state(this) };
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(Event::TypedCharacters { chars: string });
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_drag_and_drop(this: &Object, paths: id, point: NSPoint) {
|
|
let paths = unsafe {
|
|
(0..paths.count())
|
|
.map(|i| {
|
|
let directory = paths.objectAtIndex(i);
|
|
to_string(directory)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
|
|
let window = unsafe { get_window_state(this) };
|
|
let location = vec2f(point.x as f32, window.logical_size().y() - point.y as f32);
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(Event::DragAndDropFiles { paths, location });
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_file_drag(this: &Object, point: NSPoint) {
|
|
let window = unsafe { get_window_state(this) };
|
|
let location = vec2f(point.x as f32, window.logical_size().y() - point.y as f32);
|
|
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(Event::DragFiles { location });
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_handle_file_drag_exit(this: &Object) {
|
|
let window = unsafe { get_window_state(this) };
|
|
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window.clone()))
|
|
.dispatch_event(Event::DragFileExit);
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_update_ime_state(this: &mut Object, ime_active: bool) {
|
|
let state = unsafe { get_window_state(this) };
|
|
state.ime_active.set(ime_active);
|
|
}
|
|
|
|
/// Converts an NSRange to a Rust Range<usize>
|
|
/// NSRange has location (start) and length, while Rust Range has start and end
|
|
fn nsrange_to_rust_range(ns_range: NSRange) -> std::ops::Range<usize> {
|
|
let start = ns_range.location as usize;
|
|
let end = start + ns_range.length as usize;
|
|
start..end
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_marked_text_updated(
|
|
this: &mut Object,
|
|
marked_text: id,
|
|
selected_range: NSRange,
|
|
) {
|
|
let state = unsafe { get_window_state(this) };
|
|
let marked_text = unsafe { to_string(marked_text) };
|
|
let selected_range = nsrange_to_rust_range(selected_range);
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(state.clone()))
|
|
.dispatch_event(Event::SetMarkedText {
|
|
marked_text,
|
|
selected_range,
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
extern "C-unwind" fn warp_marked_text_cleared(this: &mut Object) {
|
|
let state = unsafe { get_window_state(&*this) };
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(state.clone()))
|
|
.dispatch_event(Event::ClearMarkedText);
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C-unwind" fn warp_dispatch_standard_action(this: id, tag: NSInteger) {
|
|
if let Some(action) = StandardAction::from_isize(tag as isize) {
|
|
let state = unsafe { get_window_state(&*this) };
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(state.clone()))
|
|
.dispatch_standard_action(action);
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub extern "C-unwind" fn warp_app_window_moved(this: id, rect: NSRect) {
|
|
let state = unsafe { get_window_state(&*this) };
|
|
let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
|
|
let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
|
|
|
|
let weak_window_state = Rc::downgrade(state);
|
|
state
|
|
.executor
|
|
.spawn(async move {
|
|
if let Some(window) = weak_window_state.upgrade() {
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window))
|
|
.window_moved(RectF::new(
|
|
transform_origin_from_frame_coord_to_rect_coord(point, size),
|
|
size,
|
|
));
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
/// Removes the WindowState ivar from an Objective-C object, nulls out the ivar
|
|
/// pointer within the object, and returns the reference-counted pointer to the
|
|
/// state.
|
|
unsafe fn remove_state_ivar_from_object(object: &mut Object) -> Rc<WindowState> {
|
|
let wrapper_ptr: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
|
let state = Ivar::take_state(wrapper_ptr);
|
|
object.set_ivar(WINDOW_STATE_IVAR, ptr::null::<c_void>());
|
|
state
|
|
}
|
|
|
|
// dealloc is called by AppKit when our NSWindow subclass is deallocating,
|
|
// because its retain count has dropped to zero. This is our chance to release
|
|
// our Rust resources. Do not call this manually.
|
|
#[no_mangle]
|
|
pub extern "C-unwind" fn warp_dealloc_window(native_window: &mut Object) {
|
|
log::info!("dealloc native window {native_window:p}");
|
|
let state;
|
|
unsafe {
|
|
// Remove the window state from the content NSView and drop a reference.
|
|
let view_id: id = msg_send![native_window, contentView];
|
|
let native_view = &mut (*view_id);
|
|
let _ = remove_state_ivar_from_object(native_view);
|
|
|
|
// Remove the window state from the NSWindowDelegate and drop a reference.
|
|
let delegate_id: id = msg_send![native_window, delegate];
|
|
let native_window_delegate = &mut (*delegate_id);
|
|
let _ = remove_state_ivar_from_object(native_window_delegate);
|
|
|
|
// Remove the window state from the NSWindow.
|
|
state = remove_state_ivar_from_object(native_window);
|
|
}
|
|
|
|
// Drop the final reference to the `WindowState`, which actually drops the
|
|
// underlying object and frees the memory.
|
|
drop(state);
|
|
}
|
|
|
|
pub unsafe fn get_window_state(object: &Object) -> &Rc<WindowState> {
|
|
let wrapper_ptr: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
|
|
Ivar::get_state(wrapper_ptr)
|
|
}
|
|
|
|
fn schedule_synthetic_drag(
|
|
window_state: &Rc<WindowState>,
|
|
position: Vector2F,
|
|
modifiers: ModifiersState,
|
|
) {
|
|
let drag_id = window_state.next_synthetic_drag_id();
|
|
let weak_window_state = Rc::downgrade(window_state);
|
|
let instant = Instant::now() + Duration::from_millis(16);
|
|
window_state
|
|
.executor
|
|
.spawn(async move {
|
|
Timer::at(instant).await;
|
|
if let Some(window_state) = weak_window_state.upgrade() {
|
|
if window_state.synthetic_drag_counter.get() == drag_id {
|
|
schedule_synthetic_drag(&window_state, position, modifiers);
|
|
app::callback_dispatcher()
|
|
.for_window(&Window(window_state))
|
|
.dispatch_event(Event::LeftMouseDragged {
|
|
position,
|
|
modifiers,
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
// We need this transformation as Cocoa follows the Cartesian coordinate system with rect's
|
|
// origin on the lower left corner and positive value extending along the y coordinate
|
|
// up, whereas RectF follows the flipped coordinate system with rect's origin on the upper left
|
|
// corner and positive value extending along the y coordinate down.
|
|
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Transforms/Transforms.html
|
|
//
|
|
// For example, a rect in Cocoa will look like:
|
|
// (0, 100) (100, 100)
|
|
// -------------------------
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// -------------------------
|
|
// Origin: (0, 0) (100, 0)
|
|
//
|
|
// Whereas the same rect represented in RectF should look like:
|
|
// Origin: (0, -100) (100, -100)
|
|
// -------------------------
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// | |
|
|
// -------------------------
|
|
// (0, 0) (100, 0)
|
|
//
|
|
// Note that the y_axis is flipped in RectF.
|
|
//
|
|
// To transform a Cocoa rect into RectF, we need the following two steps:
|
|
// 1. Get the upper left corner coordinate of the rect:
|
|
// * upper_left = (cocoa_origin.x(), cocoa_origin.y() + size.y())
|
|
// 2. Flip the y coordinate:
|
|
// * new_origin = (upper_left.x(), -upper_left.y())
|
|
//
|
|
// In the reverse transformation, we also need to follow two steps:
|
|
// 1. Get the lower left corner coordinate of the rect:
|
|
// * lower_left = (rectf_origin.x(), rectf_origin.y() + size.y())
|
|
// 2. Flip the y coordinate:
|
|
// * new_origin = (lower_left.x(), -lower_left.y())
|
|
pub fn transform_origin_from_frame_coord_to_rect_coord(
|
|
origin: Vector2F,
|
|
size: Vector2F,
|
|
) -> Vector2F {
|
|
Vector2F::new(origin.x(), -(origin.y() + size.y()))
|
|
}
|
|
|
|
fn transform_origin_from_rect_coord_to_frame_coord(origin: Vector2F, size: Vector2F) -> Vector2F {
|
|
Vector2F::new(origin.x(), -(origin.y() + size.y()))
|
|
}
|
|
|
|
/// Converts an Objective-C `Object` into a `String`
|
|
unsafe fn to_string(value: *mut Object) -> String {
|
|
let slice = slice::from_raw_parts(value.UTF8String() as *const c_uchar, value.len());
|
|
str::from_utf8_unchecked(slice).to_string()
|
|
}
|