//========================================================================
// GLFW 3.4 macOS - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org>
//
// 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 fo 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 <Availability.h>
#import <CoreServices/CoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <errno.h>
#include <float.h>
#include <string.h>
#include <assert.h>

#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;
    const char* ans = [characters UTF8String];
    return ans ? ans : "(nil)";
}

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_input("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 "<none>";
    while (*src) {
        int num = snprintf(p, sizeof(buf) - (p - buf), "0x%x ", (unsigned char)*(src++));
        if (num < 0) return "<error>";
        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 "<noname>";
    if ((1 <= ans[0] && ans[0] <= 31) || ans[0] == 127) ans = "<cc>";
    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<NSDictionary *> *_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<NSDictionary *> *)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<NSDictionary *> *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_input("\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 <NSFilePromiseProviderDelegate>
{
    GLFWid windowId, instanceId;
    char* mimeType;  // MIME type for this provider
    char *file_name; // Optional file name for this provider
    bool is_directory;
    NSFileHandle *file_handle;
    NSURL *file_url;
    void (^completion_handler)(NSError*);
}

- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime fileName:(const char*)name isDir:(bool)is_dir instanceId:(GLFWid)iid;
- (void)request_drag_data;
- (void)promised_data_ready:(const char*)data sz:(size_t)sz type:(int)type;
- (void)end_transfer:(int)errorCode;
- (void)end_transfer_with_error:(NSError*)err;
- (bool)is_mimetype:(const char*)mime_type;
@end

@interface GLFWDraggingSource : NSObject <NSDraggingSource> {
   NSPoint start_point, current_point;
}
@end

// }}}

// Content view class for the GLFW window {{{

@interface GLFWContentView : NSView <NSTextInputClient>
{
    _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 YES;  // 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_input("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_input("UCKeyTranslate failed for keycode: 0x%x (%s) %s\n",
                    keycode, safe_name_for_keycode(keycode), format_mods(mods));
            window->ns.deadKeyState = 0;
            return;
        }
        debug_input("\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_input("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_input("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_input("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_input("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_input("\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_input("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_input("\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_in_progress_drop_data(_GLFWwindow *window) {
    _GLFWDropData *d = &window->ns.drop_data;
    if (d->in_progress_drop.temp_dir) {
        NSError *error = nil;
        [[NSFileManager defaultManager] removeItemAtURL:d->in_progress_drop.temp_dir error:&error];
        [d->in_progress_drop.temp_dir release];
    }
    if (d->in_progress_drop.data_map) [d->in_progress_drop.data_map release];
    if (d->in_progress_drop.path_map) [d->in_progress_drop.path_map release];
    if (d->in_progress_drop.pending_requests) [d->in_progress_drop.pending_requests release];
    memset(&d->in_progress_drop, 0, sizeof(d->in_progress_drop));
}

static void
free_drop_data(_GLFWwindow *window) {
    _GLFWDropData *d = &window->ns.drop_data;
    if (d->mimes) {
        for (size_t i = 0; i < d->mimes_count; i++) free((void*)d->mimes[i]);
        free(d->mimes);
    }
    free(d->copy_mimes);  // pointer array only; strings owned by mimes[]
    d->mimes = NULL; d->copy_mimes = NULL;
    d->mimes_count = 0; d->copy_mimes_count = 0; d->drag_accepted = NO;
    free_in_progress_drop_data(window);
}

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;
}

static void
update_drop_source_actions(_GLFWwindow *window, id<NSDraggingInfo> sender) {
    NSDragOperation src_ops = [sender draggingSourceOperationMask];
    window->drop_operation.source_actions = GLFW_DRAG_OPERATION_NONE;
    if (src_ops & NSDragOperationCopy) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_COPY;
    if (src_ops & NSDragOperationMove) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_MOVE;
    if (src_ops & NSDragOperationGeneric) window->drop_operation.source_actions |= GLFW_DRAG_OPERATION_GENERIC;
}

// 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 <NSDraggingInfo>)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)
    bool has_uri_list = false;
    if ([pasteboard canReadObjectForClasses:@[[NSURL class]] options:nil]) {
        has_uri_list = true;
        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) {
            UTType *promisedType = [UTType typeWithIdentifier:uti];
            if (promisedType && [promisedType conformsToType:UTTypeFileURL]) {
                if (!has_uri_list) {
                    mime_array[mime_count++] = _glfw_strdup("text/uri-list");
                    has_uri_list = true;
                }
            } else {
                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);
    update_drop_source_actions(window, sender);
    _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 <NSDraggingInfo>)sender
{
    if (!window->ns.drop_data.mimes_count) 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);
    update_drop_source_actions(window, sender);
    _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);
    }
    switch (window->drop_operation.preferred) {
        case GLFW_DRAG_OPERATION_NONE: return NSDragOperationNone;
        case GLFW_DRAG_OPERATION_MOVE: return NSDragOperationMove;
        case GLFW_DRAG_OPERATION_COPY: return NSDragOperationCopy;
        case GLFW_DRAG_OPERATION_GENERIC: return NSDragOperationGeneric;
    }
    return NSDragOperationGeneric;
}

- (void)draggingExited:(id <NSDraggingInfo>)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) prepareForDragOperation:(id<NSDraggingInfo>) sender {
    return YES;
}

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);
    });
}

static void
create_uri_list(_GLFWDropData *d, NSArray *urls) {
    NSMutableArray<NSString *> *items = [NSMutableArray array];
    for (NSURL *url in urls) {
        if ([url isFileURL]) url = [url filePathURL];
        [items addObject:url.absoluteString];
    }
    NSString *result = [items componentsJoinedByString:@"\r\n"];
    NSData *data = [result dataUsingEncoding:NSUTF8StringEncoding];
    NSInputStream *inputStream = [NSInputStream inputStreamWithData:data];
    [inputStream open];
    d->in_progress_drop.data_map[@"text/uri-list"] = inputStream;
}

static void
build_uri_list(_GLFWDropData *d) {
    if (!d->in_progress_drop.path_map) return;
    NSMutableArray<NSURL *> *urls = [NSMutableArray array];
    for (NSString *mime in d->in_progress_drop.path_map) {
        if (![mime hasPrefix:@"kitty-internal/uri-list-item-"]) continue;
        id x = d->in_progress_drop.path_map[mime];
        if ([x isKindOfClass:[NSError class]]) {
            d->in_progress_drop.data_map[@"text/uri-list"] = x;
            return;
        }
        if ([x isKindOfClass:[NSURL class]]) [urls addObject:x];
    }
    if ([urls count] > 0) create_uri_list(d, urls);
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    free_in_progress_drop_data(window);
    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;
    NSError *mkdirError = nil;
    NSURL *tempDirURL = [[NSFileManager defaultManager]
    URLForDirectory:NSItemReplacementDirectory
           inDomain:NSUserDomainMask
  appropriateForURL:[NSURL fileURLWithPath:NSTemporaryDirectory()]
             create:YES
              error:&mkdirError];
    if (!tempDirURL) {
        NSLog(@"Failed to create temp dir for file promises: %@", mkdirError);
        return NO;
    }
    d->in_progress_drop.data_map = [[NSMutableDictionary alloc] init];
    NSPasteboard *pasteboard = [sender draggingPasteboard];
    NSMutableArray<NSURL *> *urls = [NSMutableArray array];
    for (NSURL *url in [pasteboard readObjectsForClasses:@[[NSURL class]] options:nil]) [urls addObject:url];
    if ([urls count] > 0) create_uri_list(d, urls);
    NSMutableArray<NSString *> *texts = [NSMutableArray array];
    for (NSString *text in [pasteboard readObjectsForClasses:@[[NSString class]] options:nil]) [texts addObject:text];
    if ([texts count] > 0) {
        NSString *text = [texts componentsJoinedByString:@"\n\n"];
        NSInputStream *s = [NSInputStream inputStreamWithData:[text dataUsingEncoding:NSUTF8StringEncoding]];
        [s open];
        d->in_progress_drop.data_map[@"text/plain"] = s;
    }
    for (NSPasteboardType type in [pasteboard types]) {
        const char *mime = uti_to_mime(type);
        if (!mime || d->in_progress_drop.data_map[@(mime)] != nil) continue;
        NSData *data = [pasteboard dataForType:type];
        if (data) {
            NSInputStream *s = [NSInputStream inputStreamWithData:data];
            [s open];
            d->in_progress_drop.data_map[@(mime)] = s;
        }
    }
    NSArray *receivers = [pasteboard readObjectsForClasses:@[[NSFilePromiseReceiver class]] options:nil];
    if ([receivers count] > 0) {
        d->in_progress_drop.request_id = ++window->ns.drop_request_counter;
        unsigned long long request_id = d->in_progress_drop.request_id;
        GLFWid wid = window->id;
        NSOperationQueue *workQueue = [[[NSOperationQueue alloc] init] autorelease];
        dispatch_group_t promiseGroup = dispatch_group_create();
        NSMutableDictionary *results = [[NSMutableDictionary alloc] init];
        unsigned url_counter = 0;
        for (NSFilePromiseReceiver *receiver in receivers) {
            dispatch_group_enter(promiseGroup);
            NSArray<NSString *> *types = receiver.fileTypes;
            unsigned url_list_idx = 0;
            for (NSString *type in types) {
                UTType *promisedType = [UTType typeWithIdentifier:type];
                if (promisedType && [promisedType conformsToType:UTTypeFileURL]) {
                    url_list_idx = ++url_counter;
                    break;
                }
            }
            [receiver receivePromisedFilesAtDestination:tempDirURL options:@{} operationQueue:workQueue
                reader:^(NSURL * _Nonnull fileURL, NSError * _Nullable error) {
                    if (url_list_idx) results[
                        [NSString stringWithFormat:@"kitty-internal/uri-list-item-%u", url_list_idx-1]] = error ? error : fileURL;
                    else {
                        id result = error;
                        if (!result) {
                            NSInputStream *s = [NSInputStream inputStreamWithURL:fileURL];
                            [s open];
                            if ([s streamStatus] == NSStreamStatusError) result = [s streamError];
                            else result = s;
                        }
                        for (NSString *type in types) {
                            const char *mime = uti_to_mime(type);
                            if (mime) results[@(mime)] = result;
                        }
                    }
                    dispatch_group_leave(promiseGroup);
                }];
        }
        dispatch_group_notify(promiseGroup, 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 && w->ns.drop_data.in_progress_drop.request_id == request_id) {
                _GLFWDropData *d = &w->ns.drop_data;
                d->in_progress_drop.path_map = results;
                d->in_progress_drop.promises_loaded = YES;
                d->in_progress_drop.temp_dir = [tempDirURL retain];
                build_uri_list(d);
                if (d->in_progress_drop.pending_requests) {
                    for (NSString *mime in d->in_progress_drop.pending_requests) {
                        send_data_available_event_on_next_event_loop_tick(wid, [mime UTF8String]);
                    }
                }
            } else {
                NSError *error = nil;
                [[NSFileManager defaultManager] removeItemAtURL:tempDirURL error:&error];
            }
        });
        [promiseGroup release];
    } else d->in_progress_drop.promises_loaded = true;

    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);
        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) concludeDragOperation:(id<NSDraggingInfo>) sender {
}

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.
}

int
_glfwPlatformRequestDropData(_GLFWwindow *window, const char *mime) {
    _GLFWDropData *d = &window->ns.drop_data;
    if (d->in_progress_drop.data_map[@(mime)] != nil) {
        send_data_available_event_on_next_event_loop_tick(window->id, mime);
        return 0;
    }
    if (!d->in_progress_drop.promises_loaded) {
        if (d->in_progress_drop.pending_requests == nil) d->in_progress_drop.pending_requests = [[NSMutableSet alloc] init];
        [d->in_progress_drop.pending_requests addObject:@(mime)];
        return 0;
    }
    if (d->in_progress_drop.path_map && d->in_progress_drop.path_map[@(mime)] != nil) {
        send_data_available_event_on_next_event_loop_tick(window->id, mime);
        return 0;
    }
    return ENOENT;
}

static NSString*
NSStringFromStreamStatus(NSStreamStatus status) {
    switch (status) {
        case NSStreamStatusNotOpen: return @"NSStreamStatusNotOpen";
        case NSStreamStatusOpening: return @"NSStreamStatusOpening";
        case NSStreamStatusOpen:    return @"NSStreamStatusOpen";
        case NSStreamStatusReading: return @"NSStreamStatusReading";
        case NSStreamStatusWriting: return @"NSStreamStatusWriting";
        case NSStreamStatusAtEnd:   return @"NSStreamStatusAtEnd";
        case NSStreamStatusClosed:  return @"NSStreamStatusClosed";
        case NSStreamStatusError:   return @"NSStreamStatusError";
        default:                    return [NSString stringWithFormat:@"Unknown (%lu)", (unsigned long)status];
    }
}

ssize_t
_glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffer, size_t capacity) {
    _GLFWwindow *window = (_GLFWwindow*)w; const char *mime = ev->mimes[0];
    _GLFWDropData *d = &window->ns.drop_data;
    id x = d->in_progress_drop.data_map[@(mime)];
    if (x == nil && d->in_progress_drop.promises_loaded && d->in_progress_drop.path_map != nil)
        x = d->in_progress_drop.path_map[@(mime)];
    if (x == nil) return -ENOENT;
    if ([x isKindOfClass:[NSError class]]) {
        NSError *error = x;
        if ([error.domain isEqualToString:NSPOSIXErrorDomain]) return - (int)error.code;
        return -EINVAL;
    }
    if ([x isKindOfClass:[NSInputStream class]]) {
        NSInputStream *stream = x;
        if ([stream streamStatus] == NSStreamStatusNotOpen) [stream open];
        NSInteger bytesRead = [stream read:(uint8_t*)buffer maxLength:capacity];
        if (bytesRead > 0) {
            send_data_available_event_on_next_event_loop_tick(window->id, mime);
            return (ssize_t)bytesRead;
        }
        if (bytesRead == 0) return 0;  // EOF
        NSError *error = [stream streamError];
        NSLog(@"Failed to read from drop data stream, status: %@ error: %@", NSStringFromStreamStatus([stream streamStatus]), error);
        if (error && [error.domain isEqualToString:NSPOSIXErrorDomain]) return - (int)error.code;
        return -EIO;
    }
    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_input("\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_input("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_input("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_input("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
{
    if (!string) {
        debug_input("\n\tinsertText: nil replacementRange: (%lu, %lu)\n", replacementRange.location, replacementRange.length);
        return;
    }
    const char *utf8 = polymorphic_string_as_utf8(string);
    debug_input("\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_input("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_input("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_input("\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_input("Inserting text via setAccessibilityValue: %s\n", utf8);
        GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
        _glfwInputKeyboard(window, &glfw_keyevent);
    }
}

// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/SysServices/Articles/using.html>

// 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_input("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 bool
ns_window_level_constant(const char *name, long *val) {
#define C(x) if (strcmp(name, #x) == 0) { *val = (long)x; return true; }
    C(NSNormalWindowLevel); C(NSFloatingWindowLevel); C(NSSubmenuWindowLevel); C(NSTornOffMenuWindowLevel);
    C(NSMainMenuWindowLevel); C(NSStatusWindowLevel); C(NSModalPanelWindowLevel); C(NSPopUpMenuWindowLevel);
    C(NSScreenSaverWindowLevel);
    C(kCGBaseWindowLevel); C(kCGMinimumWindowLevel); C(kCGDesktopWindowLevel); C(kCGBackstopMenuLevel);
    C(kCGNormalWindowLevel); C(kCGFloatingWindowLevel); C(kCGTornOffMenuWindowLevel); C(kCGDockWindowLevel);
    C(kCGMainMenuWindowLevel); C(kCGStatusWindowLevel); C(kCGModalPanelWindowLevel); C(kCGPopUpMenuWindowLevel);
    C(kCGDraggingWindowLevel); C(kCGScreenSaverWindowLevel); C(kCGMaximumWindowLevel); C(kCGOverlayWindowLevel);
    C(kCGHelpWindowLevel); C(kCGUtilityWindowLevel); C(kCGDesktopIconWindowLevel); C(kCGAssistiveTechHighWindowLevel);
    C(kCGCursorWindowLevel); C(kCGNumberOfWindowLevelKeys);
#undef C
    return false;
}

static bool
parse_ns_window_level_term(const char **spec, long *val) {
    const char *s = *spec;
    while (isspace(*s)) s++;
    if (!*s) return false;
    if (*s == '+' || *s == '-' || isdigit(*s)) {
        errno = 0;
        char *end = NULL;
        long v = strtol(s, &end, 0);
        if (end == s || errno) return false;
        *val = v; *spec = end;
        return true;
    }
    char name[96]; size_t i = 0;
    while ((isalnum(*s) || *s == '_') && i + 1 < sizeof(name)) name[i++] = *s++;
    name[i] = '\0';
    if (!i || !ns_window_level_constant(name, val)) return false;
    *spec = s;
    return true;
}

static bool
parse_ns_window_level(const char *spec, NSWindowLevel *level) {
    const char *original = spec;
    if (!spec) return false;
    while (isspace(*spec)) spec++;
    if (!*spec || strcmp(spec, "unset") == 0) return false;
    long total = 0;
    if (!parse_ns_window_level_term(&spec, &total)) goto invalid;
    while (true) {
        while (isspace(*spec)) spec++;
        if (!*spec) { *level = (NSWindowLevel)total; return true; }
        char op = *spec++;
        if (op != '+' && op != '-') goto invalid;
        long term = 0;
        if (!parse_ns_window_level_term(&spec, &term)) goto invalid;
        total = op == '+' ? total + term : total - term;
    }
invalid:
    _glfwInputError(GLFW_INVALID_VALUE, "Cocoa: Invalid macOS NSWindow layer expression: %s", original);
    return false;
}

static void
apply_ns_window_layer(_GLFWwindow *window, const char *spec) {
    NSWindowLevel level = 0;
    if (parse_ns_window_level(spec, &level)) [window->ns.object setLevel:level];
}

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<NSScreen *> *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<NSDeviceDescriptionKey, id> *deviceDescriptionA = [screenA deviceDescription];
    NSDictionary<NSDeviceDescriptionKey, id> *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);
    NSRect placement_frame = config.related.use_physical_screen_frame ? screen.frame : screen.visibleFrame;
    CGFloat dock_height = NSMinY(screen.visibleFrame) - NSMinY(screen.frame);
    CGFloat menubar_height = config.related.use_physical_screen_frame ? 0 : NSHeight(screen.frame) - NSHeight(screen.visibleFrame) - dock_height;
    CGFloat x = NSMinX(placement_frame), y = NSMinY(placement_frame) - 1, width = NSWidth(placement_frame), height = NSHeight(placement_frame) + 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(placement_frame);
        if (height < 1.) height = NSHeight(placement_frame);
    }

    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];
    apply_ns_window_layer(window, config.related.ns_window_layer);
    [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.
        // See https://github.com/kovidgoyal/kitty/issues/8740
        //
        // Work around this by explicitly assigning the window
        // to the current active space via the private CGS API before showing it.
        NSWindowCollectionBehavior old = nw.collectionBehavior;
        nw.collectionBehavior = (old & ~NSWindowCollectionBehaviorCanJoinAllSpaces) | NSWindowCollectionBehaviorMoveToActiveSpace;
        extern int _CGSDefaultConnection(void);
        extern int CGSGetActiveSpace(int cid);
        extern void CGSAddWindowsToSpaces(int cid, CFArrayRef windows, CFArrayRef spaces);
        int cid = _CGSDefaultConnection();
        int activeSpace = CGSGetActiveSpace(cid);
        if (activeSpace > 0) {
            NSArray *windowArray = @[@([nw windowNumber])];
            NSArray *spaceArray = @[@(activeSpace)];
            CGSAddWindowsToSpaces(cid, (__bridge CFArrayRef)windowArray, (__bridge CFArrayRef)spaceArray);
        }
        [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<NSPasteboardType> *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<NSView *> *)viewsWithIdentifier:(NSUserInterfaceItemIdentifier)identifier {
    NSMutableArray<NSView *> *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 glfwCocoaSetWindowLevel(GLFWwindow *w, const char *level_spec) { @autoreleasepool {
    _GLFWwindow* window = (_GLFWwindow*)w;
    apply_ns_window_layer(window, level_spec);
}}


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] : "<nil>",
        appearance ? [appearance.name UTF8String] : "<nil>",
        cs ? (cs.localizedName ? [cs.localizedName UTF8String] : [cs.description UTF8String]) : "<nil>",
        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<NSWindow *> *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 {{{

// Forward declarations for drag-finish helpers used in GLFWDraggingSource methods
static void fire_drag_finished(void);
static void schedule_drag_finish_timer(void);
static GLFWid drag_finish_window_id = 0;
static GLFWDragOperationType drag_finish_action = 0;
static NSTimer *drag_finish_timer = nil;
static NSMutableArray<GLFWFilePromiseProviderDelegate*> *file_promise_providers = nil;
#define DRAG_FINISH_TIMEOUT_SECONDS 2.0

@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;
    debug_input("Dragging session started at: (%f, %f)\n", screenPoint.x, screenPoint.y);
    start_point = screenPoint;
}

- (void)draggingSession:(NSDraggingSession *)session
           movedToPoint:(NSPoint)screenPoint
{
    (void)session;
    debug_input("Dragging session moved to: (%f, %f)\n", screenPoint.x, screenPoint.y);
    current_point = screenPoint;
}

NSString*
NSStringFromDragOperation(NSDragOperation operation) {
    // Check explicit presets first
    if (operation == NSDragOperationNone) return @"None";
    if (operation == NSDragOperationEvery) return @"Every";

    NSMutableArray<NSString *> *descriptions = [NSMutableArray array];

    // Evaluate individual bitwise flags
    if (operation & NSDragOperationCopy) [descriptions addObject:@"Copy"];
    if (operation & NSDragOperationLink) [descriptions addObject:@"Link"];
    if (operation & NSDragOperationGeneric) [descriptions addObject:@"Generic"];
    if (operation & NSDragOperationPrivate) [descriptions addObject:@"Private"];
    if (operation & NSDragOperationMove) [descriptions addObject:@"Move"];
    if (operation & NSDragOperationDelete) [descriptions addObject:@"Delete"];

    // Fallback for undefined or custom masks
    if (descriptions.count == 0) {
        return [NSString stringWithFormat:@"Unknown (%lu)", (unsigned long)operation];
    }

    return [descriptions componentsJoinedByString:@", "];
}

- (void)draggingSession:(NSDraggingSession *)session
           endedAtPoint:(NSPoint)screenPoint
              operation:(NSDragOperation)operation
{
    (void)session;
    debug_input("Dragging session ended at: (%f, %f) with operation: %s\n", screenPoint.x, screenPoint.y, [NSStringFromDragOperation(operation) UTF8String]);
    _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;
                    ev.drop_maybe_a_cancel = true;
                }
            } break;
            default:
                ev.type = GLFW_DRAG_DROPPED; break;
        }
        _glfwInputDragSourceRequest(window, &ev);
        if (ev.type == GLFW_DRAG_DROPPED) {
            drag_finish_window_id = _glfw.drag.window_id;
            drag_finish_action = ev.action;
            if (operation == NSDragOperationNone || !file_promise_providers || [file_promise_providers count] == 0) {
                fire_drag_finished();
            } else {
                schedule_drag_finish_timer();
            }
        } else {
            if (operation == NSDragOperationNone) _glfwFreeDragSourceData();
        }
    }
}
@end


static void
fire_drag_finished(void) {
    if (drag_finish_timer) {
        [drag_finish_timer invalidate];
        drag_finish_timer = nil;
    }
    GLFWid wid = drag_finish_window_id;
    GLFWDragOperationType action = drag_finish_action;
    drag_finish_window_id = 0;
    drag_finish_action = 0;
    _GLFWwindow *window = wid ? _glfwWindowForId(wid) : NULL;
    if (window) {
        GLFWDragEvent ev = {.type=GLFW_DRAG_FINSHED, .action=action};
        _glfwInputDragSourceRequest(window, &ev);
    }
    _glfwFreeDragSourceData();
}

static void
schedule_drag_finish_timer(void) {
    if (drag_finish_timer) {
        [drag_finish_timer invalidate];
        drag_finish_timer = nil;
    }
    drag_finish_timer = [NSTimer scheduledTimerWithTimeInterval:DRAG_FINISH_TIMEOUT_SECONDS repeats:NO block:^(NSTimer *t UNUSED) {
        drag_finish_timer = nil;
        fire_drag_finished();
    }];
}

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<NSDraggingImageComponent *> * _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;
}

static int
add_drag_item(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, NSDraggingItem* dragItem, const GLFWimage *thumbnail) {
    if (dragItems.count == 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];
    return 0;
}

static int
add_uri_list_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, const char *uri_list, size_t uri_list_sz, bool use_promises, const GLFWimage *thumbnail) {
    NSString *input = [[[NSString alloc] initWithBytes:uri_list length:uri_list_sz encoding:NSUTF8StringEncoding] autorelease];
    NSArray *lines = [input componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    NSDraggingItem *dragItem; int count = 0; char buf[256];
    for (NSString *rawLine in lines) {
        NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        if (line.length == 0 || [line hasPrefix:@"#"]) continue;
        NSURL *url = [NSURL URLWithString:line];
        if (use_promises && [[url scheme] caseInsensitiveCompare:@"file"] == NSOrderedSame) {
            snprintf(buf, sizeof(buf), "kitty-internal/uri-list-item-%d", count);
            NSString *filename = [url lastPathComponent];
            NSURLComponents *components = [NSURLComponents componentsWithString:line];
            bool is_dir = [components.percentEncodedPath hasSuffix:@"/"];
            GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
                initWithWindow:window mimeType:buf fileName:[filename UTF8String] isDir:is_dir
                    instanceId:_glfw.drag.instance_id] autorelease];
            NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
                initWithFileType:UTTypeFileURL.identifier delegate:delegate] autorelease];
            dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:provider] autorelease];
        } else dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:url] autorelease];
        int err = add_drag_item(window, dragItems, dragItem, thumbnail);
        if (err) return err;
        count++;
    }
    return 0;
}

static int
add_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, GLFWDragSourceItem *mime_item, const GLFWimage *thumbnail) {
    // URI list items get added directly to the clipboard for use by native apps since macOS does not support
    // the text/uri-list MIME type
    if (strcmp(mime_item->mime_type, "text/uri-list") == 0 && mime_item->optional_data && mime_item->data_size) {
        return add_uri_list_drag_items(
            window, dragItems, mime_item->optional_data, mime_item->data_size, mime_item->is_remote_client, thumbnail);
    }
    NSString* utiString = mime_to_uti(mime_item->mime_type);
    id w;
    // remote client URI list must be set a file promise so that the kitty drag
    // source code receives a request for it and can then fetch the remote
    // files.
    if (mime_item->optional_data) {
        NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease];
        NSData *data = [NSData dataWithBytes:mime_item->optional_data length:mime_item->data_size];
        [pbItem setData:data forType:utiString];
        w = pbItem;
    } else {
        // Create file promise provider with our delegate
        GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
            initWithWindow:window mimeType:mime_item->mime_type fileName:NULL isDir:NO instanceId:_glfw.drag.instance_id] autorelease];
        NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
            initWithFileType:utiString delegate:delegate] autorelease];
        w = provider;
    }
    NSDraggingItem* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:w] autorelease];
    return add_drag_item(window, dragItems, dragItem, thumbnail);
}


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<NSDraggingItem*>* dragItems = [[[NSMutableArray alloc] init] autorelease];
    for (size_t i = 0; i < _glfw.drag.item_count; i++) {
        int err = add_drag_items(window, dragItems, _glfw.drag.items + i, thumbnail);
        if (err) return err;
    }

    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;
    }
    if (file_promise_providers) [file_promise_providers removeObject:self];
    if (drag_finish_window_id) {
        if (!file_promise_providers || [file_promise_providers count] == 0) {
            fire_drag_finished();
        } else {
            schedule_drag_finish_timer();
        }
    }
}

- (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)promised_data_ready:(const char*)path sz:(size_t)sz type:(int)type {
    if (drag_finish_timer) {
        [drag_finish_timer invalidate];
        drag_finish_timer = nil;
    }
    if (path == NULL) return;  // progress update
    if (file_handle) [file_handle release];
    file_handle = nil;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    // Erase the empty placeholder file/directory created by writePromiseToURL:
    [fileManager removeItemAtURL:file_url error:nil];
    NSError *error = nil;
    NSURL *srcURL = [NSURL fileURLWithPath:@(path)];
    switch (type) {
        case 0:
            // Create a hard link to path at file_url or fallback to full copy
            if (![fileManager linkItemAtURL:srcURL toURL:file_url error:&error]) {
                if (![fileManager copyItemAtURL:srcURL toURL:file_url error:&error]) {
                    [self end_transfer_with_error:error];
                    return;
                }
            }
            break;
        case 1: {
            // Create a symlink to the same destination as the symlink at path
            NSString *linkDest = [fileManager destinationOfSymbolicLinkAtPath:@(path) error:&error];
            if (!linkDest) {
                [self end_transfer_with_error:error];
                return;
            }
            if (![fileManager createSymbolicLinkAtPath:file_url.path withDestinationPath:linkDest error:&error]) {
                [self end_transfer_with_error:error];
                return;
            }
            break;
        }
        default: {
            // Copy the directory at path to file_url recursively using hard links for files
            if (![fileManager createDirectoryAtURL:file_url withIntermediateDirectories:YES attributes:nil error:&error]) {
                [self end_transfer_with_error:error];
                return;
            }
            NSString *srcPath = srcURL.path;
            NSString *dstPath = file_url.path;
            __block NSError *enumError = nil;
            NSDirectoryEnumerator<NSURL*> *enumerator = [fileManager
                enumeratorAtURL:srcURL
                includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey]
                options:0
                errorHandler:^BOOL(NSURL *url, NSError *err) {
                    (void)url;
                    enumError = err;
                    return NO;
                }];
            for (NSURL *itemURL in enumerator) {
                NSString *itemPath = itemURL.path;
                if (itemPath.length <= srcPath.length) continue;
                NSString *relativePath = [itemPath substringFromIndex:srcPath.length + 1];
                NSURL *destURL = [NSURL fileURLWithPath:[dstPath stringByAppendingPathComponent:relativePath]];
                NSNumber *isSymlink = nil, *isDirectory = nil;
                if (![itemURL getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:&error]) {
                    [self end_transfer_with_error:error];
                    return;
                }
                if (![itemURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) {
                    [self end_transfer_with_error:error];
                    return;
                }
                if ([isSymlink boolValue]) {
                    NSString *linkDest = [fileManager destinationOfSymbolicLinkAtPath:itemPath error:&error];
                    if (!linkDest) {
                        [self end_transfer_with_error:error];
                        return;
                    }
                    if (![fileManager createSymbolicLinkAtPath:destURL.path withDestinationPath:linkDest error:&error]) {
                        [self end_transfer_with_error:error];
                        return;
                    }
                } else if ([isDirectory boolValue]) {
                    if (![fileManager createDirectoryAtURL:destURL withIntermediateDirectories:YES attributes:nil error:&error]) {
                        [self end_transfer_with_error:error];
                        return;
                    }
                } else {
                    if (![fileManager linkItemAtURL:itemURL toURL:destURL error:&error]) {
                        if (![fileManager copyItemAtURL:itemURL toURL:destURL error:&error]) {
                            [self end_transfer_with_error:error];
                            return;
                        }
                    }
                }
            }
            if (enumError) {
                [self end_transfer_with_error:enumError];
                return;
            }
            break;
        }
    }
    [self end_transfer: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;
    if (drag_finish_timer) {
        [drag_finish_timer invalidate];
        drag_finish_timer = nil;
    }
    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 fileName:(const char*)name isDir:(bool)is_dir instanceId:(GLFWid) instance_id {
    self = [super init];
    if (self) {
        windowId = initWindow ? initWindow->id : 0;
        mimeType = _glfw_strdup(mime);
        file_name = name ? _glfw_strdup(name) : nil;
        is_directory = is_dir;
        instanceId = instance_id;
        if (file_promise_providers == nil) file_promise_providers = [[NSMutableArray alloc] init];
        [file_promise_providers addObject:self];
    }
    return self;
}

- (void)dealloc {
    free(mimeType); mimeType = NULL;
    free(file_name); file_name = NULL;
    if (file_handle) [file_handle release];
    file_handle = nil;
    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;
    if (file_name) return [NSString stringWithUTF8String:file_name];
    // Generate a unique filename based on the MIME type
    NSString* extension = @"data";
    if (mimeType) {
        UTType *type = [UTType typeWithMIMEType:@(mimeType)];
        if (type) 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;
    }

    NSError* error = nil;
    bool ok = is_directory ? [[NSFileManager defaultManager]
        createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&error] :
        [[NSData data] writeToURL:url options:NSDataWritingAtomic error:&error];
    if (!ok) {
        NSLog(@"Failed to create file to send drag file provider promise data to with error: %@", error);
        completionHandler(error);
        return;
    }
    if (!is_directory) {
        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 retain];
    }
    completion_handler = Block_copy(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 (drag_finish_timer) {
        [drag_finish_timer invalidate];
        drag_finish_timer = nil;
    }
    drag_finish_window_id = 0;
    drag_finish_action = 0;
    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, const char *data, size_t sz, int type) {
    if (!file_promise_providers) return 0;
    for (GLFWFilePromiseProviderDelegate *d in file_promise_providers) {
        if ([d is_mimetype:mime_type]) {
            if (type == -1) [d request_drag_data];
            else [d promised_data_ready:data sz:sz type:type];
        }
    }
    return 0;
}
