StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 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. |
| 17 | static const void *kWarpWindowDelegateAssocKey = &kWarpWindowDelegateAssocKey; |
| 18 | |
| 19 | NSWindowStyleMask warpWindowMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | |
| 20 | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled; |
| 21 | |
| 22 | // The default macOS titlebar height (in points). |
| 23 | static const CGFloat DEFAULT_TITLEBAR_HEIGHT = 28.0; |
| 24 | |
| 25 | // A back-to-front ordered array of windows, identified by their `windowNumber` |
| 26 | // property. |
| 27 | NSMutableArray<NSNumber *> *windowOrderForTests; |
| 28 | dispatch_once_t windowOrderOnce; |
| 29 | |
| 30 | FullscreenWindowManager *fullscreenManager; |
| 31 | dispatch_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. |
| 80 | void warp_dealloc_window(id self); |
| 81 | void warp_dispatch_standard_action(id self, NSInteger tag); |
| 82 | void warp_app_window_moved(id self, NSRect rect); |
| 83 | void warp_open_panel_file_selected(id urls, void *callback); |
| 84 | void warp_save_panel_file_selected(id url, void *callback); |
| 85 | |
| 86 | NSNumber *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. |
| 200 | static 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. |
| 209 | static 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. |
| 285 | void 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 | |
| 678 | void 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]. |
| 697 | static 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. |
| 706 | id 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. |
| 737 | id 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 | |
| 767 | BOOL 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. |
| 773 | NSWindow *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 |
| 800 | void 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 | |
| 825 | void set_window_bounds(id window, NSRect frame) { [window setFrame:frame display:YES]; } |
| 826 | |
| 827 | void open_file_path(NSString *pathString) { |
| 828 | NSString *path = [pathString stringByExpandingTildeInPath]; |
| 829 | NSURL *url = [[NSURL fileURLWithPath:path] standardizedURL]; |
| 830 | [[NSWorkspace sharedWorkspace] openURL:url]; |
| 831 | } |
| 832 | |
| 833 | void 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 | |
| 844 | void 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 | |
| 899 | void 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. |
| 929 | void open_url(NSString *urlString) { |
| 930 | NSURL *url = [NSURL URLWithString:urlString]; |
| 931 | [[NSWorkspace sharedWorkspace] openURL:url]; |
| 932 | } |
| 933 | |
| 934 | void hide_app() { |
| 935 | NSApplication *app = [NSApplication sharedApplication]; |
| 936 | |
| 937 | if (![app isHidden]) { |
| 938 | [app hide:nil]; |
| 939 | } |
| 940 | } |
| 941 | |
| 942 | void activate_app() { |
| 943 | NSApplication *app = [NSApplication sharedApplication]; |
| 944 | |
| 945 | if (![app isActive]) { |
| 946 | [app activateIgnoringOtherApps:YES]; |
| 947 | } |
| 948 | } |
| 949 | |
| 950 | void 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 | |
| 986 | void 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 | |
| 1004 | void 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 | |
| 1015 | void set_titlebar_height(id window, CGFloat height) { |
| 1016 | if ([window conformsToProtocol:@protocol(WarpWindowProtocol)]) { |
| 1017 | [(id<WarpWindowProtocol>)window configureTitlebarHeight:height]; |
| 1018 | } |
| 1019 | } |
| 1020 | |
| 1021 | void 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 | |
| 1042 | void 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 | |
| 1050 | void order_front_without_focus(WarpWindow<WarpWindowProtocol> *window, NSPoint origin) { |
| 1051 | [window setFrameOrigin:origin]; |
| 1052 | [window orderFront:nil]; |
| 1053 | } |
| 1054 |