//======================================================================== // GLFW 3.4 macOS - www.glfw.org //------------------------------------------------------------------------ // Copyright (c) 2009-2019 Camilla Löwy // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would // be appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not // be misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. // //======================================================================== // It is fine to use C99 in this file because it will not be built with VS //======================================================================== #include "../kitty/monotonic.h" #include "glfw3.h" #include "internal.h" #include #import #import #include #include #include #include #define debug debug_rendering #define UTI_ROUNDTRIP_PREFIX @"uti-is-typical-apple-nih." static NSString* mime_to_uti(const char *mime) { if (strcmp(mime, "text/plain") == 0) return NSPasteboardTypeString; UTType *t = [UTType typeWithMIMEType:@(mime)]; // auto-released if (t != nil && !t.dynamic) return t.identifier; size_t sz = strlen(mime); NSMutableString *hex = [NSMutableString stringWithCapacity:sz * 2]; for (NSUInteger i = 0; i < sz; i++) [hex appendFormat:@"%02x", (unsigned char)mime[i]]; return [NSString stringWithFormat:@"%@%@", UTI_ROUNDTRIP_PREFIX, hex]; // auto-released } static char hexval(char c, bool *ok) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; *ok = false; return 0; } static const char* uti_to_mime(NSString *uti) { if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain"; if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) { NSString *hexPart = [uti substringFromIndex:UTI_ROUNDTRIP_PREFIX.length]; NSUInteger hexLen = hexPart.length; if (hexLen == 0 || hexLen % 2 != 0) { return ""; } const char *hex = [hexPart UTF8String]; static char buf[4096]; size_t i, j; for (i = 0, j = 0; i < hexLen && j < sizeof(buf)-1; i += 2, j++) { char hi = hex[i], lo = hex[i + 1]; bool ok = true; buf[j] = (hexval(hi, &ok) << 4) | hexval(lo, &ok); if (!ok) return ""; } buf[j] = 0; return buf; } if (@available(macOS 11.0, *)) { UTType *t = [UTType typeWithIdentifier:uti]; // auto-released if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String]; } return ""; } static const char* polymorphic_string_as_utf8(id string) { if (string == nil) return "(nil)"; NSString* characters; if ([string isKindOfClass:[NSAttributedString class]]) characters = [string string]; else characters = (NSString*) string; return [characters UTF8String]; } static bool forward_dictation_selector_to_app(SEL selector, id sender) { static SEL start_dictation_selector = NULL, stop_dictation_selector = NULL; if (start_dictation_selector == NULL) { start_dictation_selector = NSSelectorFromString(@"startDictation:"); stop_dictation_selector = NSSelectorFromString(@"stopDictation:"); } if (selector != start_dictation_selector && selector != stop_dictation_selector) return false; if ([NSApp respondsToSelector:selector]) { debug_key("Forwarding %s to NSApp\n", [NSStringFromSelector(selector) UTF8String]); [NSApp performSelector:selector withObject:sender]; return true; } return false; } static uint32_t vk_code_to_functional_key_code(uint8_t key_code) { // {{{ switch(key_code) { /* start vk to functional (auto generated by gen-key-constants.py do not edit) */ case 0x35: return GLFW_FKEY_ESCAPE; case 0x24: return GLFW_FKEY_ENTER; case 0x30: return GLFW_FKEY_TAB; case 0x33: return GLFW_FKEY_BACKSPACE; case 0x72: return GLFW_FKEY_INSERT; case 0x75: return GLFW_FKEY_DELETE; case 0x7b: return GLFW_FKEY_LEFT; case 0x7c: return GLFW_FKEY_RIGHT; case 0x7e: return GLFW_FKEY_UP; case 0x7d: return GLFW_FKEY_DOWN; case 0x74: return GLFW_FKEY_PAGE_UP; case 0x79: return GLFW_FKEY_PAGE_DOWN; case 0x73: return GLFW_FKEY_HOME; case 0x77: return GLFW_FKEY_END; case 0x39: return GLFW_FKEY_CAPS_LOCK; case 0x47: return GLFW_FKEY_NUM_LOCK; case 0x6e: return GLFW_FKEY_MENU; case 0x7a: return GLFW_FKEY_F1; case 0x78: return GLFW_FKEY_F2; case 0x63: return GLFW_FKEY_F3; case 0x76: return GLFW_FKEY_F4; case 0x60: return GLFW_FKEY_F5; case 0x61: return GLFW_FKEY_F6; case 0x62: return GLFW_FKEY_F7; case 0x64: return GLFW_FKEY_F8; case 0x65: return GLFW_FKEY_F9; case 0x6d: return GLFW_FKEY_F10; case 0x67: return GLFW_FKEY_F11; case 0x6f: return GLFW_FKEY_F12; case 0x69: return GLFW_FKEY_F13; case 0x6b: return GLFW_FKEY_F14; case 0x71: return GLFW_FKEY_F15; case 0x6a: return GLFW_FKEY_F16; case 0x40: return GLFW_FKEY_F17; case 0x4f: return GLFW_FKEY_F18; case 0x50: return GLFW_FKEY_F19; case 0x5a: return GLFW_FKEY_F20; case 0x52: return GLFW_FKEY_KP_0; case 0x53: return GLFW_FKEY_KP_1; case 0x54: return GLFW_FKEY_KP_2; case 0x55: return GLFW_FKEY_KP_3; case 0x56: return GLFW_FKEY_KP_4; case 0x57: return GLFW_FKEY_KP_5; case 0x58: return GLFW_FKEY_KP_6; case 0x59: return GLFW_FKEY_KP_7; case 0x5b: return GLFW_FKEY_KP_8; case 0x5c: return GLFW_FKEY_KP_9; case 0x41: return GLFW_FKEY_KP_DECIMAL; case 0x4b: return GLFW_FKEY_KP_DIVIDE; case 0x43: return GLFW_FKEY_KP_MULTIPLY; case 0x4e: return GLFW_FKEY_KP_SUBTRACT; case 0x45: return GLFW_FKEY_KP_ADD; case 0x4c: return GLFW_FKEY_KP_ENTER; case 0x51: return GLFW_FKEY_KP_EQUAL; case 0x38: return GLFW_FKEY_LEFT_SHIFT; case 0x3b: return GLFW_FKEY_LEFT_CONTROL; case 0x3a: return GLFW_FKEY_LEFT_ALT; case 0x37: return GLFW_FKEY_LEFT_SUPER; case 0x3c: return GLFW_FKEY_RIGHT_SHIFT; case 0x3e: return GLFW_FKEY_RIGHT_CONTROL; case 0x3d: return GLFW_FKEY_RIGHT_ALT; case 0x36: return GLFW_FKEY_RIGHT_SUPER; /* end vk to functional */ default: return 0; } } // }}} static uint32_t vk_code_to_unicode(uint8_t key_code) { // {{{ switch(key_code) { /* start vk to unicode (auto generated by gen-key-constants.py do not edit) */ case 0x0: return 0x61; case 0x1: return 0x73; case 0x2: return 0x64; case 0x3: return 0x66; case 0x4: return 0x68; case 0x5: return 0x67; case 0x6: return 0x7a; case 0x7: return 0x78; case 0x8: return 0x63; case 0x9: return 0x76; case 0xb: return 0x62; case 0xc: return 0x71; case 0xd: return 0x77; case 0xe: return 0x65; case 0xf: return 0x72; case 0x10: return 0x79; case 0x11: return 0x74; case 0x12: return 0x31; case 0x13: return 0x32; case 0x14: return 0x33; case 0x15: return 0x34; case 0x16: return 0x36; case 0x17: return 0x35; case 0x18: return 0x3d; case 0x19: return 0x39; case 0x1a: return 0x37; case 0x1b: return 0x2d; case 0x1c: return 0x38; case 0x1d: return 0x30; case 0x1e: return 0x5d; case 0x1f: return 0x6f; case 0x20: return 0x75; case 0x21: return 0x5b; case 0x22: return 0x69; case 0x23: return 0x70; case 0x25: return 0x6c; case 0x26: return 0x6a; case 0x27: return 0x27; case 0x28: return 0x6b; case 0x29: return 0x3b; case 0x2a: return 0x5c; case 0x2b: return 0x2c; case 0x2c: return 0x2f; case 0x2d: return 0x6e; case 0x2e: return 0x6d; case 0x2f: return 0x2e; case 0x31: return 0x20; case 0x32: return 0x60; /* end vk to unicode */ default: return 0; } } // }}} static uint32_t mac_ucode_to_functional(uint32_t key_code) { // {{{ switch(key_code) { /* start macu to functional (auto generated by gen-key-constants.py do not edit) */ case NSCarriageReturnCharacter: return GLFW_FKEY_ENTER; case NSTabCharacter: return GLFW_FKEY_TAB; case NSBackspaceCharacter: return GLFW_FKEY_BACKSPACE; case NSInsertFunctionKey: return GLFW_FKEY_INSERT; case NSDeleteFunctionKey: return GLFW_FKEY_DELETE; case NSLeftArrowFunctionKey: return GLFW_FKEY_LEFT; case NSRightArrowFunctionKey: return GLFW_FKEY_RIGHT; case NSUpArrowFunctionKey: return GLFW_FKEY_UP; case NSDownArrowFunctionKey: return GLFW_FKEY_DOWN; case NSPageUpFunctionKey: return GLFW_FKEY_PAGE_UP; case NSPageDownFunctionKey: return GLFW_FKEY_PAGE_DOWN; case NSHomeFunctionKey: return GLFW_FKEY_HOME; case NSEndFunctionKey: return GLFW_FKEY_END; case NSScrollLockFunctionKey: return GLFW_FKEY_SCROLL_LOCK; case NSClearLineFunctionKey: return GLFW_FKEY_NUM_LOCK; case NSPrintScreenFunctionKey: return GLFW_FKEY_PRINT_SCREEN; case NSPauseFunctionKey: return GLFW_FKEY_PAUSE; case NSMenuFunctionKey: return GLFW_FKEY_MENU; case NSF1FunctionKey: return GLFW_FKEY_F1; case NSF2FunctionKey: return GLFW_FKEY_F2; case NSF3FunctionKey: return GLFW_FKEY_F3; case NSF4FunctionKey: return GLFW_FKEY_F4; case NSF5FunctionKey: return GLFW_FKEY_F5; case NSF6FunctionKey: return GLFW_FKEY_F6; case NSF7FunctionKey: return GLFW_FKEY_F7; case NSF8FunctionKey: return GLFW_FKEY_F8; case NSF9FunctionKey: return GLFW_FKEY_F9; case NSF10FunctionKey: return GLFW_FKEY_F10; case NSF11FunctionKey: return GLFW_FKEY_F11; case NSF12FunctionKey: return GLFW_FKEY_F12; case NSF13FunctionKey: return GLFW_FKEY_F13; case NSF14FunctionKey: return GLFW_FKEY_F14; case NSF15FunctionKey: return GLFW_FKEY_F15; case NSF16FunctionKey: return GLFW_FKEY_F16; case NSF17FunctionKey: return GLFW_FKEY_F17; case NSF18FunctionKey: return GLFW_FKEY_F18; case NSF19FunctionKey: return GLFW_FKEY_F19; case NSF20FunctionKey: return GLFW_FKEY_F20; case NSF21FunctionKey: return GLFW_FKEY_F21; case NSF22FunctionKey: return GLFW_FKEY_F22; case NSF23FunctionKey: return GLFW_FKEY_F23; case NSF24FunctionKey: return GLFW_FKEY_F24; case NSF25FunctionKey: return GLFW_FKEY_F25; case NSF26FunctionKey: return GLFW_FKEY_F26; case NSF27FunctionKey: return GLFW_FKEY_F27; case NSF28FunctionKey: return GLFW_FKEY_F28; case NSF29FunctionKey: return GLFW_FKEY_F29; case NSF30FunctionKey: return GLFW_FKEY_F30; case NSF31FunctionKey: return GLFW_FKEY_F31; case NSF32FunctionKey: return GLFW_FKEY_F32; case NSF33FunctionKey: return GLFW_FKEY_F33; case NSF34FunctionKey: return GLFW_FKEY_F34; case NSF35FunctionKey: return GLFW_FKEY_F35; case NSEnterCharacter: return GLFW_FKEY_KP_ENTER; /* end macu to functional */ default: return 0; } } // }}} static bool is_surrogate(UniChar uc) { return (uc - 0xd800u) < 2048u; } static uint32_t get_first_codepoint(UniChar *utf16, UniCharCount num) { if (!num) return 0; if (!is_surrogate(*utf16)) return *utf16; if (CFStringIsSurrogateHighCharacter(*utf16) && num > 1 && CFStringIsSurrogateLowCharacter(utf16[1])) return CFStringGetLongCharacterForSurrogatePair(utf16[0], utf16[1]); return 0; } static bool is_pua_char(uint32_t ch) { return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF); } uint32_t vk_to_unicode_key_with_current_layout(uint16_t keycode) { UInt32 dead_key_state = 0; UniChar characters[256]; UniCharCount character_count = 0; uint32_t ans = vk_code_to_functional_key_code(keycode); if (ans) return ans; if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes], keycode, kUCKeyActionDisplay, 0, LMGetKbdType(), kUCKeyTranslateNoDeadKeysBit, &dead_key_state, arraysz(characters), &character_count, characters) == noErr) { uint32_t cp = get_first_codepoint(characters, character_count); if (cp) { if (cp < 32 || (0xF700 <= cp && cp <= 0xF8FF)) return mac_ucode_to_functional(cp); if (cp >= 32 && !is_pua_char(cp)) return cp; } } return vk_code_to_unicode(keycode); } // Returns the style mask corresponding to the window settings // static NSUInteger getStyleMask(_GLFWwindow* window) { NSUInteger styleMask = NSWindowStyleMaskMiniaturizable; if (window->ns.titlebar_hidden) styleMask |= NSWindowStyleMaskFullSizeContentView; if (window->monitor || !window->decorated) { styleMask |= NSWindowStyleMaskBorderless; } else { styleMask |= NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; } if (window->resizable) styleMask |= NSWindowStyleMaskResizable; return styleMask; } static void requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) { if (!callback) { w->ns.renderFrameRequested = false; w->ns.renderFrameCallback = NULL; return; } w->ns.renderFrameCallback = callback; w->ns.renderFrameRequested = true; _glfwRequestRenderFrame(w); } void _glfwRestartDisplayLinks(void) { _GLFWwindow* window; for (window = _glfw.windowListHead; window; window = window->next) { if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) { requestRenderFrame(window, window->ns.renderFrameCallback); } } } // Returns whether the cursor is in the content area of the specified window // static bool cursorInContentArea(_GLFWwindow* window) { const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream]; return [window->ns.view mouse:pos inRect:[window->ns.view frame]]; } // Hides the cursor if not already hidden // static void hideCursor(_GLFWwindow* window UNUSED) { if (!_glfw.ns.cursorHidden) { [NSCursor hide]; _glfw.ns.cursorHidden = true; } } // Shows the cursor if not already shown // static void showCursor(_GLFWwindow* window UNUSED) { if (_glfw.ns.cursorHidden) { [NSCursor unhide]; _glfw.ns.cursorHidden = false; } } // Updates the cursor image according to its cursor mode // static void updateCursorImage(_GLFWwindow* window) { if (window->cursorMode == GLFW_CURSOR_NORMAL) { showCursor(window); if (window->cursor) [(NSCursor*) window->cursor->ns.object set]; else [[NSCursor arrowCursor] set]; } else hideCursor(window); } // Apply chosen cursor mode to a focused window // static void updateCursorMode(_GLFWwindow* window) { if (window->cursorMode == GLFW_CURSOR_DISABLED) { _glfw.ns.disabledCursorWindow = window; _glfwPlatformGetCursorPos(window, &_glfw.ns.restoreCursorPosX, &_glfw.ns.restoreCursorPosY); _glfwCenterCursorInContentArea(window); CGAssociateMouseAndMouseCursorPosition(false); } else if (_glfw.ns.disabledCursorWindow == window) { _glfw.ns.disabledCursorWindow = NULL; CGAssociateMouseAndMouseCursorPosition(true); _glfwPlatformSetCursorPos(window, _glfw.ns.restoreCursorPosX, _glfw.ns.restoreCursorPosY); } if (cursorInContentArea(window)) updateCursorImage(window); } // Make the specified window and its video mode active on its monitor // static void acquireMonitor(_GLFWwindow* window) { _glfwSetVideoModeNS(window->monitor, &window->videoMode); const CGRect bounds = CGDisplayBounds(window->monitor->ns.displayID); const NSRect frame = NSMakeRect(bounds.origin.x, _glfwTransformYNS(bounds.origin.y + bounds.size.height - 1), bounds.size.width, bounds.size.height); [window->ns.object setFrame:frame display:YES]; _glfwInputMonitorWindow(window->monitor, window); } // Remove the window and restore the original video mode // static void releaseMonitor(_GLFWwindow* window) { if (window->monitor->window != window) return; _glfwInputMonitorWindow(window->monitor, NULL); _glfwRestoreVideoModeNS(window->monitor); } // Translates macOS key modifiers into GLFW ones // static int translateFlags(NSUInteger flags) { int mods = 0; if (flags & NSEventModifierFlagShift) mods |= GLFW_MOD_SHIFT; if (flags & NSEventModifierFlagControl) mods |= GLFW_MOD_CONTROL; if (flags & NSEventModifierFlagOption) mods |= GLFW_MOD_ALT; if (flags & NSEventModifierFlagCommand) mods |= GLFW_MOD_SUPER; if (flags & NSEventModifierFlagCapsLock) mods |= GLFW_MOD_CAPS_LOCK; return mods; } static const char* format_mods(int mods) { static char buf[128]; char *p = buf, *s; #define pr(x) p += snprintf(p, sizeof(buf) - (p - buf) - 1, x) pr("mods: "); s = p; if (mods & GLFW_MOD_CONTROL) pr("ctrl+"); if (mods & GLFW_MOD_ALT) pr("alt+"); if (mods & GLFW_MOD_SHIFT) pr("shift+"); if (mods & GLFW_MOD_SUPER) pr("super+"); if (mods & GLFW_MOD_CAPS_LOCK) pr("capslock+"); if (mods & GLFW_MOD_NUM_LOCK) pr("numlock+"); if (p == s) pr("none"); else p--; pr(" "); #undef pr return buf; } static const char* format_text(const char *src) { static char buf[256]; char *p = buf; const char *last_char = buf + sizeof(buf) - 1; if (!src[0]) return ""; while (*src) { int num = snprintf(p, sizeof(buf) - (p - buf), "0x%x ", (unsigned char)*(src++)); if (num < 0) return ""; if (p + num >= last_char) break; p += num; } if (p != buf) *(--p) = 0; return buf; } static const char* safe_name_for_keycode(unsigned int keycode) { const char *ans = _glfwPlatformGetNativeKeyName(keycode); if (!ans) return ""; if ((1 <= ans[0] && ans[0] <= 31) || ans[0] == 127) ans = ""; return ans; } // Translates a macOS keycode to a GLFW keycode // static uint32_t translateKey(uint16_t vk_key, bool apply_keymap) { if (apply_keymap) return vk_to_unicode_key_with_current_layout(vk_key); uint32_t ans = vk_code_to_functional_key_code(vk_key); if (!ans) ans = vk_code_to_unicode(vk_key); return ans; } static NSRect get_window_size_without_border_in_logical_pixels(_GLFWwindow *window) { return [window->ns.object contentRectForFrameRect:[window->ns.object frame]]; } // Defines a constant for empty ranges in NSTextInputClient // static const NSRange kEmptyRange = { NSNotFound, 0 }; // Delegate for window related notifications {{{ @interface GLFWWindowDelegate : NSObject { _GLFWwindow* window; NSArray *_lastScreenStates; } - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow; - (void)request_delayed_cursor_update:(id)sender; @end static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow*, bool, bool); static void _glfwUpdateNotchCover(_GLFWwindow*); @implementation GLFWWindowDelegate - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow { self = [super init]; if (self != nil) { window = initWindow; _lastScreenStates = [self captureScreenStates]; window->ns.live_resize_in_progress = false; } return self; } - (NSArray *)captureScreenStates { NSMutableArray *states = [NSMutableArray array]; for (NSScreen *screen in [NSScreen screens]) { // Use the screen's deviceDescription, which contains a stable ID. [states addObject:screen.deviceDescription]; } return [states copy]; } - (void)cleanup { [_lastScreenStates release]; _lastScreenStates = nil; } - (BOOL)windowShouldClose:(id)sender { (void)sender; if (window == nil) return YES; _glfwInputWindowCloseRequest(window); return NO; } - (void)windowDidResize:(NSNotification *)notification { (void)notification; NSArray *currentScreenStates = [self captureScreenStates]; const bool is_screen_change = ![_lastScreenStates isEqualToArray:currentScreenStates]; NSWindowStyleMask sm = [window->ns.object styleMask]; const bool is_fullscreen = (sm & NSWindowStyleMaskFullScreen) != 0; NSRect frame = [window->ns.object frame]; debug_rendering( "windowDidResize() called, is_screen_change: %d is_fullscreen: %d live_resize_in_progress: %d frame: %.1fx%.1f@(%.1f, %.1f)\n", is_screen_change, is_fullscreen, window->ns.live_resize_in_progress, frame.size.width, frame.size.height, frame.origin.x, frame.origin.y); if (is_screen_change) { // This resize likely happened because a screen was added, removed, or changed resolution. [_lastScreenStates release]; _lastScreenStates = [currentScreenStates retain]; } [currentScreenStates release]; if (window->context.client != GLFW_NO_API) [window->context.nsgl.object update]; if (_glfw.ns.disabledCursorWindow == window) _glfwCenterCursorInContentArea(window); const int maximized = [window->ns.object isZoomed]; if (window->ns.maximized != maximized) { window->ns.maximized = maximized; _glfwInputWindowMaximize(window, maximized); } const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect]; if (fbRect.size.width != window->ns.fbWidth || fbRect.size.height != window->ns.fbHeight) { window->ns.fbWidth = (int)fbRect.size.width; window->ns.fbHeight = (int)fbRect.size.height; _glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height); } if (contentRect.size.width != window->ns.width || contentRect.size.height != window->ns.height) { window->ns.width = (int)contentRect.size.width; window->ns.height = (int)contentRect.size.height; _glfwInputWindowSize(window, (int)contentRect.size.width, (int)contentRect.size.height); } // Because of a bug in macOS Tahoe we cannot redraw the window in response // to a resize event that was caused by a screen change as the OpenGL // context is not ready yet. See: https://github.com/kovidgoyal/kitty/issues/8983 if (window->ns.resizeCallback && !is_screen_change && !is_fullscreen && window->ns.live_resize_in_progress) window->ns.resizeCallback((GLFWwindow*)window); } - (void)windowDidMove:(NSNotification *)notification { (void)notification; if (window->context.client != GLFW_NO_API) [window->context.nsgl.object update]; if (_glfw.ns.disabledCursorWindow == window) _glfwCenterCursorInContentArea(window); int x, y; _glfwPlatformGetWindowPos(window, &x, &y); _glfwInputWindowPos(window, x, y); } - (void)windowDidChangeOcclusionState:(NSNotification *)notification { (void)notification; _glfwInputWindowOcclusion(window, !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible)); } - (void)windowDidMiniaturize:(NSNotification *)notification { (void)notification; if (window->monitor) releaseMonitor(window); _glfwInputWindowIconify(window, true); } - (void)windowDidDeminiaturize:(NSNotification *)notification { (void)notification; if (window->monitor) acquireMonitor(window); _glfwInputWindowIconify(window, false); } - (void)windowDidBecomeKey:(NSNotification *)notification { (void)notification; if (_glfw.ns.disabledCursorWindow == window) _glfwCenterCursorInContentArea(window); _glfwInputWindowFocus(window, true); updateCursorMode(window); if (window->cursorMode == GLFW_CURSOR_HIDDEN) hideCursor(window); if (_glfw.ns.disabledCursorWindow != window && cursorInContentArea(window)) { double x = 0, y = 0; _glfwPlatformGetCursorPos(window, &x, &y); _glfwInputCursorPos(window, x, y); } // macOS will send a delayed event to update the cursor to arrow after switching desktops. // So we need to delay and update the cursor once after that. [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3]; } - (void)windowDidResignKey:(NSNotification *)notification { (void)notification; if (window->monitor && window->autoIconify) _glfwPlatformIconifyWindow(window); showCursor(window); _glfwInputWindowFocus(window, false); // IME is cancelled when losing the focus if ([window->ns.view hasMarkedText]) { [[window->ns.view inputContext] discardMarkedText]; [window->ns.view unmarkText]; GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED}; _glfwInputKeyboard(window, &dummy); _glfw.ns.text[0] = 0; } } - (void)windowDidChangeScreen:(NSNotification *)notification { (void)notification; if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) { // Ensure that if the window changed its monitor, CVDisplayLink // is running for the new monitor requestRenderFrame(window, window->ns.renderFrameCallback); } } - (void)request_delayed_cursor_update:(id)sender { (void)sender; if (window) window->ns.delayed_cursor_update_requested = true; } - (void)windowWillEnterFullScreen:(NSNotification *)notification { (void)notification; if (window) window->ns.in_fullscreen_transition = true; } - (void)windowDidEnterFullScreen:(NSNotification *)notification { (void)notification; if (window) window->ns.in_fullscreen_transition = false; [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3]; } - (void)windowWillExitFullScreen:(NSNotification *)notification { (void)notification; if (window) window->ns.in_fullscreen_transition = true; } - (void)windowDidExitFullScreen:(NSNotification *)notification { (void)notification; if (window) { window->ns.in_fullscreen_transition = false; if (window->ns.in_traditional_fullscreen) { // macOS finished its Cocoa exit (cleared NSWindowStyleMaskFullScreen). // Defer restoration to the next run loop iteration because calling // setStyleMask: inside a delegate callback can leave the window in // an intermediate state. setStyleMask: also triggers macOS's // constrainFrameRect:toScreen: and window tiling logic which can // asynchronously reposition the window, so suppress frame // constraints during the restoration (#9572). unsigned long long wid = window->id; NSWindowStyleMask savedMask = window->ns.pre_full_screen_style_mask; CGRect savedFrame = window->ns.pre_traditional_fullscreen_frame; window->ns.in_traditional_fullscreen = false; _glfwUpdateNotchCover(window); window->ns.suppress_frame_constraints = true; dispatch_async(dispatch_get_main_queue(), ^{ _GLFWwindow *w = NULL; for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) { if (ww->id == wid) { w = ww; break; } } if (w) { NSWindow *nswindow = w->ns.object; [nswindow setStyleMask: savedMask]; [nswindow setFrame: savedFrame display:YES]; update_titlebar_button_visibility_after_fullscreen_transition(w, true, false); [nswindow makeFirstResponder:w->ns.view]; NSNotification *resize = [NSNotification notificationWithName:NSWindowDidResizeNotification object:nswindow]; [w->ns.delegate performSelector:@selector(windowDidResize:) withObject:resize afterDelay:0]; } // Lift the constraint guard after a delay, even if the window // was not found (destroyed), to keep the flag consistent (#9572). dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ _GLFWwindow *w2 = NULL; for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) { if (ww->id == wid) { w2 = ww; break; } } if (w2) w2->ns.suppress_frame_constraints = false; }); }); } } [self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3]; } @end // }}} // Text input context class for the GLFW content view {{{ @interface GLFWTextInputContext : NSTextInputContext @end @implementation GLFWTextInputContext - (void)doCommandBySelector:(SEL)selector { // interpretKeyEvents: May call insertText: or doCommandBySelector:. // With the default macOS keybindings, pressing certain key combinations // (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound. debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]); if (forward_dictation_selector_to_app(selector, nil)) return; } @end // }}} // File Promise Provider Delegate for async drag data {{{ // Structure to hold async drag state @interface GLFWFilePromiseProviderDelegate : NSObject { GLFWid windowId, instanceId; char* mimeType; // MIME type for this provider NSFileHandle *file_handle; NSURL *file_url; void (^completion_handler)(NSError*); } - (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid)iid; - (void)request_drag_data; - (void)end_transfer:(int)errorCode; - (void)end_transfer_with_error:(NSError*)err; - (bool)is_mimetype:(const char*)mime_type; @end @interface GLFWDraggingSource : NSObject { NSPoint start_point, current_point; } @end // }}} // Content view class for the GLFW window {{{ @interface GLFWContentView : NSView { _GLFWwindow* window; NSTrackingArea* trackingArea; GLFWTextInputContext* input_context; NSMutableAttributedString* markedText; NSRect markedRect; bool marked_text_cleared_by_insert; int in_key_handler; NSString *input_source_at_last_key_event; GLFWDraggingSource *dragging_source; } - (void) removeGLFWWindow; - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow; - (GLFWDraggingSource*)draggingSource; @end @implementation GLFWContentView - (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow { self = [super init]; if (self != nil) { window = initWindow; trackingArea = nil; input_context = [[GLFWTextInputContext alloc] initWithClient:self]; dragging_source = [[GLFWDraggingSource alloc] init]; markedText = [[NSMutableAttributedString alloc] init]; markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0); input_source_at_last_key_event = nil; in_key_handler = 0; self.identifier = @"kitty-content-view"; [self updateTrackingAreas]; char tab_mime[64], window_mime[64]; snprintf(tab_mime, sizeof(tab_mime), "application/net.kovidgoyal.kitty-tab-%d", getpid()); snprintf(window_mime, sizeof(window_mime), "application/net.kovidgoyal.kitty-window-%d", getpid()); NSMutableArray *types = [NSMutableArray arrayWithObjects: NSPasteboardTypeFileURL, NSPasteboardTypeString, NSPasteboardTypeURL, NSPasteboardTypeColor, NSPasteboardTypeFont, NSPasteboardTypeHTML, NSPasteboardTypePDF, NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeSound, NSPasteboardTypeTIFF, UTTypeData.identifier, UTTypeItem.identifier, UTTypeContent.identifier, mime_to_uti(tab_mime), mime_to_uti(window_mime), nil]; // Add file promise types [types addObjectsFromArray:[NSFilePromiseReceiver readableDraggedTypes]]; [self registerForDraggedTypes:types]; } return self; } - (void)dealloc { [trackingArea release]; [markedText release]; [dragging_source release]; if (input_source_at_last_key_event) [input_source_at_last_key_event release]; [input_context release]; [super dealloc]; } - (void) removeGLFWWindow { window = NULL; } - (GLFWDraggingSource*)draggingSource { return dragging_source; } - (_GLFWwindow*)glfwWindow { return window; } - (BOOL)isOpaque { return window && [window->ns.object isOpaque]; } - (BOOL)canBecomeKeyView { return YES; } - (BOOL)acceptsFirstResponder { return YES; } - (void) viewWillStartLiveResize { if (!window) return; window->ns.live_resize_in_progress = true; _glfwInputLiveResize(window, true); } - (void)viewDidEndLiveResize { if (!window) return; window->ns.live_resize_in_progress = false; _glfwInputLiveResize(window, false); } - (BOOL)wantsUpdateLayer { return YES; } - (void)updateLayer { if (!window) return; if (window->context.client != GLFW_NO_API) { @try { [window->context.nsgl.object update]; } @catch (NSException *e) { _glfwInputError(GLFW_PLATFORM_ERROR, "Failed to update NSGL Context object with error: %s (%s)", [[e name] UTF8String], [[e reason] UTF8String]); } } _glfwInputWindowDamage(window); } - (void)cursorUpdate:(NSEvent *)event { (void)event; if (window) updateCursorImage(window); } - (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return NO; // changed by Kovid, to follow cocoa platform conventions } - (void)mouseDown:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, translateFlags([event modifierFlags])); } - (void)mouseDragged:(NSEvent *)event { [self mouseMoved:event]; } - (void)mouseUp:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, translateFlags([event modifierFlags])); } - (void)mouseMoved:(NSEvent *)event { if (!window) return; if (window->cursorMode == GLFW_CURSOR_DISABLED) { const double dx = [event deltaX] - window->ns.cursorWarpDeltaX; const double dy = [event deltaY] - window->ns.cursorWarpDeltaY; _glfwInputCursorPos(window, window->virtualCursorPosX + dx, window->virtualCursorPosY + dy); } else { const NSRect contentRect = [window->ns.view frame]; // NOTE: The returned location uses base 0,1 not 0,0 const NSPoint pos = [event locationInWindow]; _glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y); } window->ns.cursorWarpDeltaX = 0; window->ns.cursorWarpDeltaY = 0; if (window->ns.delayed_cursor_update_requested) { window->ns.delayed_cursor_update_requested = false; if (cursorInContentArea(window)) updateCursorImage(window); } } - (void)rightMouseDown:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, GLFW_MOUSE_BUTTON_RIGHT, GLFW_PRESS, translateFlags([event modifierFlags])); } - (void)rightMouseDragged:(NSEvent *)event { [self mouseMoved:event]; } - (void)rightMouseUp:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, GLFW_MOUSE_BUTTON_RIGHT, GLFW_RELEASE, translateFlags([event modifierFlags])); } - (void)otherMouseDown:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, (int) [event buttonNumber], GLFW_PRESS, translateFlags([event modifierFlags])); } - (void)otherMouseDragged:(NSEvent *)event { [self mouseMoved:event]; } - (void)otherMouseUp:(NSEvent *)event { if (!window) return; _glfwInputMouseClick(window, (int) [event buttonNumber], GLFW_RELEASE, translateFlags([event modifierFlags])); } - (void)mouseExited:(NSEvent *)event { (void)event; if (!window) return; _glfwInputCursorEnter(window, false); [[NSCursor arrowCursor] set]; } - (void)mouseEntered:(NSEvent *)event { (void)event; if (!window) return; _glfwInputCursorEnter(window, true); updateCursorImage(window); } - (void)viewDidChangeBackingProperties { if (!window) return; const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect]; if (fbRect.size.width != window->ns.fbWidth || fbRect.size.height != window->ns.fbHeight) { window->ns.fbWidth = (int)fbRect.size.width; window->ns.fbHeight = (int)fbRect.size.height; _glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height); } const float xscale = fbRect.size.width / contentRect.size.width; const float yscale = fbRect.size.height / contentRect.size.height; if (xscale != window->ns.xscale || yscale != window->ns.yscale) { window->ns.xscale = xscale; window->ns.yscale = yscale; _glfwInputWindowContentScale(window, xscale, yscale); if (window->ns.retina && window->ns.layer) [window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]]; } } - (void)drawRect:(NSRect)rect { (void)rect; if (!window) return; _glfwInputWindowDamage(window); } - (void)updateTrackingAreas { if (window && [window->ns.object areCursorRectsEnabled]) [window->ns.object disableCursorRects]; if (trackingArea != nil) { [self removeTrackingArea:trackingArea]; [trackingArea release]; } const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | NSTrackingCursorUpdate | NSTrackingInVisibleRect | NSTrackingAssumeInside; trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; [self addTrackingArea:trackingArea]; [super updateTrackingAreas]; } - (NSTextInputContext *)inputContext { return input_context; } static UInt32 convert_cocoa_to_carbon_modifiers(NSUInteger flags) { UInt32 mods = 0; if (flags & NSEventModifierFlagShift) mods |= shiftKey; if (flags & NSEventModifierFlagControl) mods |= controlKey; if (flags & NSEventModifierFlagOption) mods |= optionKey; if (flags & NSEventModifierFlagCommand) mods |= cmdKey; if (flags & NSEventModifierFlagCapsLock) mods |= alphaLock; return (mods >> 8) & 0xFF; } static void convert_utf16_to_utf8(UniChar *src, UniCharCount src_length, char *dest, size_t dest_sz) { CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, src, src_length, kCFAllocatorNull); CFStringGetCString(string, dest, dest_sz, kCFStringEncodingUTF8); CFRelease(string); } static bool alternate_key_is_ok(uint32_t key, uint32_t akey) { return akey > 31 && akey != key && !is_pua_char(akey); } static void add_alternate_keys(GLFWkeyevent *ev, NSEvent *event) { ev->alternate_key = translateKey(ev->native_key, false); if (!alternate_key_is_ok(ev->key, ev->alternate_key)) ev->alternate_key = 0; if (ev->mods & GLFW_MOD_SHIFT) { NSString *ci = [event charactersIgnoringModifiers]; if (ci) { unsigned sz = [ci length]; if (sz > 0) { UniChar buf[2] = {0}; buf[0] = [ci characterAtIndex:0]; if (sz > 1) buf[1] = [ci characterAtIndex:1]; ev->shifted_key = get_first_codepoint(buf, sz); } } if (!alternate_key_is_ok(ev->key, ev->shifted_key)) ev->shifted_key = 0; } } static bool is_ascii_control_char(char x) { return x == 0 || (1 <= x && x <= 31) || x == 127; } - (void)keyDown:(NSEvent *)event { #define CLEAR_PRE_EDIT_TEXT glfw_keyevent.text = NULL; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent); #define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent); const bool previous_has_marked_text = [self hasMarkedText]; if (input_context && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:input_context.selectedKeyboardInputSource])) { if (input_source_at_last_key_event) { debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n"); GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED}; window->ns.deadKeyState = 0; _glfwInputKeyboard(window, &dummy); // clear pre-edit text [input_source_at_last_key_event release]; input_source_at_last_key_event = nil; } input_source_at_last_key_event = [input_context.selectedKeyboardInputSource retain]; [self unmarkText]; } const unsigned int keycode = [event keyCode]; const NSUInteger flags = [event modifierFlags]; const int mods = translateFlags(flags); const uint32_t key = translateKey(keycode, true); const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1); _glfw.ns.text[0] = 0; if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */ || (keycode == 0x04 /* h */ && mods == GLFW_MOD_CONTROL)) [self unmarkText]; GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods}; if (!_glfw.ns.unicodeData) { // Using the cocoa API for key handling is disabled, as there is no // reliable way to handle dead keys using it. Only use it if the // keyboard unicode data is not available. if (process_text) { // this will call insertText with the text for this event, if any [self interpretKeyEvents:@[event]]; } } else { static UniChar text[256]; UniCharCount char_count = 0; const bool in_compose_sequence = window->ns.deadKeyState != 0; if (UCKeyTranslate( [(NSData*) _glfw.ns.unicodeData bytes], keycode, kUCKeyActionDown, convert_cocoa_to_carbon_modifiers(flags), LMGetKbdType(), (process_text ? 0 : kUCKeyTranslateNoDeadKeysMask), &(window->ns.deadKeyState), sizeof(text)/sizeof(text[0]), &char_count, text ) != noErr) { debug_key("UCKeyTranslate failed for keycode: 0x%x (%s) %s\n", keycode, safe_name_for_keycode(keycode), format_mods(mods)); window->ns.deadKeyState = 0; return; } debug_key("\x1b[31mPress:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %schar_count: %lu deadKeyState: %u repeat: %d ", keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat); marked_text_cleared_by_insert = false; if (process_text) { in_key_handler = 1; // this will call insertText which will fill up _glfw.ns.text [self interpretKeyEvents:@[event]]; in_key_handler = 0; } else { window->ns.deadKeyState = 0; } if (window->ns.deadKeyState && (char_count == 0 || keycode == 0x75)) { // 0x75 is the delete key which needs to be ignored during a compose sequence debug_key("Sending pre-edit text for dead key (text: %s markedText: %s).\n", format_text(_glfw.ns.text), glfw_keyevent.text); UPDATE_PRE_EDIT_TEXT; return; } if (in_compose_sequence) { debug_key("Clearing pre-edit text at end of compose sequence\n"); CLEAR_PRE_EDIT_TEXT; } } if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // don't send text for ascii control codes debug_key("text: %s glfw_key: %s marked_text: (%s)\n", format_text(_glfw.ns.text), _glfwGetKeyName(key), [[markedText string] UTF8String]); bool bracketed_ime = false; if (!window->ns.deadKeyState) { if ([self hasMarkedText]) { if (!marked_text_cleared_by_insert) { UPDATE_PRE_EDIT_TEXT; } else bracketed_ime = true; } else if (previous_has_marked_text) { CLEAR_PRE_EDIT_TEXT; } if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) { // do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it debug_key("Ignoring key press as IME is active and it generated no text\n"); return; } } if (bracketed_ime) { // insertText followed by setMarkedText CLEAR_PRE_EDIT_TEXT; } glfw_keyevent.text = _glfw.ns.text; glfw_keyevent.ime_state = GLFW_IME_NONE; add_alternate_keys(&glfw_keyevent, event); _glfwInputKeyboard(window, &glfw_keyevent); if (bracketed_ime) { // insertText followed by setMarkedText UPDATE_PRE_EDIT_TEXT; } } static bool is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_mask, NSUInteger either_mask) { bool target_pressed = (flags & target_mask) != 0; bool other_pressed = (flags & other_mask) != 0; bool either_pressed = (flags & either_mask) != 0; if (either_pressed != (target_pressed || other_pressed)) return either_pressed; return target_pressed; } - (void)flagsChanged:(NSEvent *)event { int action = GLFW_RELEASE; const char old_first_char = _glfw.ns.text[0]; _glfw.ns.text[0] = 0; const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; const uint32_t key = vk_code_to_functional_key_code([event keyCode]); const unsigned int keycode = [event keyCode]; const int mods = translateFlags(modifierFlags); const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, modifierFlags) != 1); const char *mod_name = "unknown"; // Code for handling modifier key events copied form SDL_cocoakeyboard.m, with thanks. See IsModifierKeyPressedFunction() #define action_for(modname, target_mask, other_mask, either_mask) action = is_modifier_pressed([event modifierFlags], target_mask, other_mask, either_mask) ? GLFW_PRESS : GLFW_RELEASE; mod_name = #modname; break; switch(key) { case GLFW_FKEY_CAPS_LOCK: mod_name = "capslock"; action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break; case GLFW_FKEY_LEFT_SUPER: action_for(super, NX_DEVICELCMDKEYMASK, NX_DEVICERCMDKEYMASK, NX_COMMANDMASK); case GLFW_FKEY_RIGHT_SUPER: action_for(super, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK, NX_COMMANDMASK); case GLFW_FKEY_LEFT_CONTROL: action_for(ctrl, NX_DEVICELCTLKEYMASK, NX_DEVICERCTLKEYMASK, NX_CONTROLMASK); case GLFW_FKEY_RIGHT_CONTROL: action_for(ctrl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK, NX_CONTROLMASK); case GLFW_FKEY_LEFT_ALT: action_for(alt, NX_DEVICELALTKEYMASK, NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK); case GLFW_FKEY_RIGHT_ALT: action_for(alt, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK); case GLFW_FKEY_LEFT_SHIFT: action_for(shift, NX_DEVICELSHIFTKEYMASK, NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK); case GLFW_FKEY_RIGHT_SHIFT: action_for(shift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK); default: return; } #undef action_for GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = action, .mods = mods}; debug_key("\x1b[33mflagsChanged:\x1b[m modifier: %s native_key: 0x%x (%s) glfw_key: 0x%x %s\n", mod_name, keycode, safe_name_for_keycode(keycode), key, format_mods(mods)); marked_text_cleared_by_insert = false; if (process_text && input_context) { // this will call insertText which will fill up _glfw.ns.text in_key_handler = 2; [input_context handleEvent:event]; in_key_handler = 0; if (marked_text_cleared_by_insert) { debug_key("Clearing pre-edit text because insertText called from flagsChanged\n"); CLEAR_PRE_EDIT_TEXT; if (_glfw.ns.text[0]) glfw_keyevent.text = _glfw.ns.text; else _glfw.ns.text[0] = old_first_char; } } glfw_keyevent.ime_state = GLFW_IME_NONE; _glfwInputKeyboard(window, &glfw_keyevent); } - (void)keyUp:(NSEvent *)event { const uint32_t keycode = [event keyCode]; const uint32_t key = translateKey(keycode, true); const int mods = translateFlags([event modifierFlags]); GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_RELEASE, .mods = mods}; add_alternate_keys(&glfw_keyevent, event); debug_key("\x1b[32mRelease:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %s\n", keycode, safe_name_for_keycode(keycode), key, format_mods(mods)); _glfwInputKeyboard(window, &glfw_keyevent); } #undef CLEAR_PRE_EDIT_TEXT #undef UPDATE_PRE_EDIT_TEXT - (void)scrollWheel:(NSEvent *)event { GLFWScrollEvent ev = { .keyboard_modifiers=translateFlags([event modifierFlags]), .unscaled.x = [event scrollingDeltaX], .unscaled.y = [event scrollingDeltaY]}; ev.x_offset = ev.unscaled.x; ev.y_offset = ev.unscaled.y; if ([event hasPreciseScrollingDeltas]) { ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES; float xscale = 1, yscale = 1; _glfwPlatformGetWindowContentScale(window, &xscale, &yscale); if (xscale > 0) ev.x_offset *= xscale; if (yscale > 0) ev.y_offset *= yscale; } switch([event momentumPhase]) { case NSEventPhaseBegan: ev.momentum_type = GLFW_MOMENTUM_PHASE_BEGAN; break; case NSEventPhaseStationary: ev.momentum_type = GLFW_MOMENTUM_PHASE_STATIONARY; break; case NSEventPhaseChanged: ev.momentum_type = GLFW_MOMENTUM_PHASE_ACTIVE; break; case NSEventPhaseEnded: ev.momentum_type = GLFW_MOMENTUM_PHASE_ENDED; break; case NSEventPhaseCancelled: ev.momentum_type = GLFW_MOMENTUM_PHASE_CANCELED; break; case NSEventPhaseMayBegin: ev.momentum_type = GLFW_MOMENTUM_PHASE_MAY_BEGIN; break; case NSEventPhaseNone: break; } _glfwInputScroll(window, &ev); } // Drop implementation for drag and drop {{{ // Return YES to receive periodic dragging updates even when the mouse hasn't moved. // This allows the application to update acceptance status asynchronously. - (BOOL)wantsPeriodicDraggingUpdates { return YES; } static void free_drop_data(_GLFWwindow *window) { if (window->ns.drop_data.mimes) { for (size_t i = 0; i < window->ns.drop_data.mimes_count; i++) free((void*)window->ns.drop_data.mimes[i]); free(window->ns.drop_data.mimes); } free(window->ns.drop_data.copy_mimes); // pointer array only; strings owned by mimes[] if (window->ns.drop_data.pasteboard) [window->ns.drop_data.pasteboard release]; if (window->ns.drop_data.data_mapping) [window->ns.drop_data.data_mapping release]; if (window->ns.drop_data.file_promise_mapping) { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; for (NSString *key in window->ns.drop_data.file_promise_mapping) { NSArray *pair = [window->ns.drop_data.file_promise_mapping objectForKey:key]; error = nil; if (pair[1] != [NSNull null]) [pair[1] closeAndReturnError:&error]; error = nil; [fileManager removeItemAtURL:pair[0] error:&error]; } [window->ns.drop_data.file_promise_mapping release]; } memset(&window->ns.drop_data, 0, sizeof(_GLFWDropData)); } static void update_drop_state(_GLFWwindow *window, size_t accepted_count, GLFWDropEventType t) { _GLFWDropData *d = &window->ns.drop_data; d->copy_mimes_count = accepted_count; if (t == GLFW_DROP_ENTER || t == GLFW_DROP_MOVE) d->drag_accepted = accepted_count > 0; } // Reset the working copy of mimes so the next callback sees the full original // list. Returns false on allocation failure. static bool reset_drop_copy_mimes(_GLFWDropData *d) { if (d->mimes_count == 0) { d->copy_mimes_count = 0; return true; } if (!d->copy_mimes) { d->copy_mimes = malloc(d->mimes_count * sizeof(const char*)); if (!d->copy_mimes) return false; } memcpy(d->copy_mimes, d->mimes, d->mimes_count * sizeof(const char*)); d->copy_mimes_count = d->mimes_count; return true; } - (NSDragOperation)draggingEntered:(id )sender { const NSRect contentRect = [window->ns.view frame]; const NSPoint pos = [sender draggingLocation]; double xpos = pos.x; double ypos = contentRect.size.height - pos.y; free_drop_data(window); // Get MIME types from the dragging pasteboard NSPasteboard* pasteboard = [sender draggingPasteboard]; // Count total types across all pasteboard items plus 2 for uri-list and text/plain size_t max_types = 2; for (NSPasteboardItem* item in pasteboard.pasteboardItems) max_types += [item.types count]; NSArray *classes = @[[NSFilePromiseReceiver class]]; NSArray *receivers = [pasteboard readObjectsForClasses:classes options:@{}]; for (NSFilePromiseReceiver *receiver in receivers) max_types += [receiver.fileTypes count]; // Pre-allocate C array for MIME types const char** mime_array = (const char**)calloc(max_types, sizeof(const char*)); if (!mime_array) return NSDragOperationNone; size_t mime_count = 0; // Check for common types first (use _glfw_strdup since we need to own the strings) NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES}; if ([pasteboard canReadObjectForClasses:@[[NSURL class]] options:options]) { mime_array[mime_count++] = _glfw_strdup("text/uri-list"); } if ([pasteboard canReadObjectForClasses:@[[NSString class]] options:nil]) { mime_array[mime_count++] = _glfw_strdup("text/plain"); } #define add_mime(uti) { \ const char* mime = uti_to_mime(uti); \ if (mime && mime[0]) { \ bool duplicate = false; \ for (size_t i = 0; i < mime_count; i++) { \ if (strcmp(mime_array[i], mime) == 0) { \ duplicate = true; \ break; \ } \ } \ if (!duplicate) mime_array[mime_count++] = _glfw_strdup(mime); \ } \ } // Get file promise based types for (NSFilePromiseReceiver *receiver in receivers) { for (NSString *uti in receiver.fileTypes) { add_mime(uti); } } // Get additional types from pasteboard items for (NSPasteboardItem* item in pasteboard.pasteboardItems) { for (NSPasteboardType uti in item.types) { add_mime(uti); } } window->ns.drop_data.mimes = mime_array; window->ns.drop_data.mimes_count = mime_count; bool from_self = ([sender draggingSource] != nil); _GLFWDropData *d = &window->ns.drop_data; if (reset_drop_copy_mimes(d)) { size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_ENTER, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self); update_drop_state(window, accepted_count, GLFW_DROP_ENTER); } return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone; } - (NSDragOperation)draggingUpdated:(id )sender { if (!window->ns.drop_data.drag_accepted) return NSDragOperationNone; const NSRect contentRect = [window->ns.view frame]; const NSPoint pos = [sender draggingLocation]; double xpos = pos.x; double ypos = contentRect.size.height - pos.y; bool from_self = ([sender draggingSource] != nil); _GLFWDropData *d = &window->ns.drop_data; if (reset_drop_copy_mimes(d)) { size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self); update_drop_state(window, accepted_count, GLFW_DROP_MOVE); } return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone; } - (void)draggingExited:(id )sender { bool from_self = ([sender draggingSource] != nil); _GLFWDropData *d = &window->ns.drop_data; if (reset_drop_copy_mimes(d)) { size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_LEAVE, 0, 0, d->copy_mimes, d->copy_mimes_count, from_self); update_drop_state(window, accepted_count, GLFW_DROP_LEAVE); } free_drop_data(window); } - (BOOL)performDragOperation:(id )sender { if (!window->ns.drop_data.drag_accepted) return NO; const NSRect contentRect = [window->ns.view frame]; const NSPoint pos = [sender draggingLocation]; double xpos = pos.x; double ypos = contentRect.size.height - pos.y; bool from_self = ([sender draggingSource] != nil); _GLFWDropData *d = &window->ns.drop_data; if (!reset_drop_copy_mimes(d)) return NO; size_t num_accepted = _glfwInputDropEvent(window, GLFW_DROP_DROP, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self); if (d->copy_mimes) { update_drop_state(window, num_accepted, GLFW_DROP_DROP); window->ns.drop_data.pasteboard = [[sender draggingPasteboard] retain]; for (size_t i = 0; i < num_accepted; i++) _glfwPlatformRequestDropData(window, d->copy_mimes[i]); } // Restore first-responder status after native DnD; the drag operation can // displace the content view from first responder, silently breaking keyboard // input even though osw->is_focused remains true. [window->ns.object makeFirstResponder:window->ns.view]; return YES; } void _glfwPlatformRequestDropUpdate(_GLFWwindow* window UNUSED) { // No-op since macOS is calling the drop move callback periodically anyway // thanks to wantsPeriodicDraggingUpdates and we have no way to inform // macOS of any changes except in the cocoa callbacks. } static void send_data_available_event_on_next_event_loop_tick(GLFWid wid, const char *mime) { char *mt = _glfw_strdup(mime); dispatch_async(dispatch_get_main_queue(), ^{ _GLFWwindow *window = _glfwWindowForId(wid); if (window) { const char *mimes[1] = {mt}; _glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false); } free(mt); }); } int _glfwPlatformRequestDropData(_GLFWwindow *window UNUSED, const char *mime) { NSPasteboard* pasteboard = window->ns.drop_data.pasteboard; if (!pasteboard) return EINVAL; GLFWid wid = window->id; if (window->ns.drop_data.data_mapping == nil) window->ns.drop_data.data_mapping = [[NSMutableDictionary alloc] init]; NSArray *pair; if ((pair = window->ns.drop_data.data_mapping[@(mime)])) { window->ns.drop_data.data_mapping[@(mime)] = @[pair[0], @0]; send_data_available_event_on_next_event_loop_tick(wid, mime); return 0; } if (window->ns.drop_data.file_promise_mapping == nil) window->ns.drop_data.file_promise_mapping = [[NSMutableDictionary alloc] init]; if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) { if (pair[0] == [NSNull null]) return 0; // waiting for promise if (pair[1] != [NSNull null]) { NSFileHandle *h = pair[1]; NSError *error = nil; [h seekToOffset:0 error:&error]; } send_data_available_event_on_next_event_loop_tick(wid, mime); return 0; } NSData* data = nil; NSFilePromiseReceiver *file_promise = nil; // Handle special MIME types if (strcmp(mime, "text/uri-list") == 0) { NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES}; NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] options:options]; if (urls && [urls count] > 0) { NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; for (NSURL* url in urls) { if ([uri_list length] > 0) [uri_list appendString:@"\n"]; if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString]; else [uri_list appendString:url.absoluteString]; } data = [uri_list dataUsingEncoding:NSUTF8StringEncoding]; } } else if (strcmp(mime, "text/plain") == 0 || strcmp(mime, "text/plain;charset=utf-8") == 0) { NSArray* strings = [pasteboard readObjectsForClasses:@[[NSString class]] options:nil]; if (strings && [strings count] > 0) { NSString* str = strings[0]; data = [str dataUsingEncoding:NSUTF8StringEncoding]; } } if (data == nil) { // Try to read data for other MIME types using UTI NSString* uti = mime_to_uti(mime); if (uti) { NSPasteboardType pbType = [pasteboard availableTypeFromArray:@[uti]]; if (pbType) data = [pasteboard dataForType:pbType]; } if (data == nil) { // look in the file promise providers NSArray *receivers = [pasteboard readObjectsForClasses:@[[NSFilePromiseReceiver class]] options:@{}]; for (NSFilePromiseReceiver *receiver in receivers) { for (NSString *uti in receiver.fileTypes) { const char *q = uti_to_mime(uti); if (q && strcmp(q, mime) == 0) { file_promise = receiver; break; } } if (file_promise) break; } } } if (!data && !file_promise) return ENOENT; if (file_promise != nil) { window->ns.drop_data.file_promise_mapping[@(mime)] = @[[NSNull null], [NSNull null], [NSNull null]]; char *mt = _glfw_strdup(mime); [file_promise receivePromisedFilesAtDestination:[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES] options:@{} operationQueue:[NSOperationQueue mainQueue] reader:^(NSURL *fileURL, NSError *errorOrNil) { _GLFWwindow *window = _glfwWindowForId(wid); if (!window || !window->ns.drop_data.file_promise_mapping) return; id null = [NSNull null]; if (errorOrNil) { NSLog(@"Error receiving file: %@: %@", fileURL, errorOrNil); window->ns.drop_data.file_promise_mapping[@(mt)] = @[fileURL, null, errorOrNil]; } else { NSError *err = nil; NSFileHandle *file_handle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&err]; window->ns.drop_data.file_promise_mapping[@(mt)] = err ? @[fileURL, null, err] : @[fileURL, file_handle, null]; } const char *mimes[1] = {mt}; _glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false); free(mt); }]; } else { window->ns.drop_data.data_mapping[@(mime)] = @[data, @0]; const char *mimes[1] = {mime}; _glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false); } return 0; } ssize_t _glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffer, size_t capacity) { _GLFWwindow *window = (_GLFWwindow*)w; const char *mime = ev->mimes[0]; NSArray *pair; if ((pair = window->ns.drop_data.data_mapping[@(mime)])) { NSData *data = pair[0]; size_t offset = [pair[1] unsignedIntegerValue]; NSUInteger dataLength = [data length]; if (offset >= dataLength) return 0; // EOF NSUInteger remaining = dataLength - offset; NSUInteger to_read = (remaining < capacity) ? remaining : capacity; [data getBytes:buffer range:NSMakeRange(offset, to_read)]; offset += to_read; window->ns.drop_data.data_mapping[@(mime)] = @[data, @(offset)]; if (to_read) send_data_available_event_on_next_event_loop_tick(window->id, mime); return (ssize_t)to_read; } if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) { id null = [NSNull null]; if (pair[0] == null) { return -ENOENT; } if (pair[2] != null) { NSError *err = pair[2]; if ([err.domain isEqualToString:NSPOSIXErrorDomain]) return -err.code; NSError *underlyingError = err.userInfo[NSUnderlyingErrorKey]; if (underlyingError && [underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) return -underlyingError.code; return -EIO; } NSFileHandle *h = pair[1]; int fd = h.fileDescriptor; ssize_t bytesRead; do { bytesRead = read(fd, buffer, capacity); } while (bytesRead == -1 && errno == EINTR); bytesRead = bytesRead < 0 ? -errno : bytesRead; if (bytesRead > 0) send_data_available_event_on_next_event_loop_tick(window->id, mime); return bytesRead; } return -ENOENT; } void _glfwPlatformEndDrop(GLFWwindow *w UNUSED, GLFWDragOperationType op UNUSED) { free_drop_data((_GLFWwindow*)w); } // }}} - (BOOL)hasMarkedText { return [markedText length] > 0; } - (NSRange)markedRange { if ([markedText length] > 0) return NSMakeRange(0, [markedText length] - 1); else return kEmptyRange; } - (NSRange)selectedRange { // Return position 0 with no selection to indicate text can be inserted. // This is required for macOS dictation to work - returning kEmptyRange // (NSNotFound, 0) causes dictation to fail because the system doesn't // know where to insert text. See https://github.com/kovidgoyal/kitty/issues/3732 return NSMakeRange(0, 0); } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { const char *s = polymorphic_string_as_utf8(string); debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", s, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length); if (string == nil || !s[0]) { bool had_marked_text = [self hasMarkedText]; [self unmarkText]; if (had_marked_text && (!in_key_handler || in_key_handler == 2)) { debug_key("Clearing pre-edit because setMarkedText called from %s\n", in_key_handler ? "flagsChanged" : "event loop"); GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED}; _glfwInputKeyboard(window, &glfw_keyevent); _glfw.ns.text[0] = 0; } return; } if ([string isKindOfClass:[NSAttributedString class]]) { if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; } [markedText release]; markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; } else { if (((NSString*)string).length == 0) { [self unmarkText]; return; } [markedText release]; markedText = [[NSMutableAttributedString alloc] initWithString:string]; } if (!in_key_handler || in_key_handler == 2) { debug_key("Updating IME text in kitty from setMarkedText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text); GLFWkeyevent glfw_keyevent = {.text=[[markedText string] UTF8String], .ime_state = GLFW_IME_PREEDIT_CHANGED}; _glfwInputKeyboard(window, &glfw_keyevent); _glfw.ns.text[0] = 0; } } - (void)unmarkText { [[markedText mutableString] setString:@""]; } void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused]; } - (void)updateIMEStateFor:(GLFWIMEUpdateType)which focused:(bool)focused { if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) { [input_context discardMarkedText]; [self unmarkText]; GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED}; _glfwInputKeyboard(window, &glfw_keyevent); _glfw.ns.text[0] = 0; } if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return; if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates]; } - (NSArray*)validAttributesForMarkedText { return [NSArray array]; } - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { (void)range; (void)actualRange; return nil; } - (NSUInteger)characterIndexForPoint:(NSPoint)point { (void)point; return 0; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { (void)range; (void)actualRange; if (_glfw.callbacks.get_ime_cursor_position) { GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION }; if (window && _glfw.callbacks.get_ime_cursor_position((GLFWwindow*)window, &ev)) { const CGFloat left = (CGFloat)ev.cursor.left / window->ns.xscale; const CGFloat top = (CGFloat)ev.cursor.top / window->ns.yscale; const CGFloat cellWidth = (CGFloat)ev.cursor.width / window->ns.xscale; const CGFloat cellHeight = (CGFloat)ev.cursor.height / window->ns.yscale; debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight); const NSRect frame = [window->ns.view frame]; const NSRect rectInView = NSMakeRect(left, frame.size.height - top - cellHeight, cellWidth, cellHeight); markedRect = [window->ns.object convertRectToScreen: rectInView]; } } return markedRect; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { const char *utf8 = polymorphic_string_as_utf8(string); debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length); if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) { [self unmarkText]; marked_text_cleared_by_insert = true; if (!in_key_handler) { debug_key("Clearing pre-edit because insertText called from event loop\n"); GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED}; _glfwInputKeyboard(window, &glfw_keyevent); _glfw.ns.text[0] = 0; } } // insertText can be called multiple times for a single key event size_t existing_length = strnlen(_glfw.ns.text, sizeof(_glfw.ns.text)); size_t required_length = strlen(utf8) + 1; size_t available_length = sizeof(_glfw.ns.text) - existing_length; if (available_length >= required_length) { memcpy(_glfw.ns.text + existing_length, utf8, required_length); // copies the null terminator from utf8 as well _glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0; if ((!in_key_handler || in_key_handler == 2) && _glfw.ns.text[0]) { if (!is_ascii_control_char(_glfw.ns.text[0])) { debug_key("Sending text to kitty from insertText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text); GLFWkeyevent glfw_keyevent = {.text=_glfw.ns.text, .ime_state=GLFW_IME_COMMIT_TEXT}; _glfwInputKeyboard(window, &glfw_keyevent); } _glfw.ns.text[0] = 0; } } } - (void)doCommandBySelector:(SEL)selector { debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]); if (forward_dictation_selector_to_app(selector, self)) return; } - (void)startDictation:(id)sender { forward_dictation_selector_to_app(_cmd, sender); } - (void)stopDictation:(id)sender { forward_dictation_selector_to_app(_cmd, sender); } - (BOOL)isAccessibilityElement { return YES; } - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { // Allow accessibility selectors needed for dictation and other accessibility features // See https://github.com/kovidgoyal/kitty/issues/3732 if (selector == @selector(accessibilityRole) || selector == @selector(accessibilitySelectedText) || selector == @selector(accessibilitySelectedTextRange) || selector == @selector(accessibilityNumberOfCharacters) || selector == @selector(accessibilityInsertionPointLineNumber) || selector == @selector(accessibilityValue) || selector == @selector(setAccessibilityValue:)) return YES; // Allow accessibility selectors needed for external window management tools // (e.g. Easy Move+Resize) to find and manipulate the window. // See https://github.com/kovidgoyal/kitty/issues/5561 if (selector == @selector(accessibilityWindow) || selector == @selector(accessibilityParent) || selector == @selector(accessibilityPosition) || selector == @selector(setAccessibilityPosition:) || selector == @selector(accessibilitySize) || selector == @selector(setAccessibilitySize:)) return YES; return NO; } #if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400) - (NSAccessibilityRole)accessibilityRole { return NSAccessibilityTextAreaRole; } #endif - (NSString *)accessibilitySelectedText { NSString *text = nil; if (_glfw.callbacks.get_current_selection) { char *s = _glfw.callbacks.get_current_selection(); if (s) { text = [NSString stringWithUTF8String:s]; free(s); } } return text; } // Accessibility methods required for dictation support // See https://github.com/kovidgoyal/kitty/issues/3732 - (NSRange)accessibilitySelectedTextRange { // Return position 0 with no selection for dictation support return NSMakeRange(0, 0); } - (NSInteger)accessibilityNumberOfCharacters { // Terminal doesn't have a fixed text buffer, return 0 return 0; } - (NSInteger)accessibilityInsertionPointLineNumber { // Return line 0 as the insertion point return 0; } - (NSString *)accessibilityValue { // Terminal doesn't expose its buffer as an accessibility value return @""; } - (void)setAccessibilityValue:(NSString *)value { // When dictation or other accessibility features set text, insert it as keyboard input if (value && [value length] > 0 && window) { const char *utf8 = [value UTF8String]; debug_key("Inserting text via setAccessibilityValue: %s\n", utf8); GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT}; _glfwInputKeyboard(window, &glfw_keyevent); } } // // Support services receiving "public.utf8-plain-text" and "NSStringPboardType" - (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType { if ( (!sendType || [sendType isEqual:NSPasteboardTypeString] || [sendType isEqual:@"NSStringPboardType"]) && (!returnType || [returnType isEqual:NSPasteboardTypeString] || [returnType isEqual:@"NSStringPboardType"]) ) { if (_glfw.callbacks.has_current_selection && _glfw.callbacks.has_current_selection()) return self; } return [super validRequestorForSendType:sendType returnType:returnType]; } // Selected text as input to be sent to Services // For example, after selecting an absolute path, open the global menu bar kitty->Services and click `Show in Finder`. - (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types { if (!_glfw.callbacks.get_current_selection) return NO; char *text = _glfw.callbacks.get_current_selection(); if (!text) return NO; BOOL ans = NO; if (text[0]) { if ([types containsObject:NSPasteboardTypeString] == YES) { [pboard declareTypes:@[NSPasteboardTypeString] owner:self]; ans = [pboard setString:@(text) forType:NSPasteboardTypeString]; } else if ([types containsObject:@"NSStringPboardType"] == YES) { [pboard declareTypes:@[@"NSStringPboardType"] owner:self]; ans = [pboard setString:@(text) forType:@"NSStringPboardType"]; } free(text); } return ans; } // Service output to be handled // For example, open System Settings->Keyboard->Keyboard Shortcuts->Services->Text, enable `Convert Text to Full Width`, select some text and execute the service. - (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard { NSString* text = nil; NSArray *types = [pboard types]; if ([types containsObject:NSPasteboardTypeString] == YES) { text = [pboard stringForType:NSPasteboardTypeString]; // public.utf8-plain-text } else if ([types containsObject:@"NSStringPboardType"] == YES) { text = [pboard stringForType:@"NSStringPboardType"]; // for older services (need re-encode?) } else { return NO; } if (text && [text length] > 0) { // The service wants us to replace the selection, but we can't replace anything but insert text. const char *utf8 = polymorphic_string_as_utf8(text); debug_key("Sending text received in readSelectionFromPasteboard as key event\n"); GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT}; _glfwInputKeyboard(window, &glfw_keyevent); // Restore pre-edit text after inserting the received text if ([self hasMarkedText]) { glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent); } return YES; } return NO; } @end // }}} // GLFW window class {{{ @interface GLFWWindow : NSWindow { _GLFWwindow* glfw_window; } - (instancetype)initWithGlfwWindow:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType initWindow:(_GLFWwindow *)initWindow; - (void) removeGLFWWindow; @end @implementation GLFWWindow - (instancetype)initWithGlfwWindow:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType initWindow:(_GLFWwindow *)initWindow { self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:NO]; if (self != nil) { glfw_window = initWindow; self.tabbingMode = NSWindowTabbingModeDisallowed; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(screenParametersDidChange:) name:NSApplicationDidChangeScreenParametersNotification object:nil]; } return self; } - (void)screenParametersDidChange:(NSNotification *)notification { if (!glfw_window || !glfw_window->ns.layer_shell.is_active) return; _glfwPlatformSetLayerShellConfig(glfw_window, NULL); } - (void) removeGLFWWindow { glfw_window = NULL; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (BOOL)validateMenuItem:(NSMenuItem *)item { if (item.action == @selector(performMiniaturize:)) return YES; return [super validateMenuItem:item]; } - (void)performMiniaturize:(id)sender { if (glfw_window && (!glfw_window->decorated || glfw_window->ns.titlebar_hidden)) [self miniaturize:self]; else [super performMiniaturize:sender]; } - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(nullable NSScreen *)screen { if (glfw_window && glfw_window->ns.suppress_frame_constraints) return frameRect; return [super constrainFrameRect:frameRect toScreen:screen]; } - (BOOL)canBecomeKeyWindow { if (!glfw_window) return NO; if (glfw_window->ns.layer_shell.is_active) { if (glfw_window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND) return NO; switch(glfw_window->ns.layer_shell.config.focus_policy) { case GLFW_FOCUS_NOT_ALLOWED: return NO; case GLFW_FOCUS_EXCLUSIVE: return YES; case GLFW_FOCUS_ON_DEMAND: return YES; } } // Required for NSWindowStyleMaskBorderless windows // Also miniaturized windows should not become key return !_glfwPlatformWindowIconified(glfw_window); } - (BOOL)canBecomeMainWindow { return !glfw_window->ns.layer_shell.is_active || glfw_window->ns.layer_shell.config.type != GLFW_LAYER_SHELL_BACKGROUND; } static void apply_titlebar_color_settings(_GLFWwindow *window); static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow* w, bool traditional, bool made_fullscreen) { // Update window button visibility if (w->ns.titlebar_hidden) { NSWindow *window = w->ns.object; // The hidden buttons might be automatically reset to be visible after going full screen // to show up in the auto-hide title bar, so they need to be set back to hidden. BOOL button_hidden = YES; // When title bar is configured to be hidden, it should be shown with buttons (auto-hide) after going to full screen. if (!traditional) { button_hidden = (BOOL) !made_fullscreen; } [[window standardWindowButton: NSWindowCloseButton] setHidden:button_hidden]; [[window standardWindowButton: NSWindowMiniaturizeButton] setHidden:button_hidden]; [[window standardWindowButton: NSWindowZoomButton] setHidden:button_hidden]; } if (!made_fullscreen) apply_titlebar_color_settings(w); } - (void)toggleFullScreen:(nullable id)sender { if (glfw_window) { if (glfw_window->ns.in_fullscreen_transition) return; // Capture the windowed frame before any fullscreen transition begins. // This is more reliable than saving it inside _glfwPlatformToggleFullscreen // because setStyleMask: calls between cycles can reposition the window (#9572). if (!glfw_window->ns.in_traditional_fullscreen && !([self styleMask] & NSWindowStyleMaskFullScreen)) { glfw_window->ns.pre_traditional_fullscreen_frame = [self frame]; } if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return; glfw_window->ns.in_fullscreen_transition = true; } NSWindowStyleMask sm = [self styleMask]; bool is_fullscreen_already = (sm & NSWindowStyleMaskFullScreen) != 0; // When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen. const NSSize original = [self resizeIncrements]; [self setResizeIncrements:NSMakeSize(1.0, 1.0)]; [super toggleFullScreen:sender]; [self setResizeIncrements:original]; // When the window decoration is hidden, toggling fullscreen causes the style mask to be changed, // and causes the first responder to be cleared. if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view]; update_titlebar_button_visibility_after_fullscreen_transition(glfw_window, false, !is_fullscreen_already); } - (void)zoom:(id)sender { if (![self isZoomed]) { const NSSize original = [self resizeIncrements]; [self setResizeIncrements:NSMakeSize(1.0, 1.0)]; [super zoom:sender]; [self setResizeIncrements:original]; } else { [super zoom:sender]; } } @end // }}} // Create the Cocoa window // static bool createNativeWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWfbconfig* fbconfig) { window->ns.delegate = [[GLFWWindowDelegate alloc] initWithGlfwWindow:window]; if (window->ns.delegate == nil) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create window delegate"); return false; } NSRect contentRect; if (window->monitor) { GLFWvidmode mode; int xpos, ypos; _glfwPlatformGetVideoMode(window->monitor, &mode); _glfwPlatformGetMonitorPos(window->monitor, &xpos, &ypos); contentRect = NSMakeRect(xpos, ypos, mode.width, mode.height); } else contentRect = NSMakeRect(0, 0, wndconfig->width, wndconfig->height); window->ns.object = [[GLFWWindow alloc] initWithGlfwWindow:contentRect styleMask:getStyleMask(window) backing:NSBackingStoreBuffered initWindow:window ]; if (window->ns.object == nil) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create window"); return false; } if (window->monitor) [window->ns.object setLevel:NSMainMenuWindowLevel + 1]; else { [(NSWindow*) window->ns.object center]; CGRect screen_frame = [[(NSWindow*) window->ns.object screen] frame]; if (CGRectContainsPoint(screen_frame, _glfw.ns.cascadePoint) || CGPointEqualToPoint(CGPointZero, _glfw.ns.cascadePoint)) { _glfw.ns.cascadePoint = NSPointToCGPoint([window->ns.object cascadeTopLeftFromPoint: NSPointFromCGPoint(_glfw.ns.cascadePoint)]); } else { _glfw.ns.cascadePoint = CGPointZero; } if (wndconfig->resizable) { const NSWindowCollectionBehavior behavior = NSWindowCollectionBehaviorFullScreenPrimary | NSWindowCollectionBehaviorManaged; [window->ns.object setCollectionBehavior:behavior]; } if (wndconfig->floating) [window->ns.object setLevel:NSFloatingWindowLevel]; if (wndconfig->maximized) [window->ns.object zoom:nil]; } if (strlen(wndconfig->ns.frameName)) [window->ns.object setFrameAutosaveName:@(wndconfig->ns.frameName)]; window->ns.view = [[GLFWContentView alloc] initWithGlfwWindow:window]; window->ns.retina = wndconfig->ns.retina; if (fbconfig->transparent) { [window->ns.object setOpaque:NO]; [window->ns.object setHasShadow:NO]; [window->ns.object setBackgroundColor:[NSColor clearColor]]; } [window->ns.object setContentView:window->ns.view]; [window->ns.object makeFirstResponder:window->ns.view]; [window->ns.object setTitle:@(wndconfig->title)]; [window->ns.object setDelegate:window->ns.delegate]; [window->ns.object setAcceptsMouseMovedEvents:YES]; [window->ns.object setRestorable:NO]; _glfwPlatformGetWindowSize(window, &window->ns.width, &window->ns.height); _glfwPlatformGetFramebufferSize(window, &window->ns.fbWidth, &window->ns.fbHeight); if (wndconfig->blur_radius > 0) _glfwPlatformSetWindowBlur(window, wndconfig->blur_radius); return true; } ////////////////////////////////////////////////////////////////////////// ////// GLFW platform API ////// ////////////////////////////////////////////////////////////////////////// int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, const GLFWLayerShellConfig *lsc) { window->ns.deadKeyState = 0; if (lsc) { window->ns.layer_shell.is_active = true; window->ns.layer_shell.config = *lsc; } else window->ns.layer_shell.is_active = false; if (!_glfw.ns.finishedLaunching) { [NSApp run]; _glfw.ns.finishedLaunching = true; } if (!createNativeWindow(window, wndconfig, fbconfig)) return false; switch((GlfwCocoaColorSpaces)wndconfig->ns.color_space) { case SRGB_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace sRGBColorSpace]]; break; case DISPLAY_P3_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace displayP3ColorSpace]]; break; case DEFAULT_COLORSPACE: break; } if (ctxconfig->client != GLFW_NO_API) { if (ctxconfig->source == GLFW_NATIVE_CONTEXT_API) { if (!_glfwInitNSGL()) return false; if (!_glfwCreateContextNSGL(window, ctxconfig, fbconfig)) return false; } else if (ctxconfig->source == GLFW_EGL_CONTEXT_API) { // EGL implementation on macOS use CALayer* EGLNativeWindowType so we // need to get the layer for EGL window surface creation. [window->ns.view setWantsLayer:YES]; window->ns.layer = [window->ns.view layer]; if (!_glfwInitEGL()) return false; if (!_glfwCreateContextEGL(window, ctxconfig, fbconfig)) return false; } else if (ctxconfig->source == GLFW_OSMESA_CONTEXT_API) { if (!_glfwInitOSMesa()) return false; if (!_glfwCreateContextOSMesa(window, ctxconfig, fbconfig)) return false; } } if (window->monitor) { // Do not show the window here until after setting the window size, maximized state, and full screen // _glfwPlatformShowWindow(window); // _glfwPlatformFocusWindow(window); acquireMonitor(window); } return true; } void _glfwPlatformDestroyWindow(_GLFWwindow* window) { GLFWWindow *w = window->ns.object; if (_glfw.ns.disabledCursorWindow == window) _glfw.ns.disabledCursorWindow = NULL; free_drop_data(window); if (window->ns.notch_cover_window) { [w removeChildWindow:window->ns.notch_cover_window]; [window->ns.notch_cover_window close]; [window->ns.notch_cover_window release]; window->ns.notch_cover_window = nil; } [w orderOut:nil]; if (window->monitor) releaseMonitor(window); if (window->context.destroy) window->context.destroy(window); [w setDelegate:nil]; [window->ns.delegate cleanup]; [window->ns.delegate release]; window->ns.delegate = nil; [window->ns.view removeGLFWWindow]; [window->ns.view release]; window->ns.view = nil; [w removeGLFWWindow]; // Workaround for macOS Tahoe where if the frame is not set to zero size // even after NSWindow::close the window remains on screen as an invisible // rectangle that intercepts mouse events and takes up space in mission // control. Sigh. NSRect frame = w.frame; frame.size.width = 0; frame.size.height = 0; [w setFrame:frame display:NO]; [w setHasShadow:NO]; [w close]; // sends a release to NSWindow so we dont release it window->ns.object = nil; } static NSScreen* screen_for_window_center(_GLFWwindow *window) { NSRect windowFrame = [window->ns.object frame]; NSPoint windowCenter = NSMakePoint(NSMidX(windowFrame), NSMidY(windowFrame)); for (NSScreen *screen in [NSScreen screens]) { if (NSPointInRect(windowCenter, [screen frame])) { return screen; } } return NSScreen.mainScreen; } static NSScreen* active_screen(void) { NSPoint mouseLocation = [NSEvent mouseLocation]; NSArray *screens = [NSScreen screens]; for (NSScreen *screen in screens) { if (NSPointInRect(mouseLocation, [screen frame])) { return screen; } } // As a fallback, return the main screen return [NSScreen mainScreen]; } static bool is_same_screen(NSScreen *screenA, NSScreen * screenB) { if (screenA == screenB) return true; NSDictionary *deviceDescriptionA = [screenA deviceDescription]; NSDictionary *deviceDescriptionB = [screenB deviceDescription]; NSNumber *screenNumberA = deviceDescriptionA[@"NSScreenNumber"]; NSNumber *screenNumberB = deviceDescriptionB[@"NSScreenNumber"]; return [screenNumberA isEqualToNumber:screenNumberB]; } static void move_window_to_screen(_GLFWwindow *window, NSScreen *target) { NSRect screenFrame = [target visibleFrame]; NSRect windowFrame = [window->ns.object frame]; CGFloat newX = NSMidX(screenFrame) - (windowFrame.size.width / 2.0); CGFloat newY = NSMidY(screenFrame) - (windowFrame.size.height / 2.0); NSRect newWindowFrame = NSMakeRect(newX, newY, windowFrame.size.width, windowFrame.size.height); [window->ns.object setFrame:newWindowFrame display:NO animate:NO]; if (window->ns.layer_shell.is_active) _glfwPlatformSetLayerShellConfig(window, NULL); } const GLFWLayerShellConfig* _glfwPlatformGetLayerShellConfig(_GLFWwindow *window) { return &window->ns.layer_shell.config; } static NSScreen* screen_for_name(const char *name) { int count = 0; GLFWmonitor **monitors = glfwGetMonitors(&count); for (int i = 0; i < count; i++) { const char *q = glfwGetMonitorName(monitors[i]); if (q && strcmp(q, name) == 0) return ((_GLFWmonitor*)monitors[i])->ns.screen; } return NULL; } bool _glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) { #define config window->ns.layer_shell.config #define nswindow window->ns.object window->resizable = false; if (value) config = *value; const bool is_transparent = _glfwPlatformFramebufferTransparent(window); int background_blur = config.related.background_blur; if (!is_transparent || config.related.background_opacity >= 1.f) { background_blur = 0; } [nswindow setBackgroundColor:[NSColor clearColor]]; _glfwPlatformSetWindowBlur(window, background_blur); window->ns.titlebar_hidden = true; window->decorated = false; [nswindow setTitlebarAppearsTransparent:false]; [nswindow setHasShadow:false]; [nswindow setTitleVisibility:NSWindowTitleHidden]; NSColorSpace *cs = nil; switch (config.related.color_space) { case SRGB_COLORSPACE: cs = [NSColorSpace sRGBColorSpace]; break; case DISPLAY_P3_COLORSPACE: cs = [NSColorSpace displayP3ColorSpace]; break; case DEFAULT_COLORSPACE: cs = nil; break; // using deviceRGBColorSpace causes a hang when transitioning to fullscreen } [nswindow setColorSpace:cs]; [[nswindow standardWindowButton: NSWindowCloseButton] setHidden:true]; [[nswindow standardWindowButton: NSWindowMiniaturizeButton] setHidden:true]; [[nswindow standardWindowButton: NSWindowZoomButton] setHidden:true]; [nswindow setStyleMask:NSWindowStyleMaskBorderless]; // HACK: Changing the style mask can cause the first responder to be cleared [nswindow makeFirstResponder:window->ns.view]; NSScreen *screen = screen_for_window_center(window); if (config.output_name[0]) { NSScreen *q = screen_for_name(config.output_name); if (q) screen = q; } unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing; float xscale = (float)config.expected.xscale, yscale = (float)config.expected.yscale; _glfwPlatformGetWindowContentScale(window, &xscale, &yscale); config.size_callback((GLFWwindow*)window, xscale, yscale, &cell_width, &cell_height, &left_edge_spacing, &top_edge_spacing, &right_edge_spacing, &bottom_edge_spacing); double spacing_x = left_edge_spacing + right_edge_spacing; double spacing_y = top_edge_spacing + bottom_edge_spacing; const unsigned xsz = config.x_size_in_pixels ? (unsigned)(config.x_size_in_pixels * xscale) : (cell_width * config.x_size_in_cells); const unsigned ysz = config.y_size_in_pixels ? (unsigned)(config.y_size_in_pixels * yscale) : (cell_height * config.y_size_in_cells); CGFloat dock_height = NSMinY(screen.visibleFrame) - NSMinY(screen.frame); CGFloat menubar_height = NSHeight(screen.frame) - NSHeight(screen.visibleFrame) - dock_height; CGFloat x = NSMinX(screen.visibleFrame), y = NSMinY(screen.visibleFrame) - 1, width = NSWidth(screen.visibleFrame), height = NSHeight(screen.visibleFrame) + 2; if (config.type == GLFW_LAYER_SHELL_BACKGROUND || config.edge == GLFW_EDGE_CENTER) { x = NSMinX(screen.frame); height = NSHeight(screen.frame) - menubar_height + 1; y = NSMinY(screen.frame); width = NSWidth(screen.frame); } // Screen co-ordinate system is with origin in lower left and y increasing upwards and x increasing rightwards // NSLog(@"frame: %@ visibleFrame: %@\n", NSStringFromRect(screen.frame), NSStringFromRect(screen.visibleFrame)); NSWindowLevel level = NSPopUpMenuWindowLevel - 1; // so that popup menus from globalmenubar function NSWindowAnimationBehavior animation_behavior = NSWindowAnimationBehaviorUtilityWindow; switch (config.type) { case GLFW_LAYER_SHELL_BACKGROUND: animation_behavior = NSWindowAnimationBehaviorNone; // See: https://stackoverflow.com/questions/4982584/how-do-i-draw-the-desktop-on-mac-os-x/4982619#4982619 level = kCGDesktopWindowLevel; break; case GLFW_LAYER_SHELL_OVERLAY: case GLFW_LAYER_SHELL_NONE: break; case GLFW_LAYER_SHELL_PANEL: level = NSNormalWindowLevel - 1; break; case GLFW_LAYER_SHELL_TOP: level--; break; } if (config.type != GLFW_LAYER_SHELL_BACKGROUND && config.edge != GLFW_EDGE_CENTER) { double panel_height = spacing_y + ysz / yscale, panel_width = spacing_x + xsz / xscale; switch (config.edge) { case GLFW_EDGE_BOTTOM: height = panel_height; break; case GLFW_EDGE_TOP: y += height - panel_height + 1.; height = panel_height; break; case GLFW_EDGE_LEFT: width = panel_width; break; case GLFW_EDGE_RIGHT: x += width - panel_width + 1.; width = panel_width; break; case GLFW_EDGE_CENTER_SIZED: x += (width - panel_width) / 2; y += (height - panel_height) / 2; width = panel_width; height = panel_height; break; default: // top left y += height - panel_height + 1.; height = panel_height; width = panel_width; break; } if (width < 1.) width = NSWidth(screen.visibleFrame); if (height < 1.) height = NSWidth(screen.visibleFrame); } if (config.edge != GLFW_EDGE_CENTER_SIZED) { x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin; y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin; } [nswindow setAnimationBehavior:animation_behavior]; [nswindow setLevel:level]; [nswindow setCollectionBehavior: (NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorIgnoresCycle)]; [nswindow setFrame:NSMakeRect(x, y, width, height) display:YES]; return true; #undef config #undef nswindow } void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title) { if (!title) return; NSString* string = @(title); if (!string) return; // the runtime failed to convert title to an NSString [window->ns.object setTitle:string]; // HACK: Set the miniwindow title explicitly as setTitle: doesn't update it // if the window lacks NSWindowStyleMaskTitled [window->ns.object setMiniwindowTitle:string]; } void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED, int count UNUSED, const GLFWimage* images UNUSED) { _glfwInputError(GLFW_FEATURE_UNAVAILABLE, "Cocoa: Regular windows do not have icons on macOS"); } void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos) { const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); if (xpos) *xpos = (int)contentRect.origin.x; if (ypos) *ypos = (int)_glfwTransformYNS(contentRect.origin.y + contentRect.size.height - 1); } void _glfwPlatformSetWindowPos(_GLFWwindow* window, int x, int y) { const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); const NSRect dummyRect = NSMakeRect(x, _glfwTransformYNS(y + contentRect.size.height - 1), 0, 0); const NSRect frameRect = [window->ns.object frameRectForContentRect:dummyRect]; [window->ns.object setFrameOrigin:frameRect.origin]; } void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height) { const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); if (width) *width = (int)contentRect.size.width; if (height) *height = (int)contentRect.size.height; } void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height) { if (window->ns.layer_shell.is_active) return; if (window->monitor) { if (window->monitor->window == window) acquireMonitor(window); } else { // Disable window resizing in fullscreen. if ([window->ns.object styleMask] & NSWindowStyleMaskFullScreen || window->ns.in_traditional_fullscreen) return; NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); contentRect.origin.y += contentRect.size.height - height; contentRect.size = NSMakeSize(width, height); [window->ns.object setFrame:[window->ns.object frameRectForContentRect:contentRect] display:YES]; } } void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window, int minwidth, int minheight, int maxwidth, int maxheight) { if (minwidth == GLFW_DONT_CARE || minheight == GLFW_DONT_CARE) [window->ns.object setContentMinSize:NSMakeSize(0, 0)]; else [window->ns.object setContentMinSize:NSMakeSize(minwidth, minheight)]; if (maxwidth == GLFW_DONT_CARE || maxheight == GLFW_DONT_CARE) [window->ns.object setContentMaxSize:NSMakeSize(DBL_MAX, DBL_MAX)]; else [window->ns.object setContentMaxSize:NSMakeSize(maxwidth, maxheight)]; } void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom) { if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE) [window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)]; else [window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)]; } void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr) { if (widthincr != GLFW_DONT_CARE && heightincr != GLFW_DONT_CARE) { float xscale = 1, yscale = 1; _glfwPlatformGetWindowContentScale(window, &xscale, &yscale); [window->ns.object setResizeIncrements:NSMakeSize(widthincr / xscale, heightincr / yscale)]; } else { [window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)]; } } void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height) { const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect]; if (width) *width = (int) fbRect.size.width; if (height) *height = (int) fbRect.size.height; } void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window, int* left, int* top, int* right, int* bottom) { const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window); const NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect]; if (left) *left = (int)(contentRect.origin.x - frameRect.origin.x); if (top) *top = (int)(frameRect.origin.y + frameRect.size.height - contentRect.origin.y - contentRect.size.height); if (right) *right = (int)(frameRect.origin.x + frameRect.size.width - contentRect.origin.x - contentRect.size.width); if (bottom) *bottom = (int)(contentRect.origin.y - frameRect.origin.y); } void _glfwPlatformGetWindowContentScale(_GLFWwindow* window, float* xscale, float* yscale) { const NSRect points = get_window_size_without_border_in_logical_pixels(window); const NSRect pixels = [window->ns.view convertRectToBacking:points]; if (xscale) *xscale = (float) (pixels.size.width / points.size.width); if (yscale) *yscale = (float) (pixels.size.height / points.size.height); } monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED) { return s_double_to_monotonic_t([NSEvent doubleClickInterval]); } void _glfwPlatformGetKeyboardRepeatDelay(monotonic_t *delay, monotonic_t *interval) { if (delay) *delay = s_double_to_monotonic_t([NSEvent keyRepeatDelay]); if (interval) *interval = s_double_to_monotonic_t([NSEvent keyRepeatInterval]); } void _glfwPlatformIconifyWindow(_GLFWwindow* window) { [window->ns.object miniaturize:nil]; } void _glfwPlatformRestoreWindow(_GLFWwindow* window) { if ([window->ns.object isMiniaturized]) [window->ns.object deminiaturize:nil]; else if ([window->ns.object isZoomed]) [window->ns.object zoom:nil]; } void _glfwPlatformMaximizeWindow(_GLFWwindow* window) { if (![window->ns.object isZoomed]) { [window->ns.object zoom:nil]; } } void _glfwPlatformShowWindow(_GLFWwindow* window, bool move_to_active_screen) { const bool is_background = window->ns.layer_shell.is_active && window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND; NSWindow *nw = window->ns.object; if (move_to_active_screen) { NSScreen *current_screen = screen_for_window_center(window); NSScreen *target_screen = active_screen(); if (!is_same_screen(current_screen, target_screen)) { debug_rendering("Moving OS window %llu to active screen\n", window->id); move_window_to_screen(window, target_screen); } } if (is_background) { [nw orderBack:nil]; } else { // Cocoa has a bug where when showing a hidden window after // fullscreening an application, the window does not get added // to the current space even though it has NSWindowCollectionBehaviorCanJoinAllSpaces // probably because it wasnt added to the temp space used for // fullscreen. So to work around that, we change the collection // behavior temporarily to NSWindowCollectionBehaviorMoveToActiveSpace // and then change it back asynchronously. // See https://github.com/kovidgoyal/kitty/issues/8740 NSWindowCollectionBehavior old = nw.collectionBehavior; nw.collectionBehavior = (old & !NSWindowCollectionBehaviorCanJoinAllSpaces) | NSWindowCollectionBehaviorMoveToActiveSpace; [nw orderFront:nil]; __block __typeof__(nw) weakSelf = nw; dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.collectionBehavior = old; }); } } void _glfwPlatformHideWindow(_GLFWwindow* window) { [window->ns.object orderOut:nil]; pid_t prev_app_pid = _glfw.ns.previous_front_most_application; _glfw.ns.previous_front_most_application = 0; NSRunningApplication *app; if (window->ns.layer_shell.is_active && prev_app_pid > 0 && (app = [NSRunningApplication runningApplicationWithProcessIdentifier:prev_app_pid])) { unsigned num_visible = 0; for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) { if (_glfwPlatformWindowVisible(w)) num_visible++; } if (!num_visible) { // yieldActivationToApplication was introduced in macOS 14 (Sonoma) SEL selector = NSSelectorFromString(@"yieldActivationToApplication:"); if ([NSApp respondsToSelector:selector]) { [NSApp performSelector:selector withObject:app]; [app activateWithOptions:0]; } else { #define NSApplicationActivateIgnoringOtherApps 2 [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; #undef NSApplicationActivateIgnoringOtherApps } } } } void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED) { [NSApp requestUserAttention:NSInformationalRequest]; } int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED) { NSBeep(); return true; } void _glfwPlatformFocusWindow(_GLFWwindow* window) { if (_glfwPlatformWindowIconified(window)) { // miniaturized windows return false in canBecomeKeyWindow therefore // unminiaturize first [window->ns.object deminiaturize:nil]; } if ([window->ns.object canBecomeKeyWindow]) { // Make us the active application [NSApp activateIgnoringOtherApps:YES]; [window->ns.object makeKeyAndOrderFront:nil]; } } void _glfwPlatformSetWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor, int xpos, int ypos, int width, int height, int refreshRate UNUSED) { if (window->monitor == monitor) { if (monitor) { if (monitor->window == window) acquireMonitor(window); } else { const NSRect contentRect = NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1), width, height); const NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect styleMask:getStyleMask(window)]; [window->ns.object setFrame:frameRect display:YES]; } return; } if (window->monitor) releaseMonitor(window); _glfwInputWindowMonitor(window, monitor); const NSUInteger styleMask = getStyleMask(window); [window->ns.object setStyleMask:styleMask]; // HACK: Changing the style mask can cause the first responder to be cleared [window->ns.object makeFirstResponder:window->ns.view]; if (window->monitor) { [window->ns.object setLevel:NSMainMenuWindowLevel + 1]; [window->ns.object setHasShadow:NO]; acquireMonitor(window); } else { NSRect contentRect = NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1), width, height); NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect styleMask:styleMask]; [window->ns.object setFrame:frameRect display:YES]; if (window->numer != GLFW_DONT_CARE && window->denom != GLFW_DONT_CARE) { [window->ns.object setContentAspectRatio:NSMakeSize(window->numer, window->denom)]; } if (window->minwidth != GLFW_DONT_CARE && window->minheight != GLFW_DONT_CARE) { [window->ns.object setContentMinSize:NSMakeSize(window->minwidth, window->minheight)]; } if (window->maxwidth != GLFW_DONT_CARE && window->maxheight != GLFW_DONT_CARE) { [window->ns.object setContentMaxSize:NSMakeSize(window->maxwidth, window->maxheight)]; } if (window->floating) [window->ns.object setLevel:NSFloatingWindowLevel]; else [window->ns.object setLevel:NSNormalWindowLevel]; [window->ns.object setHasShadow:YES]; // HACK: Clearing NSWindowStyleMaskTitled resets and disables the window // title property but the miniwindow title property is unaffected [window->ns.object setTitle:[window->ns.object miniwindowTitle]]; } } int _glfwPlatformWindowFocused(_GLFWwindow* window) { return [window->ns.object isKeyWindow]; } int _glfwPlatformWindowOccluded(_GLFWwindow* window) { return !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible); } int _glfwPlatformWindowIconified(_GLFWwindow* window) { return [window->ns.object isMiniaturized]; } int _glfwPlatformWindowVisible(_GLFWwindow* window) { return [window->ns.object isVisible]; } int _glfwPlatformWindowMaximized(_GLFWwindow* window) { return [window->ns.object isZoomed]; } int _glfwPlatformWindowHovered(_GLFWwindow* window) { const NSPoint point = [NSEvent mouseLocation]; if ([NSWindow windowNumberAtPoint:point belowWindowWithWindowNumber:0] != [window->ns.object windowNumber]) { return false; } return NSMouseInRect(point, [window->ns.object convertRectToScreen:[window->ns.view frame]], NO); } int _glfwPlatformFramebufferTransparent(_GLFWwindow* window) { return ![window->ns.object isOpaque] && ![window->ns.view isOpaque]; } void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED) { [window->ns.object setStyleMask:getStyleMask(window)]; [window->ns.object makeFirstResponder:window->ns.view]; } void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled UNUSED) { [window->ns.object setStyleMask:getStyleMask(window)]; [window->ns.object makeFirstResponder:window->ns.view]; } void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled) { if (enabled) [window->ns.object setLevel:NSFloatingWindowLevel]; else [window->ns.object setLevel:NSNormalWindowLevel]; } void _glfwPlatformSetWindowMousePassthrough(_GLFWwindow* window, bool enabled) { [window->ns.object setIgnoresMouseEvents:enabled]; } float _glfwPlatformGetWindowOpacity(_GLFWwindow* window) { return (float) [window->ns.object alphaValue]; } void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity) { [window->ns.object setAlphaValue:opacity]; } void _glfwPlatformSetRawMouseMotion(_GLFWwindow *window UNUSED, bool enabled UNUSED) { _glfwInputError(GLFW_FEATURE_UNIMPLEMENTED, "Cocoa: Raw mouse motion not yet implemented"); } bool _glfwPlatformRawMouseMotionSupported(void) { return false; } void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos) { const NSRect contentRect = [window->ns.view frame]; // NOTE: The returned location uses base 0,1 not 0,0 const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream]; if (xpos) *xpos = pos.x; if (ypos) *ypos = contentRect.size.height - pos.y; } void _glfwPlatformSetCursorPos(_GLFWwindow* window, double x, double y) { updateCursorImage(window); const NSRect contentRect = [window->ns.view frame]; // NOTE: The returned location uses base 0,1 not 0,0 const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream]; window->ns.cursorWarpDeltaX += x - pos.x; window->ns.cursorWarpDeltaY += y - contentRect.size.height + pos.y; if (window->monitor) { CGDisplayMoveCursorToPoint(window->monitor->ns.displayID, CGPointMake(x, y)); } else { const NSRect localRect = NSMakeRect(x, contentRect.size.height - y - 1, 0, 0); const NSRect globalRect = [window->ns.object convertRectToScreen:localRect]; const NSPoint globalPoint = globalRect.origin; CGWarpMouseCursorPosition(CGPointMake(globalPoint.x, _glfwTransformYNS(globalPoint.y))); } } void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED) { if (_glfwPlatformWindowFocused(window)) updateCursorMode(window); } const char* _glfwPlatformGetNativeKeyName(int keycode) { UInt32 deadKeyState = 0; UniChar characters[8]; UniCharCount characterCount = 0; if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes], keycode, kUCKeyActionDisplay, 0, LMGetKbdType(), kUCKeyTranslateNoDeadKeysBit, &deadKeyState, sizeof(characters) / sizeof(characters[0]), &characterCount, characters) != noErr) { return NULL; } if (!characterCount) return NULL; convert_utf16_to_utf8(characters, characterCount, _glfw.ns.keyName, sizeof(_glfw.ns.keyName)); return _glfw.ns.keyName; } int _glfwPlatformGetNativeKeyForKey(uint32_t glfw_key) { if (GLFW_FKEY_FIRST <= glfw_key && glfw_key <= GLFW_FKEY_LAST) { // {{{ switch(glfw_key) { /* start functional to macu (auto generated by gen-key-constants.py do not edit) */ case GLFW_FKEY_ENTER: return NSCarriageReturnCharacter; case GLFW_FKEY_TAB: return NSTabCharacter; case GLFW_FKEY_BACKSPACE: return NSBackspaceCharacter; case GLFW_FKEY_INSERT: return NSInsertFunctionKey; case GLFW_FKEY_DELETE: return NSDeleteFunctionKey; case GLFW_FKEY_LEFT: return NSLeftArrowFunctionKey; case GLFW_FKEY_RIGHT: return NSRightArrowFunctionKey; case GLFW_FKEY_UP: return NSUpArrowFunctionKey; case GLFW_FKEY_DOWN: return NSDownArrowFunctionKey; case GLFW_FKEY_PAGE_UP: return NSPageUpFunctionKey; case GLFW_FKEY_PAGE_DOWN: return NSPageDownFunctionKey; case GLFW_FKEY_HOME: return NSHomeFunctionKey; case GLFW_FKEY_END: return NSEndFunctionKey; case GLFW_FKEY_SCROLL_LOCK: return NSScrollLockFunctionKey; case GLFW_FKEY_NUM_LOCK: return NSClearLineFunctionKey; case GLFW_FKEY_PRINT_SCREEN: return NSPrintScreenFunctionKey; case GLFW_FKEY_PAUSE: return NSPauseFunctionKey; case GLFW_FKEY_MENU: return NSMenuFunctionKey; case GLFW_FKEY_F1: return NSF1FunctionKey; case GLFW_FKEY_F2: return NSF2FunctionKey; case GLFW_FKEY_F3: return NSF3FunctionKey; case GLFW_FKEY_F4: return NSF4FunctionKey; case GLFW_FKEY_F5: return NSF5FunctionKey; case GLFW_FKEY_F6: return NSF6FunctionKey; case GLFW_FKEY_F7: return NSF7FunctionKey; case GLFW_FKEY_F8: return NSF8FunctionKey; case GLFW_FKEY_F9: return NSF9FunctionKey; case GLFW_FKEY_F10: return NSF10FunctionKey; case GLFW_FKEY_F11: return NSF11FunctionKey; case GLFW_FKEY_F12: return NSF12FunctionKey; case GLFW_FKEY_F13: return NSF13FunctionKey; case GLFW_FKEY_F14: return NSF14FunctionKey; case GLFW_FKEY_F15: return NSF15FunctionKey; case GLFW_FKEY_F16: return NSF16FunctionKey; case GLFW_FKEY_F17: return NSF17FunctionKey; case GLFW_FKEY_F18: return NSF18FunctionKey; case GLFW_FKEY_F19: return NSF19FunctionKey; case GLFW_FKEY_F20: return NSF20FunctionKey; case GLFW_FKEY_F21: return NSF21FunctionKey; case GLFW_FKEY_F22: return NSF22FunctionKey; case GLFW_FKEY_F23: return NSF23FunctionKey; case GLFW_FKEY_F24: return NSF24FunctionKey; case GLFW_FKEY_F25: return NSF25FunctionKey; case GLFW_FKEY_F26: return NSF26FunctionKey; case GLFW_FKEY_F27: return NSF27FunctionKey; case GLFW_FKEY_F28: return NSF28FunctionKey; case GLFW_FKEY_F29: return NSF29FunctionKey; case GLFW_FKEY_F30: return NSF30FunctionKey; case GLFW_FKEY_F31: return NSF31FunctionKey; case GLFW_FKEY_F32: return NSF32FunctionKey; case GLFW_FKEY_F33: return NSF33FunctionKey; case GLFW_FKEY_F34: return NSF34FunctionKey; case GLFW_FKEY_F35: return NSF35FunctionKey; case GLFW_FKEY_KP_ENTER: return NSEnterCharacter; /* end functional to macu */ default: return 0; } } // }}} if (!is_pua_char(glfw_key)) return glfw_key; return 0; } int _glfwPlatformCreateCursor(_GLFWcursor* cursor, const GLFWimage* image, int xhot, int yhot, int count) { NSImage* native; NSBitmapImageRep* rep; native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)]; if (native == nil) return false; for (int i = 0; i < count; i++) { const GLFWimage *src = image + i; rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:src->width pixelsHigh:src->height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bitmapFormat:NSBitmapFormatAlphaNonpremultiplied bytesPerRow:src->width * 4 bitsPerPixel:32]; if (rep == nil) { [native release]; return false; } memcpy([rep bitmapData], src->pixels, src->width * src->height * 4); [native addRepresentation:rep]; [rep release]; } cursor->ns.object = [[NSCursor alloc] initWithImage:native hotSpot:NSMakePoint(xhot, yhot)]; [native release]; if (cursor->ns.object == nil) return false; return true; } static NSCursor* load_hidden_system_cursor(NSString *name, SEL fallback) { // this implementation comes from SDL_cocoamouse.m NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:name]; NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]]; /* we can't do animation atm. :/ */ const int frames = (int)[[info valueForKey:@"frames"] integerValue]; NSCursor *cursor; NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]]; if ((image == nil) || (image.isValid == NO)) { return [NSCursor performSelector:fallback]; } if (frames > 1) { const NSCompositingOperation operation = NSCompositingOperationCopy; const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames)); NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size]; if (cropped == nil) { return [NSCursor performSelector:fallback]; } [cropped lockFocus]; { const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height); [image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1]; } [cropped unlockFocus]; image = cropped; } cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])]; return cursor; } int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape) { #define C(name, val) case name: cursor->ns.object = [NSCursor val]; break; #define U(name, val) case name: cursor->ns.object = [NSCursor performSelector:@selector(val)]; break; #define S(name, val, fallback) case name: cursor->ns.object = load_hidden_system_cursor(@#val, @selector(val)); break; switch(shape) { /* start glfw to cocoa (auto generated by gen-key-constants.py do not edit) */ C(GLFW_DEFAULT_CURSOR, arrowCursor); C(GLFW_TEXT_CURSOR, IBeamCursor); C(GLFW_POINTER_CURSOR, pointingHandCursor); S(GLFW_HELP_CURSOR, help, arrowCursor); S(GLFW_WAIT_CURSOR, busybutclickable, arrowCursor); S(GLFW_PROGRESS_CURSOR, busybutclickable, arrowCursor); C(GLFW_CROSSHAIR_CURSOR, crosshairCursor); S(GLFW_CELL_CURSOR, cell, crosshairCursor); C(GLFW_VERTICAL_TEXT_CURSOR, IBeamCursorForVerticalLayout); S(GLFW_MOVE_CURSOR, move, openHandCursor); C(GLFW_E_RESIZE_CURSOR, resizeRightCursor); S(GLFW_NE_RESIZE_CURSOR, resizenortheast, _windowResizeNorthEastSouthWestCursor); S(GLFW_NW_RESIZE_CURSOR, resizenorthwest, _windowResizeNorthWestSouthEastCursor); C(GLFW_N_RESIZE_CURSOR, resizeUpCursor); S(GLFW_SE_RESIZE_CURSOR, resizesoutheast, _windowResizeNorthWestSouthEastCursor); S(GLFW_SW_RESIZE_CURSOR, resizesouthwest, _windowResizeNorthEastSouthWestCursor); C(GLFW_S_RESIZE_CURSOR, resizeDownCursor); C(GLFW_W_RESIZE_CURSOR, resizeLeftCursor); C(GLFW_EW_RESIZE_CURSOR, resizeLeftRightCursor); C(GLFW_NS_RESIZE_CURSOR, resizeUpDownCursor); U(GLFW_NESW_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor); U(GLFW_NWSE_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor); S(GLFW_ZOOM_IN_CURSOR, zoomin, arrowCursor); S(GLFW_ZOOM_OUT_CURSOR, zoomout, arrowCursor); C(GLFW_ALIAS_CURSOR, dragLinkCursor); C(GLFW_COPY_CURSOR, dragCopyCursor); C(GLFW_NOT_ALLOWED_CURSOR, operationNotAllowedCursor); C(GLFW_NO_DROP_CURSOR, operationNotAllowedCursor); C(GLFW_GRAB_CURSOR, openHandCursor); C(GLFW_GRABBING_CURSOR, closedHandCursor); /* end glfw to cocoa */ case GLFW_INVALID_CURSOR: return false; } #undef C #undef U #undef S if (!cursor->ns.object) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve standard cursor"); return false; } [cursor->ns.object retain]; return true; } void _glfwPlatformDestroyCursor(_GLFWcursor* cursor) { if (cursor->ns.object) [(NSCursor*) cursor->ns.object release]; } void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED) { if (cursorInContentArea(window)) updateCursorImage(window); } bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) { NSWindow *window = w->ns.object; bool traditional = !(flags & 1); if (traditional) { if(@available(macOS 10.15.7, *)) return w->ns.in_traditional_fullscreen; } NSWindowStyleMask sm = [window styleMask]; return sm & NSWindowStyleMaskFullScreen; } static void make_window_fullscreen_after_show(unsigned long long timer_id, void* data) { (void)timer_id; unsigned long long window_id = (uintptr_t)data; for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) { if (w->id == window_id) { NSWindow *window = w->ns.object; [window toggleFullScreen: nil]; update_titlebar_button_visibility_after_fullscreen_transition(w, false, true); break; } } } static void _glfwUpdateNotchCover(_GLFWwindow* w) { NSWindow *window = w->ns.object; if (w->ns.notch_cover_window) { [window removeChildWindow:w->ns.notch_cover_window]; [w->ns.notch_cover_window close]; [w->ns.notch_cover_window release]; w->ns.notch_cover_window = nil; } if (!w->ns.in_traditional_fullscreen) return; if (@available(macOS 12.0, *)) { CGFloat insetTop = window.screen.safeAreaInsets.top; if (insetTop <= 0) return; NSRect sf = [window.screen frame]; NSWindow *bg_window = [[NSWindow alloc] initWithContentRect:sf styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; [bg_window setBackgroundColor:[NSColor clearColor]]; [bg_window setHasShadow:NO]; [bg_window setOpaque:NO]; [bg_window setIgnoresMouseEvents:YES]; [bg_window setReleasedWhenClosed:NO]; [bg_window setColorSpace:[window colorSpace]]; // Add a colored subview only in the notch strip area NSView *notchView = [[NSView alloc] initWithFrame:NSMakeRect(0, sf.size.height - insetTop, sf.size.width, insetTop)]; notchView.wantsLayer = YES; unsigned int c = w->ns.notch_cover_color; float a = w->ns.notch_cover_opacity; notchView.layer.backgroundColor = [NSColor colorWithSRGBRed:((c >> 16) & 0xFF) / 255.0 green:((c >> 8) & 0xFF) / 255.0 blue:(c & 0xFF) / 255.0 alpha:a].CGColor; [bg_window.contentView addSubview:notchView]; // must be above otherwise shadow of main window is rendered over bg_window [window addChildWindow:bg_window ordered:NSWindowAbove]; w->ns.notch_cover_window = bg_window; [notchView release]; } } bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) { NSWindow *window = w->ns.object; bool made_fullscreen = true; bool traditional = !(flags & 1); bool ignore_safe_area_insets = flags & 2; NSWindowStyleMask sm = [window styleMask]; if (traditional) { if (@available(macOS 10.15.7, *)) { // As of Big Turd NSWindowStyleMaskFullScreen is no longer usable // Also no longer compatible after a minor release of macOS 10.15.7 if (!w->ns.in_traditional_fullscreen) { // Apple throws NSGenericException if setStyleMask: clears // NSWindowStyleMaskFullScreen outside a transition (see #9572). // Split View sets this flag via the system, so fall back to // Cocoa fullscreen toggle instead of the traditional path. if (sm & NSWindowStyleMaskFullScreen) { [window toggleFullScreen:nil]; return false; } w->ns.pre_full_screen_style_mask = sm; w->ns.pre_traditional_fullscreen_frame = [window frame]; [window setStyleMask: NSWindowStyleMaskBorderless]; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock]; NSRect screenFrame = [window.screen frame]; if (!ignore_safe_area_insets) { if (@available(macOS 12.0, *)) { screenFrame.size.height -= window.screen.safeAreaInsets.top; } } [window setFrame:screenFrame display:YES]; w->ns.in_traditional_fullscreen = true; if (!ignore_safe_area_insets) _glfwUpdateNotchCover(w); } else { made_fullscreen = false; if (sm & NSWindowStyleMaskFullScreen) { // Split View added NSWindowStyleMaskFullScreen on top of our // traditional fullscreen. We can't clear that flag directly // (NSGenericException), so trigger a Cocoa exit and defer the // traditional fullscreen cleanup to windowDidExitFullScreen: // which fires after macOS finishes its async transition (#9572). // Return true to prevent the caller from setting the window // frame during the Cocoa exit animation. [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; [window toggleFullScreen:nil]; return true; } else { [window setStyleMask: w->ns.pre_full_screen_style_mask]; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; w->ns.in_traditional_fullscreen = false; _glfwUpdateNotchCover(w); } } } else { bool in_fullscreen = sm & NSWindowStyleMaskFullScreen; if (!(in_fullscreen)) { sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen; [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock]; } else { made_fullscreen = false; sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen); [[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault]; } [window setStyleMask: sm]; } // Changing the style mask causes the first responder to be cleared [window makeFirstResponder:w->ns.view]; // If the dock and menubar are hidden going from maximized to fullscreen doesn't change the window size // and macOS forgets to trigger windowDidResize, so call it ourselves NSNotification *notification = [NSNotification notificationWithName:NSWindowDidResizeNotification object:window]; [w->ns.delegate performSelector:@selector(windowDidResize:) withObject:notification afterDelay:0]; } else { bool in_fullscreen = sm & NSWindowStyleMaskFullScreen; if (!in_fullscreen && !_glfwPlatformWindowVisible(w)) { // Bug in Apple's fullscreen implementation causes fullscreen to // not work before window is shown (at creation) if another window // is already fullscreen. Le sigh. https://github.com/kovidgoyal/kitty/issues/7448 _glfwPlatformAddTimer(0, false, make_window_fullscreen_after_show, (void*)(uintptr_t)(w->id), NULL); return made_fullscreen; } if (in_fullscreen) made_fullscreen = false; [window toggleFullScreen: nil]; } update_titlebar_button_visibility_after_fullscreen_transition(w, traditional, made_fullscreen); return made_fullscreen; } // Clipboard {{{ static void list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) { #define w(x) { if (ok) ok = write_data(object, x, strlen(x)); } NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES}; BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options]; BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil]; /* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */ bool ok = true; if (has_strings) w("text/plain"); if (has_file_urls) w("text/local-path-list"); for (NSPasteboardItem * item in pasteboard.pasteboardItems) { for (NSPasteboardType type in item.types) { /* NSLog(@"%@", type); */ const char *mime = uti_to_mime(type); if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) { /* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */ w(mime); } } } #undef w } static void get_text_plain(GLFWclipboardwritedatafun write_data, void *object) { NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES}; NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options]; bool found = false; if (objs) { const NSUInteger count = [objs count]; if (count) { NSMutableData *path_list = [NSMutableData dataWithCapacity:4096]; // auto-released NSMutableData *text_list = [NSMutableData dataWithCapacity:4096]; // auto-released for (NSUInteger i = 0; i < count; i++) { id obj = objs[i]; if ([obj isKindOfClass:[NSURL class]]) { NSURL *url = (NSURL*)obj; if (url.fileURL && url.fileSystemRepresentation) { if ([path_list length] > 0) [path_list appendBytes:"\n" length:1]; [path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)]; } } else if ([obj isKindOfClass:[NSString class]]) { if ([text_list length] > 0) [text_list appendBytes:"\n" length:1]; [text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]]; } } const NSMutableData *text = nil; if (path_list.length > 0) text = path_list; else if (text_list.length > 0) text = text_list; if (text) { found = true; write_data(object, text.mutableBytes, text.length); } } } if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard"); } void _glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) { if (clipboard_type != GLFW_CLIPBOARD) return; if (mime_type == NULL) { list_clipboard_mimetypes(write_data, object); return; } if (strcmp(mime_type, "text/plain") == 0) { get_text_plain(write_data, object); return; } NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; /* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */ NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]]; /* NSLog(@"available type: %@", t); */ if (t != nil) { NSData *data = [pasteboard dataForType:t]; // auto-released /* NSLog(@"data: %@", data); */ if (data != nil && data.length > 0) { write_data(object, data.bytes, data.length); } } } static NSMutableData* get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) { NSMutableData *ans = [NSMutableData dataWithCapacity:8192]; if (ans == nil) return nil; GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype); void *iter = chunk.iter; if (!iter) return ans; while (true) { chunk = cd->get_data(mime, iter, cd->ctype); if (!chunk.sz) break; [ans appendBytes:chunk.data length:chunk.sz]; if (chunk.free) chunk.free((void*)chunk.free_data); } cd->get_data(NULL, iter, cd->ctype); return ans; } void _glfwPlatformSetClipboard(GLFWClipboardType t) { if (t != GLFW_CLIPBOARD) return; NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; NSMutableArray *ptypes = [NSMutableArray arrayWithCapacity:_glfw.clipboard.num_mime_types]; // auto-released for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) { [ptypes addObject:mime_to_uti(_glfw.clipboard.mime_types[i])]; } [pasteboard declareTypes:ptypes owner:nil]; for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) { NSMutableData *data = get_clipboard_data(&_glfw.clipboard, _glfw.clipboard.mime_types[i]); // auto-released /* NSLog(@"putting data: %@ for: %s with UTI: %@", data, _glfw.clipboard.mime_types[i], ptypes[i]); */ if (data != nil) [pasteboard setData:data forType:ptypes[i]]; } } // }}} EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs) { if (_glfw.egl.ANGLE_platform_angle) { int type = 0; if (_glfw.egl.ANGLE_platform_angle_opengl) { if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_OPENGL) type = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE; } if (_glfw.egl.ANGLE_platform_angle_metal) { if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_METAL) type = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE; } if (type) { *attribs = calloc(3, sizeof(EGLint)); (*attribs)[0] = EGL_PLATFORM_ANGLE_TYPE_ANGLE; (*attribs)[1] = type; (*attribs)[2] = EGL_NONE; return EGL_PLATFORM_ANGLE_ANGLE; } } return 0; } EGLNativeDisplayType _glfwPlatformGetEGLNativeDisplay(void) { return EGL_DEFAULT_DISPLAY; } EGLNativeWindowType _glfwPlatformGetEGLNativeWindow(_GLFWwindow* window) { return window->ns.layer; } void _glfwPlatformGetRequiredInstanceExtensions(char** extensions) { if (_glfw.vk.KHR_surface && _glfw.vk.EXT_metal_surface) { extensions[0] = "VK_KHR_surface"; extensions[1] = "VK_EXT_metal_surface"; } else if (_glfw.vk.KHR_surface && _glfw.vk.MVK_macos_surface) { extensions[0] = "VK_KHR_surface"; extensions[1] = "VK_MVK_macos_surface"; } } int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance UNUSED, VkPhysicalDevice device UNUSED, uint32_t queuefamily UNUSED) { return true; } VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, _GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface) { #if MAC_OS_X_VERSION_MAX_ALLOWED >= 101100 // HACK: Dynamically load Core Animation to avoid adding an extra // dependency for the majority who don't use MoltenVK NSBundle* bundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/QuartzCore.framework"]; if (!bundle) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to find QuartzCore.framework"); return VK_ERROR_EXTENSION_NOT_PRESENT; } // NOTE: Create the layer here as makeBackingLayer should not return nil window->ns.layer = [[bundle classNamed:@"CAMetalLayer"] layer]; if (!window->ns.layer) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create layer for view"); return VK_ERROR_EXTENSION_NOT_PRESENT; } if (window->ns.retina) [window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]]; [window->ns.view setLayer:window->ns.layer]; [window->ns.view setWantsLayer:YES]; VkResult err; if (_glfw.vk.EXT_metal_surface) { VkMetalSurfaceCreateInfoEXT sci; PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT; vkCreateMetalSurfaceEXT = (PFN_vkCreateMetalSurfaceEXT) vkGetInstanceProcAddr(instance, "vkCreateMetalSurfaceEXT"); if (!vkCreateMetalSurfaceEXT) { _glfwInputError(GLFW_API_UNAVAILABLE, "Cocoa: Vulkan instance missing VK_EXT_metal_surface extension"); return VK_ERROR_EXTENSION_NOT_PRESENT; } memset(&sci, 0, sizeof(sci)); sci.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; sci.pLayer = window->ns.layer; err = vkCreateMetalSurfaceEXT(instance, &sci, allocator, surface); } else { VkMacOSSurfaceCreateInfoMVK sci; PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK; vkCreateMacOSSurfaceMVK = (PFN_vkCreateMacOSSurfaceMVK) vkGetInstanceProcAddr(instance, "vkCreateMacOSSurfaceMVK"); if (!vkCreateMacOSSurfaceMVK) { _glfwInputError(GLFW_API_UNAVAILABLE, "Cocoa: Vulkan instance missing VK_MVK_macos_surface extension"); return VK_ERROR_EXTENSION_NOT_PRESENT; } memset(&sci, 0, sizeof(sci)); sci.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; sci.pView = window->ns.view; err = vkCreateMacOSSurfaceMVK(instance, &sci, allocator, surface); } if (err) { _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create Vulkan surface: %s", _glfwGetVulkanResultString(err)); } return err; #else return VK_ERROR_EXTENSION_NOT_PRESENT; #endif } int _glfwPlatformSetWindowBlur(_GLFWwindow *window, int radius) { int orig = window->ns.blur_radius; if (radius > -1 && radius != window->ns.blur_radius) { extern OSStatus CGSSetWindowBackgroundBlurRadius(void* connection, NSInteger windowNumber, int radius); extern void* CGSDefaultConnectionForThread(void); CGSSetWindowBackgroundBlurRadius(CGSDefaultConnectionForThread(), [window->ns.object windowNumber], radius); window->ns.blur_radius = radius; } return orig; } ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// ////////////////////////////////////////////////////////////////////////// GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle) { _GLFWwindow* window = (_GLFWwindow*) handle; assert(window != NULL); _GLFW_REQUIRE_INIT_OR_RETURN(nil); return window->ns.object; } GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) { _GLFWwindow* window = (_GLFWwindow*) handle; _GLFW_REQUIRE_INIT_OR_RETURN(nil); GLFWcocoatextinputfilterfun previous = window->ns.textInputFilterCallback; window->ns.textInputFilterCallback = callback; return previous; } GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) { _GLFW_REQUIRE_INIT_OR_RETURN(nil); GLFWhandleurlopen prev = _glfw.ns.url_open_callback; _glfw.ns.url_open_callback = callback; return prev; } GLFWAPI GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *handle, GLFWcocoatogglefullscreenfun callback) { _GLFWwindow* window = (_GLFWwindow*) handle; _GLFW_REQUIRE_INIT_OR_RETURN(nil); GLFWcocoatogglefullscreenfun previous = window->ns.toggleFullscreenCallback; window->ns.toggleFullscreenCallback = callback; return previous; } GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback) { requestRenderFrame((_GLFWwindow*)w, callback); } GLFWAPI GLFWcocoarenderframefun glfwCocoaSetWindowResizeCallback(GLFWwindow *w, GLFWcocoarenderframefun cb) { _GLFWwindow* window = (_GLFWwindow*)w; GLFWcocoarenderframefun current = window->ns.resizeCallback; window->ns.resizeCallback = cb; return current; } @implementation NSView (FindByIdentifier) - (NSArray *)viewsWithIdentifier:(NSUserInterfaceItemIdentifier)identifier { NSMutableArray *result = [NSMutableArray array]; if ([self.identifier isEqual:identifier]) { [result addObject:self]; } for (NSView *sub in self.subviews) { [result addObjectsFromArray:[sub viewsWithIdentifier:identifier]]; } return result; } @end static void clear_title_bar_background_views(NSWindow *window) { #define tag @"kitty-for-transparent-titlebar" NSView *contentView = window.contentView, *titlebarContainer = contentView ? contentView.superview : nil; if (titlebarContainer) { for (NSView *subview in [titlebarContainer viewsWithIdentifier:tag]) [subview removeFromSuperview]; } } static void set_title_bar_background(NSWindow *window, NSColor *backgroundColor) { // add an extra view that just renders the background color under the transparent titlebar NSView *contentView = window.contentView, *titlebarContainer = contentView ? contentView.superview : nil; if (!titlebarContainer) return; for (NSView *subview in [titlebarContainer viewsWithIdentifier:tag]) [subview removeFromSuperview]; if (!backgroundColor) return; NSButton *b = [window standardWindowButton:NSWindowCloseButton]; if (b) { NSView *titlebarView = b.superview; NSView *bgView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, titlebarView.bounds.size.width, titlebarView.bounds.size.height)]; bgView.translatesAutoresizingMaskIntoConstraints = NO; bgView.wantsLayer = YES; bgView.layer.backgroundColor = backgroundColor.CGColor; bgView.identifier = tag; [titlebarView addSubview:bgView positioned:NSWindowBelow relativeTo:titlebarView.subviews[0]]; [NSLayoutConstraint activateConstraints:@[ // Pin to the top of the content view. [bgView.topAnchor constraintEqualToAnchor:titlebarView.topAnchor], // Pin to the leading edge of the content view. [bgView.leadingAnchor constraintEqualToAnchor:titlebarView.leadingAnchor], // Pin to the trailing edge of the content view. [bgView.trailingAnchor constraintEqualToAnchor:titlebarView.trailingAnchor], // Give it a fixed height [bgView.bottomAnchor constraintEqualToAnchor:titlebarView.bottomAnchor] ]]; [bgView release]; return; } NSView *bgView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, titlebarContainer.bounds.size.width, 32)]; bgView.translatesAutoresizingMaskIntoConstraints = NO; bgView.wantsLayer = YES; bgView.layer.backgroundColor = backgroundColor.CGColor; bgView.identifier = tag; // position the background view above the content view but below the titlebar view [titlebarContainer addSubview:bgView positioned:NSWindowAbove relativeTo:contentView]; // for (NSView *subview in titlebarContainer.subviews) NSLog(@"sv: %@", subview.identifier); [NSLayoutConstraint activateConstraints:@[ // Pin to the top of the content view. [bgView.topAnchor constraintEqualToAnchor:titlebarContainer.topAnchor], // Pin to the leading edge of the content view. [bgView.leadingAnchor constraintEqualToAnchor:titlebarContainer.leadingAnchor], // Pin to the trailing edge of the content view. [bgView.trailingAnchor constraintEqualToAnchor:titlebarContainer.trailingAnchor], // Give it a fixed height [bgView.bottomAnchor constraintEqualToAnchor:contentView.topAnchor] ]]; [bgView release]; #undef tag } static void apply_titlebar_color_settings(_GLFWwindow *window) { #define tc window->ns.last_applied_titlebar_settings.color GLFWWindow *nsw = window->ns.object; if (!window->ns.titlebar_hidden && window->decorated && tc.was_set && window->ns.last_applied_titlebar_settings.transparent) { NSColor *titlebar_color = [NSColor colorWithSRGBRed:tc.red green:tc.green blue:tc.blue alpha:tc.alpha]; set_title_bar_background(nsw, titlebar_color); } else clear_title_bar_background_views(nsw); #undef tc } static void apply_window_corner_curve(_GLFWwindow *window) { if (!window || !window->decorated) return; if (@available(macOS 26.0, *)) { GLFWWindow *nsw = window->ns.object; NSView *frame_view = nsw.contentView.superview; CALayer *layer = frame_view.layer; if (!layer) return; layer.cornerCurve = kCACornerCurveContinuous; } } GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool use_system_color, unsigned int system_color, int background_blur, unsigned int hide_window_decorations, bool show_text_in_titlebar, int color_space, float background_opacity, bool resizable) { @autoreleasepool { _GLFWwindow* window = (_GLFWwindow*)w; if (window->ns.layer_shell.is_active) return; GLFWWindow *nsw = window->ns.object; const bool is_transparent = _glfwPlatformFramebufferTransparent(window); if (!is_transparent) { background_opacity = 1.0; background_blur = 0; } NSColor *window_background = [NSColor windowBackgroundColor]; if (background_opacity < 1.0) { // use a clear color (fully transparent) so that the final color is just the color from the surface. // prevent blurring of shadows at window corners with desktop background by setting a low alpha background window_background = background_blur > 0 ? [NSColor colorWithWhite: 0 alpha: 0.001f] : [NSColor clearColor]; } NSAppearance *appearance = nil; #define tc window->ns.last_applied_titlebar_settings.color tc.was_set = false; window->ns.last_applied_titlebar_settings.transparent = false; const NSWindowStyleMask current_style_mask = [nsw styleMask]; const bool in_fullscreen = ((current_style_mask & NSWindowStyleMaskFullScreen) != 0) || window->ns.in_traditional_fullscreen; NSAppearance *light_appearance = is_transparent ? [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight] : [NSAppearance appearanceNamed:NSAppearanceNameAqua]; NSAppearance *dark_appearance = is_transparent ? [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark] : [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; if (use_system_color) { switch (system_color) { case 1: appearance = light_appearance; break; case 2: appearance = dark_appearance; break; } } else { tc.red = ((color >> 16) & 0xFF) / 255.0; tc.green = ((color >> 8) & 0xFF) / 255.0; tc.blue = (color & 0xFF) / 255.0; tc.alpha = background_opacity; tc.was_set = true; double luma = 0.2126 * tc.red + 0.7152 * tc.green + 0.0722 * tc.blue; appearance = luma < 0.5 ? dark_appearance : light_appearance; window->ns.last_applied_titlebar_settings.transparent = true; } [nsw setBackgroundColor:window_background]; [nsw setAppearance:appearance]; _glfwPlatformSetWindowBlur(window, background_blur); bool has_shadow = false; const char *decorations_desc = "full"; window->ns.titlebar_hidden = false; switch (hide_window_decorations) { case 1: decorations_desc = "none"; window->decorated = false; break; case 2: decorations_desc = "no-titlebar"; window->decorated = true; has_shadow = true; window->ns.last_applied_titlebar_settings.transparent = true; window->ns.titlebar_hidden = true; show_text_in_titlebar = false; break; case 4: decorations_desc = "no-titlebar-and-no-corners"; window->decorated = false; has_shadow = true; break; default: window->decorated = true; has_shadow = true; break; } // shadow causes burn-in/ghosting because cocoa doesnt invalidate it on OS window resize/minimize/restore. // https://github.com/kovidgoyal/kitty/issues/6439 if (is_transparent) has_shadow = false; bool hide_titlebar_buttons = !in_fullscreen && window->ns.titlebar_hidden; [nsw setTitlebarAppearsTransparent:window->ns.last_applied_titlebar_settings.transparent]; [nsw setHasShadow:has_shadow]; [nsw setTitleVisibility:(show_text_in_titlebar) ? NSWindowTitleVisible : NSWindowTitleHidden]; NSColorSpace *cs = nil; switch (color_space) { case SRGB_COLORSPACE: cs = [NSColorSpace sRGBColorSpace]; break; case DISPLAY_P3_COLORSPACE: cs = [NSColorSpace displayP3ColorSpace]; break; case DEFAULT_COLORSPACE: cs = nil; break; // using deviceRGBColorSpace causes a hang when transitioning to fullscreen } window->resizable = resizable; debug( "Window Chrome state:\n\tbackground: %s\n\tappearance: %s color_space: %s\n\t" "blur: %d has_shadow: %d resizable: %d decorations: %s (%d)\n\t" "titlebar_transparent: %d titlebar_color_set: %d title_visibility: %d hidden: %d buttons_hidden: %d" "\n", window_background ? [window_background.description UTF8String] : "", appearance ? [appearance.name UTF8String] : "", cs ? (cs.localizedName ? [cs.localizedName UTF8String] : [cs.description UTF8String]) : "", background_blur, has_shadow, resizable, decorations_desc, window->decorated, window->ns.last_applied_titlebar_settings.transparent, tc.was_set, show_text_in_titlebar, window->ns.titlebar_hidden, hide_titlebar_buttons ); [nsw setColorSpace:cs]; [[nsw standardWindowButton: NSWindowCloseButton] setHidden:hide_titlebar_buttons]; [[nsw standardWindowButton: NSWindowMiniaturizeButton] setHidden:hide_titlebar_buttons]; [[nsw standardWindowButton: NSWindowZoomButton] setHidden:hide_titlebar_buttons]; // Apple throws a hissy fit if one attempts to clear the value of NSWindowStyleMaskFullScreen outside of a full screen transition // event. See https://github.com/kovidgoyal/kitty/issues/7106 NSWindowStyleMask fsmask = current_style_mask & NSWindowStyleMaskFullScreen; window->ns.pre_full_screen_style_mask = getStyleMask(window); NSWindowStyleMask desired_mask; if (in_fullscreen && window->ns.in_traditional_fullscreen) { desired_mask = NSWindowStyleMaskBorderless; } else { desired_mask = window->ns.pre_full_screen_style_mask | fsmask; } // Only call setStyleMask: when the mask actually changes. Redundant // calls can trigger macOS to reposition the window (#9572). if (desired_mask != current_style_mask) { [nsw setStyleMask:desired_mask]; } #undef tc apply_titlebar_color_settings(window); apply_window_corner_curve(window); // HACK: Changing the style mask can cause the first responder to be cleared [nsw makeFirstResponder:window->ns.view]; window->ns.notch_cover_color = color; window->ns.notch_cover_opacity = background_opacity; if (window->ns.notch_cover_window) _glfwUpdateNotchCover(window); }} GLFWAPI uint32_t glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) { *cocoa_mods = 0; if (glfw_mods & GLFW_MOD_SHIFT) *cocoa_mods |= NSEventModifierFlagShift; if (glfw_mods & GLFW_MOD_CONTROL) *cocoa_mods |= NSEventModifierFlagControl; if (glfw_mods & GLFW_MOD_ALT) *cocoa_mods |= NSEventModifierFlagOption; if (glfw_mods & GLFW_MOD_SUPER) *cocoa_mods |= NSEventModifierFlagCommand; if (glfw_mods & GLFW_MOD_CAPS_LOCK) *cocoa_mods |= NSEventModifierFlagCapsLock; return _glfwPlatformGetNativeKeyForKey(glfw_key); } GLFWAPI bool glfwIsLayerShellSupported(void) { return true; } GLFWAPI void glfwCocoaCycleThroughOSWindows(bool backwards) { NSArray *allWindows = [NSApp windows]; if (allWindows.count < 2) return; NSMutableArray *filteredWindows = [NSMutableArray array]; for (NSWindow *window in allWindows) { NSRect windowFrame = [window frame]; // Exclude zero size windows which are likely zombie windows from the Tahoe bug // if ([obj isMemberOfClass:[MyClass class]]) { if ( windowFrame.size.width > 0 && windowFrame.size.height > 0 && \ !window.isMiniaturized && window.isVisible && \ [window isMemberOfClass:[GLFWWindow class]] ) [filteredWindows addObject:window]; } if (filteredWindows.count < 2) return; [filteredWindows sortUsingComparator:^NSComparisonResult(NSWindow *a, NSWindow *b) { return [@(a.windowNumber) compare:@(b.windowNumber)]; }]; NSWindow *keyWindow = [NSApp keyWindow]; NSUInteger index = [filteredWindows indexOfObject:keyWindow]; NSUInteger nextIndex = 0; if (index != NSNotFound) { if (backwards) { nextIndex = (index == 0) ? [filteredWindows count] - 1 : index - 1; } else nextIndex = (index + 1) % filteredWindows.count; } NSWindow *nextWindow = filteredWindows[nextIndex]; [nextWindow makeKeyAndOrderFront:nil]; } GLFWAPI void glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count) { _GLFWwindow *w = (_GLFWwindow*)window; NSArray *currentTypes = [w->ns.view registeredDraggedTypes]; NSMutableArray *updatedTypes = [NSMutableArray arrayWithArray:currentTypes]; for (size_t i = 0; i < count; i++) { NSString *uti = mime_to_uti(mimes[i]); if (![updatedTypes containsObject:uti]) [updatedTypes addObject:uti]; } [w->ns.view registerForDraggedTypes:updatedTypes]; } ////////////////////////////////////////////////////////////////////////// ////// GLFW internal API ////// ////////////////////////////////////////////////////////////////////////// // Transforms a y-coordinate between the CG display and NS screen spaces // float _glfwTransformYNS(float y) { return CGDisplayBounds(CGMainDisplayID()).size.height - y - 1; } void _glfwCocoaPostEmptyEvent(void) { NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil subtype:0 data1:0 data2:0]; [NSApp postEvent:event atStart:YES]; } // Drag source implementation {{{ @implementation GLFWDraggingSource - (NSDragOperation)draggingSession:(NSDraggingSession*)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { (void)session; (void)context; // Return the operation based on the stored drag operations bitfield NSDragOperation ops = NSDragOperationCopy; int q = _glfw.drag.operations; if (q & GLFW_DRAG_OPERATION_COPY) ops |= NSDragOperationCopy; if (q & GLFW_DRAG_OPERATION_MOVE) ops |= NSDragOperationMove; if (q & GLFW_DRAG_OPERATION_GENERIC) ops |= NSDragOperationGeneric; return ops; } - (void)draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint { (void)session; start_point = screenPoint; } - (void)draggingSession:(NSDraggingSession *)session movedToPoint:(NSPoint)screenPoint { (void)session; current_point = screenPoint; } - (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation { (void)session; _glfwPlatformFreeDragSourceData(); _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); if (window) { GLFWDragEvent ev = {0}; switch(operation) { case NSDragOperationCopy: case NSDragOperationLink: ev.action = GLFW_DRAG_OPERATION_COPY; break; case NSDragOperationMove: case NSDragOperationDelete: ev.action = GLFW_DRAG_OPERATION_MOVE; break; case NSDragOperationNone: break; default: ev.action = GLFW_DRAG_OPERATION_GENERIC; break; } switch (operation) { case NSDragOperationDelete: ev.type = GLFW_DRAG_CANCELLED; break; case NSDragOperationNone: { NSEvent *currentEvent = [NSApp currentEvent]; if (currentEvent && currentEvent.type == NSEventTypeKeyDown && currentEvent.keyCode == 53) { ev.type = GLFW_DRAG_CANCELLED; } else ev.type = GLFW_DRAG_DROPPED; } break; default: ev.type = GLFW_DRAG_DROPPED; break; } _glfwInputDragSourceRequest(window, &ev); if (operation == NSDragOperationNone) _glfwFreeDragSourceData(); } } @end static NSMutableArray *file_promise_providers = nil; static int set_image_for_dragging_item(NSDraggingItem *draggingItem, const GLFWimage *thumbnail, NSWindow *window) { CGFloat scaleFactor = 1.0; [draggingItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil]; if (_glfw.ns.drag_image) [_glfw.ns.drag_image release]; _glfw.ns.drag_image = nil; if (thumbnail && thumbnail->pixels && window) { scaleFactor = [window backingScaleFactor]; if (scaleFactor == 0) scaleFactor = [NSScreen mainScreen].backingScaleFactor; unsigned height = thumbnail->height + 20; // add empty padding at bottom NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:thumbnail->width pixelsHigh:height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bytesPerRow:thumbnail->width * 4 bitsPerPixel:32]; if (!imageRep) return ENOMEM; memcpy([imageRep bitmapData], thumbnail->pixels, thumbnail->width * thumbnail->height * 4); NSSize pointSize = NSMakeSize(thumbnail->width / scaleFactor, height / scaleFactor); [imageRep setSize:pointSize]; _glfw.ns.drag_image = [[NSImage alloc] initWithSize:pointSize]; if (!_glfw.ns.drag_image) { [imageRep release]; return ENOMEM; } [_glfw.ns.drag_image addRepresentation:imageRep]; [imageRep release]; if (!_glfw.ns.drag_session) { _GLFWwindow *glfw_window = _glfwWindowForId(_glfw.drag.window_id); if (glfw_window && glfw_window->ns.view) { NSRect contentRect = [glfw_window->ns.view frame]; NSPoint cursor = NSMakePoint(glfw_window->virtualCursorPosX, contentRect.size.height - glfw_window->virtualCursorPosY); [draggingItem setDraggingFrame:NSMakeRect(cursor.x, cursor.y, pointSize.width, pointSize.height) contents:nil]; } else { [draggingItem setDraggingFrame:NSMakeRect(0, 0, pointSize.width, pointSize.height) contents:nil]; } } [draggingItem setImageComponentsProvider:^NSArray * _Nonnull{ NSDraggingImageComponent *icon = [NSDraggingImageComponent draggingImageComponentWithKey:NSDraggingImageComponentIconKey]; NSImage *image = _glfw.ns.drag_image; icon.contents = image; icon.frame = NSMakeRect(0, 0, pointSize.width, pointSize.height); return @[icon]; }]; } return 0; } int _glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {@autoreleasepool{ if (file_promise_providers) { for (NSInteger i = [file_promise_providers count] - 1; i >= 0; i--) { GLFWFilePromiseProviderDelegate* d = file_promise_providers[i]; [d end_transfer:EINVAL]; } } // Obtain the event and view early so we can position the drag image relative to the cursor NSEvent* event = [NSApp currentEvent]; if (!event || ([event type] != NSEventTypeLeftMouseDown && [event type] != NSEventTypeLeftMouseDragged)) { // Create a synthetic left mouse down event using stored cursor position // Convert window coordinates to screen coordinates NSRect contentRect = [window->ns.view frame]; NSPoint windowPos = NSMakePoint(window->virtualCursorPosX, contentRect.size.height - window->virtualCursorPosY); event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown location:windowPos modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window->ns.object windowNumber] context:nil eventNumber:0 clickCount:1 pressure:1.0]; } if (!event) return EIO; GLFWContentView *v = window->ns.view; NSMutableArray* dragItems = [[[NSMutableArray alloc] init] autorelease]; for (size_t i = 0; i < _glfw.drag.item_count; i++) { NSString* utiString = mime_to_uti(_glfw.drag.items[i].mime_type); id w; if (_glfw.drag.items[i].optional_data) { NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease]; NSData *data = [NSData dataWithBytes:_glfw.drag.items[i].optional_data length:_glfw.drag.items[i].data_size]; [pbItem setData:data forType:utiString]; w = pbItem; } else { // Create file promise provider with our delegate GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc] initWithWindow:window mimeType:_glfw.drag.items[i].mime_type instanceId:_glfw.drag.instance_id] autorelease]; NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc] initWithFileType:utiString delegate:delegate] autorelease]; // Store the delegate in the provider's user info so it's retained provider.userInfo = delegate; w = provider; } NSDraggingItem* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:w] autorelease]; if (i == 0 && thumbnail && thumbnail->pixels) { int err = set_image_for_dragging_item(dragItem, thumbnail, window->ns.object); if (err) return err; } else { [dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil]; } [dragItems addObject:dragItem]; } NSDraggingSession *s = [v beginDraggingSessionWithItems:dragItems event:event source:[v draggingSource]]; _glfw.ns.drag_session = [s retain]; _glfw.ns.drag_view = [v retain]; return 0; }} @implementation GLFWFilePromiseProviderDelegate - (void)end_transfer_with_error:(NSError*)err { if (err && file_url) { NSError *error; NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtURL:file_url error:&error]; } if (file_handle) [file_handle release]; file_handle = nil; if (completion_handler) { completion_handler(err); Block_release(completion_handler); completion_handler = nil; } [file_promise_providers removeObject:self]; } - (void)end_transfer:(int)errorCode { [self end_transfer_with_error:errorCode ? [NSError errorWithDomain:NSPOSIXErrorDomain code:errorCode userInfo:nil] : nil]; } - (bool)is_mimetype:(const char*)q { return strcmp(q, mimeType) == 0; } - (void)request_drag_data { if (instanceId != _glfw.drag.instance_id) { [self end_transfer:EINVAL]; return; } _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); if (!window) { [self end_transfer:EINVAL]; return; } bool keep_going = true; while (keep_going) { GLFWDragEvent ev = {.type=GLFW_DRAG_DATA_REQUEST, .mime_type=mimeType}; _glfwInputDragSourceRequest(window, &ev); if (ev.err_num) { keep_going = false; if (ev.err_num != EAGAIN) [self end_transfer:ev.err_num]; } else { if (ev.data_sz) { NSData* nsData = [NSData dataWithBytes:ev.data length:ev.data_sz]; NSError* error = nil; if (![file_handle writeData:nsData error:&error]) { keep_going = false; [self end_transfer_with_error:error]; } } else { keep_going = false; [self end_transfer_with_error:nil]; } _glfwInputDragSourceRequest(window, &ev); } } } - (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid) instance_id { self = [super init]; if (self) { windowId = initWindow ? initWindow->id : 0; mimeType = _glfw_strdup(mime); instanceId = instance_id; if (file_promise_providers == nil) file_promise_providers = [NSMutableArray array]; [file_promise_providers addObject:self]; } return self; } - (void)dealloc { free(mimeType); mimeType = NULL; if (file_url) [file_url release]; file_url = nil; [self end_transfer:EINVAL]; [super dealloc]; } - (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType { (void)filePromiseProvider; (void)fileType; // Generate a unique filename based on the MIME type NSString* extension = @"data"; if (mimeType) { UTType *type = [UTType typeWithMIMEType:@(mimeType)]; extension = type.preferredFilenameExtension; } return [NSString stringWithFormat:@"kitty-drag-source-%@.%@", [[NSUUID UUID] UUIDString], extension]; } - (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider writePromiseToURL:(NSURL*)url completionHandler:(void (^)(NSError*))completionHandler { (void)filePromiseProvider; _GLFWwindow* window = _glfwWindowForId(windowId); if (!window || instanceId != _glfw.drag.instance_id) { completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]); return; } // Create the file NSError* error = nil; if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) { error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil]; completionHandler(error); return; } NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error]; if (!fileHandle) { completionHandler(error); NSError *error; NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtURL:url error:&error]; return; } file_handle = fileHandle; completion_handler = completionHandler; file_url = [url retain]; [self request_drag_data]; } @end void _glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) {@autoreleasepool{ if (!_glfw.drag.window_id) return; _GLFWwindow* drag_win = _glfwWindowForId(_glfw.drag.window_id); if (drag_win) { GLFWDragEvent ev = {.type = GLFW_DRAG_CANCELLED}; _glfwInputDragSourceRequest(drag_win, &ev); } _glfwFreeDragSourceData(); }} void _glfwPlatformFreeDragSourceData(void) { if (_glfw.ns.drag_session) [_glfw.ns.drag_session release]; _glfw.ns.drag_session = nil; if (_glfw.ns.drag_view) [_glfw.ns.drag_view release]; _glfw.ns.drag_view = nil; if (_glfw.ns.drag_image) [_glfw.ns.drag_image release]; _glfw.ns.drag_image = nil; } int _glfwPlatformChangeDragImage(const GLFWimage *thumbnail) {@autoreleasepool{ if (!_glfw.ns.drag_session || !_glfw.ns.drag_view) return 0; _GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id); [((NSDraggingSession*)_glfw.ns.drag_session) enumerateDraggingItemsWithOptions:0 forView:(NSView*)_glfw.ns.drag_view classes:@[[NSPasteboardItem class]] searchOptions:@{} usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) { if (idx == 0) { set_image_for_dragging_item(draggingItem, thumbnail, window->ns.object); *stop = YES; } }]; return 0; }} int _glfwPlatformDragDataReady(const char *mime_type) { if (!file_promise_providers) return 0; for (GLFWFilePromiseProviderDelegate *d in file_promise_providers) { if ([d is_mimetype:mime_type]) [d request_drag_data]; } return 0; }