Files
kitty/glfw/cocoa_window.m
zhaolei b16221a1d3 macos: explicitly enable modern window corners on macOS 26
Use an explicit runtime check to apply the newer corner styling on macOS 26 while leaving behavior unchanged on older macOS versions for compatibility.

Also add the required QuartzCore framework for linking the corner curve API.
2026-04-18 20:41:38 +08:00

4488 lines
175 KiB
Objective-C

//========================================================================
// 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 for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================
#include "../kitty/monotonic.h"
#include "glfw3.h"
#include "internal.h"
#include <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;
return [characters UTF8String];
}
static bool
forward_dictation_selector_to_app(SEL selector, id sender) {
static SEL start_dictation_selector = NULL, stop_dictation_selector = NULL;
if (start_dictation_selector == NULL) {
start_dictation_selector = NSSelectorFromString(@"startDictation:");
stop_dictation_selector = NSSelectorFromString(@"stopDictation:");
}
if (selector != start_dictation_selector && selector != stop_dictation_selector) return false;
if ([NSApp respondsToSelector:selector]) {
debug_key("Forwarding %s to NSApp\n", [NSStringFromSelector(selector) UTF8String]);
[NSApp performSelector:selector withObject:sender];
return true;
}
return false;
}
static uint32_t
vk_code_to_functional_key_code(uint8_t key_code) { // {{{
switch(key_code) {
/* start vk to functional (auto generated by gen-key-constants.py do not edit) */
case 0x35: return GLFW_FKEY_ESCAPE;
case 0x24: return GLFW_FKEY_ENTER;
case 0x30: return GLFW_FKEY_TAB;
case 0x33: return GLFW_FKEY_BACKSPACE;
case 0x72: return GLFW_FKEY_INSERT;
case 0x75: return GLFW_FKEY_DELETE;
case 0x7b: return GLFW_FKEY_LEFT;
case 0x7c: return GLFW_FKEY_RIGHT;
case 0x7e: return GLFW_FKEY_UP;
case 0x7d: return GLFW_FKEY_DOWN;
case 0x74: return GLFW_FKEY_PAGE_UP;
case 0x79: return GLFW_FKEY_PAGE_DOWN;
case 0x73: return GLFW_FKEY_HOME;
case 0x77: return GLFW_FKEY_END;
case 0x39: return GLFW_FKEY_CAPS_LOCK;
case 0x47: return GLFW_FKEY_NUM_LOCK;
case 0x6e: return GLFW_FKEY_MENU;
case 0x7a: return GLFW_FKEY_F1;
case 0x78: return GLFW_FKEY_F2;
case 0x63: return GLFW_FKEY_F3;
case 0x76: return GLFW_FKEY_F4;
case 0x60: return GLFW_FKEY_F5;
case 0x61: return GLFW_FKEY_F6;
case 0x62: return GLFW_FKEY_F7;
case 0x64: return GLFW_FKEY_F8;
case 0x65: return GLFW_FKEY_F9;
case 0x6d: return GLFW_FKEY_F10;
case 0x67: return GLFW_FKEY_F11;
case 0x6f: return GLFW_FKEY_F12;
case 0x69: return GLFW_FKEY_F13;
case 0x6b: return GLFW_FKEY_F14;
case 0x71: return GLFW_FKEY_F15;
case 0x6a: return GLFW_FKEY_F16;
case 0x40: return GLFW_FKEY_F17;
case 0x4f: return GLFW_FKEY_F18;
case 0x50: return GLFW_FKEY_F19;
case 0x5a: return GLFW_FKEY_F20;
case 0x52: return GLFW_FKEY_KP_0;
case 0x53: return GLFW_FKEY_KP_1;
case 0x54: return GLFW_FKEY_KP_2;
case 0x55: return GLFW_FKEY_KP_3;
case 0x56: return GLFW_FKEY_KP_4;
case 0x57: return GLFW_FKEY_KP_5;
case 0x58: return GLFW_FKEY_KP_6;
case 0x59: return GLFW_FKEY_KP_7;
case 0x5b: return GLFW_FKEY_KP_8;
case 0x5c: return GLFW_FKEY_KP_9;
case 0x41: return GLFW_FKEY_KP_DECIMAL;
case 0x4b: return GLFW_FKEY_KP_DIVIDE;
case 0x43: return GLFW_FKEY_KP_MULTIPLY;
case 0x4e: return GLFW_FKEY_KP_SUBTRACT;
case 0x45: return GLFW_FKEY_KP_ADD;
case 0x4c: return GLFW_FKEY_KP_ENTER;
case 0x51: return GLFW_FKEY_KP_EQUAL;
case 0x38: return GLFW_FKEY_LEFT_SHIFT;
case 0x3b: return GLFW_FKEY_LEFT_CONTROL;
case 0x3a: return GLFW_FKEY_LEFT_ALT;
case 0x37: return GLFW_FKEY_LEFT_SUPER;
case 0x3c: return GLFW_FKEY_RIGHT_SHIFT;
case 0x3e: return GLFW_FKEY_RIGHT_CONTROL;
case 0x3d: return GLFW_FKEY_RIGHT_ALT;
case 0x36: return GLFW_FKEY_RIGHT_SUPER;
/* end vk to functional */
default:
return 0;
}
} // }}}
static uint32_t
vk_code_to_unicode(uint8_t key_code) { // {{{
switch(key_code) {
/* start vk to unicode (auto generated by gen-key-constants.py do not edit) */
case 0x0: return 0x61;
case 0x1: return 0x73;
case 0x2: return 0x64;
case 0x3: return 0x66;
case 0x4: return 0x68;
case 0x5: return 0x67;
case 0x6: return 0x7a;
case 0x7: return 0x78;
case 0x8: return 0x63;
case 0x9: return 0x76;
case 0xb: return 0x62;
case 0xc: return 0x71;
case 0xd: return 0x77;
case 0xe: return 0x65;
case 0xf: return 0x72;
case 0x10: return 0x79;
case 0x11: return 0x74;
case 0x12: return 0x31;
case 0x13: return 0x32;
case 0x14: return 0x33;
case 0x15: return 0x34;
case 0x16: return 0x36;
case 0x17: return 0x35;
case 0x18: return 0x3d;
case 0x19: return 0x39;
case 0x1a: return 0x37;
case 0x1b: return 0x2d;
case 0x1c: return 0x38;
case 0x1d: return 0x30;
case 0x1e: return 0x5d;
case 0x1f: return 0x6f;
case 0x20: return 0x75;
case 0x21: return 0x5b;
case 0x22: return 0x69;
case 0x23: return 0x70;
case 0x25: return 0x6c;
case 0x26: return 0x6a;
case 0x27: return 0x27;
case 0x28: return 0x6b;
case 0x29: return 0x3b;
case 0x2a: return 0x5c;
case 0x2b: return 0x2c;
case 0x2c: return 0x2f;
case 0x2d: return 0x6e;
case 0x2e: return 0x6d;
case 0x2f: return 0x2e;
case 0x31: return 0x20;
case 0x32: return 0x60;
/* end vk to unicode */
default:
return 0;
}
} // }}}
static uint32_t
mac_ucode_to_functional(uint32_t key_code) { // {{{
switch(key_code) {
/* start macu to functional (auto generated by gen-key-constants.py do not edit) */
case NSCarriageReturnCharacter: return GLFW_FKEY_ENTER;
case NSTabCharacter: return GLFW_FKEY_TAB;
case NSBackspaceCharacter: return GLFW_FKEY_BACKSPACE;
case NSInsertFunctionKey: return GLFW_FKEY_INSERT;
case NSDeleteFunctionKey: return GLFW_FKEY_DELETE;
case NSLeftArrowFunctionKey: return GLFW_FKEY_LEFT;
case NSRightArrowFunctionKey: return GLFW_FKEY_RIGHT;
case NSUpArrowFunctionKey: return GLFW_FKEY_UP;
case NSDownArrowFunctionKey: return GLFW_FKEY_DOWN;
case NSPageUpFunctionKey: return GLFW_FKEY_PAGE_UP;
case NSPageDownFunctionKey: return GLFW_FKEY_PAGE_DOWN;
case NSHomeFunctionKey: return GLFW_FKEY_HOME;
case NSEndFunctionKey: return GLFW_FKEY_END;
case NSScrollLockFunctionKey: return GLFW_FKEY_SCROLL_LOCK;
case NSClearLineFunctionKey: return GLFW_FKEY_NUM_LOCK;
case NSPrintScreenFunctionKey: return GLFW_FKEY_PRINT_SCREEN;
case NSPauseFunctionKey: return GLFW_FKEY_PAUSE;
case NSMenuFunctionKey: return GLFW_FKEY_MENU;
case NSF1FunctionKey: return GLFW_FKEY_F1;
case NSF2FunctionKey: return GLFW_FKEY_F2;
case NSF3FunctionKey: return GLFW_FKEY_F3;
case NSF4FunctionKey: return GLFW_FKEY_F4;
case NSF5FunctionKey: return GLFW_FKEY_F5;
case NSF6FunctionKey: return GLFW_FKEY_F6;
case NSF7FunctionKey: return GLFW_FKEY_F7;
case NSF8FunctionKey: return GLFW_FKEY_F8;
case NSF9FunctionKey: return GLFW_FKEY_F9;
case NSF10FunctionKey: return GLFW_FKEY_F10;
case NSF11FunctionKey: return GLFW_FKEY_F11;
case NSF12FunctionKey: return GLFW_FKEY_F12;
case NSF13FunctionKey: return GLFW_FKEY_F13;
case NSF14FunctionKey: return GLFW_FKEY_F14;
case NSF15FunctionKey: return GLFW_FKEY_F15;
case NSF16FunctionKey: return GLFW_FKEY_F16;
case NSF17FunctionKey: return GLFW_FKEY_F17;
case NSF18FunctionKey: return GLFW_FKEY_F18;
case NSF19FunctionKey: return GLFW_FKEY_F19;
case NSF20FunctionKey: return GLFW_FKEY_F20;
case NSF21FunctionKey: return GLFW_FKEY_F21;
case NSF22FunctionKey: return GLFW_FKEY_F22;
case NSF23FunctionKey: return GLFW_FKEY_F23;
case NSF24FunctionKey: return GLFW_FKEY_F24;
case NSF25FunctionKey: return GLFW_FKEY_F25;
case NSF26FunctionKey: return GLFW_FKEY_F26;
case NSF27FunctionKey: return GLFW_FKEY_F27;
case NSF28FunctionKey: return GLFW_FKEY_F28;
case NSF29FunctionKey: return GLFW_FKEY_F29;
case NSF30FunctionKey: return GLFW_FKEY_F30;
case NSF31FunctionKey: return GLFW_FKEY_F31;
case NSF32FunctionKey: return GLFW_FKEY_F32;
case NSF33FunctionKey: return GLFW_FKEY_F33;
case NSF34FunctionKey: return GLFW_FKEY_F34;
case NSF35FunctionKey: return GLFW_FKEY_F35;
case NSEnterCharacter: return GLFW_FKEY_KP_ENTER;
/* end macu to functional */
default:
return 0;
}
} // }}}
static bool
is_surrogate(UniChar uc) { return (uc - 0xd800u) < 2048u; }
static uint32_t
get_first_codepoint(UniChar *utf16, UniCharCount num) {
if (!num) return 0;
if (!is_surrogate(*utf16)) return *utf16;
if (CFStringIsSurrogateHighCharacter(*utf16) && num > 1 && CFStringIsSurrogateLowCharacter(utf16[1])) return CFStringGetLongCharacterForSurrogatePair(utf16[0], utf16[1]);
return 0;
}
static bool
is_pua_char(uint32_t ch) {
return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF);
}
uint32_t
vk_to_unicode_key_with_current_layout(uint16_t keycode)
{
UInt32 dead_key_state = 0;
UniChar characters[256];
UniCharCount character_count = 0;
uint32_t ans = vk_code_to_functional_key_code(keycode);
if (ans) return ans;
if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDisplay,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&dead_key_state,
arraysz(characters),
&character_count,
characters) == noErr) {
uint32_t cp = get_first_codepoint(characters, character_count);
if (cp) {
if (cp < 32 || (0xF700 <= cp && cp <= 0xF8FF)) return mac_ucode_to_functional(cp);
if (cp >= 32 && !is_pua_char(cp)) return cp;
}
}
return vk_code_to_unicode(keycode);
}
// Returns the style mask corresponding to the window settings
//
static NSUInteger getStyleMask(_GLFWwindow* window)
{
NSUInteger styleMask = NSWindowStyleMaskMiniaturizable;
if (window->ns.titlebar_hidden) styleMask |= NSWindowStyleMaskFullSizeContentView;
if (window->monitor || !window->decorated) {
styleMask |= NSWindowStyleMaskBorderless;
} else {
styleMask |= NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
}
if (window->resizable) styleMask |= NSWindowStyleMaskResizable;
return styleMask;
}
static void
requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
if (!callback) {
w->ns.renderFrameRequested = false;
w->ns.renderFrameCallback = NULL;
return;
}
w->ns.renderFrameCallback = callback;
w->ns.renderFrameRequested = true;
_glfwRequestRenderFrame(w);
}
void
_glfwRestartDisplayLinks(void) {
_GLFWwindow* window;
for (window = _glfw.windowListHead; window; window = window->next) {
if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) {
requestRenderFrame(window, window->ns.renderFrameCallback);
}
}
}
// Returns whether the cursor is in the content area of the specified window
//
static bool cursorInContentArea(_GLFWwindow* window)
{
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
return [window->ns.view mouse:pos inRect:[window->ns.view frame]];
}
// Hides the cursor if not already hidden
//
static void hideCursor(_GLFWwindow* window UNUSED)
{
if (!_glfw.ns.cursorHidden)
{
[NSCursor hide];
_glfw.ns.cursorHidden = true;
}
}
// Shows the cursor if not already shown
//
static void showCursor(_GLFWwindow* window UNUSED)
{
if (_glfw.ns.cursorHidden)
{
[NSCursor unhide];
_glfw.ns.cursorHidden = false;
}
}
// Updates the cursor image according to its cursor mode
//
static void updateCursorImage(_GLFWwindow* window)
{
if (window->cursorMode == GLFW_CURSOR_NORMAL)
{
showCursor(window);
if (window->cursor)
[(NSCursor*) window->cursor->ns.object set];
else
[[NSCursor arrowCursor] set];
}
else
hideCursor(window);
}
// Apply chosen cursor mode to a focused window
//
static void updateCursorMode(_GLFWwindow* window)
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
{
_glfw.ns.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.ns.restoreCursorPosX,
&_glfw.ns.restoreCursorPosY);
_glfwCenterCursorInContentArea(window);
CGAssociateMouseAndMouseCursorPosition(false);
}
else if (_glfw.ns.disabledCursorWindow == window)
{
_glfw.ns.disabledCursorWindow = NULL;
CGAssociateMouseAndMouseCursorPosition(true);
_glfwPlatformSetCursorPos(window,
_glfw.ns.restoreCursorPosX,
_glfw.ns.restoreCursorPosY);
}
if (cursorInContentArea(window))
updateCursorImage(window);
}
// Make the specified window and its video mode active on its monitor
//
static void acquireMonitor(_GLFWwindow* window)
{
_glfwSetVideoModeNS(window->monitor, &window->videoMode);
const CGRect bounds = CGDisplayBounds(window->monitor->ns.displayID);
const NSRect frame = NSMakeRect(bounds.origin.x,
_glfwTransformYNS(bounds.origin.y + bounds.size.height - 1),
bounds.size.width,
bounds.size.height);
[window->ns.object setFrame:frame display:YES];
_glfwInputMonitorWindow(window->monitor, window);
}
// Remove the window and restore the original video mode
//
static void releaseMonitor(_GLFWwindow* window)
{
if (window->monitor->window != window)
return;
_glfwInputMonitorWindow(window->monitor, NULL);
_glfwRestoreVideoModeNS(window->monitor);
}
// Translates macOS key modifiers into GLFW ones
//
static int
translateFlags(NSUInteger flags)
{
int mods = 0;
if (flags & NSEventModifierFlagShift)
mods |= GLFW_MOD_SHIFT;
if (flags & NSEventModifierFlagControl)
mods |= GLFW_MOD_CONTROL;
if (flags & NSEventModifierFlagOption)
mods |= GLFW_MOD_ALT;
if (flags & NSEventModifierFlagCommand)
mods |= GLFW_MOD_SUPER;
if (flags & NSEventModifierFlagCapsLock)
mods |= GLFW_MOD_CAPS_LOCK;
return mods;
}
static const char*
format_mods(int mods) {
static char buf[128];
char *p = buf, *s;
#define pr(x) p += snprintf(p, sizeof(buf) - (p - buf) - 1, x)
pr("mods: ");
s = p;
if (mods & GLFW_MOD_CONTROL) pr("ctrl+");
if (mods & GLFW_MOD_ALT) pr("alt+");
if (mods & GLFW_MOD_SHIFT) pr("shift+");
if (mods & GLFW_MOD_SUPER) pr("super+");
if (mods & GLFW_MOD_CAPS_LOCK) pr("capslock+");
if (mods & GLFW_MOD_NUM_LOCK) pr("numlock+");
if (p == s) pr("none");
else p--;
pr(" ");
#undef pr
return buf;
}
static const char*
format_text(const char *src) {
static char buf[256];
char *p = buf;
const char *last_char = buf + sizeof(buf) - 1;
if (!src[0]) return "<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_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
if (forward_dictation_selector_to_app(selector, nil)) return;
}
@end // }}}
// File Promise Provider Delegate for async drag data {{{
// Structure to hold async drag state
@interface GLFWFilePromiseProviderDelegate : NSObject <NSFilePromiseProviderDelegate>
{
GLFWid windowId, instanceId;
char* mimeType; // MIME type for this provider
NSFileHandle *file_handle;
NSURL *file_url;
void (^completion_handler)(NSError*);
}
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid)iid;
- (void)request_drag_data;
- (void)end_transfer:(int)errorCode;
- (void)end_transfer_with_error:(NSError*)err;
- (bool)is_mimetype:(const char*)mime_type;
@end
@interface GLFWDraggingSource : NSObject <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 NO; // changed by Kovid, to follow cocoa platform conventions
}
- (void)mouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_LEFT,
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)mouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)mouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_LEFT,
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)mouseMoved:(NSEvent *)event
{
if (!window) return;
if (window->cursorMode == GLFW_CURSOR_DISABLED)
{
const double dx = [event deltaX] - window->ns.cursorWarpDeltaX;
const double dy = [event deltaY] - window->ns.cursorWarpDeltaY;
_glfwInputCursorPos(window,
window->virtualCursorPosX + dx,
window->virtualCursorPosY + dy);
}
else
{
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [event locationInWindow];
_glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);
}
window->ns.cursorWarpDeltaX = 0;
window->ns.cursorWarpDeltaY = 0;
if (window->ns.delayed_cursor_update_requested) {
window->ns.delayed_cursor_update_requested = false;
if (cursorInContentArea(window)) updateCursorImage(window);
}
}
- (void)rightMouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_RIGHT,
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)rightMouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)rightMouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_RIGHT,
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)otherMouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
(int) [event buttonNumber],
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)otherMouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)otherMouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
(int) [event buttonNumber],
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)mouseExited:(NSEvent *)event
{
(void)event;
if (!window) return;
_glfwInputCursorEnter(window, false);
[[NSCursor arrowCursor] set];
}
- (void)mouseEntered:(NSEvent *)event
{
(void)event;
if (!window) return;
_glfwInputCursorEnter(window, true);
updateCursorImage(window);
}
- (void)viewDidChangeBackingProperties
{
if (!window) return;
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];
if (fbRect.size.width != window->ns.fbWidth ||
fbRect.size.height != window->ns.fbHeight)
{
window->ns.fbWidth = (int)fbRect.size.width;
window->ns.fbHeight = (int)fbRect.size.height;
_glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height);
}
const float xscale = fbRect.size.width / contentRect.size.width;
const float yscale = fbRect.size.height / contentRect.size.height;
if (xscale != window->ns.xscale || yscale != window->ns.yscale)
{
window->ns.xscale = xscale;
window->ns.yscale = yscale;
_glfwInputWindowContentScale(window, xscale, yscale);
if (window->ns.retina && window->ns.layer)
[window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]];
}
}
- (void)drawRect:(NSRect)rect
{
(void)rect;
if (!window) return;
_glfwInputWindowDamage(window);
}
- (void)updateTrackingAreas
{
if (window && [window->ns.object areCursorRectsEnabled])
[window->ns.object disableCursorRects];
if (trackingArea != nil)
{
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways |
NSTrackingEnabledDuringMouseDrag |
NSTrackingCursorUpdate |
NSTrackingInVisibleRect |
NSTrackingAssumeInside;
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:options
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
[super updateTrackingAreas];
}
- (NSTextInputContext *)inputContext
{
return input_context;
}
static UInt32
convert_cocoa_to_carbon_modifiers(NSUInteger flags) {
UInt32 mods = 0;
if (flags & NSEventModifierFlagShift)
mods |= shiftKey;
if (flags & NSEventModifierFlagControl)
mods |= controlKey;
if (flags & NSEventModifierFlagOption)
mods |= optionKey;
if (flags & NSEventModifierFlagCommand)
mods |= cmdKey;
if (flags & NSEventModifierFlagCapsLock)
mods |= alphaLock;
return (mods >> 8) & 0xFF;
}
static void
convert_utf16_to_utf8(UniChar *src, UniCharCount src_length, char *dest, size_t dest_sz) {
CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
src,
src_length,
kCFAllocatorNull);
CFStringGetCString(string,
dest,
dest_sz,
kCFStringEncodingUTF8);
CFRelease(string);
}
static bool
alternate_key_is_ok(uint32_t key, uint32_t akey) {
return akey > 31 && akey != key && !is_pua_char(akey);
}
static void
add_alternate_keys(GLFWkeyevent *ev, NSEvent *event) {
ev->alternate_key = translateKey(ev->native_key, false);
if (!alternate_key_is_ok(ev->key, ev->alternate_key)) ev->alternate_key = 0;
if (ev->mods & GLFW_MOD_SHIFT) {
NSString *ci = [event charactersIgnoringModifiers];
if (ci) {
unsigned sz = [ci length];
if (sz > 0) {
UniChar buf[2] = {0};
buf[0] = [ci characterAtIndex:0];
if (sz > 1) buf[1] = [ci characterAtIndex:1];
ev->shifted_key = get_first_codepoint(buf, sz);
}
}
if (!alternate_key_is_ok(ev->key, ev->shifted_key)) ev->shifted_key = 0;
}
}
static bool
is_ascii_control_char(char x) {
return x == 0 || (1 <= x && x <= 31) || x == 127;
}
- (void)keyDown:(NSEvent *)event
{
#define CLEAR_PRE_EDIT_TEXT glfw_keyevent.text = NULL; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
#define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
const bool previous_has_marked_text = [self hasMarkedText];
if (input_context && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:input_context.selectedKeyboardInputSource])) {
if (input_source_at_last_key_event) {
debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n");
GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
window->ns.deadKeyState = 0;
_glfwInputKeyboard(window, &dummy); // clear pre-edit text
[input_source_at_last_key_event release];
input_source_at_last_key_event = nil;
}
input_source_at_last_key_event = [input_context.selectedKeyboardInputSource retain];
[self unmarkText];
}
const unsigned int keycode = [event keyCode];
const NSUInteger flags = [event modifierFlags];
const int mods = translateFlags(flags);
const uint32_t key = translateKey(keycode, true);
const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1);
_glfw.ns.text[0] = 0;
if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */ || (keycode == 0x04 /* h */ && mods == GLFW_MOD_CONTROL)) [self unmarkText];
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods};
if (!_glfw.ns.unicodeData) {
// Using the cocoa API for key handling is disabled, as there is no
// reliable way to handle dead keys using it. Only use it if the
// keyboard unicode data is not available.
if (process_text) {
// this will call insertText with the text for this event, if any
[self interpretKeyEvents:@[event]];
}
} else {
static UniChar text[256];
UniCharCount char_count = 0;
const bool in_compose_sequence = window->ns.deadKeyState != 0;
if (UCKeyTranslate(
[(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDown,
convert_cocoa_to_carbon_modifiers(flags),
LMGetKbdType(),
(process_text ? 0 : kUCKeyTranslateNoDeadKeysMask),
&(window->ns.deadKeyState),
sizeof(text)/sizeof(text[0]),
&char_count,
text
) != noErr) {
debug_key("UCKeyTranslate failed for keycode: 0x%x (%s) %s\n",
keycode, safe_name_for_keycode(keycode), format_mods(mods));
window->ns.deadKeyState = 0;
return;
}
debug_key("\x1b[31mPress:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %schar_count: %lu deadKeyState: %u repeat: %d ",
keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat);
marked_text_cleared_by_insert = false;
if (process_text) {
in_key_handler = 1;
// this will call insertText which will fill up _glfw.ns.text
[self interpretKeyEvents:@[event]];
in_key_handler = 0;
} else {
window->ns.deadKeyState = 0;
}
if (window->ns.deadKeyState && (char_count == 0 || keycode == 0x75)) {
// 0x75 is the delete key which needs to be ignored during a compose sequence
debug_key("Sending pre-edit text for dead key (text: %s markedText: %s).\n", format_text(_glfw.ns.text), glfw_keyevent.text);
UPDATE_PRE_EDIT_TEXT;
return;
}
if (in_compose_sequence) {
debug_key("Clearing pre-edit text at end of compose sequence\n");
CLEAR_PRE_EDIT_TEXT;
}
}
if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // don't send text for ascii control codes
debug_key("text: %s glfw_key: %s marked_text: (%s)\n",
format_text(_glfw.ns.text), _glfwGetKeyName(key), [[markedText string] UTF8String]);
bool bracketed_ime = false;
if (!window->ns.deadKeyState) {
if ([self hasMarkedText]) {
if (!marked_text_cleared_by_insert) {
UPDATE_PRE_EDIT_TEXT;
} else bracketed_ime = true;
} else if (previous_has_marked_text) {
CLEAR_PRE_EDIT_TEXT;
}
if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) {
// do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it
debug_key("Ignoring key press as IME is active and it generated no text\n");
return;
}
}
if (bracketed_ime) {
// insertText followed by setMarkedText
CLEAR_PRE_EDIT_TEXT;
}
glfw_keyevent.text = _glfw.ns.text;
glfw_keyevent.ime_state = GLFW_IME_NONE;
add_alternate_keys(&glfw_keyevent, event);
_glfwInputKeyboard(window, &glfw_keyevent);
if (bracketed_ime) {
// insertText followed by setMarkedText
UPDATE_PRE_EDIT_TEXT;
}
}
static bool
is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_mask, NSUInteger either_mask) {
bool target_pressed = (flags & target_mask) != 0;
bool other_pressed = (flags & other_mask) != 0;
bool either_pressed = (flags & either_mask) != 0;
if (either_pressed != (target_pressed || other_pressed)) return either_pressed;
return target_pressed;
}
- (void)flagsChanged:(NSEvent *)event
{
int action = GLFW_RELEASE;
const char old_first_char = _glfw.ns.text[0];
_glfw.ns.text[0] = 0;
const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
const uint32_t key = vk_code_to_functional_key_code([event keyCode]);
const unsigned int keycode = [event keyCode];
const int mods = translateFlags(modifierFlags);
const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, modifierFlags) != 1);
const char *mod_name = "unknown";
// Code for handling modifier key events copied form SDL_cocoakeyboard.m, with thanks. See IsModifierKeyPressedFunction()
#define action_for(modname, target_mask, other_mask, either_mask) action = is_modifier_pressed([event modifierFlags], target_mask, other_mask, either_mask) ? GLFW_PRESS : GLFW_RELEASE; mod_name = #modname; break;
switch(key) {
case GLFW_FKEY_CAPS_LOCK:
mod_name = "capslock";
action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_SUPER: action_for(super, NX_DEVICELCMDKEYMASK, NX_DEVICERCMDKEYMASK, NX_COMMANDMASK);
case GLFW_FKEY_RIGHT_SUPER: action_for(super, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK, NX_COMMANDMASK);
case GLFW_FKEY_LEFT_CONTROL: action_for(ctrl, NX_DEVICELCTLKEYMASK, NX_DEVICERCTLKEYMASK, NX_CONTROLMASK);
case GLFW_FKEY_RIGHT_CONTROL: action_for(ctrl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK, NX_CONTROLMASK);
case GLFW_FKEY_LEFT_ALT: action_for(alt, NX_DEVICELALTKEYMASK, NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK);
case GLFW_FKEY_RIGHT_ALT: action_for(alt, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK);
case GLFW_FKEY_LEFT_SHIFT: action_for(shift, NX_DEVICELSHIFTKEYMASK, NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK);
case GLFW_FKEY_RIGHT_SHIFT: action_for(shift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK);
default:
return;
}
#undef action_for
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = action, .mods = mods};
debug_key("\x1b[33mflagsChanged:\x1b[m modifier: %s native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
mod_name, keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
marked_text_cleared_by_insert = false;
if (process_text && input_context) {
// this will call insertText which will fill up _glfw.ns.text
in_key_handler = 2;
[input_context handleEvent:event];
in_key_handler = 0;
if (marked_text_cleared_by_insert) {
debug_key("Clearing pre-edit text because insertText called from flagsChanged\n");
CLEAR_PRE_EDIT_TEXT;
if (_glfw.ns.text[0]) glfw_keyevent.text = _glfw.ns.text;
else _glfw.ns.text[0] = old_first_char;
}
}
glfw_keyevent.ime_state = GLFW_IME_NONE;
_glfwInputKeyboard(window, &glfw_keyevent);
}
- (void)keyUp:(NSEvent *)event
{
const uint32_t keycode = [event keyCode];
const uint32_t key = translateKey(keycode, true);
const int mods = translateFlags([event modifierFlags]);
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_RELEASE, .mods = mods};
add_alternate_keys(&glfw_keyevent, event);
debug_key("\x1b[32mRelease:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
_glfwInputKeyboard(window, &glfw_keyevent);
}
#undef CLEAR_PRE_EDIT_TEXT
#undef UPDATE_PRE_EDIT_TEXT
- (void)scrollWheel:(NSEvent *)event
{
GLFWScrollEvent ev = {
.keyboard_modifiers=translateFlags([event modifierFlags]), .unscaled.x = [event scrollingDeltaX], .unscaled.y = [event scrollingDeltaY]};
ev.x_offset = ev.unscaled.x;
ev.y_offset = ev.unscaled.y;
if ([event hasPreciseScrollingDeltas]) {
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
float xscale = 1, yscale = 1;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
if (xscale > 0) ev.x_offset *= xscale;
if (yscale > 0) ev.y_offset *= yscale;
}
switch([event momentumPhase]) {
case NSEventPhaseBegan: ev.momentum_type = GLFW_MOMENTUM_PHASE_BEGAN; break;
case NSEventPhaseStationary: ev.momentum_type = GLFW_MOMENTUM_PHASE_STATIONARY; break;
case NSEventPhaseChanged: ev.momentum_type = GLFW_MOMENTUM_PHASE_ACTIVE; break;
case NSEventPhaseEnded: ev.momentum_type = GLFW_MOMENTUM_PHASE_ENDED; break;
case NSEventPhaseCancelled: ev.momentum_type = GLFW_MOMENTUM_PHASE_CANCELED; break;
case NSEventPhaseMayBegin: ev.momentum_type = GLFW_MOMENTUM_PHASE_MAY_BEGIN; break;
case NSEventPhaseNone: break;
}
_glfwInputScroll(window, &ev);
}
// Drop implementation for drag and drop {{{
// Return YES to receive periodic dragging updates even when the mouse hasn't moved.
// This allows the application to update acceptance status asynchronously.
- (BOOL)wantsPeriodicDraggingUpdates
{
return YES;
}
static void
free_drop_data(_GLFWwindow *window) {
if (window->ns.drop_data.mimes) {
for (size_t i = 0; i < window->ns.drop_data.mimes_count; i++) free((void*)window->ns.drop_data.mimes[i]);
free(window->ns.drop_data.mimes);
}
free(window->ns.drop_data.copy_mimes); // pointer array only; strings owned by mimes[]
if (window->ns.drop_data.pasteboard) [window->ns.drop_data.pasteboard release];
if (window->ns.drop_data.data_mapping) [window->ns.drop_data.data_mapping release];
if (window->ns.drop_data.file_promise_mapping) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
for (NSString *key in window->ns.drop_data.file_promise_mapping) {
NSArray *pair = [window->ns.drop_data.file_promise_mapping objectForKey:key];
error = nil; if (pair[1] != [NSNull null]) [pair[1] closeAndReturnError:&error];
error = nil; [fileManager removeItemAtURL:pair[0] error:&error];
}
[window->ns.drop_data.file_promise_mapping release];
}
memset(&window->ns.drop_data, 0, sizeof(_GLFWDropData));
}
static void
update_drop_state(_GLFWwindow *window, size_t accepted_count, GLFWDropEventType t) {
_GLFWDropData *d = &window->ns.drop_data;
d->copy_mimes_count = accepted_count;
if (t == GLFW_DROP_ENTER || t == GLFW_DROP_MOVE) d->drag_accepted = accepted_count > 0;
}
// Reset the working copy of mimes so the next callback sees the full original
// list. Returns false on allocation failure.
static bool
reset_drop_copy_mimes(_GLFWDropData *d) {
if (d->mimes_count == 0) { d->copy_mimes_count = 0; return true; }
if (!d->copy_mimes) {
d->copy_mimes = malloc(d->mimes_count * sizeof(const char*));
if (!d->copy_mimes) return false;
}
memcpy(d->copy_mimes, d->mimes, d->mimes_count * sizeof(const char*));
d->copy_mimes_count = d->mimes_count;
return true;
}
- (NSDragOperation)draggingEntered:(id <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)
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
if ([pasteboard canReadObjectForClasses:@[[NSURL class]] options:options]) {
mime_array[mime_count++] = _glfw_strdup("text/uri-list");
}
if ([pasteboard canReadObjectForClasses:@[[NSString class]] options:nil]) {
mime_array[mime_count++] = _glfw_strdup("text/plain");
}
#define add_mime(uti) { \
const char* mime = uti_to_mime(uti); \
if (mime && mime[0]) { \
bool duplicate = false; \
for (size_t i = 0; i < mime_count; i++) { \
if (strcmp(mime_array[i], mime) == 0) { \
duplicate = true; \
break; \
} \
} \
if (!duplicate) mime_array[mime_count++] = _glfw_strdup(mime); \
} \
}
// Get file promise based types
for (NSFilePromiseReceiver *receiver in receivers) {
for (NSString *uti in receiver.fileTypes) {
add_mime(uti);
}
}
// Get additional types from pasteboard items
for (NSPasteboardItem* item in pasteboard.pasteboardItems) {
for (NSPasteboardType uti in item.types) {
add_mime(uti);
}
}
window->ns.drop_data.mimes = mime_array;
window->ns.drop_data.mimes_count = mime_count;
bool from_self = ([sender draggingSource] != nil);
_GLFWDropData *d = &window->ns.drop_data;
if (reset_drop_copy_mimes(d)) {
size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_ENTER, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
update_drop_state(window, accepted_count, GLFW_DROP_ENTER);
}
return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone;
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
if (!window->ns.drop_data.drag_accepted) return NSDragOperationNone;
const NSRect contentRect = [window->ns.view frame];
const NSPoint pos = [sender draggingLocation];
double xpos = pos.x;
double ypos = contentRect.size.height - pos.y;
bool from_self = ([sender draggingSource] != nil);
_GLFWDropData *d = &window->ns.drop_data;
if (reset_drop_copy_mimes(d)) {
size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
update_drop_state(window, accepted_count, GLFW_DROP_MOVE);
}
return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone;
}
- (void)draggingExited:(id <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)performDragOperation:(id <NSDraggingInfo>)sender
{
if (!window->ns.drop_data.drag_accepted) return NO;
const NSRect contentRect = [window->ns.view frame];
const NSPoint pos = [sender draggingLocation];
double xpos = pos.x;
double ypos = contentRect.size.height - pos.y;
bool from_self = ([sender draggingSource] != nil);
_GLFWDropData *d = &window->ns.drop_data;
if (!reset_drop_copy_mimes(d)) return NO;
size_t num_accepted = _glfwInputDropEvent(window, GLFW_DROP_DROP, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
if (d->copy_mimes) {
update_drop_state(window, num_accepted, GLFW_DROP_DROP);
window->ns.drop_data.pasteboard = [[sender draggingPasteboard] retain];
for (size_t i = 0; i < num_accepted; i++)
_glfwPlatformRequestDropData(window, d->copy_mimes[i]);
}
// Restore first-responder status after native DnD; the drag operation can
// displace the content view from first responder, silently breaking keyboard
// input even though osw->is_focused remains true.
[window->ns.object makeFirstResponder:window->ns.view];
return YES;
}
void
_glfwPlatformRequestDropUpdate(_GLFWwindow* window UNUSED) {
// No-op since macOS is calling the drop move callback periodically anyway
// thanks to wantsPeriodicDraggingUpdates and we have no way to inform
// macOS of any changes except in the cocoa callbacks.
}
static void
send_data_available_event_on_next_event_loop_tick(GLFWid wid, const char *mime) {
char *mt = _glfw_strdup(mime);
dispatch_async(dispatch_get_main_queue(), ^{
_GLFWwindow *window = _glfwWindowForId(wid);
if (window) {
const char *mimes[1] = {mt};
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
}
free(mt);
});
}
int
_glfwPlatformRequestDropData(_GLFWwindow *window UNUSED, const char *mime) {
NSPasteboard* pasteboard = window->ns.drop_data.pasteboard;
if (!pasteboard) return EINVAL;
GLFWid wid = window->id;
if (window->ns.drop_data.data_mapping == nil) window->ns.drop_data.data_mapping = [[NSMutableDictionary alloc] init];
NSArray *pair;
if ((pair = window->ns.drop_data.data_mapping[@(mime)])) {
window->ns.drop_data.data_mapping[@(mime)] = @[pair[0], @0];
send_data_available_event_on_next_event_loop_tick(wid, mime);
return 0;
}
if (window->ns.drop_data.file_promise_mapping == nil) window->ns.drop_data.file_promise_mapping = [[NSMutableDictionary alloc] init];
if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) {
if (pair[0] == [NSNull null]) return 0; // waiting for promise
if (pair[1] != [NSNull null]) {
NSFileHandle *h = pair[1]; NSError *error = nil;
[h seekToOffset:0 error:&error];
}
send_data_available_event_on_next_event_loop_tick(wid, mime);
return 0;
}
NSData* data = nil; NSFilePromiseReceiver *file_promise = nil;
// Handle special MIME types
if (strcmp(mime, "text/uri-list") == 0) {
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] options:options];
if (urls && [urls count] > 0) {
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096];
for (NSURL* url in urls) {
if ([uri_list length] > 0) [uri_list appendString:@"\n"];
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
else [uri_list appendString:url.absoluteString];
}
data = [uri_list dataUsingEncoding:NSUTF8StringEncoding];
}
} else if (strcmp(mime, "text/plain") == 0 || strcmp(mime, "text/plain;charset=utf-8") == 0) {
NSArray* strings = [pasteboard readObjectsForClasses:@[[NSString class]] options:nil];
if (strings && [strings count] > 0) {
NSString* str = strings[0];
data = [str dataUsingEncoding:NSUTF8StringEncoding];
}
}
if (data == nil) {
// Try to read data for other MIME types using UTI
NSString* uti = mime_to_uti(mime);
if (uti) {
NSPasteboardType pbType = [pasteboard availableTypeFromArray:@[uti]];
if (pbType) data = [pasteboard dataForType:pbType];
}
if (data == nil) {
// look in the file promise providers
NSArray *receivers = [pasteboard readObjectsForClasses:@[[NSFilePromiseReceiver class]] options:@{}];
for (NSFilePromiseReceiver *receiver in receivers) {
for (NSString *uti in receiver.fileTypes) {
const char *q = uti_to_mime(uti);
if (q && strcmp(q, mime) == 0) {
file_promise = receiver;
break;
}
}
if (file_promise) break;
}
}
}
if (!data && !file_promise) return ENOENT;
if (file_promise != nil) {
window->ns.drop_data.file_promise_mapping[@(mime)] = @[[NSNull null], [NSNull null], [NSNull null]];
char *mt = _glfw_strdup(mime);
[file_promise receivePromisedFilesAtDestination:[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
options:@{} operationQueue:[NSOperationQueue mainQueue] reader:^(NSURL *fileURL, NSError *errorOrNil) {
_GLFWwindow *window = _glfwWindowForId(wid);
if (!window || !window->ns.drop_data.file_promise_mapping) return;
id null = [NSNull null];
if (errorOrNil) {
NSLog(@"Error receiving file: %@: %@", fileURL, errorOrNil);
window->ns.drop_data.file_promise_mapping[@(mt)] = @[fileURL, null, errorOrNil];
} else {
NSError *err = nil;
NSFileHandle *file_handle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&err];
window->ns.drop_data.file_promise_mapping[@(mt)] = err ? @[fileURL, null, err] : @[fileURL, file_handle, null];
}
const char *mimes[1] = {mt};
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
free(mt);
}];
} else {
window->ns.drop_data.data_mapping[@(mime)] = @[data, @0];
const char *mimes[1] = {mime};
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
}
return 0;
}
ssize_t
_glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffer, size_t capacity) {
_GLFWwindow *window = (_GLFWwindow*)w; const char *mime = ev->mimes[0];
NSArray *pair;
if ((pair = window->ns.drop_data.data_mapping[@(mime)])) {
NSData *data = pair[0];
size_t offset = [pair[1] unsignedIntegerValue];
NSUInteger dataLength = [data length];
if (offset >= dataLength) return 0; // EOF
NSUInteger remaining = dataLength - offset;
NSUInteger to_read = (remaining < capacity) ? remaining : capacity;
[data getBytes:buffer range:NSMakeRange(offset, to_read)];
offset += to_read;
window->ns.drop_data.data_mapping[@(mime)] = @[data, @(offset)];
if (to_read) send_data_available_event_on_next_event_loop_tick(window->id, mime);
return (ssize_t)to_read;
}
if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) {
id null = [NSNull null];
if (pair[0] == null) { return -ENOENT; }
if (pair[2] != null) {
NSError *err = pair[2];
if ([err.domain isEqualToString:NSPOSIXErrorDomain]) return -err.code;
NSError *underlyingError = err.userInfo[NSUnderlyingErrorKey];
if (underlyingError && [underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) return -underlyingError.code;
return -EIO;
}
NSFileHandle *h = pair[1];
int fd = h.fileDescriptor;
ssize_t bytesRead; do {
bytesRead = read(fd, buffer, capacity);
} while (bytesRead == -1 && errno == EINTR);
bytesRead = bytesRead < 0 ? -errno : bytesRead;
if (bytesRead > 0) send_data_available_event_on_next_event_loop_tick(window->id, mime);
return bytesRead;
}
return -ENOENT;
}
void
_glfwPlatformEndDrop(GLFWwindow *w UNUSED, GLFWDragOperationType op UNUSED) {
free_drop_data((_GLFWwindow*)w);
}
// }}}
- (BOOL)hasMarkedText
{
return [markedText length] > 0;
}
- (NSRange)markedRange
{
if ([markedText length] > 0)
return NSMakeRange(0, [markedText length] - 1);
else
return kEmptyRange;
}
- (NSRange)selectedRange
{
// Return position 0 with no selection to indicate text can be inserted.
// This is required for macOS dictation to work - returning kEmptyRange
// (NSNotFound, 0) causes dictation to fail because the system doesn't
// know where to insert text. See https://github.com/kovidgoyal/kitty/issues/3732
return NSMakeRange(0, 0);
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selectedRange
replacementRange:(NSRange)replacementRange
{
const char *s = polymorphic_string_as_utf8(string);
debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", s, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length);
if (string == nil || !s[0]) {
bool had_marked_text = [self hasMarkedText];
[self unmarkText];
if (had_marked_text && (!in_key_handler || in_key_handler == 2)) {
debug_key("Clearing pre-edit because setMarkedText called from %s\n", in_key_handler ? "flagsChanged" : "event loop");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
return;
}
if ([string isKindOfClass:[NSAttributedString class]]) {
if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; }
[markedText release];
markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
} else {
if (((NSString*)string).length == 0) { [self unmarkText]; return; }
[markedText release];
markedText = [[NSMutableAttributedString alloc] initWithString:string];
}
if (!in_key_handler || in_key_handler == 2) {
debug_key("Updating IME text in kitty from setMarkedText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=[[markedText string] UTF8String], .ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
}
- (void)unmarkText
{
[[markedText mutableString] setString:@""];
}
void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
[w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused];
}
- (void)updateIMEStateFor:(GLFWIMEUpdateType)which
focused:(bool)focused
{
if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) {
[input_context discardMarkedText];
[self unmarkText];
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return;
if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates];
}
- (NSArray*)validAttributesForMarkedText
{
return [NSArray array];
}
- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange
{
(void)range; (void)actualRange;
return nil;
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
(void)point;
return 0;
}
- (NSRect)firstRectForCharacterRange:(NSRange)range
actualRange:(NSRangePointer)actualRange
{
(void)range; (void)actualRange;
if (_glfw.callbacks.get_ime_cursor_position) {
GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION };
if (window && _glfw.callbacks.get_ime_cursor_position((GLFWwindow*)window, &ev)) {
const CGFloat left = (CGFloat)ev.cursor.left / window->ns.xscale;
const CGFloat top = (CGFloat)ev.cursor.top / window->ns.yscale;
const CGFloat cellWidth = (CGFloat)ev.cursor.width / window->ns.xscale;
const CGFloat cellHeight = (CGFloat)ev.cursor.height / window->ns.yscale;
debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight);
const NSRect frame = [window->ns.view frame];
const NSRect rectInView = NSMakeRect(left,
frame.size.height - top - cellHeight,
cellWidth, cellHeight);
markedRect = [window->ns.object convertRectToScreen: rectInView];
}
}
return markedRect;
}
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
const char *utf8 = polymorphic_string_as_utf8(string);
debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length);
if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) {
[self unmarkText];
marked_text_cleared_by_insert = true;
if (!in_key_handler) {
debug_key("Clearing pre-edit because insertText called from event loop\n");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
}
// insertText can be called multiple times for a single key event
size_t existing_length = strnlen(_glfw.ns.text, sizeof(_glfw.ns.text));
size_t required_length = strlen(utf8) + 1;
size_t available_length = sizeof(_glfw.ns.text) - existing_length;
if (available_length >= required_length) {
memcpy(_glfw.ns.text + existing_length, utf8, required_length); // copies the null terminator from utf8 as well
_glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0;
if ((!in_key_handler || in_key_handler == 2) && _glfw.ns.text[0]) {
if (!is_ascii_control_char(_glfw.ns.text[0])) {
debug_key("Sending text to kitty from insertText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=_glfw.ns.text, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
}
_glfw.ns.text[0] = 0;
}
}
}
- (void)doCommandBySelector:(SEL)selector
{
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
if (forward_dictation_selector_to_app(selector, self)) return;
}
- (void)startDictation:(id)sender
{
forward_dictation_selector_to_app(_cmd, sender);
}
- (void)stopDictation:(id)sender
{
forward_dictation_selector_to_app(_cmd, sender);
}
- (BOOL)isAccessibilityElement
{
return YES;
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
// Allow accessibility selectors needed for dictation and other accessibility features
// See https://github.com/kovidgoyal/kitty/issues/3732
if (selector == @selector(accessibilityRole) ||
selector == @selector(accessibilitySelectedText) ||
selector == @selector(accessibilitySelectedTextRange) ||
selector == @selector(accessibilityNumberOfCharacters) ||
selector == @selector(accessibilityInsertionPointLineNumber) ||
selector == @selector(accessibilityValue) ||
selector == @selector(setAccessibilityValue:)) return YES;
// Allow accessibility selectors needed for external window management tools
// (e.g. Easy Move+Resize) to find and manipulate the window.
// See https://github.com/kovidgoyal/kitty/issues/5561
if (selector == @selector(accessibilityWindow) ||
selector == @selector(accessibilityParent) ||
selector == @selector(accessibilityPosition) ||
selector == @selector(setAccessibilityPosition:) ||
selector == @selector(accessibilitySize) ||
selector == @selector(setAccessibilitySize:)) return YES;
return NO;
}
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400)
- (NSAccessibilityRole)accessibilityRole
{
return NSAccessibilityTextAreaRole;
}
#endif
- (NSString *)accessibilitySelectedText
{
NSString *text = nil;
if (_glfw.callbacks.get_current_selection) {
char *s = _glfw.callbacks.get_current_selection();
if (s) {
text = [NSString stringWithUTF8String:s];
free(s);
}
}
return text;
}
// Accessibility methods required for dictation support
// See https://github.com/kovidgoyal/kitty/issues/3732
- (NSRange)accessibilitySelectedTextRange
{
// Return position 0 with no selection for dictation support
return NSMakeRange(0, 0);
}
- (NSInteger)accessibilityNumberOfCharacters
{
// Terminal doesn't have a fixed text buffer, return 0
return 0;
}
- (NSInteger)accessibilityInsertionPointLineNumber
{
// Return line 0 as the insertion point
return 0;
}
- (NSString *)accessibilityValue
{
// Terminal doesn't expose its buffer as an accessibility value
return @"";
}
- (void)setAccessibilityValue:(NSString *)value
{
// When dictation or other accessibility features set text, insert it as keyboard input
if (value && [value length] > 0 && window) {
const char *utf8 = [value UTF8String];
debug_key("Inserting text via setAccessibilityValue: %s\n", utf8);
GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
}
}
// <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_key("Sending text received in readSelectionFromPasteboard as key event\n");
GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
// Restore pre-edit text after inserting the received text
if ([self hasMarkedText]) {
glfw_keyevent.text = [[markedText string] UTF8String];
glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED;
_glfwInputKeyboard(window, &glfw_keyevent);
}
return YES;
}
return NO;
}
@end
// }}}
// GLFW window class {{{
@interface GLFWWindow : NSWindow {
_GLFWwindow* glfw_window;
}
- (instancetype)initWithGlfwWindow:(NSRect)contentRect
styleMask:(NSWindowStyleMask)style
backing:(NSBackingStoreType)backingStoreType
initWindow:(_GLFWwindow *)initWindow;
- (void) removeGLFWWindow;
@end
@implementation GLFWWindow
- (instancetype)initWithGlfwWindow:(NSRect)contentRect
styleMask:(NSWindowStyleMask)style
backing:(NSBackingStoreType)backingStoreType
initWindow:(_GLFWwindow *)initWindow
{
self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:NO];
if (self != nil) {
glfw_window = initWindow;
self.tabbingMode = NSWindowTabbingModeDisallowed;
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(screenParametersDidChange:) name:NSApplicationDidChangeScreenParametersNotification object:nil];
}
return self;
}
- (void)screenParametersDidChange:(NSNotification *)notification {
if (!glfw_window || !glfw_window->ns.layer_shell.is_active) return;
_glfwPlatformSetLayerShellConfig(glfw_window, NULL);
}
- (void) removeGLFWWindow
{
glfw_window = NULL;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(performMiniaturize:)) return YES;
return [super validateMenuItem:item];
}
- (void)performMiniaturize:(id)sender
{
if (glfw_window && (!glfw_window->decorated || glfw_window->ns.titlebar_hidden)) [self miniaturize:self];
else [super performMiniaturize:sender];
}
- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(nullable NSScreen *)screen
{
if (glfw_window && glfw_window->ns.suppress_frame_constraints) return frameRect;
return [super constrainFrameRect:frameRect toScreen:screen];
}
- (BOOL)canBecomeKeyWindow
{
if (!glfw_window) return NO;
if (glfw_window->ns.layer_shell.is_active) {
if (glfw_window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND) return NO;
switch(glfw_window->ns.layer_shell.config.focus_policy) {
case GLFW_FOCUS_NOT_ALLOWED: return NO;
case GLFW_FOCUS_EXCLUSIVE: return YES;
case GLFW_FOCUS_ON_DEMAND: return YES;
}
}
// Required for NSWindowStyleMaskBorderless windows
// Also miniaturized windows should not become key
return !_glfwPlatformWindowIconified(glfw_window);
}
- (BOOL)canBecomeMainWindow
{
return !glfw_window->ns.layer_shell.is_active || glfw_window->ns.layer_shell.config.type != GLFW_LAYER_SHELL_BACKGROUND;
}
static void apply_titlebar_color_settings(_GLFWwindow *window);
static void
update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow* w, bool traditional, bool made_fullscreen) {
// Update window button visibility
if (w->ns.titlebar_hidden) {
NSWindow *window = w->ns.object;
// The hidden buttons might be automatically reset to be visible after going full screen
// to show up in the auto-hide title bar, so they need to be set back to hidden.
BOOL button_hidden = YES;
// When title bar is configured to be hidden, it should be shown with buttons (auto-hide) after going to full screen.
if (!traditional) {
button_hidden = (BOOL) !made_fullscreen;
}
[[window standardWindowButton: NSWindowCloseButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowMiniaturizeButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowZoomButton] setHidden:button_hidden];
}
if (!made_fullscreen) apply_titlebar_color_settings(w);
}
- (void)toggleFullScreen:(nullable id)sender
{
if (glfw_window) {
if (glfw_window->ns.in_fullscreen_transition) return;
// Capture the windowed frame before any fullscreen transition begins.
// This is more reliable than saving it inside _glfwPlatformToggleFullscreen
// because setStyleMask: calls between cycles can reposition the window (#9572).
if (!glfw_window->ns.in_traditional_fullscreen && !([self styleMask] & NSWindowStyleMaskFullScreen)) {
glfw_window->ns.pre_traditional_fullscreen_frame = [self frame];
}
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
glfw_window->ns.in_fullscreen_transition = true;
}
NSWindowStyleMask sm = [self styleMask];
bool is_fullscreen_already = (sm & NSWindowStyleMaskFullScreen) != 0;
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
const NSSize original = [self resizeIncrements];
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
[super toggleFullScreen:sender];
[self setResizeIncrements:original];
// When the window decoration is hidden, toggling fullscreen causes the style mask to be changed,
// and causes the first responder to be cleared.
if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view];
update_titlebar_button_visibility_after_fullscreen_transition(glfw_window, false, !is_fullscreen_already);
}
- (void)zoom:(id)sender
{
if (![self isZoomed]) {
const NSSize original = [self resizeIncrements];
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
[super zoom:sender];
[self setResizeIncrements:original];
} else {
[super zoom:sender];
}
}
@end
// }}}
// Create the Cocoa window
//
static bool createNativeWindow(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig,
const _GLFWfbconfig* fbconfig)
{
window->ns.delegate = [[GLFWWindowDelegate alloc] initWithGlfwWindow:window];
if (window->ns.delegate == nil)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to create window delegate");
return false;
}
NSRect contentRect;
if (window->monitor)
{
GLFWvidmode mode;
int xpos, ypos;
_glfwPlatformGetVideoMode(window->monitor, &mode);
_glfwPlatformGetMonitorPos(window->monitor, &xpos, &ypos);
contentRect = NSMakeRect(xpos, ypos, mode.width, mode.height);
}
else
contentRect = NSMakeRect(0, 0, wndconfig->width, wndconfig->height);
window->ns.object = [[GLFWWindow alloc]
initWithGlfwWindow:contentRect
styleMask:getStyleMask(window)
backing:NSBackingStoreBuffered
initWindow:window
];
if (window->ns.object == nil)
{
_glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create window");
return false;
}
if (window->monitor)
[window->ns.object setLevel:NSMainMenuWindowLevel + 1];
else
{
[(NSWindow*) window->ns.object center];
CGRect screen_frame = [[(NSWindow*) window->ns.object screen] frame];
if (CGRectContainsPoint(screen_frame, _glfw.ns.cascadePoint)
|| CGPointEqualToPoint(CGPointZero, _glfw.ns.cascadePoint)) {
_glfw.ns.cascadePoint =
NSPointToCGPoint([window->ns.object cascadeTopLeftFromPoint:
NSPointFromCGPoint(_glfw.ns.cascadePoint)]);
} else {
_glfw.ns.cascadePoint = CGPointZero;
}
if (wndconfig->resizable)
{
const NSWindowCollectionBehavior behavior =
NSWindowCollectionBehaviorFullScreenPrimary |
NSWindowCollectionBehaviorManaged;
[window->ns.object setCollectionBehavior:behavior];
}
if (wndconfig->floating)
[window->ns.object setLevel:NSFloatingWindowLevel];
if (wndconfig->maximized)
[window->ns.object zoom:nil];
}
if (strlen(wndconfig->ns.frameName))
[window->ns.object setFrameAutosaveName:@(wndconfig->ns.frameName)];
window->ns.view = [[GLFWContentView alloc] initWithGlfwWindow:window];
window->ns.retina = wndconfig->ns.retina;
if (fbconfig->transparent)
{
[window->ns.object setOpaque:NO];
[window->ns.object setHasShadow:NO];
[window->ns.object setBackgroundColor:[NSColor clearColor]];
}
[window->ns.object setContentView:window->ns.view];
[window->ns.object makeFirstResponder:window->ns.view];
[window->ns.object setTitle:@(wndconfig->title)];
[window->ns.object setDelegate:window->ns.delegate];
[window->ns.object setAcceptsMouseMovedEvents:YES];
[window->ns.object setRestorable:NO];
_glfwPlatformGetWindowSize(window, &window->ns.width, &window->ns.height);
_glfwPlatformGetFramebufferSize(window, &window->ns.fbWidth, &window->ns.fbHeight);
if (wndconfig->blur_radius > 0) _glfwPlatformSetWindowBlur(window, wndconfig->blur_radius);
return true;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
//////////////////////////////////////////////////////////////////////////
int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, const GLFWLayerShellConfig *lsc) {
window->ns.deadKeyState = 0;
if (lsc) {
window->ns.layer_shell.is_active = true;
window->ns.layer_shell.config = *lsc;
} else window->ns.layer_shell.is_active = false;
if (!_glfw.ns.finishedLaunching)
{
[NSApp run];
_glfw.ns.finishedLaunching = true;
}
if (!createNativeWindow(window, wndconfig, fbconfig))
return false;
switch((GlfwCocoaColorSpaces)wndconfig->ns.color_space) {
case SRGB_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace sRGBColorSpace]]; break;
case DISPLAY_P3_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace displayP3ColorSpace]]; break;
case DEFAULT_COLORSPACE: break;
}
if (ctxconfig->client != GLFW_NO_API)
{
if (ctxconfig->source == GLFW_NATIVE_CONTEXT_API)
{
if (!_glfwInitNSGL())
return false;
if (!_glfwCreateContextNSGL(window, ctxconfig, fbconfig))
return false;
}
else if (ctxconfig->source == GLFW_EGL_CONTEXT_API)
{
// EGL implementation on macOS use CALayer* EGLNativeWindowType so we
// need to get the layer for EGL window surface creation.
[window->ns.view setWantsLayer:YES];
window->ns.layer = [window->ns.view layer];
if (!_glfwInitEGL())
return false;
if (!_glfwCreateContextEGL(window, ctxconfig, fbconfig))
return false;
}
else if (ctxconfig->source == GLFW_OSMESA_CONTEXT_API)
{
if (!_glfwInitOSMesa())
return false;
if (!_glfwCreateContextOSMesa(window, ctxconfig, fbconfig))
return false;
}
}
if (window->monitor)
{
// Do not show the window here until after setting the window size, maximized state, and full screen
// _glfwPlatformShowWindow(window);
// _glfwPlatformFocusWindow(window);
acquireMonitor(window);
}
return true;
}
void _glfwPlatformDestroyWindow(_GLFWwindow* window)
{
GLFWWindow *w = window->ns.object;
if (_glfw.ns.disabledCursorWindow == window)
_glfw.ns.disabledCursorWindow = NULL;
free_drop_data(window);
if (window->ns.notch_cover_window) {
[w removeChildWindow:window->ns.notch_cover_window];
[window->ns.notch_cover_window close];
[window->ns.notch_cover_window release];
window->ns.notch_cover_window = nil;
}
[w orderOut:nil];
if (window->monitor)
releaseMonitor(window);
if (window->context.destroy)
window->context.destroy(window);
[w setDelegate:nil];
[window->ns.delegate cleanup];
[window->ns.delegate release];
window->ns.delegate = nil;
[window->ns.view removeGLFWWindow];
[window->ns.view release];
window->ns.view = nil;
[w removeGLFWWindow];
// Workaround for macOS Tahoe where if the frame is not set to zero size
// even after NSWindow::close the window remains on screen as an invisible
// rectangle that intercepts mouse events and takes up space in mission
// control. Sigh.
NSRect frame = w.frame; frame.size.width = 0; frame.size.height = 0;
[w setFrame:frame display:NO];
[w setHasShadow:NO];
[w close]; // sends a release to NSWindow so we dont release it
window->ns.object = nil;
}
static NSScreen*
screen_for_window_center(_GLFWwindow *window) {
NSRect windowFrame = [window->ns.object frame];
NSPoint windowCenter = NSMakePoint(NSMidX(windowFrame), NSMidY(windowFrame));
for (NSScreen *screen in [NSScreen screens]) {
if (NSPointInRect(windowCenter, [screen frame])) {
return screen;
}
}
return NSScreen.mainScreen;
}
static NSScreen*
active_screen(void) {
NSPoint mouseLocation = [NSEvent mouseLocation];
NSArray<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);
CGFloat dock_height = NSMinY(screen.visibleFrame) - NSMinY(screen.frame);
CGFloat menubar_height = NSHeight(screen.frame) - NSHeight(screen.visibleFrame) - dock_height;
CGFloat x = NSMinX(screen.visibleFrame), y = NSMinY(screen.visibleFrame) - 1, width = NSWidth(screen.visibleFrame), height = NSHeight(screen.visibleFrame) + 2;
if (config.type == GLFW_LAYER_SHELL_BACKGROUND || config.edge == GLFW_EDGE_CENTER) {
x = NSMinX(screen.frame); height = NSHeight(screen.frame) - menubar_height + 1; y = NSMinY(screen.frame); width = NSWidth(screen.frame);
}
// Screen co-ordinate system is with origin in lower left and y increasing upwards and x increasing rightwards
// NSLog(@"frame: %@ visibleFrame: %@\n", NSStringFromRect(screen.frame), NSStringFromRect(screen.visibleFrame));
NSWindowLevel level = NSPopUpMenuWindowLevel - 1; // so that popup menus from globalmenubar function
NSWindowAnimationBehavior animation_behavior = NSWindowAnimationBehaviorUtilityWindow;
switch (config.type) {
case GLFW_LAYER_SHELL_BACKGROUND:
animation_behavior = NSWindowAnimationBehaviorNone;
// See: https://stackoverflow.com/questions/4982584/how-do-i-draw-the-desktop-on-mac-os-x/4982619#4982619
level = kCGDesktopWindowLevel;
break;
case GLFW_LAYER_SHELL_OVERLAY: case GLFW_LAYER_SHELL_NONE: break;
case GLFW_LAYER_SHELL_PANEL: level = NSNormalWindowLevel - 1; break;
case GLFW_LAYER_SHELL_TOP: level--; break;
}
if (config.type != GLFW_LAYER_SHELL_BACKGROUND && config.edge != GLFW_EDGE_CENTER) {
double panel_height = spacing_y + ysz / yscale, panel_width = spacing_x + xsz / xscale;
switch (config.edge) {
case GLFW_EDGE_BOTTOM: height = panel_height; break;
case GLFW_EDGE_TOP:
y += height - panel_height + 1.;
height = panel_height;
break;
case GLFW_EDGE_LEFT: width = panel_width; break;
case GLFW_EDGE_RIGHT:
x += width - panel_width + 1.;
width = panel_width;
break;
case GLFW_EDGE_CENTER_SIZED:
x += (width - panel_width) / 2;
y += (height - panel_height) / 2;
width = panel_width; height = panel_height;
break;
default: // top left
y += height - panel_height + 1.;
height = panel_height; width = panel_width;
break;
}
if (width < 1.) width = NSWidth(screen.visibleFrame);
if (height < 1.) height = NSWidth(screen.visibleFrame);
}
if (config.edge != GLFW_EDGE_CENTER_SIZED) {
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
}
[nswindow setAnimationBehavior:animation_behavior];
[nswindow setLevel:level];
[nswindow setCollectionBehavior: (NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorIgnoresCycle)];
[nswindow setFrame:NSMakeRect(x, y, width, height) display:YES];
return true;
#undef config
#undef nswindow
}
void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
{
if (!title) return;
NSString* string = @(title);
if (!string) return; // the runtime failed to convert title to an NSString
[window->ns.object setTitle:string];
// HACK: Set the miniwindow title explicitly as setTitle: doesn't update it
// if the window lacks NSWindowStyleMaskTitled
[window->ns.object setMiniwindowTitle:string];
}
void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
int count UNUSED, const GLFWimage* images UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
"Cocoa: Regular windows do not have icons on macOS");
}
void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
if (xpos)
*xpos = (int)contentRect.origin.x;
if (ypos)
*ypos = (int)_glfwTransformYNS(contentRect.origin.y + contentRect.size.height - 1);
}
void _glfwPlatformSetWindowPos(_GLFWwindow* window, int x, int y)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect dummyRect = NSMakeRect(x, _glfwTransformYNS(y + contentRect.size.height - 1), 0, 0);
const NSRect frameRect = [window->ns.object frameRectForContentRect:dummyRect];
[window->ns.object setFrameOrigin:frameRect.origin];
}
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
if (width)
*width = (int)contentRect.size.width;
if (height)
*height = (int)contentRect.size.height;
}
void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
{
if (window->ns.layer_shell.is_active) return;
if (window->monitor)
{
if (window->monitor->window == window)
acquireMonitor(window);
}
else
{
// Disable window resizing in fullscreen.
if ([window->ns.object styleMask] & NSWindowStyleMaskFullScreen || window->ns.in_traditional_fullscreen) return;
NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
contentRect.origin.y += contentRect.size.height - height;
contentRect.size = NSMakeSize(width, height);
[window->ns.object setFrame:[window->ns.object frameRectForContentRect:contentRect]
display:YES];
}
}
void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
int minwidth, int minheight,
int maxwidth, int maxheight)
{
if (minwidth == GLFW_DONT_CARE || minheight == GLFW_DONT_CARE)
[window->ns.object setContentMinSize:NSMakeSize(0, 0)];
else
[window->ns.object setContentMinSize:NSMakeSize(minwidth, minheight)];
if (maxwidth == GLFW_DONT_CARE || maxheight == GLFW_DONT_CARE)
[window->ns.object setContentMaxSize:NSMakeSize(DBL_MAX, DBL_MAX)];
else
[window->ns.object setContentMaxSize:NSMakeSize(maxwidth, maxheight)];
}
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
{
if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
else
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
{
if (widthincr != GLFW_DONT_CARE && heightincr != GLFW_DONT_CARE) {
float xscale = 1, yscale = 1;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
[window->ns.object setResizeIncrements:NSMakeSize(widthincr / xscale, heightincr / yscale)];
} else {
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
}
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];
if (width)
*width = (int) fbRect.size.width;
if (height)
*height = (int) fbRect.size.height;
}
void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
int* left, int* top,
int* right, int* bottom)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect];
if (left)
*left = (int)(contentRect.origin.x - frameRect.origin.x);
if (top)
*top = (int)(frameRect.origin.y + frameRect.size.height -
contentRect.origin.y - contentRect.size.height);
if (right)
*right = (int)(frameRect.origin.x + frameRect.size.width -
contentRect.origin.x - contentRect.size.width);
if (bottom)
*bottom = (int)(contentRect.origin.y - frameRect.origin.y);
}
void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
float* xscale, float* yscale)
{
const NSRect points = get_window_size_without_border_in_logical_pixels(window);
const NSRect pixels = [window->ns.view convertRectToBacking:points];
if (xscale)
*xscale = (float) (pixels.size.width / points.size.width);
if (yscale)
*yscale = (float) (pixels.size.height / points.size.height);
}
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
return s_double_to_monotonic_t([NSEvent doubleClickInterval]);
}
void _glfwPlatformGetKeyboardRepeatDelay(monotonic_t *delay, monotonic_t *interval)
{
if (delay) *delay = s_double_to_monotonic_t([NSEvent keyRepeatDelay]);
if (interval) *interval = s_double_to_monotonic_t([NSEvent keyRepeatInterval]);
}
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
{
[window->ns.object miniaturize:nil];
}
void _glfwPlatformRestoreWindow(_GLFWwindow* window)
{
if ([window->ns.object isMiniaturized])
[window->ns.object deminiaturize:nil];
else if ([window->ns.object isZoomed])
[window->ns.object zoom:nil];
}
void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{
if (![window->ns.object isZoomed]) {
[window->ns.object zoom:nil];
}
}
void _glfwPlatformShowWindow(_GLFWwindow* window, bool move_to_active_screen)
{
const bool is_background = window->ns.layer_shell.is_active && window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND;
NSWindow *nw = window->ns.object;
if (move_to_active_screen) {
NSScreen *current_screen = screen_for_window_center(window);
NSScreen *target_screen = active_screen();
if (!is_same_screen(current_screen, target_screen)) {
debug_rendering("Moving OS window %llu to active screen\n", window->id);
move_window_to_screen(window, target_screen);
}
}
if (is_background) {
[nw orderBack:nil];
} else {
// Cocoa has a bug where when showing a hidden window after
// fullscreening an application, the window does not get added
// to the current space even though it has NSWindowCollectionBehaviorCanJoinAllSpaces
// probably because it wasnt added to the temp space used for
// fullscreen. So to work around that, we change the collection
// behavior temporarily to NSWindowCollectionBehaviorMoveToActiveSpace
// and then change it back asynchronously.
// See https://github.com/kovidgoyal/kitty/issues/8740
NSWindowCollectionBehavior old = nw.collectionBehavior;
nw.collectionBehavior = (old & !NSWindowCollectionBehaviorCanJoinAllSpaces) | NSWindowCollectionBehaviorMoveToActiveSpace;
[nw orderFront:nil];
__block __typeof__(nw) weakSelf = nw;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.collectionBehavior = old;
});
}
}
void _glfwPlatformHideWindow(_GLFWwindow* window)
{
[window->ns.object orderOut:nil];
pid_t prev_app_pid = _glfw.ns.previous_front_most_application; _glfw.ns.previous_front_most_application = 0;
NSRunningApplication *app;
if (window->ns.layer_shell.is_active && prev_app_pid > 0 && (app = [NSRunningApplication runningApplicationWithProcessIdentifier:prev_app_pid])) {
unsigned num_visible = 0;
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
if (_glfwPlatformWindowVisible(w)) num_visible++;
}
if (!num_visible) {
// yieldActivationToApplication was introduced in macOS 14 (Sonoma)
SEL selector = NSSelectorFromString(@"yieldActivationToApplication:");
if ([NSApp respondsToSelector:selector]) {
[NSApp performSelector:selector withObject:app];
[app activateWithOptions:0];
} else {
#define NSApplicationActivateIgnoringOtherApps 2
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
#undef NSApplicationActivateIgnoringOtherApps
}
}
}
}
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
{
[NSApp requestUserAttention:NSInformationalRequest];
}
int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
{
NSBeep();
return true;
}
void _glfwPlatformFocusWindow(_GLFWwindow* window)
{
if (_glfwPlatformWindowIconified(window)) {
// miniaturized windows return false in canBecomeKeyWindow therefore
// unminiaturize first
[window->ns.object deminiaturize:nil];
}
if ([window->ns.object canBecomeKeyWindow]) {
// Make us the active application
[NSApp activateIgnoringOtherApps:YES];
[window->ns.object makeKeyAndOrderFront:nil];
}
}
void _glfwPlatformSetWindowMonitor(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,
int width, int height,
int refreshRate UNUSED)
{
if (window->monitor == monitor)
{
if (monitor)
{
if (monitor->window == window)
acquireMonitor(window);
}
else
{
const NSRect contentRect =
NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1), width, height);
const NSRect frameRect =
[window->ns.object frameRectForContentRect:contentRect
styleMask:getStyleMask(window)];
[window->ns.object setFrame:frameRect display:YES];
}
return;
}
if (window->monitor)
releaseMonitor(window);
_glfwInputWindowMonitor(window, monitor);
const NSUInteger styleMask = getStyleMask(window);
[window->ns.object setStyleMask:styleMask];
// HACK: Changing the style mask can cause the first responder to be cleared
[window->ns.object makeFirstResponder:window->ns.view];
if (window->monitor)
{
[window->ns.object setLevel:NSMainMenuWindowLevel + 1];
[window->ns.object setHasShadow:NO];
acquireMonitor(window);
}
else
{
NSRect contentRect = NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1),
width, height);
NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect
styleMask:styleMask];
[window->ns.object setFrame:frameRect display:YES];
if (window->numer != GLFW_DONT_CARE &&
window->denom != GLFW_DONT_CARE)
{
[window->ns.object setContentAspectRatio:NSMakeSize(window->numer,
window->denom)];
}
if (window->minwidth != GLFW_DONT_CARE &&
window->minheight != GLFW_DONT_CARE)
{
[window->ns.object setContentMinSize:NSMakeSize(window->minwidth,
window->minheight)];
}
if (window->maxwidth != GLFW_DONT_CARE &&
window->maxheight != GLFW_DONT_CARE)
{
[window->ns.object setContentMaxSize:NSMakeSize(window->maxwidth,
window->maxheight)];
}
if (window->floating)
[window->ns.object setLevel:NSFloatingWindowLevel];
else
[window->ns.object setLevel:NSNormalWindowLevel];
[window->ns.object setHasShadow:YES];
// HACK: Clearing NSWindowStyleMaskTitled resets and disables the window
// title property but the miniwindow title property is unaffected
[window->ns.object setTitle:[window->ns.object miniwindowTitle]];
}
}
int _glfwPlatformWindowFocused(_GLFWwindow* window)
{
return [window->ns.object isKeyWindow];
}
int _glfwPlatformWindowOccluded(_GLFWwindow* window)
{
return !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible);
}
int _glfwPlatformWindowIconified(_GLFWwindow* window)
{
return [window->ns.object isMiniaturized];
}
int _glfwPlatformWindowVisible(_GLFWwindow* window)
{
return [window->ns.object isVisible];
}
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
{
return [window->ns.object isZoomed];
}
int _glfwPlatformWindowHovered(_GLFWwindow* window)
{
const NSPoint point = [NSEvent mouseLocation];
if ([NSWindow windowNumberAtPoint:point belowWindowWithWindowNumber:0] !=
[window->ns.object windowNumber])
{
return false;
}
return NSMouseInRect(point,
[window->ns.object convertRectToScreen:[window->ns.view frame]], NO);
}
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
{
return ![window->ns.object isOpaque] && ![window->ns.view isOpaque];
}
void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED)
{
[window->ns.object setStyleMask:getStyleMask(window)];
[window->ns.object makeFirstResponder:window->ns.view];
}
void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled UNUSED)
{
[window->ns.object setStyleMask:getStyleMask(window)];
[window->ns.object makeFirstResponder:window->ns.view];
}
void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
{
if (enabled)
[window->ns.object setLevel:NSFloatingWindowLevel];
else
[window->ns.object setLevel:NSNormalWindowLevel];
}
void _glfwPlatformSetWindowMousePassthrough(_GLFWwindow* window, bool enabled)
{
[window->ns.object setIgnoresMouseEvents:enabled];
}
float _glfwPlatformGetWindowOpacity(_GLFWwindow* window)
{
return (float) [window->ns.object alphaValue];
}
void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity)
{
[window->ns.object setAlphaValue:opacity];
}
void _glfwPlatformSetRawMouseMotion(_GLFWwindow *window UNUSED, bool enabled UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNIMPLEMENTED,
"Cocoa: Raw mouse motion not yet implemented");
}
bool _glfwPlatformRawMouseMotionSupported(void)
{
return false;
}
void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos)
{
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
if (xpos)
*xpos = pos.x;
if (ypos)
*ypos = contentRect.size.height - pos.y;
}
void _glfwPlatformSetCursorPos(_GLFWwindow* window, double x, double y)
{
updateCursorImage(window);
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
window->ns.cursorWarpDeltaX += x - pos.x;
window->ns.cursorWarpDeltaY += y - contentRect.size.height + pos.y;
if (window->monitor)
{
CGDisplayMoveCursorToPoint(window->monitor->ns.displayID,
CGPointMake(x, y));
}
else
{
const NSRect localRect = NSMakeRect(x, contentRect.size.height - y - 1, 0, 0);
const NSRect globalRect = [window->ns.object convertRectToScreen:localRect];
const NSPoint globalPoint = globalRect.origin;
CGWarpMouseCursorPosition(CGPointMake(globalPoint.x,
_glfwTransformYNS(globalPoint.y)));
}
}
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED)
{
if (_glfwPlatformWindowFocused(window))
updateCursorMode(window);
}
const char* _glfwPlatformGetNativeKeyName(int keycode)
{
UInt32 deadKeyState = 0;
UniChar characters[8];
UniCharCount characterCount = 0;
if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDisplay,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&deadKeyState,
sizeof(characters) / sizeof(characters[0]),
&characterCount,
characters) != noErr)
{
return NULL;
}
if (!characterCount)
return NULL;
convert_utf16_to_utf8(characters, characterCount, _glfw.ns.keyName, sizeof(_glfw.ns.keyName));
return _glfw.ns.keyName;
}
int _glfwPlatformGetNativeKeyForKey(uint32_t glfw_key)
{
if (GLFW_FKEY_FIRST <= glfw_key && glfw_key <= GLFW_FKEY_LAST) { // {{{
switch(glfw_key) {
/* start functional to macu (auto generated by gen-key-constants.py do not edit) */
case GLFW_FKEY_ENTER: return NSCarriageReturnCharacter;
case GLFW_FKEY_TAB: return NSTabCharacter;
case GLFW_FKEY_BACKSPACE: return NSBackspaceCharacter;
case GLFW_FKEY_INSERT: return NSInsertFunctionKey;
case GLFW_FKEY_DELETE: return NSDeleteFunctionKey;
case GLFW_FKEY_LEFT: return NSLeftArrowFunctionKey;
case GLFW_FKEY_RIGHT: return NSRightArrowFunctionKey;
case GLFW_FKEY_UP: return NSUpArrowFunctionKey;
case GLFW_FKEY_DOWN: return NSDownArrowFunctionKey;
case GLFW_FKEY_PAGE_UP: return NSPageUpFunctionKey;
case GLFW_FKEY_PAGE_DOWN: return NSPageDownFunctionKey;
case GLFW_FKEY_HOME: return NSHomeFunctionKey;
case GLFW_FKEY_END: return NSEndFunctionKey;
case GLFW_FKEY_SCROLL_LOCK: return NSScrollLockFunctionKey;
case GLFW_FKEY_NUM_LOCK: return NSClearLineFunctionKey;
case GLFW_FKEY_PRINT_SCREEN: return NSPrintScreenFunctionKey;
case GLFW_FKEY_PAUSE: return NSPauseFunctionKey;
case GLFW_FKEY_MENU: return NSMenuFunctionKey;
case GLFW_FKEY_F1: return NSF1FunctionKey;
case GLFW_FKEY_F2: return NSF2FunctionKey;
case GLFW_FKEY_F3: return NSF3FunctionKey;
case GLFW_FKEY_F4: return NSF4FunctionKey;
case GLFW_FKEY_F5: return NSF5FunctionKey;
case GLFW_FKEY_F6: return NSF6FunctionKey;
case GLFW_FKEY_F7: return NSF7FunctionKey;
case GLFW_FKEY_F8: return NSF8FunctionKey;
case GLFW_FKEY_F9: return NSF9FunctionKey;
case GLFW_FKEY_F10: return NSF10FunctionKey;
case GLFW_FKEY_F11: return NSF11FunctionKey;
case GLFW_FKEY_F12: return NSF12FunctionKey;
case GLFW_FKEY_F13: return NSF13FunctionKey;
case GLFW_FKEY_F14: return NSF14FunctionKey;
case GLFW_FKEY_F15: return NSF15FunctionKey;
case GLFW_FKEY_F16: return NSF16FunctionKey;
case GLFW_FKEY_F17: return NSF17FunctionKey;
case GLFW_FKEY_F18: return NSF18FunctionKey;
case GLFW_FKEY_F19: return NSF19FunctionKey;
case GLFW_FKEY_F20: return NSF20FunctionKey;
case GLFW_FKEY_F21: return NSF21FunctionKey;
case GLFW_FKEY_F22: return NSF22FunctionKey;
case GLFW_FKEY_F23: return NSF23FunctionKey;
case GLFW_FKEY_F24: return NSF24FunctionKey;
case GLFW_FKEY_F25: return NSF25FunctionKey;
case GLFW_FKEY_F26: return NSF26FunctionKey;
case GLFW_FKEY_F27: return NSF27FunctionKey;
case GLFW_FKEY_F28: return NSF28FunctionKey;
case GLFW_FKEY_F29: return NSF29FunctionKey;
case GLFW_FKEY_F30: return NSF30FunctionKey;
case GLFW_FKEY_F31: return NSF31FunctionKey;
case GLFW_FKEY_F32: return NSF32FunctionKey;
case GLFW_FKEY_F33: return NSF33FunctionKey;
case GLFW_FKEY_F34: return NSF34FunctionKey;
case GLFW_FKEY_F35: return NSF35FunctionKey;
case GLFW_FKEY_KP_ENTER: return NSEnterCharacter;
/* end functional to macu */
default:
return 0;
}
} // }}}
if (!is_pua_char(glfw_key)) return glfw_key;
return 0;
}
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot, int count)
{
NSImage* native;
NSBitmapImageRep* rep;
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
if (native == nil)
return false;
for (int i = 0; i < count; i++) {
const GLFWimage *src = image + i;
rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:src->width
pixelsHigh:src->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
bytesPerRow:src->width * 4
bitsPerPixel:32];
if (rep == nil) {
[native release];
return false;
}
memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
[native addRepresentation:rep];
[rep release];
}
cursor->ns.object = [[NSCursor alloc] initWithImage:native
hotSpot:NSMakePoint(xhot, yhot)];
[native release];
if (cursor->ns.object == nil)
return false;
return true;
}
static NSCursor*
load_hidden_system_cursor(NSString *name, SEL fallback) {
// this implementation comes from SDL_cocoamouse.m
NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:name];
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]];
/* we can't do animation atm. :/ */
const int frames = (int)[[info valueForKey:@"frames"] integerValue];
NSCursor *cursor;
NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]];
if ((image == nil) || (image.isValid == NO)) {
return [NSCursor performSelector:fallback];
}
if (frames > 1) {
const NSCompositingOperation operation = NSCompositingOperationCopy;
const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames));
NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size];
if (cropped == nil) {
return [NSCursor performSelector:fallback];
}
[cropped lockFocus];
{
const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height);
[image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1];
}
[cropped unlockFocus];
image = cropped;
}
cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])];
return cursor;
}
int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape) {
#define C(name, val) case name: cursor->ns.object = [NSCursor val]; break;
#define U(name, val) case name: cursor->ns.object = [NSCursor performSelector:@selector(val)]; break;
#define S(name, val, fallback) case name: cursor->ns.object = load_hidden_system_cursor(@#val, @selector(val)); break;
switch(shape) {
/* start glfw to cocoa (auto generated by gen-key-constants.py do not edit) */
C(GLFW_DEFAULT_CURSOR, arrowCursor);
C(GLFW_TEXT_CURSOR, IBeamCursor);
C(GLFW_POINTER_CURSOR, pointingHandCursor);
S(GLFW_HELP_CURSOR, help, arrowCursor);
S(GLFW_WAIT_CURSOR, busybutclickable, arrowCursor);
S(GLFW_PROGRESS_CURSOR, busybutclickable, arrowCursor);
C(GLFW_CROSSHAIR_CURSOR, crosshairCursor);
S(GLFW_CELL_CURSOR, cell, crosshairCursor);
C(GLFW_VERTICAL_TEXT_CURSOR, IBeamCursorForVerticalLayout);
S(GLFW_MOVE_CURSOR, move, openHandCursor);
C(GLFW_E_RESIZE_CURSOR, resizeRightCursor);
S(GLFW_NE_RESIZE_CURSOR, resizenortheast, _windowResizeNorthEastSouthWestCursor);
S(GLFW_NW_RESIZE_CURSOR, resizenorthwest, _windowResizeNorthWestSouthEastCursor);
C(GLFW_N_RESIZE_CURSOR, resizeUpCursor);
S(GLFW_SE_RESIZE_CURSOR, resizesoutheast, _windowResizeNorthWestSouthEastCursor);
S(GLFW_SW_RESIZE_CURSOR, resizesouthwest, _windowResizeNorthEastSouthWestCursor);
C(GLFW_S_RESIZE_CURSOR, resizeDownCursor);
C(GLFW_W_RESIZE_CURSOR, resizeLeftCursor);
C(GLFW_EW_RESIZE_CURSOR, resizeLeftRightCursor);
C(GLFW_NS_RESIZE_CURSOR, resizeUpDownCursor);
U(GLFW_NESW_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
U(GLFW_NWSE_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
S(GLFW_ZOOM_IN_CURSOR, zoomin, arrowCursor);
S(GLFW_ZOOM_OUT_CURSOR, zoomout, arrowCursor);
C(GLFW_ALIAS_CURSOR, dragLinkCursor);
C(GLFW_COPY_CURSOR, dragCopyCursor);
C(GLFW_NOT_ALLOWED_CURSOR, operationNotAllowedCursor);
C(GLFW_NO_DROP_CURSOR, operationNotAllowedCursor);
C(GLFW_GRAB_CURSOR, openHandCursor);
C(GLFW_GRABBING_CURSOR, closedHandCursor);
/* end glfw to cocoa */
case GLFW_INVALID_CURSOR:
return false;
}
#undef C
#undef U
#undef S
if (!cursor->ns.object)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to retrieve standard cursor");
return false;
}
[cursor->ns.object retain];
return true;
}
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor)
{
if (cursor->ns.object)
[(NSCursor*) cursor->ns.object release];
}
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
{
if (cursorInContentArea(window))
updateCursorImage(window);
}
bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) {
NSWindow *window = w->ns.object;
bool traditional = !(flags & 1);
if (traditional) { if(@available(macOS 10.15.7, *)) return w->ns.in_traditional_fullscreen; }
NSWindowStyleMask sm = [window styleMask];
return sm & NSWindowStyleMaskFullScreen;
}
static void
make_window_fullscreen_after_show(unsigned long long timer_id, void* data) {
(void)timer_id;
unsigned long long window_id = (uintptr_t)data;
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
if (w->id == window_id) {
NSWindow *window = w->ns.object;
[window toggleFullScreen: nil];
update_titlebar_button_visibility_after_fullscreen_transition(w, false, true);
break;
}
}
}
static void
_glfwUpdateNotchCover(_GLFWwindow* w) {
NSWindow *window = w->ns.object;
if (w->ns.notch_cover_window) {
[window removeChildWindow:w->ns.notch_cover_window];
[w->ns.notch_cover_window close];
[w->ns.notch_cover_window release];
w->ns.notch_cover_window = nil;
}
if (!w->ns.in_traditional_fullscreen) return;
if (@available(macOS 12.0, *)) {
CGFloat insetTop = window.screen.safeAreaInsets.top;
if (insetTop <= 0) return;
NSRect sf = [window.screen frame];
NSWindow *bg_window = [[NSWindow alloc] initWithContentRect:sf styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO];
[bg_window setBackgroundColor:[NSColor clearColor]];
[bg_window setHasShadow:NO];
[bg_window setOpaque:NO];
[bg_window setIgnoresMouseEvents:YES];
[bg_window setReleasedWhenClosed:NO];
[bg_window setColorSpace:[window colorSpace]];
// Add a colored subview only in the notch strip area
NSView *notchView = [[NSView alloc] initWithFrame:NSMakeRect(0, sf.size.height - insetTop, sf.size.width, insetTop)];
notchView.wantsLayer = YES;
unsigned int c = w->ns.notch_cover_color;
float a = w->ns.notch_cover_opacity;
notchView.layer.backgroundColor = [NSColor colorWithSRGBRed:((c >> 16) & 0xFF) / 255.0
green:((c >> 8) & 0xFF) / 255.0
blue:(c & 0xFF) / 255.0
alpha:a].CGColor;
[bg_window.contentView addSubview:notchView];
// must be above otherwise shadow of main window is rendered over bg_window
[window addChildWindow:bg_window ordered:NSWindowAbove];
w->ns.notch_cover_window = bg_window;
[notchView release];
}
}
bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
NSWindow *window = w->ns.object;
bool made_fullscreen = true;
bool traditional = !(flags & 1);
bool ignore_safe_area_insets = flags & 2;
NSWindowStyleMask sm = [window styleMask];
if (traditional) {
if (@available(macOS 10.15.7, *)) {
// As of Big Turd NSWindowStyleMaskFullScreen is no longer usable
// Also no longer compatible after a minor release of macOS 10.15.7
if (!w->ns.in_traditional_fullscreen) {
// Apple throws NSGenericException if setStyleMask: clears
// NSWindowStyleMaskFullScreen outside a transition (see #9572).
// Split View sets this flag via the system, so fall back to
// Cocoa fullscreen toggle instead of the traditional path.
if (sm & NSWindowStyleMaskFullScreen) {
[window toggleFullScreen:nil];
return false;
}
w->ns.pre_full_screen_style_mask = sm;
w->ns.pre_traditional_fullscreen_frame = [window frame];
[window setStyleMask: NSWindowStyleMaskBorderless];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
NSRect screenFrame = [window.screen frame];
if (!ignore_safe_area_insets) {
if (@available(macOS 12.0, *)) {
screenFrame.size.height -= window.screen.safeAreaInsets.top;
}
}
[window setFrame:screenFrame display:YES];
w->ns.in_traditional_fullscreen = true;
if (!ignore_safe_area_insets) _glfwUpdateNotchCover(w);
} else {
made_fullscreen = false;
if (sm & NSWindowStyleMaskFullScreen) {
// Split View added NSWindowStyleMaskFullScreen on top of our
// traditional fullscreen. We can't clear that flag directly
// (NSGenericException), so trigger a Cocoa exit and defer the
// traditional fullscreen cleanup to windowDidExitFullScreen:
// which fires after macOS finishes its async transition (#9572).
// Return true to prevent the caller from setting the window
// frame during the Cocoa exit animation.
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
[window toggleFullScreen:nil];
return true;
} else {
[window setStyleMask: w->ns.pre_full_screen_style_mask];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
w->ns.in_traditional_fullscreen = false;
_glfwUpdateNotchCover(w);
}
}
} else {
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (!(in_fullscreen)) {
sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen;
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
} else {
made_fullscreen = false;
sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen);
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
}
[window setStyleMask: sm];
}
// Changing the style mask causes the first responder to be cleared
[window makeFirstResponder:w->ns.view];
// If the dock and menubar are hidden going from maximized to fullscreen doesn't change the window size
// and macOS forgets to trigger windowDidResize, so call it ourselves
NSNotification *notification = [NSNotification notificationWithName:NSWindowDidResizeNotification object:window];
[w->ns.delegate performSelector:@selector(windowDidResize:) withObject:notification afterDelay:0];
} else {
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (!in_fullscreen && !_glfwPlatformWindowVisible(w)) {
// Bug in Apple's fullscreen implementation causes fullscreen to
// not work before window is shown (at creation) if another window
// is already fullscreen. Le sigh. https://github.com/kovidgoyal/kitty/issues/7448
_glfwPlatformAddTimer(0, false, make_window_fullscreen_after_show, (void*)(uintptr_t)(w->id), NULL);
return made_fullscreen;
}
if (in_fullscreen) made_fullscreen = false;
[window toggleFullScreen: nil];
}
update_titlebar_button_visibility_after_fullscreen_transition(w, traditional, made_fullscreen);
return made_fullscreen;
}
// Clipboard {{{
static void
list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) {
#define w(x) { if (ok) ok = write_data(object, x, strlen(x)); }
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options];
BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil];
/* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */
bool ok = true;
if (has_strings) w("text/plain");
if (has_file_urls) w("text/local-path-list");
for (NSPasteboardItem * item in pasteboard.pasteboardItems) {
for (NSPasteboardType type in item.types) {
/* NSLog(@"%@", type); */
const char *mime = uti_to_mime(type);
if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) {
/* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */
w(mime);
}
}
}
#undef w
}
static void
get_text_plain(GLFWclipboardwritedatafun write_data, void *object) {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
bool found = false;
if (objs) {
const NSUInteger count = [objs count];
if (count) {
NSMutableData *path_list = [NSMutableData dataWithCapacity:4096]; // auto-released
NSMutableData *text_list = [NSMutableData dataWithCapacity:4096]; // auto-released
for (NSUInteger i = 0; i < count; i++) {
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if (url.fileURL && url.fileSystemRepresentation) {
if ([path_list length] > 0) [path_list appendBytes:"\n" length:1];
[path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)];
}
} else if ([obj isKindOfClass:[NSString class]]) {
if ([text_list length] > 0) [text_list appendBytes:"\n" length:1];
[text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
}
}
const NSMutableData *text = nil;
if (path_list.length > 0) text = path_list;
else if (text_list.length > 0) text = text_list;
if (text) {
found = true;
write_data(object, text.mutableBytes, text.length);
}
}
}
if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard");
}
void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
if (clipboard_type != GLFW_CLIPBOARD) return;
if (mime_type == NULL) {
list_clipboard_mimetypes(write_data, object);
return;
}
if (strcmp(mime_type, "text/plain") == 0) {
get_text_plain(write_data, object);
return;
}
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
/* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */
NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]];
/* NSLog(@"available type: %@", t); */
if (t != nil) {
NSData *data = [pasteboard dataForType:t]; // auto-released
/* NSLog(@"data: %@", data); */
if (data != nil && data.length > 0) {
write_data(object, data.bytes, data.length);
}
}
}
static NSMutableData*
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) {
NSMutableData *ans = [NSMutableData dataWithCapacity:8192];
if (ans == nil) return nil;
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
void *iter = chunk.iter;
if (!iter) return ans;
while (true) {
chunk = cd->get_data(mime, iter, cd->ctype);
if (!chunk.sz) break;
[ans appendBytes:chunk.data length:chunk.sz];
if (chunk.free) chunk.free((void*)chunk.free_data);
}
cd->get_data(NULL, iter, cd->ctype);
return ans;
}
void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
if (t != GLFW_CLIPBOARD) return;
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSMutableArray<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 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;
NSWindow *keyWindow = [NSApp keyWindow];
NSUInteger index = [filteredWindows indexOfObject:keyWindow];
NSUInteger nextIndex = 0;
if (index != NSNotFound) {
if (backwards) {
nextIndex = (index == 0) ? [filteredWindows count] - 1 : index - 1;
} else nextIndex = (index + 1) % filteredWindows.count;
}
NSWindow *nextWindow = filteredWindows[nextIndex];
[nextWindow makeKeyAndOrderFront:nil];
}
GLFWAPI void
glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count) {
_GLFWwindow *w = (_GLFWwindow*)window;
NSArray *currentTypes = [w->ns.view registeredDraggedTypes];
NSMutableArray *updatedTypes = [NSMutableArray arrayWithArray:currentTypes];
for (size_t i = 0; i < count; i++) {
NSString *uti = mime_to_uti(mimes[i]);
if (![updatedTypes containsObject:uti]) [updatedTypes addObject:uti];
}
[w->ns.view registerForDraggedTypes:updatedTypes];
}
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
// Transforms a y-coordinate between the CG display and NS screen spaces
//
float _glfwTransformYNS(float y)
{
return CGDisplayBounds(CGMainDisplayID()).size.height - y - 1;
}
void _glfwCocoaPostEmptyEvent(void) {
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
// Drag source implementation {{{
@implementation GLFWDraggingSource
- (NSDragOperation)draggingSession:(NSDraggingSession*)session
sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
(void)session; (void)context;
// Return the operation based on the stored drag operations bitfield
NSDragOperation ops = NSDragOperationCopy;
int q = _glfw.drag.operations;
if (q & GLFW_DRAG_OPERATION_COPY) ops |= NSDragOperationCopy;
if (q & GLFW_DRAG_OPERATION_MOVE) ops |= NSDragOperationMove;
if (q & GLFW_DRAG_OPERATION_GENERIC) ops |= NSDragOperationGeneric;
return ops;
}
- (void)draggingSession:(NSDraggingSession *)session
willBeginAtPoint:(NSPoint)screenPoint
{
(void)session;
start_point = screenPoint;
}
- (void)draggingSession:(NSDraggingSession *)session
movedToPoint:(NSPoint)screenPoint
{
(void)session;
current_point = screenPoint;
}
- (void)draggingSession:(NSDraggingSession *)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation
{
(void)session;
_glfwPlatformFreeDragSourceData();
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (window) {
GLFWDragEvent ev = {0};
switch(operation) {
case NSDragOperationCopy: case NSDragOperationLink: ev.action = GLFW_DRAG_OPERATION_COPY; break;
case NSDragOperationMove: case NSDragOperationDelete: ev.action = GLFW_DRAG_OPERATION_MOVE; break;
case NSDragOperationNone: break;
default: ev.action = GLFW_DRAG_OPERATION_GENERIC; break;
}
switch (operation) {
case NSDragOperationDelete: ev.type = GLFW_DRAG_CANCELLED; break;
case NSDragOperationNone: {
NSEvent *currentEvent = [NSApp currentEvent];
if (currentEvent && currentEvent.type == NSEventTypeKeyDown && currentEvent.keyCode == 53) {
ev.type = GLFW_DRAG_CANCELLED;
} else ev.type = GLFW_DRAG_DROPPED;
} break;
default:
ev.type = GLFW_DRAG_DROPPED; break;
}
_glfwInputDragSourceRequest(window, &ev);
if (operation == NSDragOperationNone) _glfwFreeDragSourceData();
}
}
@end
static NSMutableArray<GLFWFilePromiseProviderDelegate*> *file_promise_providers = nil;
static int
set_image_for_dragging_item(NSDraggingItem *draggingItem, const GLFWimage *thumbnail, NSWindow *window) {
CGFloat scaleFactor = 1.0;
[draggingItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
_glfw.ns.drag_image = nil;
if (thumbnail && thumbnail->pixels && window) {
scaleFactor = [window backingScaleFactor];
if (scaleFactor == 0) scaleFactor = [NSScreen mainScreen].backingScaleFactor;
unsigned height = thumbnail->height + 20; // add empty padding at bottom
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:thumbnail->width
pixelsHigh:height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:thumbnail->width * 4
bitsPerPixel:32];
if (!imageRep) return ENOMEM;
memcpy([imageRep bitmapData], thumbnail->pixels, thumbnail->width * thumbnail->height * 4);
NSSize pointSize = NSMakeSize(thumbnail->width / scaleFactor, height / scaleFactor);
[imageRep setSize:pointSize];
_glfw.ns.drag_image = [[NSImage alloc] initWithSize:pointSize];
if (!_glfw.ns.drag_image) { [imageRep release]; return ENOMEM; }
[_glfw.ns.drag_image addRepresentation:imageRep];
[imageRep release];
if (!_glfw.ns.drag_session) {
_GLFWwindow *glfw_window = _glfwWindowForId(_glfw.drag.window_id);
if (glfw_window && glfw_window->ns.view) {
NSRect contentRect = [glfw_window->ns.view frame];
NSPoint cursor = NSMakePoint(glfw_window->virtualCursorPosX,
contentRect.size.height - glfw_window->virtualCursorPosY);
[draggingItem setDraggingFrame:NSMakeRect(cursor.x, cursor.y, pointSize.width, pointSize.height) contents:nil];
} else {
[draggingItem setDraggingFrame:NSMakeRect(0, 0, pointSize.width, pointSize.height) contents:nil];
}
}
[draggingItem setImageComponentsProvider:^NSArray<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;
}
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++) {
NSString* utiString = mime_to_uti(_glfw.drag.items[i].mime_type);
id w;
if (_glfw.drag.items[i].optional_data) {
NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease];
NSData *data = [NSData dataWithBytes:_glfw.drag.items[i].optional_data length:_glfw.drag.items[i].data_size];
[pbItem setData:data forType:utiString];
w = pbItem;
} else {
// Create file promise provider with our delegate
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
initWithWindow:window mimeType:_glfw.drag.items[i].mime_type instanceId:_glfw.drag.instance_id] autorelease];
NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
initWithFileType:utiString delegate:delegate] autorelease];
// Store the delegate in the provider's user info so it's retained
provider.userInfo = delegate;
w = provider;
}
NSDraggingItem* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:w] autorelease];
if (i == 0 && thumbnail && thumbnail->pixels) {
int err = set_image_for_dragging_item(dragItem, thumbnail, window->ns.object);
if (err) return err;
} else {
[dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
}
[dragItems addObject:dragItem];
}
NSDraggingSession *s = [v beginDraggingSessionWithItems:dragItems event:event source:[v draggingSource]];
_glfw.ns.drag_session = [s retain];
_glfw.ns.drag_view = [v retain];
return 0;
}}
@implementation GLFWFilePromiseProviderDelegate
- (void)end_transfer_with_error:(NSError*)err {
if (err && file_url) {
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:file_url error:&error];
}
if (file_handle) [file_handle release];
file_handle = nil;
if (completion_handler) {
completion_handler(err);
Block_release(completion_handler);
completion_handler = nil;
}
[file_promise_providers removeObject:self];
}
- (void)end_transfer:(int)errorCode {
[self end_transfer_with_error:errorCode ? [NSError errorWithDomain:NSPOSIXErrorDomain code:errorCode userInfo:nil] : nil];
}
- (bool)is_mimetype:(const char*)q { return strcmp(q, mimeType) == 0; }
- (void)request_drag_data {
if (instanceId != _glfw.drag.instance_id) { [self end_transfer:EINVAL]; return; }
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (!window) { [self end_transfer:EINVAL]; return; }
bool keep_going = true;
while (keep_going) {
GLFWDragEvent ev = {.type=GLFW_DRAG_DATA_REQUEST, .mime_type=mimeType};
_glfwInputDragSourceRequest(window, &ev);
if (ev.err_num) {
keep_going = false;
if (ev.err_num != EAGAIN) [self end_transfer:ev.err_num];
} else {
if (ev.data_sz) {
NSData* nsData = [NSData dataWithBytes:ev.data length:ev.data_sz];
NSError* error = nil;
if (![file_handle writeData:nsData error:&error]) {
keep_going = false;
[self end_transfer_with_error:error];
}
} else {
keep_going = false;
[self end_transfer_with_error:nil];
}
_glfwInputDragSourceRequest(window, &ev);
}
}
}
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid) instance_id {
self = [super init];
if (self) {
windowId = initWindow ? initWindow->id : 0;
mimeType = _glfw_strdup(mime);
instanceId = instance_id;
if (file_promise_providers == nil) file_promise_providers = [NSMutableArray array];
[file_promise_providers addObject:self];
}
return self;
}
- (void)dealloc {
free(mimeType); mimeType = NULL;
if (file_url) [file_url release];
file_url = nil;
[self end_transfer:EINVAL];
[super dealloc];
}
- (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType {
(void)filePromiseProvider; (void)fileType;
// Generate a unique filename based on the MIME type
NSString* extension = @"data";
if (mimeType) {
UTType *type = [UTType typeWithMIMEType:@(mimeType)];
extension = type.preferredFilenameExtension;
}
return [NSString stringWithFormat:@"kitty-drag-source-%@.%@", [[NSUUID UUID] UUIDString], extension];
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider
writePromiseToURL:(NSURL*)url
completionHandler:(void (^)(NSError*))completionHandler
{
(void)filePromiseProvider;
_GLFWwindow* window = _glfwWindowForId(windowId);
if (!window || instanceId != _glfw.drag.instance_id) {
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]);
return;
}
// Create the file
NSError* error = nil;
if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) {
error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil];
completionHandler(error);
return;
}
NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error];
if (!fileHandle) {
completionHandler(error);
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:url error:&error];
return;
}
file_handle = fileHandle; completion_handler = completionHandler;
file_url = [url retain];
[self request_drag_data];
}
@end
void
_glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) {@autoreleasepool{
if (!_glfw.drag.window_id) return;
_GLFWwindow* drag_win = _glfwWindowForId(_glfw.drag.window_id);
if (drag_win) {
GLFWDragEvent ev = {.type = GLFW_DRAG_CANCELLED};
_glfwInputDragSourceRequest(drag_win, &ev);
}
_glfwFreeDragSourceData();
}}
void
_glfwPlatformFreeDragSourceData(void) {
if (_glfw.ns.drag_session) [_glfw.ns.drag_session release];
_glfw.ns.drag_session = nil;
if (_glfw.ns.drag_view) [_glfw.ns.drag_view release];
_glfw.ns.drag_view = nil;
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
_glfw.ns.drag_image = nil;
}
int
_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) {@autoreleasepool{
if (!_glfw.ns.drag_session || !_glfw.ns.drag_view) return 0;
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
[((NSDraggingSession*)_glfw.ns.drag_session)
enumerateDraggingItemsWithOptions:0
forView:(NSView*)_glfw.ns.drag_view
classes:@[[NSPasteboardItem class]]
searchOptions:@{}
usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) {
if (idx == 0) {
set_image_for_dragging_item(draggingItem, thumbnail, window->ns.object);
*stop = YES;
}
}];
return 0;
}}
int
_glfwPlatformDragDataReady(const char *mime_type) {
if (!file_promise_providers) return 0;
for (GLFWFilePromiseProviderDelegate *d in file_promise_providers) {
if ([d is_mimetype:mime_type]) [d request_drag_data];
}
return 0;
}