Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/window.rs
1use super::delegate::DispatchDelegate;
2use super::{
3 app, make_nsstring,
4 rendering::{is_integrated_gpu, Device, RendererManager},
5 RectFExt as _,
6};
7use anyhow::{anyhow, Result};
8use cocoa::appkit::{NSWindowButton, NSWindowStyleMask};
9use cocoa::foundation::{NSArray, NSPoint, NSRange, NSString, NSUInteger};
10use cocoa::{
11 appkit::{NSApp, NSView, NSWindow},
12 base::{id, nil},
13 foundation::{NSAutoreleasePool, NSInteger, NSRect, NSSize},
14};
15use foreign_types::ForeignType;
16use itertools::Itertools;
17use num_traits::FromPrimitive;
18use objc::class;
19use objc::{
20 msg_send,
21 runtime::{Object, BOOL, NO, YES},
22 sel, sel_impl,
23};
24use pathfinder_geometry::{
25 rect::RectF,
26 vector::{vec2f, Vector2F},
27};
28use strato_ui_core::event::ModifiersState;
29use strato_ui_core::platform::{
30 file_picker, FilePickerCallback, FilePickerConfiguration, FullscreenState, GraphicsBackend,
31 TerminationMode, WindowFocusBehavior,
32};
33use strato_ui_core::r#async::Timer;
34use strato_ui_core::rendering::GPUPowerPreference;
35use strato_ui_core::windowing::WindowCallbacks;
36use strato_ui_core::{
37 accessibility::AccessibilityContent,
38 actions::StandardAction,
39 platform::{self, WindowBounds, WindowOptions, WindowStyle},
40 r#async::executor,
41 Event, OptionalPlatformWindow, Scene, WindowId,
42};
43use strato_ui_core::{DisplayId, DisplayIdx};
44 
45use instant::Instant;
46use std::collections::HashMap;
47use std::sync::Arc;
48use std::time::Duration;
49use std::{cell::Cell, os::raw::c_uchar, panic, path::Path, ptr};
50use std::{cell::RefCell, ffi::c_void, rc::Rc};
51use std::{slice, str};
52 
53extern "C" {
54 fn screenFrame() -> NSRect;
55 fn activeScreenId() -> NSUInteger;
56}
57 
58pub const WINDOW_STATE_IVAR: &str = "windowState";
59const INITIAL_WINDOW_WIDTH: f32 = 1280.;
60const INITIAL_WINDOW_HEIGHT: f32 = 800.;
61const DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS: u8 = 1;
62 
63// A mac::window::Window holds a reference to a WindowState.
64// The NSWindow also holds a reference, so that either may be deallocated first.
65pub struct Window(Rc<WindowState>);
66 
67pub(crate) struct WindowManager {
68 windows: HashMap<WindowId, Rc<Window>>,
69 renderer_manager: Rc<RefCell<RendererManager>>,
70}
71 
72impl WindowManager {
73 pub(crate) fn new() -> Self {
74 Self {
75 windows: Default::default(),
76 renderer_manager: Rc::new(RefCell::new(RendererManager::new())),
77 }
78 }
79}
80 
81impl platform::WindowManager for WindowManager {
82 fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow {
83 self.windows
84 .get(&window_id)
85 .map(Rc::clone)
86 .map(|inner| inner as Rc<dyn crate::platform::Window>)
87 }
88 
89 fn open_window(
90 &mut self,
91 window_id: WindowId,
92 options: WindowOptions,
93 callbacks: WindowCallbacks,
94 ) -> Result<()> {
95 let executor = Rc::new(executor::Foreground::platform(Arc::new(DispatchDelegate))?);
96 let window = Rc::new(Window::open(
97 options,
98 window_id,
99 callbacks,
100 executor,
101 Rc::clone(&self.renderer_manager),
102 )?);
103 self.windows.insert(window_id, Rc::clone(&window));
104 Ok(())
105 }
106 
107 fn remove_window(&mut self, window_id: WindowId) {
108 self.windows.remove(&window_id);
109 }
110 
111 fn active_window_id(&self) -> Option<WindowId> {
112 Window::active_window_id()
113 }
114 
115 fn key_window_is_modal_panel(&self) -> bool {
116 Window::key_window_is_modal_panel()
117 }
118 
119 fn app_is_active(&self) -> bool {
120 let res: BOOL = unsafe { msg_send![app::get_warp_app(), isActive] };
121 res == YES
122 }
123 
124 fn hide_app(&self) {
125 unsafe {
126 hide_app();
127 }
128 }
129 
130 fn activate_app(&self, _last_active_window: Option<WindowId>) -> Option<WindowId> {
131 unsafe {
132 activate_app();
133 }
134 Window::frontmost_window_id()
135 }
136 
137 fn show_window_and_focus_app(&self, window_id: WindowId, behavior: WindowFocusBehavior) {
138 if matches!(behavior, WindowFocusBehavior::BringToFront) {
139 Window::show_window_and_focus_app(window_id, true)
140 } else {
141 Window::show_window_and_focus_app(window_id, false)
142 }
143 }
144 
145 fn hide_window(&self, window_id: WindowId) {
146 Window::hide_window(window_id)
147 }
148 
149 fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
150 Window::set_window_bounds(window_id, bound)
151 }
152 
153 fn set_all_windows_background_blur_radius(&self, blur_radius_pixels: u8) {
154 Window::set_all_windows_background_blur_radius(blur_radius_pixels)
155 }
156 
157 fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
158 // no-op on MacOS. This is only available on Windows.
159 }
160 
161 fn set_window_title(&self, window_id: WindowId, title: &str) {
162 Window::set_window_title(window_id, title)
163 }
164 
165 fn close_window_async(&self, window_id: WindowId, termination_mode: TerminationMode) {
166 Window::close_window_async(window_id, termination_mode);
167 }
168 
169 fn display_count(&self) -> usize {
170 unsafe {
171 let screens: id = msg_send![class!(NSScreen), screens];
172 screens.count() as usize
173 }
174 }
175 
176 fn active_display_bounds(&self) -> RectF {
177 let rect = unsafe { screenFrame() };
178 let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
179 let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
180 RectF::new(
181 transform_origin_from_frame_coord_to_rect_coord(point, size),
182 size,
183 )
184 }
185 
186 fn active_display_id(&self) -> DisplayId {
187 let id = unsafe { activeScreenId() };
188 (id as usize).into()
189 }
190 
191 fn bounds_for_display_idx(&self, display_idx: DisplayIdx) -> Option<RectF> {
192 let rect: NSRect = unsafe {
193 let screens: id = msg_send![class!(NSScreen), screens];
194 
195 let idx: u64 = match display_idx {
196 DisplayIdx::Primary => 0,
197 DisplayIdx::External(idx) => (idx + 1) as u64,
198 };
199 
200 if idx >= screens.count() {
201 return None;
202 }
203 
204 let screen: id = screens.objectAtIndex(idx);
205 msg_send![screen, frame]
206 };
207 
208 let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
209 let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
210 
211 Some(RectF::new(
212 transform_origin_from_frame_coord_to_rect_coord(point, size),
213 size,
214 ))
215 }
216 
217 fn active_cursor_position_updated(&self) {
218 // no-op on macOS
219 }
220 
221 fn windowing_system(&self) -> Option<crate::windowing::System> {
222 Some(crate::windowing::System::AppKit)
223 }
224 
225 fn os_window_manager_name(&self) -> Option<String> {
226 None
227 }
228 
229 fn is_tiling_window_manager(&self) -> bool {
230 false
231 }
232 
233 fn ordered_window_ids(&self) -> Vec<WindowId> {
234 unsafe {
235 let ordered_windows: id = msg_send![NSApp(), orderedWindows];
236 let count = ordered_windows.count();
237 let mut result = Vec::with_capacity(count as usize);
238 for i in 0..count {
239 let window: id = ordered_windows.objectAtIndex(i);
240 if is_warp_window(window) == YES {
241 result.push(get_window_state(&*window).window_id);
242 }
243 }
244 result
245 }
246 }
247 
248 /// Cancels any in-flight synthetic mouse-drag event loop for the given window.
249 ///
250 /// During tab drag, we run a synthetic drag loop (pumping mouse-moved events)
251 /// to track the cursor across windows. When a tab is handed off to another
252 /// window or the drag is finalized, we need to stop the old loop so stale
253 /// events don't continue driving the source window's drag state.
254 /// Incrementing the drag ID causes the running loop to see a mismatched ID
255 /// and exit on its next iteration.
256 fn cancel_synthetic_drag(&self, window_id: WindowId) {
257 if let Some(window) = self.windows.get(&window_id) {
258 window.0.next_synthetic_drag_id();
259 }
260 }
261}
262 
263pub(crate) struct IntegrationTestWindowManager {
264 window_manager: WindowManager,
265}
266 
267impl IntegrationTestWindowManager {
268 pub(crate) fn new() -> Self {
269 Self {
270 window_manager: WindowManager::new(),
271 }
272 }
273}
274 
275impl platform::WindowManager for IntegrationTestWindowManager {
276 fn open_window(
277 &mut self,
278 window_id: WindowId,
279 window_options: WindowOptions,
280 callbacks: WindowCallbacks,
281 ) -> Result<()> {
282 self.window_manager
283 .open_window(window_id, window_options, callbacks)
284 }
285 
286 fn platform_window(&self, window_id: WindowId) -> OptionalPlatformWindow {
287 self.window_manager.platform_window(window_id)
288 }
289 
290 fn remove_window(&mut self, window_id: WindowId) {
291 self.window_manager.remove_window(window_id)
292 }
293 
294 fn active_window_id(&self) -> Option<WindowId> {
295 // Pretend that the frontmost window is the active one. This aligns with forcing
296 // `app_is_active()` to always return `true`.
297 Window::frontmost_window_id()
298 }
299 
300 fn key_window_is_modal_panel(&self) -> bool {
301 false
302 }
303 
304 fn app_is_active(&self) -> bool {
305 // always assume active for tests.
306 true
307 }
308 
309 fn activate_app(&self, last_active_window: Option<WindowId>) -> Option<WindowId> {
310 self.window_manager.activate_app(last_active_window)
311 }
312 
313 fn show_window_and_focus_app(&self, window_id: WindowId, behavior: WindowFocusBehavior) {
314 self.window_manager
315 .show_window_and_focus_app(window_id, behavior)
316 }
317 
318 fn hide_app(&self) {
319 self.window_manager.hide_app()
320 }
321 
322 fn hide_window(&self, window_id: WindowId) {
323 self.window_manager.hide_window(window_id)
324 }
325 
326 fn set_window_bounds(&self, window_id: WindowId, bound: RectF) {
327 self.window_manager.set_window_bounds(window_id, bound)
328 }
329 
330 fn set_all_windows_background_blur_radius(&self, _blur_radius_pixels: u8) {
331 // no-op for tests
332 }
333 
334 fn set_all_windows_background_blur_texture(&self, _use_blur_texture: bool) {
335 // no-op for tests
336 }
337 
338 fn set_window_title(&self, window_id: WindowId, title: &str) {
339 self.window_manager.set_window_title(window_id, title)
340 }
341 
342 fn close_window_async(&self, window_id: WindowId, termination_mode: TerminationMode) {
343 self.window_manager
344 .close_window_async(window_id, termination_mode)
345 }
346 
347 fn active_display_bounds(&self) -> RectF {
348 self.window_manager.active_display_bounds()
349 }
350 
351 fn active_display_id(&self) -> DisplayId {
352 self.window_manager.active_display_id()
353 }
354 
355 fn display_count(&self) -> usize {
356 1
357 }
358 
359 fn bounds_for_display_idx(&self, idx: DisplayIdx) -> Option<RectF> {
360 self.window_manager.bounds_for_display_idx(idx)
361 }
362 
363 fn active_cursor_position_updated(&self) {
364 // no-op on macOS
365 }
366 
367 fn windowing_system(&self) -> Option<crate::windowing::System> {
368 None
369 }
370 
371 fn os_window_manager_name(&self) -> Option<String> {
372 None
373 }
374 
375 fn is_tiling_window_manager(&self) -> bool {
376 false
377 }
378 
379 fn ordered_window_ids(&self) -> Vec<WindowId> {
380 self.window_manager.ordered_window_ids()
381 }
382 
383 fn cancel_synthetic_drag(&self, window_id: WindowId) {
384 self.window_manager.cancel_synthetic_drag(window_id)
385 }
386}
387 
388// We put a pointer type into NSWindow and sometimes NSView ivars.
389// The pointer type is a Box<Rc<WindowState>>, into raw.
390// Note that the ivar holds a strong reference to the window state.
391#[allow(non_snake_case)]
392mod Ivar {
393 use super::*;
394 type WindowStatePtr = *mut Rc<WindowState>;
395 
396 // Convert an Rc<WindowState> into our ivar pointer type.
397 // This increments the reference count.
398 pub fn from_state(ws: &Rc<WindowState>) -> *const c_void {
399 // Note this increments the reference count.
400 let reference = ws.clone();
401 Box::into_raw(Box::new(reference)) as *const c_void
402 }
403 
404 // Get a reference to the window state from an ivar pointer.
405 // Note the lifetime here is suspicious: the caller must ensure that the object
406 // is not deallocated while the reference is alive.
407 pub fn get_state<'a>(ptr: *mut c_void) -> &'a Rc<WindowState> {
408 unsafe { (ptr as WindowStatePtr).as_ref().expect("Pointer was null") }
409 }
410 
411 // Extract the window state from an ivar pointer.
412 // After calling this, the pointer is now dangling, and the ivar should be cleared.
413 pub fn take_state(ptr: *mut c_void) -> Rc<WindowState> {
414 // Note dereferencing a box consumes it, because Rust is confused.
415 unsafe { *Box::from_raw(ptr as WindowStatePtr) }
416 }
417}
418 
419// Declarations of functions implemented in ObjC files.
420// These signatures must be manually synced - there's no type checking here.
421extern "C" {
422 fn create_warp_nswindow(
423 contentRect: NSRect,
424 metalDevice: id,
425 hideTitleBar: BOOL,
426 backgroundBlurRadiusPixels: u8,
427 testMode: BOOL,
428 ) -> id;
429 fn create_warp_nspanel(
430 contentRect: NSRect,
431 metalDevice: id,
432 hideTitleBar: BOOL,
433 backgroundBlurRadiusPixels: u8,
434 testMode: BOOL,
435 ) -> id;
436 fn is_warp_window(window: id) -> BOOL;
437 fn get_frontmost_window() -> id;
438 fn set_accessibility_contents(
439 window: id,
440 value: id, /* NSString */
441 help: id, /* NSString */
442 warpRole: id, /* NSString */
443 setFrame: BOOL,
444 frame: NSRect,
445 );
446 
447 fn hide_app();
448 fn activate_app();
449 fn show_window_and_focus_app(window: id, bringToFront: BOOL);
450 fn hide_window(window: id);
451 fn position_and_order_front(window: id);
452 fn position_at_given_location(window: id, origin: NSPoint);
453 fn order_front_without_focus(window: id, origin: NSPoint);
454 fn set_window_title(window: id, title: id);
455 fn set_window_bounds(window: id, bound: NSRect);
456 fn set_window_background_blur_radius(window: id, blurRadiusPixels: u8);
457 fn open_file_path(pathString: id);
458 fn open_file_path_in_explorer(pathString: id);
459 fn open_file_picker(
460 callback: *mut c_void,
461 file_types: id,
462 allow_files: BOOL,
463 allow_folders: BOOL,
464 allow_multi_selection: BOOL,
465 );
466 fn open_save_file_picker(callback: *mut c_void, default_filename: id, default_directory: id);
467 fn open_url(urlString: id);
468 fn set_titlebar_height(window: id, height: f64);
469}
470 
471pub type FrameCaptureCallback = Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>;
472 
473pub struct WindowState {
474 native_window: id,
475 window_id: WindowId,
476 callbacks: WindowCallbacks,
477 next_scene: RefCell<Option<Rc<Scene>>>,
478 device: Option<Device>,
479 renderer_manager: Option<Rc<RefCell<RendererManager>>>,
480 synthetic_drag_counter: Cell<usize>,
481 executor: Rc<executor::Foreground>,
482 ime_active: Cell<bool>,
483 pub(super) capture_callback: RefCell<Option<FrameCaptureCallback>>,
484}
485 
486impl Window {
487 pub fn open(
488 options: WindowOptions,
489 window_id: WindowId,
490 callbacks: WindowCallbacks,
491 executor: Rc<executor::Foreground>,
492 renderer_manager: Rc<RefCell<RendererManager>>,
493 ) -> Result<Self> {
494 log::info!("Opening window with id {window_id}");
495 unsafe {
496 let pool = NSAutoreleasePool::new(nil);
497 let frame = match options.bounds {
498 WindowBounds::ExactPosition(bounds) => RectF::new(
499 transform_origin_from_rect_coord_to_frame_coord(bounds.origin(), bounds.size()),
500 bounds.size(),
501 )
502 .to_ns_rect(),
503 WindowBounds::ExactSize(size) => RectF::new(Vector2F::zero(), size).to_ns_rect(),
504 WindowBounds::Default => RectF::new(
505 Vector2F::zero(),
506 Vector2F::new(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT),
507 )
508 .to_ns_rect(),
509 };
510 
511 let test_mode = cfg!(feature = "integration_tests")
512 && std::env::var("WARPUI_USE_REAL_DISPLAY_IN_INTEGRATION_TESTS").is_err();
513 
514 let (metal_device, metal_device_ptr) = if test_mode {
515 (None, nil)
516 } else {
517 // TODO: device appears to be leaked here.
518 let metal_device = match options.gpu_power_preference {
519 GPUPowerPreference::LowPower => metal::Device::all()
520 .into_iter()
521 .find(is_integrated_gpu)
522 .or_else(metal::Device::system_default),
523 _ => metal::Device::system_default(),
524 }
525 .ok_or_else(|| anyhow!("could not obtain metal device"))?;
526 log::info!(
527 "Using {} GPU for rendering new window.",
528 if is_integrated_gpu(&metal_device) {
529 "integrated"
530 } else {
531 "discrete"
532 }
533 );
534 
535 let ptr = metal_device.as_ptr() as id;
536 (Some(metal_device), ptr)
537 };
538 
539 let native_window: id = match options.style {
540 WindowStyle::Pin => {
541 let panel: id = create_warp_nspanel(
542 frame,
543 metal_device_ptr,
544 options.hide_title_bar as BOOL,
545 options
546 .background_blur_radius_pixels
547 .unwrap_or(DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS),
548 test_mode as BOOL,
549 );
550 
551 let _: () = msg_send![panel, positionPinnedPanel];
552 panel
553 }
554 _ => create_warp_nswindow(
555 frame,
556 metal_device_ptr,
557 options.hide_title_bar as BOOL,
558 options
559 .background_blur_radius_pixels
560 .unwrap_or(DEFAULT_WINDOW_BACKGROUND_BLUR_RADIUS),
561 test_mode as BOOL,
562 ),
563 };
564 if native_window == nil {
565 return Err(anyhow!("WarpWindow returned nil from initializer"));
566 }
567 
568 if options.fullscreen_state == FullscreenState::Fullscreen {
569 // Instead of directly calling toggleFullScreen, we call a wrapper method that
570 // ensures MacOS window animations don't overlap.
571 let _: () = msg_send![native_window, enqueueFullscreenTransition];
572 }
573 
574 let native_view: id = msg_send![native_window, contentView];
575 
576 let device = metal_device.map(move |metal_device| {
577 Device::new(
578 metal_device,
579 native_view as id,
580 native_window as id,
581 options.gpu_power_preference,
582 options.on_gpu_device_info_reported,
583 )
584 });
585 
586 let window_state = Rc::new(WindowState {
587 native_window,
588 window_id,
589 callbacks,
590 next_scene: Default::default(),
591 renderer_manager: Some(renderer_manager),
592 device,
593 synthetic_drag_counter: Cell::new(0),
594 executor,
595 ime_active: Cell::new(false),
596 capture_callback: RefCell::new(None),
597 });
598 
599 (*native_window).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
600 (*native_view).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
601 
602 let native_window_delegate: id = msg_send![native_window, delegate];
603 (*native_window_delegate).set_ivar(WINDOW_STATE_IVAR, Ivar::from_state(&window_state));
604 
605 // Set the initial scale properly.
606 warp_view_did_change_backing_properties(&*native_view, true);
607 
608 match options.style {
609 WindowStyle::Normal | WindowStyle::Pin => {
610 match options.bounds {
611 WindowBounds::ExactPosition(_) => {
612 // If specfied, we should set the window to the exact position.
613 // Note that the final position could be different from the set one as the original
614 // frame may no longer exist (e.g. user unplugs the monitor).
615 position_at_given_location(native_window, frame.origin)
616 }
617 WindowBounds::ExactSize(_) | WindowBounds::Default => {
618 // Otherwise we put it in the center of the window or cascade from the previous window.
619 position_and_order_front(native_window)
620 }
621 }
622 }
623 WindowStyle::Cascade => position_and_order_front(native_window),
624 WindowStyle::NotStealFocus => (),
625 WindowStyle::PositionedNoFocus => {
626 order_front_without_focus(native_window, frame.origin)
627 }
628 }
629 
630 pool.drain();
631 
632 Ok(Self(window_state))
633 }
634 }
635 
636 /// Returns the key window, if any.
637 pub fn key_window() -> Option<id> {
638 unsafe {
639 let native_window: id = msg_send![NSApp(), keyWindow];
640 if native_window == nil {
641 None
642 } else {
643 Some(native_window)
644 }
645 }
646 }
647 
648 pub fn active_window_id() -> Option<WindowId> {
649 unsafe {
650 let native_window = Self::key_window()?;
651 let is_ours: bool = is_warp_window(native_window) == YES;
652 if is_ours {
653 Some(get_window_state(&*native_window).window_id)
654 } else {
655 None
656 }
657 }
658 }
659 
660 pub fn frontmost_window_id() -> Option<WindowId> {
661 unsafe {
662 let native_window: id = get_frontmost_window();
663 let is_ours: bool = is_warp_window(native_window) == YES;
664 if is_ours {
665 Some(get_window_state(&*native_window).window_id)
666 } else {
667 None
668 }
669 }
670 }
671 
672 pub fn key_window_is_modal_panel() -> bool {
673 unsafe {
674 let Some(native_window) = Self::key_window() else {
675 return false;
676 };
677 let is_modal_panel: BOOL = msg_send![native_window, isModalPanel];
678 is_modal_panel == YES
679 }
680 }
681 
682 pub fn close_ime_on_active_window() {
683 unsafe {
684 let Some(native_window) = Self::key_window() else {
685 return;
686 };
687 let is_ours: bool = is_warp_window(native_window) == YES;
688 if is_ours {
689 Self::send_close_ime_msg(&*native_window);
690 }
691 }
692 }
693 
694 fn send_close_ime_msg(native_window: &Object) {
695 unsafe {
696 let view = get_window_state(native_window).native_window.contentView();
697 let _: () = msg_send![view, closeIMEAsync];
698 }
699 }
700 
701 pub fn close_ime_async(window_id: WindowId) {
702 unsafe {
703 if let Some(native_window) = Self::find_window_with_id(window_id) {
704 Self::send_close_ime_msg(&*native_window);
705 }
706 }
707 }
708 
709 pub fn open_url(url: &str) {
710 unsafe {
711 open_url(make_nsstring(url));
712 }
713 }
714 
715 pub fn open_file_path(path: &Path) {
716 unsafe {
717 if let Some(path_string) = path.to_str() {
718 open_file_path(make_nsstring(path_string));
719 }
720 }
721 }
722 
723 pub fn open_file_path_in_explorer(path: &Path) {
724 unsafe {
725 if let Some(path_string) = path.to_str() {
726 open_file_path_in_explorer(make_nsstring(path_string));
727 }
728 }
729 }
730 
731 pub fn open_file_picker(callback: FilePickerCallback, config: FilePickerConfiguration) {
732 let file_types = config
733 .file_types()
734 .iter()
735 .map(|file_type| make_nsstring(file_type.to_string()))
736 .collect_vec();
737 unsafe {
738 open_file_picker(
739 Box::into_raw(Box::new(callback)) as *mut c_void,
740 NSArray::arrayWithObjects(nil, &file_types),
741 config.allows_files() as BOOL,
742 config.allows_folder() as BOOL,
743 config.allows_multi_select() as BOOL,
744 );
745 }
746 }
747 
748 pub fn open_save_file_picker(
749 callback: file_picker::SaveFilePickerCallback,
750 config: file_picker::SaveFilePickerConfiguration,
751 ) {
752 let default_directory = config
753 .default_directory
754 .map(|path| make_nsstring(path.display().to_string()))
755 .unwrap_or_else(|| make_nsstring(""));
756 let default_filename = config
757 .default_filename
758 .map(make_nsstring)
759 .unwrap_or_else(|| make_nsstring(""));
760 unsafe {
761 open_save_file_picker(
762 Box::into_raw(Box::new(callback)) as *mut c_void,
763 default_filename,
764 default_directory,
765 );
766 }
767 }
768 
769 pub fn is_ime_open() -> bool {
770 unsafe {
771 let Some(native_window) = Self::key_window() else {
772 return false;
773 };
774 let is_ours: bool = is_warp_window(native_window) == YES;
775 if is_ours {
776 get_window_state(&*native_window).ime_active.get()
777 } else {
778 false
779 }
780 }
781 }
782 
783 pub fn set_accessibility_contents(content: AccessibilityContent) {
784 unsafe {
785 let Some(native_window) = Self::key_window() else {
786 return;
787 };
788 if is_warp_window(native_window) == YES {
789 let frame = if let Some(frame) = content.frame {
790 RectF::new(
791 transform_origin_from_rect_coord_to_frame_coord(
792 frame.origin(),
793 frame.size(),
794 ),
795 frame.size(),
796 )
797 .to_ns_rect()
798 } else {
799 RectF::default().to_ns_rect()
800 };
801 // Wrap in a local autorelease pool: under VoiceOver this is invoked
802 // from `AppContext::handle_action` on every user action, so it's a hot
803 // path. The ObjC `set_accessibility_contents` callee retains the strings,
804 // so draining after the call safely bounds peak memory.
805 let pool = NSAutoreleasePool::new(nil);
806 set_accessibility_contents(
807 native_window,
808 make_nsstring(&content.value),
809 make_nsstring(content.help.unwrap_or_default()),
810 make_nsstring(content.role.to_string()),
811 content.frame.is_some() as BOOL,
812 frame,
813 );
814 pool.drain();
815 }
816 }
817 }
818 
819 /// Sets the background blur radius for all active windows to `blur_radius_pixels`.
820 pub fn set_all_windows_background_blur_radius(blur_radius_pixels: u8) {
821 unsafe {
822 let windows: id = msg_send![NSApp(), windows];
823 for i in 0..windows.count() {
824 let window: id = windows.objectAtIndex(i);
825 if is_warp_window(window) == YES {
826 set_window_background_blur_radius(window, blur_radius_pixels)
827 }
828 }
829 }
830 }
831 
832 pub fn show_window_and_focus_app(window_id: WindowId, bring_to_front: bool) {
833 unsafe {
834 if let Some(window) = Self::find_window_with_id(window_id) {
835 show_window_and_focus_app(window, bring_to_front as BOOL);
836 }
837 }
838 }
839 
840 pub fn hide_window(window_id: WindowId) {
841 unsafe {
842 if let Some(window) = Self::find_window_with_id(window_id) {
843 hide_window(window)
844 }
845 }
846 }
847 
848 /// Returns a reference to a `WarpWindow` identified by `window_id`, if any.
849 ///
850 /// # Safety
851 /// This code is unsafe since it requires interfacing with platform code.
852 unsafe fn find_window_with_id(window_id: WindowId) -> Option<id> {
853 let windows: id = msg_send![NSApp(), windows];
854 (0..windows.count())
855 .find(|i| {
856 let window: id = windows.objectAtIndex(*i);
857 let is_ours: bool = is_warp_window(window) == YES;
858 
859 is_ours && get_window_state(&*window).window_id == window_id
860 })
861 .map(|idx| windows.objectAtIndex(idx))
862 }
863 
864 pub fn close_window_async(window_id: WindowId, termination_mode: TerminationMode) {
865 let force_terminate = match termination_mode {
866 TerminationMode::Cancellable => NO,
867 TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => YES,
868 };
869 unsafe {
870 if let Some(window) = Self::find_window_with_id(window_id) {
871 let _: id = msg_send![window, closeWindowAsync: force_terminate];
872 }
873 }
874 }
875 
876 pub fn set_window_bounds(window_id: WindowId, bound: RectF) {
877 unsafe {
878 if let Some(window) = Self::find_window_with_id(window_id) {
879 set_window_bounds(
880 window,
881 RectF::new(
882 // Transform the bound from the UI internal coordinate system
883 // to Cocoa's coordinate system.
884 transform_origin_from_rect_coord_to_frame_coord(
885 bound.origin(),
886 bound.size(),
887 ),
888 bound.size(),
889 )
890 .to_ns_rect(),
891 );
892 }
893 }
894 }
895 
896 pub fn set_window_title(window_id: WindowId, title: &str) {
897 unsafe {
898 if let Some(window) = Self::find_window_with_id(window_id) {
899 set_window_title(window, make_nsstring(title));
900 }
901 }
902 }
903}
904 
905impl platform::Window for Window {
906 fn minimize(&self) {
907 let native_window = self.0.native_window;
908 unsafe {
909 let _: () = msg_send![native_window, miniaturize: nil];
910 }
911 }
912 
913 fn fullscreen_state(&self) -> FullscreenState {
914 let native_window = self.0.native_window;
915 let zoomed: BOOL = unsafe { msg_send![native_window, isZoomed] };
916 if unsafe {
917 NSWindow::styleMask(native_window).contains(NSWindowStyleMask::NSFullScreenWindowMask)
918 } {
919 FullscreenState::Fullscreen
920 } else if zoomed == YES {
921 FullscreenState::Maximized
922 } else {
923 FullscreenState::Normal
924 }
925 }
926 
927 fn toggle_fullscreen(&self) {
928 let native_window = self.0.native_window;
929 unsafe {
930 let _: () = msg_send![native_window, enqueueFullscreenTransition];
931 }
932 }
933 
934 fn toggle_maximized(&self) {
935 let native_window = self.0.native_window;
936 unsafe {
937 let _: () = msg_send![native_window, zoomAsync: nil];
938 }
939 }
940 
941 fn as_ctx(&self) -> &dyn platform::WindowContext {
942 self
943 }
944 
945 fn as_any(&self) -> &dyn std::any::Any {
946 self
947 }
948 
949 fn callbacks(&self) -> &WindowCallbacks {
950 &self.0.callbacks
951 }
952 
953 fn supports_transparency(&self) -> bool {
954 true
955 }
956 
957 fn graphics_backend(&self) -> GraphicsBackend {
958 GraphicsBackend::Metal
959 }
960 
961 fn supported_backends(&self) -> Vec<GraphicsBackend> {
962 vec![GraphicsBackend::Metal]
963 }
964 
965 /// We never use the MacOS native window frame.
966 fn uses_native_window_decorations(&self) -> bool {
967 false
968 }
969 
970 fn set_titlebar_height(&self, height: f64) {
971 self.0.set_titlebar_height(height);
972 }
973}
974 
975impl platform::WindowContext for Window {
976 fn size(&self) -> Vector2F {
977 self.0.logical_size()
978 }
979 
980 fn origin(&self) -> Vector2F {
981 self.0.origin()
982 }
983 
984 fn backing_scale_factor(&self) -> f32 {
985 self.0.backing_scale_factor() as f32
986 }
987 
988 fn max_texture_dimension_2d(&self) -> Option<u32> {
989 self.0.max_texture_dimension_2d()
990 }
991 
992 fn render_scene(&self, scene: Rc<Scene>) {
993 self.0.render_scene(scene)
994 }
995 
996 fn request_redraw(&self) {
997 self.0.request_redraw();
998 }
999 
1000 fn request_frame_capture(
1001 &self,
1002 callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
1003 ) {
1004 self.0.request_frame_capture(callback);
1005 }
1006}
1007 
1008impl WindowState {
1009 pub fn id(&self) -> WindowId {
1010 self.window_id
1011 }
1012 
1013 /// Returns the logical (resolution-agnostic) size of the current window.
1014 pub fn logical_size(&self) -> Vector2F {
1015 let view_frame = unsafe { NSView::frame(self.native_window.contentView()) };
1016 vec2f(view_frame.size.width as f32, view_frame.size.height as f32)
1017 }
1018 
1019 /// Returns the physical (resolution-aware) size of the current window.
1020 pub fn physical_size(&self) -> Vector2F {
1021 self.logical_size() * self.backing_scale_factor() as f32
1022 }
1023 
1024 pub fn backing_scale_factor(&self) -> f64 {
1025 unsafe { NSWindow::backingScaleFactor(self.native_window) }
1026 }
1027 
1028 fn next_synthetic_drag_id(&self) -> usize {
1029 let next_id = self.synthetic_drag_counter.get() + 1;
1030 self.synthetic_drag_counter.set(next_id);
1031 next_id
1032 }
1033 
1034 /// Attempts to resize the renderer with the new physical size of the window. Noops if there is
1035 /// no device or the renderer manager is unset.
1036 fn resize_renderer(&self) {
1037 if let Some((renderer_manager, device)) = self.renderer_manager.as_ref().zip(self.device())
1038 {
1039 let mut renderer_manager = renderer_manager.borrow_mut();
1040 let renderer = renderer_manager.renderer_for_device(device, self.physical_size());
1041 
1042 renderer.resize(self);
1043 }
1044 }
1045 
1046 /// Returns an `id` to the current `NSView` of the window.
1047 pub(super) fn native_view(&self) -> id {
1048 unsafe { msg_send![self.native_window, contentView] }
1049 }
1050 
1051 /// Returns the current [`Device`] for rendering. `None` if the window was configured with no
1052 /// display.
1053 pub fn device(&self) -> Option<&Device> {
1054 self.device.as_ref()
1055 }
1056 
1057 fn has_window_buttons(&self) -> bool {
1058 unsafe {
1059 // Use the close button as a proxy, since we modify all standard buttons together.
1060 let button = NSWindow::standardWindowButton_(
1061 self.native_window,
1062 NSWindowButton::NSWindowCloseButton,
1063 );
1064 let is_hidden: BOOL = msg_send![button, isHidden];
1065 is_hidden == NO
1066 }
1067 }
1068 
1069 fn set_window_buttons(&self, show_window_buttons: bool) {
1070 let hide_buttons = !show_window_buttons as BOOL;
1071 unsafe {
1072 let close_button = NSWindow::standardWindowButton_(
1073 self.native_window,
1074 NSWindowButton::NSWindowCloseButton,
1075 );
1076 let _: () = msg_send![close_button, setHidden: hide_buttons];
1077 let miniaturize_button = NSWindow::standardWindowButton_(
1078 self.native_window,
1079 NSWindowButton::NSWindowMiniaturizeButton,
1080 );
1081 let _: () = msg_send![miniaturize_button, setHidden: hide_buttons];
1082 let zoom_button = NSWindow::standardWindowButton_(
1083 self.native_window,
1084 NSWindowButton::NSWindowZoomButton,
1085 );
1086 let _: () = msg_send![zoom_button, setHidden: hide_buttons];
1087 }
1088 }
1089 
1090 fn set_titlebar_height(&self, height: f64) {
1091 unsafe {
1092 set_titlebar_height(self.native_window, height);
1093 }
1094 }
1095}
1096 
1097impl platform::WindowContext for WindowState {
1098 fn size(&self) -> Vector2F {
1099 let view_frame = unsafe { NSView::frame(self.native_window.contentView()) };
1100 vec2f(view_frame.size.width as f32, view_frame.size.height as f32)
1101 }
1102 
1103 fn origin(&self) -> Vector2F {
1104 let view_frame = unsafe { NSWindow::frame(self.native_window) };
1105 transform_origin_from_frame_coord_to_rect_coord(
1106 vec2f(view_frame.origin.x as f32, view_frame.origin.y as f32),
1107 vec2f(view_frame.size.width as f32, view_frame.size.height as f32),
1108 )
1109 }
1110 
1111 fn backing_scale_factor(&self) -> f32 {
1112 unsafe { NSWindow::backingScaleFactor(self.native_window) as f32 }
1113 }
1114 
1115 fn max_texture_dimension_2d(&self) -> Option<u32> {
1116 // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
1117 Some(8192)
1118 }
1119 
1120 fn render_scene(&self, scene: Rc<Scene>) {
1121 *self.next_scene.borrow_mut() = Some(scene);
1122 unsafe {
1123 let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
1124 }
1125 }
1126 
1127 fn request_redraw(&self) {
1128 let _ = self.next_scene.borrow_mut().take();
1129 unsafe {
1130 let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
1131 }
1132 }
1133 
1134 fn request_frame_capture(
1135 &self,
1136 callback: Box<dyn FnOnce(platform::CapturedFrame) + Send + 'static>,
1137 ) {
1138 *self.capture_callback.borrow_mut() = Some(callback);
1139 unsafe {
1140 let _: () = msg_send![self.native_window, setNeedsDisplayAsync];
1141 }
1142 }
1143}
1144 
1145/// An extension trait defining additional window-management options when running on macOS.
1146pub trait WindowExt {
1147 /// Returns whether or not the native macOS window buttons are visible.
1148 fn has_window_buttons(&self) -> bool;
1149 
1150 /// Sets whether or not to show the native macOS window buttons (traffic lights).
1151 fn set_window_buttons(&self, window_buttons: bool);
1152}
1153 
1154/// Utility for interacting with the native [`Window`] implementation. The native window is always
1155/// available if the app is running, but not in unit tests.
1156fn native_window(window: &dyn platform::Window) -> Option<&Window> {
1157 let native_window = window.as_any().downcast_ref::<Window>();
1158 if native_window.is_none() {
1159 let is_headless_window = window.as_any().is::<crate::platform::headless::Window>();
1160 let is_test_window = cfg!(any(test, feature = "test-util"));
1161 assert!(
1162 is_headless_window || is_test_window,
1163 "Should not fail to downcast the platform window to its concrete type"
1164 );
1165 }
1166 native_window
1167}
1168 
1169impl WindowExt for &dyn platform::Window {
1170 fn has_window_buttons(&self) -> bool {
1171 native_window(*self).is_none_or(|window| window.0.has_window_buttons())
1172 }
1173 
1174 fn set_window_buttons(&self, window_buttons: bool) {
1175 if let Some(window) = native_window(*self) {
1176 window.0.set_window_buttons(window_buttons)
1177 }
1178 }
1179}
1180 
1181#[no_mangle]
1182extern "C-unwind" fn warp_view_did_change_backing_properties(this: &Object, async_callback: bool) {
1183 let window;
1184 unsafe {
1185 window = get_window_state(this);
1186 let layer: id = msg_send![this, layer];
1187 let _: () = msg_send![layer, setContentsScale: window.backing_scale_factor()];
1188 let size = window.logical_size();
1189 
1190 let scale_factor = window.backing_scale_factor();
1191 
1192 if !size.is_zero() {
1193 // Manually convert the size into the drawable size by multiplying by the scale factor. For
1194 // some reason using `convertSizeToBacking` incorrectly upscales the drawable size in some
1195 // cases even when the backing scale factor is 1.0.
1196 let drawable_size: NSSize = NSSize::new(
1197 size.x() as f64 * scale_factor,
1198 size.y() as f64 * scale_factor,
1199 );
1200 let _: () = msg_send![layer, setDrawableSize: drawable_size];
1201 }
1202 }
1203 
1204 window.resize_renderer();
1205 
1206 if async_callback {
1207 // Dispatch the callback asynchronously if async_callback is true.
1208 let weak_window_state = Rc::downgrade(window);
1209 window
1210 .executor
1211 .spawn(async move {
1212 if let Some(window_state) = weak_window_state.upgrade() {
1213 app::callback_dispatcher()
1214 .for_window(&Window(window_state.clone()))
1215 .window_resized(window_state.as_ref());
1216 }
1217 })
1218 .detach();
1219 } else {
1220 app::callback_dispatcher()
1221 .for_window(&Window(window.clone()))
1222 .window_resized(window.as_ref());
1223 }
1224}
1225 
1226#[no_mangle]
1227pub extern "C-unwind" fn warp_get_accessibility_contents(object: &mut Object) -> id {
1228 let state = unsafe { get_window_state(object) };
1229 let window_id = state.window_id;
1230 let accessibility_data = app::callback_dispatcher()
1231 .with_mutable_app_context(|app| app.focused_view_accessibility_data(window_id));
1232 
1233 let accessibility_contents = accessibility_data
1234 .map(|data| data.content)
1235 .unwrap_or_default();
1236 make_nsstring(accessibility_contents.as_str())
1237}
1238 
1239#[no_mangle]
1240pub extern "C-unwind" fn warp_ime_position(object: &mut Object, content_rect: NSRect) -> NSRect {
1241 let state = unsafe { get_window_state(object) };
1242 
1243 let cursor_info = app::callback_dispatcher()
1244 .for_window(&Window(state.clone()))
1245 .get_active_cursor_position();
1246 
1247 let size = Vector2F::new(
1248 content_rect.size.width as f32,
1249 content_rect.size.height as f32,
1250 );
1251 
1252 NSRect {
1253 origin: match cursor_info {
1254 Some(cursor_info) => NSPoint {
1255 x: content_rect.origin.x + cursor_info.position.origin_x() as f64,
1256 y: content_rect.origin.y + (size.y() - cursor_info.position.origin_y()) as f64
1257 - (1.2 * cursor_info.font_size) as f64,
1258 },
1259 None => NSPoint {
1260 x: content_rect.origin.x,
1261 y: content_rect.origin.y + size.y() as f64,
1262 },
1263 },
1264 size: NSSize::new(0., 0.),
1265 }
1266}
1267 
1268#[no_mangle]
1269extern "C-unwind" fn warp_view_set_frame_size(this: &Object, size: NSSize, async_callback: bool) {
1270 let window;
1271 unsafe {
1272 window = get_window_state(this);
1273 // Manually convert the size into the drawable size by multiplying by the scale factor. For
1274 // some reason using `convertSizeToBacking` incorrectly upscales the drawable size in some
1275 // cases even when the backing scale factor is 1.0.
1276 let scale_factor = window.backing_scale_factor();
1277 let drawable_size: NSSize = NSSize {
1278 width: size.width * scale_factor,
1279 height: size.height * scale_factor,
1280 };
1281 let layer: id = msg_send![this, layer];
1282 let _: () = msg_send![layer, setDrawableSize: drawable_size];
1283 }
1284 
1285 window.resize_renderer();
1286 
1287 if async_callback {
1288 // Dispatch the callback asynchronously if async_callback is true.
1289 let weak_window_state = Rc::downgrade(window);
1290 window
1291 .executor
1292 .spawn(async move {
1293 if let Some(window_state) = weak_window_state.upgrade() {
1294 app::callback_dispatcher()
1295 .for_window(&Window(window_state.clone()))
1296 .window_resized(window_state.as_ref());
1297 }
1298 })
1299 .detach();
1300 } else {
1301 app::callback_dispatcher()
1302 .for_window(&Window(window.clone()))
1303 .window_resized(window.as_ref());
1304 }
1305}
1306 
1307#[no_mangle]
1308extern "C-unwind" fn warp_update_layer(this: &Object) {
1309 if !app::callback_dispatcher().can_borrow_mut() {
1310 #[cfg(debug_assertions)]
1311 log::warn!(
1312 "Tried to update window's backing CAMetalDrawable but app was already mutably borrowed!\nStack trace:\n{:#}",
1313 std::backtrace::Backtrace::force_capture()
1314 );
1315 return;
1316 }
1317 
1318 unsafe {
1319 let window = get_window_state(this);
1320 
1321 let scene = {
1322 if window.next_scene.borrow().is_none() {
1323 // Do this without holding a mutable borrow on
1324 // window.next_scene, to ensure that we don't hit BorrowMut
1325 // errors if `build_scene()` ends up invoking `request_redraw`.
1326 let scene = app::callback_dispatcher()
1327 .for_window(&Window(window.clone()))
1328 .build_scene(window.as_ref());
1329 *window.next_scene.borrow_mut() = Some(scene);
1330 }
1331 
1332 let Some(scene) = window.next_scene.borrow().clone() else {
1333 return;
1334 };
1335 
1336 scene
1337 };
1338 debug_assert!(
1339 window.next_scene.try_borrow_mut().is_ok(),
1340 "Should not be holding a borrow of the scene RefCell before beginning to render."
1341 );
1342 
1343 // SAFETY: warp_update_layer should only be invoked for windows
1344 // created via Window::open(), which always sets a non-None device.
1345 let device = window
1346 .device
1347 .as_ref()
1348 .expect("warp_update_layer should not be called for a window that has no real display");
1349 // SAFETY: warp_update_layer is only invoked by the event loop,
1350 // which should never attempt to draw a window while it is already
1351 // being drawn.
1352 let mut renderer_manager = window
1353 .renderer_manager
1354 .as_ref()
1355 .expect("warp_update_layer should never be called twice in parallel")
1356 .borrow_mut();
1357 let renderer = renderer_manager.renderer_for_device(device, window.physical_size());
1358 
1359 app::callback_dispatcher().with_mutable_app_context(|ctx| {
1360 renderer.render(&scene, window.as_ref(), ctx.font_cache());
1361 });
1362 
1363 app::callback_dispatcher()
1364 .for_window(&Window(window.clone()))
1365 .frame_drawn();
1366 }
1367}
1368 
1369/// Returns whether this event was handled.
1370#[no_mangle]
1371extern "C-unwind" fn warp_handle_view_event(
1372 this: &Object,
1373 native_event: id,
1374 composing_state: bool,
1375) -> bool {
1376 let window = unsafe { get_window_state(this) };
1377 let event = unsafe {
1378 super::event::from_native(
1379 native_event,
1380 Some(window.logical_size().y()),
1381 false, /* is_first_mouse */
1382 )
1383 };
1384 if let Some(mut event) = event {
1385 match event {
1386 Event::LeftMouseDragged {
1387 position,
1388 modifiers,
1389 ..
1390 } => schedule_synthetic_drag(window, position, modifiers),
1391 Event::LeftMouseUp { .. } => {
1392 window.next_synthetic_drag_id();
1393 }
1394 Event::KeyDown {
1395 ref mut is_composing,
1396 ..
1397 } => {
1398 *is_composing = composing_state;
1399 }
1400 _ => {}
1401 }
1402 
1403 return app::callback_dispatcher()
1404 .for_window(&Window(window.clone()))
1405 .dispatch_event(event)
1406 .handled;
1407 }
1408 
1409 false
1410}
1411 
1412/// Handles the "first mouse event" - the first mouse event fired that causes an unfocused window to
1413/// gain focus.
1414/// Returns whether this event was handled.
1415#[no_mangle]
1416extern "C-unwind" fn warp_handle_first_mouse_event(this: &Object, native_event: id) -> bool {
1417 let window = unsafe { get_window_state(this) };
1418 let event =
1419 unsafe { super::event::from_native(native_event, Some(window.logical_size().y()), true) };
1420 if let Some(event) = event {
1421 return app::callback_dispatcher()
1422 .for_window(&Window(window.clone()))
1423 .dispatch_event(event)
1424 .handled;
1425 }
1426 false
1427}
1428 
1429#[no_mangle]
1430extern "C-unwind" fn warp_handle_insert_text(this: &Object, characters: id) {
1431 let string = unsafe { to_string(characters) };
1432 let window = unsafe { get_window_state(this) };
1433 app::callback_dispatcher()
1434 .for_window(&Window(window.clone()))
1435 .dispatch_event(Event::TypedCharacters { chars: string });
1436}
1437 
1438#[no_mangle]
1439extern "C-unwind" fn warp_handle_drag_and_drop(this: &Object, paths: id, point: NSPoint) {
1440 let paths = unsafe {
1441 (0..paths.count())
1442 .map(|i| {
1443 let directory = paths.objectAtIndex(i);
1444 to_string(directory)
1445 })
1446 .collect::<Vec<_>>()
1447 };
1448 
1449 let window = unsafe { get_window_state(this) };
1450 let location = vec2f(point.x as f32, window.logical_size().y() - point.y as f32);
1451 app::callback_dispatcher()
1452 .for_window(&Window(window.clone()))
1453 .dispatch_event(Event::DragAndDropFiles { paths, location });
1454}
1455 
1456#[no_mangle]
1457extern "C-unwind" fn warp_handle_file_drag(this: &Object, point: NSPoint) {
1458 let window = unsafe { get_window_state(this) };
1459 let location = vec2f(point.x as f32, window.logical_size().y() - point.y as f32);
1460 
1461 app::callback_dispatcher()
1462 .for_window(&Window(window.clone()))
1463 .dispatch_event(Event::DragFiles { location });
1464}
1465 
1466#[no_mangle]
1467extern "C-unwind" fn warp_handle_file_drag_exit(this: &Object) {
1468 let window = unsafe { get_window_state(this) };
1469 
1470 app::callback_dispatcher()
1471 .for_window(&Window(window.clone()))
1472 .dispatch_event(Event::DragFileExit);
1473}
1474 
1475#[no_mangle]
1476extern "C-unwind" fn warp_update_ime_state(this: &mut Object, ime_active: bool) {
1477 let state = unsafe { get_window_state(this) };
1478 state.ime_active.set(ime_active);
1479}
1480 
1481/// Converts an NSRange to a Rust Range<usize>
1482/// NSRange has location (start) and length, while Rust Range has start and end
1483fn nsrange_to_rust_range(ns_range: NSRange) -> std::ops::Range<usize> {
1484 let start = ns_range.location as usize;
1485 let end = start + ns_range.length as usize;
1486 start..end
1487}
1488 
1489#[no_mangle]
1490extern "C-unwind" fn warp_marked_text_updated(
1491 this: &mut Object,
1492 marked_text: id,
1493 selected_range: NSRange,
1494) {
1495 let state = unsafe { get_window_state(this) };
1496 let marked_text = unsafe { to_string(marked_text) };
1497 let selected_range = nsrange_to_rust_range(selected_range);
1498 app::callback_dispatcher()
1499 .for_window(&Window(state.clone()))
1500 .dispatch_event(Event::SetMarkedText {
1501 marked_text,
1502 selected_range,
1503 });
1504}
1505 
1506#[no_mangle]
1507extern "C-unwind" fn warp_marked_text_cleared(this: &mut Object) {
1508 let state = unsafe { get_window_state(&*this) };
1509 app::callback_dispatcher()
1510 .for_window(&Window(state.clone()))
1511 .dispatch_event(Event::ClearMarkedText);
1512}
1513 
1514#[no_mangle]
1515pub extern "C-unwind" fn warp_dispatch_standard_action(this: id, tag: NSInteger) {
1516 if let Some(action) = StandardAction::from_isize(tag as isize) {
1517 let state = unsafe { get_window_state(&*this) };
1518 app::callback_dispatcher()
1519 .for_window(&Window(state.clone()))
1520 .dispatch_standard_action(action);
1521 }
1522}
1523 
1524#[no_mangle]
1525pub extern "C-unwind" fn warp_app_window_moved(this: id, rect: NSRect) {
1526 let state = unsafe { get_window_state(&*this) };
1527 let point = Vector2F::new(rect.origin.x as f32, rect.origin.y as f32);
1528 let size = Vector2F::new(rect.size.width as f32, rect.size.height as f32);
1529 
1530 let weak_window_state = Rc::downgrade(state);
1531 state
1532 .executor
1533 .spawn(async move {
1534 if let Some(window) = weak_window_state.upgrade() {
1535 app::callback_dispatcher()
1536 .for_window(&Window(window))
1537 .window_moved(RectF::new(
1538 transform_origin_from_frame_coord_to_rect_coord(point, size),
1539 size,
1540 ));
1541 }
1542 })
1543 .detach();
1544}
1545 
1546/// Removes the WindowState ivar from an Objective-C object, nulls out the ivar
1547/// pointer within the object, and returns the reference-counted pointer to the
1548/// state.
1549unsafe fn remove_state_ivar_from_object(object: &mut Object) -> Rc<WindowState> {
1550 let wrapper_ptr: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
1551 let state = Ivar::take_state(wrapper_ptr);
1552 object.set_ivar(WINDOW_STATE_IVAR, ptr::null::<c_void>());
1553 state
1554}
1555 
1556// dealloc is called by AppKit when our NSWindow subclass is deallocating,
1557// because its retain count has dropped to zero. This is our chance to release
1558// our Rust resources. Do not call this manually.
1559#[no_mangle]
1560pub extern "C-unwind" fn warp_dealloc_window(native_window: &mut Object) {
1561 log::info!("dealloc native window {native_window:p}");
1562 let state;
1563 unsafe {
1564 // Remove the window state from the content NSView and drop a reference.
1565 let view_id: id = msg_send![native_window, contentView];
1566 let native_view = &mut (*view_id);
1567 let _ = remove_state_ivar_from_object(native_view);
1568 
1569 // Remove the window state from the NSWindowDelegate and drop a reference.
1570 let delegate_id: id = msg_send![native_window, delegate];
1571 let native_window_delegate = &mut (*delegate_id);
1572 let _ = remove_state_ivar_from_object(native_window_delegate);
1573 
1574 // Remove the window state from the NSWindow.
1575 state = remove_state_ivar_from_object(native_window);
1576 }
1577 
1578 // Drop the final reference to the `WindowState`, which actually drops the
1579 // underlying object and frees the memory.
1580 drop(state);
1581}
1582 
1583pub unsafe fn get_window_state(object: &Object) -> &Rc<WindowState> {
1584 let wrapper_ptr: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
1585 Ivar::get_state(wrapper_ptr)
1586}
1587 
1588fn schedule_synthetic_drag(
1589 window_state: &Rc<WindowState>,
1590 position: Vector2F,
1591 modifiers: ModifiersState,
1592) {
1593 let drag_id = window_state.next_synthetic_drag_id();
1594 let weak_window_state = Rc::downgrade(window_state);
1595 let instant = Instant::now() + Duration::from_millis(16);
1596 window_state
1597 .executor
1598 .spawn(async move {
1599 Timer::at(instant).await;
1600 if let Some(window_state) = weak_window_state.upgrade() {
1601 if window_state.synthetic_drag_counter.get() == drag_id {
1602 schedule_synthetic_drag(&window_state, position, modifiers);
1603 app::callback_dispatcher()
1604 .for_window(&Window(window_state))
1605 .dispatch_event(Event::LeftMouseDragged {
1606 position,
1607 modifiers,
1608 });
1609 }
1610 }
1611 })
1612 .detach();
1613}
1614 
1615// We need this transformation as Cocoa follows the Cartesian coordinate system with rect's
1616// origin on the lower left corner and positive value extending along the y coordinate
1617// up, whereas RectF follows the flipped coordinate system with rect's origin on the upper left
1618// corner and positive value extending along the y coordinate down.
1619// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Transforms/Transforms.html
1620//
1621// For example, a rect in Cocoa will look like:
1622// (0, 100) (100, 100)
1623// -------------------------
1624// | |
1625// | |
1626// | |
1627// | |
1628// | |
1629// -------------------------
1630// Origin: (0, 0) (100, 0)
1631//
1632// Whereas the same rect represented in RectF should look like:
1633// Origin: (0, -100) (100, -100)
1634// -------------------------
1635// | |
1636// | |
1637// | |
1638// | |
1639// | |
1640// -------------------------
1641// (0, 0) (100, 0)
1642//
1643// Note that the y_axis is flipped in RectF.
1644//
1645// To transform a Cocoa rect into RectF, we need the following two steps:
1646// 1. Get the upper left corner coordinate of the rect:
1647// * upper_left = (cocoa_origin.x(), cocoa_origin.y() + size.y())
1648// 2. Flip the y coordinate:
1649// * new_origin = (upper_left.x(), -upper_left.y())
1650//
1651// In the reverse transformation, we also need to follow two steps:
1652// 1. Get the lower left corner coordinate of the rect:
1653// * lower_left = (rectf_origin.x(), rectf_origin.y() + size.y())
1654// 2. Flip the y coordinate:
1655// * new_origin = (lower_left.x(), -lower_left.y())
1656pub fn transform_origin_from_frame_coord_to_rect_coord(
1657 origin: Vector2F,
1658 size: Vector2F,
1659) -> Vector2F {
1660 Vector2F::new(origin.x(), -(origin.y() + size.y()))
1661}
1662 
1663fn transform_origin_from_rect_coord_to_frame_coord(origin: Vector2F, size: Vector2F) -> Vector2F {
1664 Vector2F::new(origin.x(), -(origin.y() + size.y()))
1665}
1666 
1667/// Converts an Objective-C `Object` into a `String`
1668unsafe fn to_string(value: *mut Object) -> String {
1669 let slice = slice::from_raw_parts(value.UTF8String() as *const c_uchar, value.len());
1670 str::from_utf8_unchecked(slice).to_string()
1671}
1672