kitty/glfw/cocoa_window.m

4612 lines
180 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.mimes_count) return NSDragOperationNone;
const NSRect contentRect = [window->ns.view frame];
const NSPoint pos = [sender draggingLocation];
double xpos = pos.x;
double ypos = contentRect.size.height - pos.y;
bool from_self = ([sender draggingSource] != nil);
_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;
[filteredWindows sortUsingComparator:^NSComparisonResult(NSWindow *a, NSWindow *b) {
return [@(a.windowNumber) compare:@(b.windowNumber)];
}];
NSWindow *keyWindow = [NSApp keyWindow];
NSUInteger index = [filteredWindows indexOfObject:keyWindow];
NSUInteger nextIndex = 0;
if (index != NSNotFound) {
if (backwards) {
nextIndex = (index == 0) ? [filteredWindows count] - 1 : index - 1;
} else nextIndex = (index + 1) % filteredWindows.count;
}
NSWindow *nextWindow = filteredWindows[nextIndex];
[nextWindow makeKeyAndOrderFront:nil];
}
GLFWAPI void
glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count) {
_GLFWwindow *w = (_GLFWwindow*)window;
NSArray *currentTypes = [w->ns.view registeredDraggedTypes];
NSMutableArray *updatedTypes = [NSMutableArray arrayWithArray:currentTypes];
for (size_t i = 0; i < count; i++) {
NSString *uti = mime_to_uti(mimes[i]);
if (![updatedTypes containsObject:uti]) [updatedTypes addObject:uti];
}
[w->ns.view registerForDraggedTypes:updatedTypes];
}
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
// Transforms a y-coordinate between the CG display and NS screen spaces
//
float _glfwTransformYNS(float y)
{
return CGDisplayBounds(CGMainDisplayID()).size.height - y - 1;
}
void _glfwCocoaPostEmptyEvent(void) {
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
// Drag source implementation {{{
// Forward declarations for drag-finish helpers used in GLFWDraggingSource methods
static void fire_drag_finished(void);
static void schedule_drag_finish_timer(void);
static GLFWid drag_finish_window_id = 0;
static GLFWDragOperationType drag_finish_action = 0;
static NSTimer *drag_finish_timer = nil;
static NSMutableArray<GLFWFilePromiseProviderDelegate*> *file_promise_providers = nil;
#define DRAG_FINISH_TIMEOUT_SECONDS 2.0
@implementation GLFWDraggingSource
- (NSDragOperation)draggingSession:(NSDraggingSession*)session
sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
(void)session; (void)context;
// Return the operation based on the stored drag operations bitfield
NSDragOperation ops = NSDragOperationCopy;
int q = _glfw.drag.operations;
if (q & GLFW_DRAG_OPERATION_COPY) ops |= NSDragOperationCopy;
if (q & GLFW_DRAG_OPERATION_MOVE) ops |= NSDragOperationMove;
if (q & GLFW_DRAG_OPERATION_GENERIC) ops |= NSDragOperationGeneric;
return ops;
}
- (void)draggingSession:(NSDraggingSession *)session
willBeginAtPoint:(NSPoint)screenPoint
{
(void)session;
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;
ev.drop_maybe_a_cancel = true;
}
} break;
default:
ev.type = GLFW_DRAG_DROPPED; break;
}
_glfwInputDragSourceRequest(window, &ev);
if (ev.type == GLFW_DRAG_DROPPED) {
drag_finish_window_id = _glfw.drag.window_id;
drag_finish_action = ev.action;
if (operation == NSDragOperationNone || !file_promise_providers || [file_promise_providers count] == 0) {
fire_drag_finished();
} else {
schedule_drag_finish_timer();
}
} else {
if (operation == NSDragOperationNone) _glfwFreeDragSourceData();
}
}
}
@end
static void
fire_drag_finished(void) {
if (drag_finish_timer) {
[drag_finish_timer invalidate];
drag_finish_timer = nil;
}
if (!drag_finish_window_id) return;
GLFWid wid = drag_finish_window_id;
GLFWDragOperationType action = drag_finish_action;
drag_finish_window_id = 0;
drag_finish_action = 0;
_GLFWwindow *window = _glfwWindowForId(wid);
if (window) {
GLFWDragEvent ev = {.type=GLFW_DRAG_FINSHED, .action=action};
_glfwInputDragSourceRequest(window, &ev);
}
_glfwFreeDragSourceData();
}
static void
schedule_drag_finish_timer(void) {
if (drag_finish_timer) {
[drag_finish_timer invalidate];
drag_finish_timer = nil;
}
drag_finish_timer = [NSTimer scheduledTimerWithTimeInterval:DRAG_FINISH_TIMEOUT_SECONDS repeats:NO block:^(NSTimer *t UNUSED) {
drag_finish_timer = nil;
fire_drag_finished();
}];
}
static int
set_image_for_dragging_item(NSDraggingItem *draggingItem, const GLFWimage *thumbnail, NSWindow *window) {
CGFloat scaleFactor = 1.0;
[draggingItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
_glfw.ns.drag_image = nil;
if (thumbnail && thumbnail->pixels && window) {
scaleFactor = [window backingScaleFactor];
if (scaleFactor == 0) scaleFactor = [NSScreen mainScreen].backingScaleFactor;
unsigned height = thumbnail->height + 20; // add empty padding at bottom
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:thumbnail->width
pixelsHigh:height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:thumbnail->width * 4
bitsPerPixel:32];
if (!imageRep) return ENOMEM;
memcpy([imageRep bitmapData], thumbnail->pixels, thumbnail->width * thumbnail->height * 4);
NSSize pointSize = NSMakeSize(thumbnail->width / scaleFactor, height / scaleFactor);
[imageRep setSize:pointSize];
_glfw.ns.drag_image = [[NSImage alloc] initWithSize:pointSize];
if (!_glfw.ns.drag_image) { [imageRep release]; return ENOMEM; }
[_glfw.ns.drag_image addRepresentation:imageRep];
[imageRep release];
if (!_glfw.ns.drag_session) {
_GLFWwindow *glfw_window = _glfwWindowForId(_glfw.drag.window_id);
if (glfw_window && glfw_window->ns.view) {
NSRect contentRect = [glfw_window->ns.view frame];
NSPoint cursor = NSMakePoint(glfw_window->virtualCursorPosX,
contentRect.size.height - glfw_window->virtualCursorPosY);
[draggingItem setDraggingFrame:NSMakeRect(cursor.x, cursor.y, pointSize.width, pointSize.height) contents:nil];
} else {
[draggingItem setDraggingFrame:NSMakeRect(0, 0, pointSize.width, pointSize.height) contents:nil];
}
}
[draggingItem setImageComponentsProvider:^NSArray<NSDraggingImageComponent *> * _Nonnull{
NSDraggingImageComponent *icon = [NSDraggingImageComponent draggingImageComponentWithKey:NSDraggingImageComponentIconKey];
NSImage *image = _glfw.ns.drag_image;
icon.contents = image;
icon.frame = NSMakeRect(0, 0, pointSize.width, pointSize.height);
return @[icon];
}];
}
return 0;
}
static int
add_drag_item(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, NSDraggingItem* dragItem, const GLFWimage *thumbnail) {
if (dragItems.count == 0 && thumbnail && thumbnail->pixels) {
int err = set_image_for_dragging_item(dragItem, thumbnail, window->ns.object);
if (err) return err;
} else {
[dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
}
[dragItems addObject:dragItem];
return 0;
}
static int
add_uri_list_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, const char *uri_list, size_t uri_list_sz, bool use_promises, const GLFWimage *thumbnail) {
NSString *input = [[[NSString alloc] initWithBytes:uri_list length:uri_list_sz encoding:NSUTF8StringEncoding] autorelease];
NSArray *lines = [input componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
id dragItem; int count = 0; char buf[256];
for (NSString *rawLine in lines) {
NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (line.length == 0 || [line hasPrefix:@"#"]) continue;
NSURL *url = [NSURL URLWithString:line];
if (use_promises && [[url scheme] caseInsensitiveCompare:@"file"] == NSOrderedSame) {
NSString *extension = [url pathExtension];
UTType *type = [UTType typeWithFilenameExtension:extension];
if (!type) type = UTTypeItem;
snprintf(buf, sizeof(buf), "text/uri-list:%d", count);
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
initWithWindow:window mimeType:buf instanceId:_glfw.drag.instance_id] autorelease];
NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
initWithFileType:type.identifier delegate:delegate] autorelease];
// Store the delegate in the provider's user info so it's retained
provider.userInfo = delegate;
dragItem = provider;
} else dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:url] autorelease];
int err = add_drag_item(window, dragItems, dragItem, thumbnail);
if (err) return err;
count++;
}
return 0;
}
static int
add_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, GLFWDragSourceItem *mime_item, const GLFWimage *thumbnail) {
// URI list items get added directly to the clipboard for use by native apps since macOS does not support
// the text/uri-list MIME type, however this type remains as a private
// kitty type to enable kitty to kitty DnD for remote clients.
bool is_uri_list = false;
if (strcmp(mime_item->mime_type, "text/uri-list") == 0 && mime_item->optional_data && mime_item->data_size) {
is_uri_list = true;
int err = add_uri_list_drag_items(window, dragItems, mime_item->optional_data, mime_item->data_size, mime_item->is_remote_client, thumbnail);
if (err != 0) return err;
}
NSString* utiString = mime_to_uti(mime_item->mime_type);
id w;
// remote client URI list must be set a file promise so that the kitty drag
// source code receives a request for it and can then fetch the remote
// files.
if (mime_item->optional_data && !(is_uri_list && mime_item->is_remote_client)) {
NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease];
NSData *data = [NSData dataWithBytes:mime_item->optional_data length:mime_item->data_size];
[pbItem setData:data forType:utiString];
w = pbItem;
} else {
// Create file promise provider with our delegate
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
initWithWindow:window mimeType:mime_item->mime_type 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];
return add_drag_item(window, dragItems, dragItem, thumbnail);
}
int
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {@autoreleasepool{
if (file_promise_providers) {
for (NSInteger i = [file_promise_providers count] - 1; i >= 0; i--) {
GLFWFilePromiseProviderDelegate* d = file_promise_providers[i];
[d end_transfer:EINVAL];
}
}
// Obtain the event and view early so we can position the drag image relative to the cursor
NSEvent* event = [NSApp currentEvent];
if (!event || ([event type] != NSEventTypeLeftMouseDown &&
[event type] != NSEventTypeLeftMouseDragged)) {
// Create a synthetic left mouse down event using stored cursor position
// Convert window coordinates to screen coordinates
NSRect contentRect = [window->ns.view frame];
NSPoint windowPos = NSMakePoint(window->virtualCursorPosX,
contentRect.size.height - window->virtualCursorPosY);
event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:windowPos
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[window->ns.object windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
}
if (!event) return EIO;
GLFWContentView *v = window->ns.view;
NSMutableArray<NSDraggingItem*>* dragItems = [[[NSMutableArray alloc] init] autorelease];
for (size_t i = 0; i < _glfw.drag.item_count; i++) {
int err = add_drag_items(window, dragItems, _glfw.drag.items + i, thumbnail);
if (err) return err;
}
NSDraggingSession *s = [v beginDraggingSessionWithItems:dragItems event:event source:[v draggingSource]];
_glfw.ns.drag_session = [s retain];
_glfw.ns.drag_view = [v retain];
return 0;
}}
@implementation GLFWFilePromiseProviderDelegate
- (void)end_transfer_with_error:(NSError*)err {
if (err && file_url) {
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:file_url error:&error];
}
if (file_handle) [file_handle release];
file_handle = nil;
if (completion_handler) {
completion_handler(err);
Block_release(completion_handler);
completion_handler = nil;
}
[file_promise_providers removeObject:self];
if (drag_finish_window_id) {
if (!file_promise_providers || [file_promise_providers count] == 0) {
fire_drag_finished();
} else {
schedule_drag_finish_timer();
}
}
}
- (void)end_transfer:(int)errorCode {
[self end_transfer_with_error:errorCode ? [NSError errorWithDomain:NSPOSIXErrorDomain code:errorCode userInfo:nil] : nil];
}
- (bool)is_mimetype:(const char*)q { return strcmp(q, mimeType) == 0; }
- (void)request_drag_data {
if (instanceId != _glfw.drag.instance_id) { [self end_transfer:EINVAL]; return; }
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (!window) { [self end_transfer:EINVAL]; return; }
bool keep_going = true;
if (drag_finish_timer) {
[drag_finish_timer invalidate];
drag_finish_timer = nil;
}
while (keep_going) {
GLFWDragEvent ev = {.type=GLFW_DRAG_DATA_REQUEST, .mime_type=mimeType};
_glfwInputDragSourceRequest(window, &ev);
if (ev.err_num) {
keep_going = false;
if (ev.err_num != EAGAIN) [self end_transfer:ev.err_num];
} else {
if (ev.data_sz) {
NSData* nsData = [NSData dataWithBytes:ev.data length:ev.data_sz];
NSError* error = nil;
if (![file_handle writeData:nsData error:&error]) {
keep_going = false;
[self end_transfer_with_error:error];
}
} else {
keep_going = false;
[self end_transfer_with_error:nil];
}
_glfwInputDragSourceRequest(window, &ev);
}
}
}
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime 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 (drag_finish_timer) {
[drag_finish_timer invalidate];
drag_finish_timer = nil;
}
drag_finish_window_id = 0;
drag_finish_action = 0;
if (_glfw.ns.drag_session) [_glfw.ns.drag_session release];
_glfw.ns.drag_session = nil;
if (_glfw.ns.drag_view) [_glfw.ns.drag_view release];
_glfw.ns.drag_view = nil;
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
_glfw.ns.drag_image = nil;
}
int
_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) {@autoreleasepool{
if (!_glfw.ns.drag_session || !_glfw.ns.drag_view) return 0;
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
[((NSDraggingSession*)_glfw.ns.drag_session)
enumerateDraggingItemsWithOptions:0
forView:(NSView*)_glfw.ns.drag_view
classes:@[[NSPasteboardItem class]]
searchOptions:@{}
usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) {
if (idx == 0) {
set_image_for_dragging_item(draggingItem, thumbnail, window->ns.object);
*stop = YES;
}
}];
return 0;
}}
int
_glfwPlatformDragDataReady(const char *mime_type) {
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;
}