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/objc/window.m
1#import <AppKit/AppKit.h>
2#import <AppKit/NSAccessibility.h>
3#import <AppKit/NSAccessibilityConstants.h>
4#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
5#import <objc/runtime.h>
6 
7#import "alert.h"
8#import "app.h"
9#import "fullscreen_queue.h"
10#import "host_view.h"
11#import "window_blur.h"
12 
13// NSWindow.delegate is a weak reference, so the WarpWindowDelegate we create in
14// `create_warp_nswindow` / `create_warp_nspanel` would otherwise be leaked with a +1
15// retain count. Associating it with the window ties its lifetime to the window: the
16// associated object is released by the runtime when the window itself is deallocated.
17static const void *kWarpWindowDelegateAssocKey = &kWarpWindowDelegateAssocKey;
18 
19NSWindowStyleMask warpWindowMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable |
20 NSWindowStyleMaskResizable | NSWindowStyleMaskTitled;
21 
22// The default macOS titlebar height (in points).
23static const CGFloat DEFAULT_TITLEBAR_HEIGHT = 28.0;
24 
25// A back-to-front ordered array of windows, identified by their `windowNumber`
26// property.
27NSMutableArray<NSNumber *> *windowOrderForTests;
28dispatch_once_t windowOrderOnce;
29 
30FullscreenWindowManager *fullscreenManager;
31dispatch_once_t fullscreenQueueOnce;
32 
33// This extends the NSWindow API with an implementation of toggleFullScreen
34// that enforces one window transition at a time, preventing concurrent
35// animations.
36@interface NSWindow (Fullscreen)
37- (void)enqueueFullscreenTransition;
38@end
39 
40@implementation NSWindow (Fullscreen)
41- (void)enqueueFullscreenTransition {
42 // If the queue doesn't already exist, allocate it.
43 dispatch_once(&fullscreenQueueOnce, ^{
44 fullscreenManager = [[FullscreenWindowManager alloc] init];
45 });
46 
47 // Enqueue the window into the fullscreen manager asynchronously, to ensure
48 // there are no synchronous callbacks into Rust code.
49 dispatch_async(dispatch_get_main_queue(), ^{
50 [fullscreenManager enqueueWindow:self];
51 });
52}
53@end
54 
55@protocol WarpWindowProtocol
56 
57@property BOOL testMode;
58 
59@property BOOL hideTitleBar;
60 
61// Asynchronously marks the content view as being dirty.
62- (void)setNeedsDisplayAsync;
63 
64// Configures the titlebar height and traffic light button constraints.
65- (void)configureTitlebarHeight:(CGFloat)height;
66 
67// Resets the titlebar height to the default macOS value for fullscreen. Fullscreen has a different
68// titlebar which cannot honor user-configured height.
69- (void)applyFullscreenTitlebarHeight;
70 
71// Restores the titlebar height to the last value passed to configureTitlebarHeight:.
72- (void)restoreConfiguredTitlebarHeight;
73 
74@end
75 
76@class WarpWindow;
77@class WarpPanel;
78 
79// Declaration of functions implemented in Rust.
80void warp_dealloc_window(id self);
81void warp_dispatch_standard_action(id self, NSInteger tag);
82void warp_app_window_moved(id self, NSRect rect);
83void warp_open_panel_file_selected(id urls, void *callback);
84void warp_save_panel_file_selected(id url, void *callback);
85 
86NSNumber *previouslyActiveAppPID;
87 
88@interface PreviousStateHelper : NSObject
89@end
90 
91@implementation PreviousStateHelper
92+ (NSNumber *)storePreviousState {
93 NSRunningApplication *runningApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
94 NSString *bundleIdentifier = runningApp.bundleIdentifier;
95 if ([bundleIdentifier isEqualToString:[[NSBundle mainBundle] bundleIdentifier]]) {
96 return nil;
97 } else {
98 return @(runningApp.processIdentifier);
99 }
100}
101 
102+ (void)activatePreviousState:(NSNumber *)previousPID {
103 if (previousPID) {
104 NSRunningApplication *app =
105 [NSRunningApplication runningApplicationWithProcessIdentifier:[previousPID intValue]];
106 if (app) {
107 // Use the default behavior here which only activates the main and key window.
108 [app activateWithOptions:(NSApplicationActivateAllWindows |
109 NSApplicationActivateIgnoringOtherApps)];
110 }
111 }
112}
113@end
114 
115@interface WarpWindow : NSWindow <WarpWindowProtocol>
116@end
117 
118@interface WarpWindowDelegate : NSObject <NSWindowDelegate>
119@end
120 
121@implementation WarpWindowDelegate {
122 void *windowState;
123 
124 BOOL forceTermination;
125}
126 
127- (void)windowDidMove:(NSNotification *)notification {
128 if (windowState) {
129 NSWindow *window = notification.object;
130 warp_app_window_moved(self, window.frame);
131 }
132}
133 
134- (void)windowWillStartLiveResize:(NSNotification *)notification {
135 WarpWindow *warp_window = notification.object;
136 WarpHostView *warp_view = warp_window.contentView;
137 
138 // This is a hack to get around `borrowMut` errors within the UI framework
139 // caused by the fact that it incorrectly assumes that callbacks cannot
140 // synchronously cause another callback to be triggered. To avoid this for now,
141 // we explicitly force callbacks to be synchronous if it's caused by the user instead
142 // of another system call (such as the active screen changing)
143 [warp_view setAsyncCallback:NO];
144}
145 
146- (void)windowDidEndLiveResize:(NSNotification *)notification {
147 WarpWindow *warp_window = notification.object;
148 WarpHostView *warp_view = warp_window.contentView;
149 [warp_view setAsyncCallback:YES];
150}
151 
152- (void)setForceTermination {
153 forceTermination = YES;
154}
155 
156- (BOOL)windowShouldClose:(NSWindow *)window {
157 if (forceTermination) {
158 return YES;
159 }
160 
161 NSApplication *application = [NSApplication sharedApplication];
162 BOOL okToClose = warp_app_should_close_window(application, window);
163 
164 if (okToClose) {
165 return YES;
166 } else {
167 return NO;
168 }
169}
170 
171- (void)windowWillClose:(NSNotification *)note {
172 if (windowState) {
173 warp_app_window_will_close([NSApplication sharedApplication], self);
174 }
175}
176 
177- (NSApplicationPresentationOptions)window:(NSWindow *)window
178 willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions {
179 return proposedOptions | NSApplicationPresentationAutoHideToolbar;
180}
181 
182- (void)windowWillEnterFullScreen:(NSNotification *)notification {
183 NSWindow<WarpWindowProtocol> *window = notification.object;
184 [window applyFullscreenTitlebarHeight];
185 // macOS automatically detaches the title bar in fullscreen (see
186 // willUseFullScreenPresentationOptions), and shows it along with the mac menu on hover. Since
187 // the title bar is overlaid in this case, it should be visible.
188 window.titlebarAppearsTransparent = NO;
189}
190 
191- (void)windowWillExitFullScreen:(NSNotification *)notification {
192 NSWindow<WarpWindowProtocol> *window = notification.object;
193 window.titlebarAppearsTransparent = window.hideTitleBar;
194 [window restoreConfiguredTitlebarHeight];
195}
196 
197@end
198 
199// Returns the titlebar container view for the given window, or nil if not found.
200static NSView *get_titlebar_container_view(NSWindow *window) {
201 NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton];
202 if (!closeButton) return nil;
203 NSView *titleBarView = [closeButton superview];
204 return [titleBarView superview];
205}
206 
207// Configures titlebar height and traffic light button constraints for a window.
208// Returns the height constraint if newly created, or NULL if just updating.
209static NSLayoutConstraint *configure_titlebar_height(NSWindow *window, CGFloat height,
210 NSLayoutConstraint *existingConstraint) {
211 if (height <= 0) {
212 return existingConstraint;
213 }
214 
215 NSView *titleBarContainerView = get_titlebar_container_view(window);
216 if (!titleBarContainerView) {
217 return existingConstraint;
218 }
219 NSView *titleBarView = [titleBarContainerView.subviews firstObject];
220 if (!titleBarView) {
221 return existingConstraint;
222 }
223 
224 // Set title bar container's height and origin.
225 NSRect containerFrame = [titleBarContainerView frame];
226 CGFloat windowHeight = [window frame].size.height;
227 containerFrame.size.height = height;
228 containerFrame.origin.y = windowHeight - height;
229 [titleBarContainerView setFrame:containerFrame];
230 
231 // Edit existing constraint if already constructed.
232 if (existingConstraint) {
233 existingConstraint.constant = height;
234 return existingConstraint;
235 }
236 
237 // Otherwise, we're building for the first time.
238 titleBarView.translatesAutoresizingMaskIntoConstraints = NO;
239 
240 NSLayoutConstraint *heightConstraint =
241 [titleBarView.heightAnchor constraintEqualToConstant:height];
242 heightConstraint.priority = NSLayoutPriorityRequired;
243 heightConstraint.active = YES;
244 
245 // Pin titlebar to top, left, and right of container.
246 [[titleBarView.topAnchor constraintEqualToAnchor:titleBarContainerView.topAnchor]
247 setActive:YES];
248 [[titleBarView.leadingAnchor constraintEqualToAnchor:titleBarContainerView.leadingAnchor]
249 setActive:YES];
250 [[titleBarView.trailingAnchor constraintEqualToAnchor:titleBarContainerView.trailingAnchor]
251 setActive:YES];
252 
253 NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton];
254 NSButton *miniaturizeButton = [window standardWindowButton:NSWindowMiniaturizeButton];
255 NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
256 
257 if (!closeButton || !miniaturizeButton || !zoomButton) {
258 return heightConstraint;
259 }
260 
261 // Standard macOS traffic light button spacing.
262 CGFloat buttonSpacing = 6.0;
263 CGFloat leftMargin = 12.0;
264 CGFloat buttonSize = 14.0;
265 
266 NSArray *buttons = @[ closeButton, miniaturizeButton, zoomButton ];
267 for (NSUInteger i = 0; i < buttons.count; i++) {
268 NSButton *button = buttons[i];
269 button.translatesAutoresizingMaskIntoConstraints = NO;
270 
271 [[button.widthAnchor constraintEqualToConstant:buttonSize] setActive:YES];
272 [[button.heightAnchor constraintEqualToConstant:buttonSize] setActive:YES];
273 
274 CGFloat xOffset = leftMargin + i * (buttonSize + buttonSpacing);
275 [[button.leadingAnchor constraintEqualToAnchor:titleBarView.leadingAnchor
276 constant:xOffset] setActive:YES];
277 [[button.centerYAnchor constraintEqualToAnchor:titleBarView.centerYAnchor
278 constant:1.0] setActive:YES];
279 }
280 
281 return heightConstraint;
282}
283 
284// Initializes an NSWindow that conforms to our window protocol.
285void init_warp_nswindow(NSWindow<WarpWindowProtocol> *window, bool testMode, bool hideTitleBar) {
286 window.testMode = testMode;
287 window.hideTitleBar = hideTitleBar;
288 
289 // Set the background color to clear to support window background transparency. When this is set
290 // to NSColor.clearColor with alpha = 0 and window drop shadows are enabled, MacOS renders a
291 // small 'gap' between the window border and the contents. We don't know why; its likely an
292 // internal Cocoa bug. https://stackoverflow.com/questions/6167692/nswindow-shadow-outline
293 // provides evidence that we're not the only one observing this issue.
294 //
295 // Setting some non-zero alpha component for the background color fixes the issue.
296 window.backgroundColor = [NSColor.clearColor colorWithAlphaComponent:0.01];
297 window.releasedWhenClosed = YES;
298 window.acceptsMouseMovedEvents = YES;
299 window.titlebarAppearsTransparent = hideTitleBar;
300 window.titleVisibility = hideTitleBar ? NSWindowTitleHidden : NSWindowTitleVisible;
301}
302 
303@implementation WarpWindow {
304 // The windowState is managed on the Rust side.
305 void *windowState;
306 // Height constraint for the titlebar view (also indicates if constraints are configured)
307 NSLayoutConstraint *_titleBarHeightConstraint;
308 // The last height set via configureTitlebarHeight: (i.e. from Rust).
309 CGFloat _configuredTitlebarHeight;
310 // Whether we have registered for titlebar container frame change notifications. Needed to
311 // uphold the user-configured titlebar height.
312 BOOL _observingTitlebarContainer;
313 // Guard to prevent re-entrancy when we change the titlebar container frame ourselves.
314 BOOL _isApplyingTitlebarHeight;
315 // When YES, constrainFrameRect:toScreen: returns the requested frame unmodified. This prevents
316 // macOS from cascading or clamping the window position while a tab-drag preview window is
317 // being created and positioned under the cursor.
318 BOOL _suppressFrameConstraintsDuringDrag;
319}
320 
321@synthesize testMode;
322@synthesize hideTitleBar;
323 
324- (void)applyTitlebarHeight:(CGFloat)height {
325 _isApplyingTitlebarHeight = YES;
326 _titleBarHeightConstraint = configure_titlebar_height(self, height, _titleBarHeightConstraint);
327 _isApplyingTitlebarHeight = NO;
328}
329 
330- (void)configureTitlebarHeight:(CGFloat)height {
331 _configuredTitlebarHeight = height;
332 [self applyTitlebarHeight:height];
333 [self observeTitlebarContainerIfNeeded];
334}
335 
336- (void)applyFullscreenTitlebarHeight {
337 [self applyTitlebarHeight:DEFAULT_TITLEBAR_HEIGHT];
338}
339 
340- (void)restoreConfiguredTitlebarHeight {
341 if (_configuredTitlebarHeight > 0) {
342 [self applyTitlebarHeight:_configuredTitlebarHeight];
343 }
344}
345 
346- (void)observeTitlebarContainerIfNeeded {
347 if (_observingTitlebarContainer) return;
348 NSView *containerView = get_titlebar_container_view(self);
349 if (!containerView) return;
350 [containerView setPostsFrameChangedNotifications:YES];
351 [[NSNotificationCenter defaultCenter] addObserver:self
352 selector:@selector(titlebarContainerFrameDidChange:)
353 name:NSViewFrameDidChangeNotification
354 object:containerView];
355 _observingTitlebarContainer = YES;
356}
357 
358- (void)titlebarContainerFrameDidChange:(NSNotification *)notification {
359 if (_isApplyingTitlebarHeight) return;
360 if (_configuredTitlebarHeight <= 0) return;
361 BOOL isFullscreen = (self.styleMask & NSWindowStyleMaskFullScreen) != 0;
362 if (isFullscreen) return;
363 // Defer to avoid modifying constraints in the middle of an active layout pass.
364 dispatch_async(dispatch_get_main_queue(), ^{
365 [self applyTitlebarHeight:_configuredTitlebarHeight];
366 });
367}
368 
369- (void)setSuppressFrameConstraintsDuringDrag:(BOOL)value {
370 _suppressFrameConstraintsDuringDrag = value;
371}
372 
373- (BOOL)canBecomeMainWindow {
374 return YES;
375}
376 
377- (BOOL)canBecomeKeyWindow {
378 return YES;
379}
380 
381- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen {
382 if (_suppressFrameConstraintsDuringDrag) {
383 return frameRect;
384 }
385 return [super constrainFrameRect:frameRect toScreen:screen];
386}
387 
388- (void)sendEvent:(NSEvent *)event {
389 switch (event.type) {
390 // In some cases, NSWindow's default sendEvent: implementation will dispatch a MouseDown
391 // event and subsequent MouseDragged events to the content view, but then dispatch the
392 // remaining MouseDragged events and MouseUp event elsewhere.
393 // This is inconsistent with the Cocoa event architecture documentation
394 // (https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/EventArchitecture/EventArchitecture.html),
395 // but it's unclear how or why the events get redirected.
396 // This breaks drag-and-drop for panes and tabs (see CLD-2581), so we work around it with
397 // custom dispatching.
398 case NSEventTypeLeftMouseUp:
399 [self.contentView mouseUp:event];
400 break;
401 case NSEventTypeLeftMouseDragged:
402 [self.contentView mouseDragged:event];
403 break;
404 
405 // The NSWindow's default sendEvent: implementation does not propagate RightMouseDown events
406 // from the application title bar to the content view when running a development build
407 // locally, though it is unclear why. This breaks the right-click context menu for tabs on
408 // local builds, so we propagate the RightMouseDown event manually.
409 case NSEventTypeRightMouseDown:
410 [self.contentView rightMouseDown:event];
411 break;
412 default:
413 [super sendEvent:event];
414 break;
415 }
416}
417 
418- (void)dealloc {
419 [[NSNotificationCenter defaultCenter] removeObserver:self];
420 warp_dealloc_window(self);
421 [super dealloc];
422}
423 
424- (void)setNeedsDisplayAsync {
425 NSView *contentView = [self contentView];
426 dispatch_async(dispatch_get_main_queue(), ^{
427 [contentView setNeedsDisplay:YES];
428 });
429}
430 
431- (BOOL)performKeyEquivalent:(NSEvent *)event {
432 // We need to bypass the default performKeyEquivalent implementation which, in the case of
433 // having keybinding conflicts with MacOS itself, yields priority to the OS.
434 if ([event type] == NSEventTypeKeyDown) {
435 NSApplication *application = [NSApplication sharedApplication];
436 
437 // If we are recording a keystroke for an EditableBinding.
438 BOOL keyBindingsDisabled = warp_app_are_key_bindings_disabled_for_window(application, self);
439 // If Warp has assigned a binding for this keystroke.
440 BOOL keystrokeIsAssigned = warp_app_has_binding_for_keystroke(application, event);
441 
442 BOOL triggersCustomAction = warp_app_has_custom_action_for_keystroke(application, event);
443 
444 if (keyBindingsDisabled || (keystrokeIsAssigned && !triggersCustomAction)) {
445 if ([self.contentView keyDownImpl:event]) {
446 return YES;
447 }
448 }
449 }
450 
451 return [super performKeyEquivalent:event];
452}
453 
454- (void)closeWindowAsync:(BOOL)forceTermination {
455 dispatch_async(dispatch_get_main_queue(), ^{
456 WarpWindowDelegate *delegate = self.delegate;
457 if (forceTermination) {
458 [delegate setForceTermination];
459 }
460 [self performClose:nil];
461 });
462}
463 
464- (void)makeKeyAndOrderFront:(id)sender {
465 if ([self testMode]) {
466 // To avoid any issues due to the behavior of the developer using their
467 // machine and modifying the global window stack, we instead hide the
468 // window entirely, and track z-positioning in our own window position
469 // stack.
470 [self orderOut:sender];
471 [windowOrderForTests addObject:@(self.windowNumber)];
472 } else {
473 [super makeKeyAndOrderFront:sender];
474 }
475}
476 
477- (void)zoomAsync:(id)sender {
478 dispatch_async(dispatch_get_main_queue(), ^{
479 [self zoom:sender];
480 });
481}
482 
483- (void)orderOut:(id)sender {
484 if ([self testMode]) {
485 [windowOrderForTests removeObject:@(self.windowNumber)];
486 }
487 
488 [super orderOut:sender];
489}
490 
491// Note this returns a retained object ("create" rule).
492+ (WarpWindow *)createWithContentRect:(NSRect)contentRect
493 metalDevice:(id)metalDevice
494 hidingTitleBar:(BOOL)hideTitleBar
495 backgroundBlurRadiusPixels:(uint8)backgoundBlurRadiusPixels
496 withTestMode:(BOOL)testMode {
497 NSWindowStyleMask mask = warpWindowMask;
498 
499 if (hideTitleBar) {
500 mask |= NSWindowStyleMaskFullSizeContentView;
501 }
502 
503 WarpWindow *window_result = [[WarpWindow alloc] initWithContentRect:contentRect
504 styleMask:mask
505 backing:NSBackingStoreBuffered
506 defer:NO];
507 init_warp_nswindow(window_result, testMode, hideTitleBar);
508 
509 return window_result;
510}
511 
512@end
513 
514// A panel is basically a NSWindow with the exception that it could be displayed
515// above fullscreen apps.
516@interface WarpPanel : NSPanel <WarpWindowProtocol>
517@end
518 
519@implementation WarpPanel {
520 // The windowState is managed on the Rust side.
521 void *windowState;
522 // Height constraint for the titlebar view (also indicates if constraints are configured)
523 NSLayoutConstraint *_titleBarHeightConstraint;
524 // The last height set via configureTitlebarHeight: (i.e. from Rust).
525 CGFloat _configuredTitlebarHeight;
526 // Whether we have registered for titlebar container frame change notifications.
527 BOOL _observingTitlebarContainer;
528 // Guard to prevent re-entrancy when we change the container frame ourselves.
529 BOOL _isApplyingTitlebarHeight;
530}
531 
532@synthesize testMode;
533@synthesize hideTitleBar;
534 
535- (void)applyTitlebarHeight:(CGFloat)height {
536 _isApplyingTitlebarHeight = YES;
537 _titleBarHeightConstraint = configure_titlebar_height(self, height, _titleBarHeightConstraint);
538 _isApplyingTitlebarHeight = NO;
539}
540 
541- (void)configureTitlebarHeight:(CGFloat)height {
542 _configuredTitlebarHeight = height;
543 [self applyTitlebarHeight:height];
544 [self observeTitlebarContainerIfNeeded];
545}
546 
547- (void)applyFullscreenTitlebarHeight {
548 [self applyTitlebarHeight:DEFAULT_TITLEBAR_HEIGHT];
549}
550 
551- (void)restoreConfiguredTitlebarHeight {
552 if (_configuredTitlebarHeight > 0) {
553 [self applyTitlebarHeight:_configuredTitlebarHeight];
554 }
555}
556 
557- (void)observeTitlebarContainerIfNeeded {
558 if (_observingTitlebarContainer) return;
559 NSView *containerView = get_titlebar_container_view(self);
560 if (!containerView) return;
561 [containerView setPostsFrameChangedNotifications:YES];
562 [[NSNotificationCenter defaultCenter] addObserver:self
563 selector:@selector(titlebarContainerFrameDidChange:)
564 name:NSViewFrameDidChangeNotification
565 object:containerView];
566 _observingTitlebarContainer = YES;
567}
568 
569- (void)titlebarContainerFrameDidChange:(NSNotification *)notification {
570 if (_isApplyingTitlebarHeight) return;
571 if (_configuredTitlebarHeight <= 0) return;
572 BOOL isFullscreen = (self.styleMask & NSWindowStyleMaskFullScreen) != 0;
573 if (isFullscreen) return;
574 // Defer to avoid modifying constraints in the middle of an active layout pass.
575 dispatch_async(dispatch_get_main_queue(), ^{
576 [self applyTitlebarHeight:_configuredTitlebarHeight];
577 });
578}
579 
580- (BOOL)canBecomeMainWindow {
581 return YES;
582}
583 
584- (BOOL)canBecomeKeyWindow {
585 return YES;
586}
587 
588- (BOOL)isExcludedFromWindowsMenu {
589 return NO;
590}
591 
592- (void)dealloc {
593 [[NSNotificationCenter defaultCenter] removeObserver:self];
594 warp_dealloc_window(self);
595 [super dealloc];
596}
597 
598- (void)setNeedsDisplayAsync {
599 NSView *contentView = [self contentView];
600 dispatch_async(dispatch_get_main_queue(), ^{
601 [contentView setNeedsDisplay:YES];
602 });
603}
604 
605- (void)closeWindowAsync:(BOOL)forceTermination {
606 dispatch_async(dispatch_get_main_queue(), ^{
607 WarpWindowDelegate *delegate = self.delegate;
608 [delegate setForceTermination];
609 [self close];
610 });
611}
612 
613- (void)performClose:(id)sender {
614 warp_dispatch_standard_action(self, [sender tag]);
615}
616 
617- (void)makeKeyAndOrderFront:(id)sender {
618 if ([self testMode]) {
619 // To avoid any issues due to the behavior of the developer using their
620 // machine and modifying the global window stack, we instead hide the
621 // window entirely, and track z-positioning in our own window position
622 // stack.
623 [self orderOut:sender];
624 [windowOrderForTests addObject:@(self.windowNumber)];
625 } else {
626 [super makeKeyAndOrderFront:sender];
627 }
628}
629 
630- (void)orderOut:(id)sender {
631 if ([self testMode]) {
632 [windowOrderForTests removeObject:@(self.windowNumber)];
633 }
634 
635 [super orderOut:sender];
636}
637 
638- (void)positionPinnedPanel {
639 previouslyActiveAppPID = [PreviousStateHelper storePreviousState];
640 
641 // NSFloatingWindowLevel allows us to float above all other normal application
642 // windows but also not overlap with user's dock, menu bar, spotlight and Raycast.
643 self.level = NSFloatingWindowLevel;
644 
645 // These collectionBehavior makes sure the panel could join fullscreen space.
646 self.collectionBehavior =
647 (self.collectionBehavior | NSWindowCollectionBehaviorCanJoinAllSpaces |
648 NSWindowCollectionBehaviorFullScreenAuxiliary);
649 
650 [self setMovable:NO];
651 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
652 [self makeKeyAndOrderFront:nil];
653}
654 
655// Note this returns a retained object ("create" rule).
656+ (WarpPanel *)createWithContentRect:(NSRect)contentRect
657 metalDevice:(id)metalDevice
658 hidingTitleBar:(BOOL)hideTitleBar
659 backgroundBlurRadiusPixels:(uint8)backgoundBlurRadiusPixels
660 withTestMode:(BOOL)testMode {
661 NSWindowStyleMask mask = warpWindowMask | NSWindowStyleMaskNonactivatingPanel;
662 
663 if (hideTitleBar) {
664 mask |= NSWindowStyleMaskFullSizeContentView;
665 }
666 
667 WarpPanel *window_result = [[WarpPanel alloc] initWithContentRect:contentRect
668 styleMask:mask
669 backing:NSBackingStoreBuffered
670 defer:NO];
671 init_warp_nswindow(window_result, testMode, hideTitleBar);
672 
673 return window_result;
674}
675 
676@end
677 
678void set_window_background_blur_radius(id window, uint8 blurRadiusPixels) {
679 int windowNumber = [window windowNumber];
680 CGSConnectionID con = CGSDefaultConnectionForThread();
681 if (con) {
682 CGSSetWindowBackgroundBlurRadiusFunction *function =
683 GetCGSSetWindowBackgroundBlurRadiusFunction();
684 if (function) {
685 function(con, windowNumber, (int)MAX(1, blurRadiusPixels));
686 }
687 }
688}
689 
690// Attaches a WarpWindowDelegate to |window| and ties its lifetime to the window.
691//
692// NSWindow.delegate is a weak property, so the delegate must be kept alive
693// externally. We do this by associating it with the window via
694// objc_setAssociatedObject, which retains the delegate and releases it when
695// the window is deallocated. The caller's +1 from alloc/init is then balanced
696// by the final [delegate release].
697static void attach_warp_window_delegate(NSWindow *window) {
698 WarpWindowDelegate *delegate = [[WarpWindowDelegate alloc] init];
699 [window setDelegate:delegate];
700 objc_setAssociatedObject(window, kWarpWindowDelegateAssocKey, delegate,
701 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
702 [delegate release];
703}
704 
705// \return a new, retained WarpPanel with the given content rect.
706id create_warp_nspanel(NSRect contentRect, id metalDevice, BOOL hideTitleBar,
707 uint8 backgroundBlurRadiusPixels, BOOL testMode) {
708 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
709 
710 if (testMode) {
711 dispatch_once(&windowOrderOnce, ^{
712 windowOrderForTests = [[NSMutableArray alloc] init];
713 });
714 }
715 
716 WarpPanel *window = [WarpPanel createWithContentRect:contentRect
717 metalDevice:metalDevice
718 hidingTitleBar:hideTitleBar
719 backgroundBlurRadiusPixels:backgroundBlurRadiusPixels
720 withTestMode:testMode];
721 
722 WarpHostView *hostView = [[[WarpHostView alloc] initWithFrame:contentRect
723 metalDevice:metalDevice
724 enableTitlebarDrag:NO
725 testMode:testMode] autorelease];
726 
727 attach_warp_window_delegate(window);
728 
729 window.contentView = hostView;
730 [window makeFirstResponder:hostView];
731 set_window_background_blur_radius(window, backgroundBlurRadiusPixels);
732 [pool release];
733 return window;
734}
735 
736// \return a new, retained WarpWindow with the given content rect.
737id create_warp_nswindow(NSRect contentRect, id metalDevice, BOOL hideTitleBar,
738 uint8 backgroundBlurRadiusPixels, BOOL testMode) {
739 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
740 
741 if (testMode) {
742 dispatch_once(&windowOrderOnce, ^{
743 windowOrderForTests = [[NSMutableArray alloc] init];
744 });
745 }
746 
747 WarpWindow *window = [WarpWindow createWithContentRect:contentRect
748 metalDevice:metalDevice
749 hidingTitleBar:hideTitleBar
750 backgroundBlurRadiusPixels:backgroundBlurRadiusPixels
751 withTestMode:testMode];
752 
753 WarpHostView *hostView = [[[WarpHostView alloc] initWithFrame:contentRect
754 metalDevice:metalDevice
755 enableTitlebarDrag:YES
756 testMode:testMode] autorelease];
757 
758 attach_warp_window_delegate(window);
759 
760 window.contentView = hostView;
761 [window makeFirstResponder:hostView];
762 set_window_background_blur_radius(window, backgroundBlurRadiusPixels);
763 [pool release];
764 return window;
765}
766 
767BOOL is_warp_window(id window) {
768 return [window isKindOfClass:[WarpWindow class]] || [window isKindOfClass:[WarpPanel class]];
769}
770 
771// Returns the front-most window in the app's window list, or null if there are
772// no open windows.
773NSWindow *get_frontmost_window() {
774 NSApplication *app = [NSApplication sharedApplication];
775 
776 if (windowOrderForTests != NULL) {
777 if ([windowOrderForTests count] == 0) {
778 return NULL;
779 }
780 return [app windowWithWindowNumber:[[windowOrderForTests lastObject] intValue]];
781 }
782 
783 __block NSWindow *frontmost_window = NULL;
784 [app enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
785 usingBlock:^(NSWindow *window, BOOL *stop) {
786 frontmost_window = window;
787 *stop = YES;
788 }];
789 return frontmost_window;
790}
791 
792// |sends accessibility notification and sets appropriate a11y-related fields.
793// @param window - id of the window for which the a11y content is set
794// @param value - the value of the hovered field
795// @param help - helper text (the difference between this and value is mostly in semantics)
796// @param warpRole - the role of the given element (we're using our own, internally defined roles,
797// check strato_ui::accessibility)
798// @param setFrame - boolean value that determines whether the passed frame should be set
799// @param frame - rectangle that describes where the actual highlighted element is on the screen
800void set_accessibility_contents(id window, NSString *value, NSString *help, NSString *warpRole,
801 BOOL setFrame, NSRect frame) {
802 // Setting the standard parameters used for indicating accessibility features
803 [window setAccessibilityLabel:help];
804 [window setAccessibilityValue:value];
805 // "use" the role variable temporarily until we re-introduce its usage.
806 (void)warpRole;
807 [window setAccessibilityValueDescription:value];
808 if (setFrame) {
809 [window setAccessibilityFrame:frame];
810 }
811 
812 [window setAccessibilityElement:YES];
813 [window setAccessibilityFocused:YES];
814 
815 // Sending an Accessibility notification with highest priority, effecivaly making our content
816 // be most important and read first.
817 id objects[] = {[NSString stringWithFormat:@"%@ %@", value, help], @"90" /* high priority */};
818 id keys[] = {NSAccessibilityAnnouncementKey, NSAccessibilityPriorityKey};
819 NSUInteger count = sizeof(objects) / sizeof(id);
820 NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:count];
821 NSAccessibilityPostNotificationWithUserInfo(
822 window, NSAccessibilityAnnouncementRequestedNotification, userInfo);
823}
824 
825void set_window_bounds(id window, NSRect frame) { [window setFrame:frame display:YES]; }
826 
827void open_file_path(NSString *pathString) {
828 NSString *path = [pathString stringByExpandingTildeInPath];
829 NSURL *url = [[NSURL fileURLWithPath:path] standardizedURL];
830 [[NSWorkspace sharedWorkspace] openURL:url];
831}
832 
833void open_file_path_in_explorer(NSString *pathString) {
834 NSString *path = [pathString stringByExpandingTildeInPath];
835 NSURL *url = [[NSURL fileURLWithPath:path] standardizedURL];
836 
837 // Dispatch this asynchronously on the main thread to avoid double-borrow
838 // errors; see https://warpdotdev.sentry.io/issues/4264975772.
839 dispatch_async(dispatch_get_main_queue(), ^{
840 [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[ url ]];
841 });
842}
843 
844void open_file_picker(void *callback, NSArray<NSString *> *fileTypes, BOOL allowFiles,
845 BOOL allowFolders, BOOL allowMultiSelection) {
846 // Create an open panel.
847 NSOpenPanel *openPanel = [NSOpenPanel openPanel];
848 // Set restrictions on which types of files users can pick.
849 [openPanel setAllowsMultipleSelection:allowMultiSelection];
850 [openPanel setCanChooseDirectories:allowFolders];
851 [openPanel setCanCreateDirectories:allowFolders];
852 [openPanel setCanChooseFiles:allowFiles];
853 
854 if (@available(macOS 11, *)) {
855 NSMutableArray *contentTypes = [NSMutableArray array];
856 for (NSString *fileType in fileTypes) {
857 if ([fileType isEqualToString:@"Image"]) {
858 [contentTypes addObject:UTTypeImage];
859 } else if ([fileType isEqualToString:@"Markdown"]) {
860 UTType *markdownType = [UTType typeWithFilenameExtension:@"md"];
861 [contentTypes addObject:markdownType];
862 } else if ([fileType isEqualToString:@"Yaml"]) {
863 [contentTypes addObject:UTTypeYAML];
864 }
865 }
866 
867 [openPanel setAllowedContentTypes:contentTypes];
868 } else {
869 NSMutableArray *contentTypes = [NSMutableArray array];
870 for (NSString *fileType in fileTypes) {
871 if ([fileType isEqualToString:@"Image"]) {
872 [contentTypes addObjectsFromArray:[NSImage imageTypes]];
873 } else if ([fileType isEqualToString:@"Markdown"]) {
874 [contentTypes addObject:@"md"];
875 } else if ([fileType isEqualToString:@"Yaml"]) {
876 [contentTypes addObject:@"yaml"];
877 [contentTypes addObject:@"yml"];
878 }
879 }
880 
881 [openPanel setAllowedFileTypes:contentTypes];
882 }
883 
884 // Open panel as sheet on main window.
885 [openPanel beginWithCompletionHandler:^(NSInteger result) {
886 // warp_open_panel_file_selected must be called unconditionally to avoid a memory leak
887 if (result == NSModalResponseOK) {
888 dispatch_async(dispatch_get_main_queue(), ^{
889 warp_open_panel_file_selected([openPanel URLs], callback);
890 });
891 } else {
892 dispatch_async(dispatch_get_main_queue(), ^{
893 warp_open_panel_file_selected([NSArray array], callback);
894 });
895 }
896 }];
897}
898 
899void open_save_file_picker(void *callback, NSString *defaultFilename, NSString *defaultDirectory) {
900 NSSavePanel *savePanel = [NSSavePanel savePanel];
901 
902 // Hide the NSSavePanel title bar entirely.
903 [savePanel setTitlebarAppearsTransparent:YES];
904 [savePanel setTitleVisibility:NSWindowTitleHidden];
905 
906 [savePanel setNameFieldStringValue:defaultFilename];
907 
908 if ([defaultDirectory length] > 0) {
909 NSURL *directoryURL = [NSURL fileURLWithPath:defaultDirectory];
910 [savePanel setDirectoryURL:directoryURL];
911 }
912 
913 // Show save panel as sheet
914 [savePanel beginWithCompletionHandler:^(NSInteger result) {
915 // warp_save_panel_file_selected must be called unconditionally to avoid a memory leak
916 if (result == NSModalResponseOK) {
917 dispatch_async(dispatch_get_main_queue(), ^{
918 warp_save_panel_file_selected([savePanel URL], callback);
919 });
920 } else {
921 dispatch_async(dispatch_get_main_queue(), ^{
922 warp_save_panel_file_selected(nil, callback);
923 });
924 }
925 }];
926}
927 
928// Open a given url.
929void open_url(NSString *urlString) {
930 NSURL *url = [NSURL URLWithString:urlString];
931 [[NSWorkspace sharedWorkspace] openURL:url];
932}
933 
934void hide_app() {
935 NSApplication *app = [NSApplication sharedApplication];
936 
937 if (![app isHidden]) {
938 [app hide:nil];
939 }
940}
941 
942void activate_app() {
943 NSApplication *app = [NSApplication sharedApplication];
944 
945 if (![app isActive]) {
946 [app activateIgnoringOtherApps:YES];
947 }
948}
949 
950void show_window_and_focus_app(WarpWindow<WarpWindowProtocol> *window, bool bringToFront) {
951 previouslyActiveAppPID = [PreviousStateHelper storePreviousState];
952 
953 // Make sure the window is included in the application's window list. This
954 // is automatically done by the framework for normal windows, but we need to
955 // do this explicitly for hotkey windows, as they subclass NSPanel (which
956 // requires explicit registration in the window list).
957 [NSApp addWindowsItem:window title:[window title] filename:NO];
958 
959 if (bringToFront) {
960 [window makeKeyAndOrderFront:nil];
961 } else {
962 [window makeKeyWindow];
963 }
964 
965 // There are some edge cases with the hot key window in a multi-screen setup that toggling
966 // the hotkey will activate the app and only bring forward a normal window. This code makes
967 // sure that we are bringing forward the hotkey window
968 if (![[NSApplication sharedApplication] isActive]) {
969 // Creates a static observer so it can be referenced in the observer callback.
970 __block id observer;
971 observer = [[NSNotificationCenter defaultCenter]
972 addObserverForName:NSApplicationDidBecomeActiveNotification
973 object:nil
974 queue:NULL
975 usingBlock:^(NSNotification *note __unused) {
976 // Make key and order front again after the app has activated to make
977 // sure the toggled window is focused after initializing.
978 [window makeKeyAndOrderFront:nil];
979 [[NSNotificationCenter defaultCenter] removeObserver:observer];
980 }];
981 
982 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
983 }
984}
985 
986void hide_window(WarpWindow<WarpWindowProtocol> *window) {
987 NSRunningApplication *runningApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
988 
989 // Don't activate to previous state if:
990 // 1. user is explicitly switching app by clicking into another app or hitting cmd-tab.
991 // We only want to focus the previous app if the window was hidden while our app is active.
992 // 2. The window was hidden because a modal popped up. We don't want to hide the modal.
993 NSWindow *activeWindow = [[NSApplication sharedApplication] keyWindow];
994 if ([runningApp.bundleIdentifier isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] &&
995 ![activeWindow isModalPanel]) {
996 [PreviousStateHelper activatePreviousState:previouslyActiveAppPID];
997 }
998 previouslyActiveAppPID = nil;
999 
1000 // Order out removes window from the screen but still maintains the NSWindow object.
1001 [window orderOut:nil];
1002}
1003 
1004void set_window_title(id window, NSString *title) {
1005 if ([window isKindOfClass:[WarpPanel class]] && [window isVisible]) {
1006 // For the hotkey window (which is an NSPanel), we need to explicitly
1007 // add the panel to the windows list. `changeWindowsItem` will add the
1008 // panel to the list if it isn't already there.
1009 [NSApp changeWindowsItem:window title:title filename:NO];
1010 }
1011 
1012 [window setTitle:title];
1013}
1014 
1015void set_titlebar_height(id window, CGFloat height) {
1016 if ([window conformsToProtocol:@protocol(WarpWindowProtocol)]) {
1017 [(id<WarpWindowProtocol>)window configureTitlebarHeight:height];
1018 }
1019}
1020 
1021void position_and_order_front(WarpWindow<WarpWindowProtocol> *window) {
1022 // Called from Rust to position ourselves and order front.
1023 // TODO: use NSUserDefaults to remember window locations.
1024 // We cascade relative to the front-most window. This will typically be the
1025 // main/key window, but when the app is inactive, we want to cascade
1026 // relative to the top window in the application's stack.
1027 NSWindow *mainWindow = get_frontmost_window();
1028 if (!mainWindow) {
1029 // No window onscreen.
1030 [window center];
1031 } else {
1032 // Cascade relative to the main window.
1033 // The first cascade does not move the main window as the argument is 0.
1034 // The next cascade moves this window.
1035 NSPoint cascadePoint = [mainWindow cascadeTopLeftFromPoint:NSZeroPoint];
1036 [window cascadeTopLeftFromPoint:cascadePoint];
1037 }
1038 
1039 [window makeKeyAndOrderFront:nil];
1040}
1041 
1042void position_at_given_location(WarpWindow<WarpWindowProtocol> *window, NSPoint origin) {
1043 // Use an explicit top-left point for drag handoff windows. Unlike the cascade helper above,
1044 // tab transfer needs deterministic placement at a Rust-provided screen position.
1045 NSPoint topLeft = NSMakePoint(origin.x, origin.y + [window frame].size.height);
1046 [window setFrameTopLeftPoint:topLeft];
1047 [window makeKeyAndOrderFront:nil];
1048}
1049 
1050void order_front_without_focus(WarpWindow<WarpWindowProtocol> *window, NSPoint origin) {
1051 [window setFrameOrigin:origin];
1052 [window orderFront:nil];
1053}
1054