Merge upstream/master into feat-draggable-window-title-bars

Resolves conflict in kitty/tabs.py by keeping both force_show_title_bars
and renaming_in_window fields.
This commit is contained in:
mcrmck
2026-03-17 22:40:28 -04:00
49 changed files with 1286 additions and 176 deletions

View File

@@ -311,6 +311,24 @@ static NSDictionary<NSString*,NSNumber*> *global_shortcuts = nil;
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
(void)notification;
// When the application becomes active after switching spaces (e.g., swiping
// back from a fullscreen app on another space), macOS may not send
// windowDidBecomeKey: for the already-key window. This leaves GLFW thinking
// no window has focus (since windowDidResignKey: was sent when leaving).
// Ensure GLFW's focus state is updated to match the actual key window.
NSWindow *keyWindow = [NSApp keyWindow];
if (keyWindow && !_glfw.focusedWindowId) {
for (_GLFWwindow *window = _glfw.windowListHead; window; window = window->next) {
if (window->ns.object == keyWindow) {
if (_glfw.focusedWindowId != window->id) _glfwInputWindowFocus(window, true);
break;
}
}
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
(void)sender;
@@ -839,6 +857,24 @@ is_apple_jis_layout_function_key(NSEvent *event) {
return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
}
static bool
has_apple_fn_global_shortcut(void) {
NSDictionary *hitoolbox_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.HIToolbox"];
id obj = [hitoolbox_settings objectForKey:@"AppleFnUsageType"];
if (![obj isKindOfClass:[NSNumber class]]) return false;
// Non-zero AppleFnUsageType means macOS has reserved Fn/Globe for a
// system action such as input source switching, emoji picker, or dictation.
return [obj integerValue] != 0;
}
static bool
is_apple_fn_global_shortcut(NSEvent *event) {
if ([event keyCode] != 0x3f /* kVK_Function */) return false;
NSEventModifierFlags mods = USEFUL_MODS([event modifierFlags]);
if (mods != 0 && mods != NSEventModifierFlagFunction) return false;
return has_apple_fn_global_shortcut();
}
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
handle_reopen_callback = callback;
@@ -949,6 +985,10 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
debug_key("-------------- flags changed -----------------\n");
debug_key("%s\n", [[event description] UTF8String]);
last_keydown_shortcut_event.virtual_key_code = 0xffff;
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed && is_apple_fn_global_shortcut(event)) {
debug_key("flagsChanged triggered global fn shortcut ignoring\n");
return event;
}
// switching to the next input source is only confirmed when all modifier keys are released
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))

View File

@@ -117,8 +117,11 @@ typedef UInt8 (*PFN_LMGetKbdType)(void);
#define LMGetKbdType _glfw.ns.tis.GetKbdType
typedef struct _GLFWDropData {
const char **mimes;
const char **mimes; // Original MIME list; strings are owned here, never reordered
size_t mimes_count;
const char **copy_mimes; // Working copy passed to callbacks; pointers into mimes[]
size_t copy_mimes_count; // Accepted count after last callback
bool drag_accepted;
id pasteboard;
id data_mapping;
id file_promise_mapping;
@@ -138,6 +141,9 @@ typedef struct _GLFWwindowNS
bool in_traditional_fullscreen;
bool in_fullscreen_transition;
bool suppress_frame_constraints;
id notch_cover_window;
unsigned int notch_cover_color;
float notch_cover_opacity;
bool titlebar_hidden;
unsigned long pre_full_screen_style_mask;
CGRect pre_traditional_fullscreen_frame;

View File

@@ -99,6 +99,22 @@ polymorphic_string_as_utf8(id 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) {
@@ -579,6 +595,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
@end
static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow*, bool, bool);
static void _glfwUpdateNotchCover(_GLFWwindow*);
@implementation GLFWWindowDelegate
@@ -798,6 +815,7 @@ static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWw
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;
@@ -842,6 +860,7 @@ static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWw
// 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 // }}}
@@ -1459,8 +1478,10 @@ is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_m
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(window->ns.drop_data.mimes + i);
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) {
@@ -1477,12 +1498,24 @@ free_drop_data(_GLFWwindow *window) {
}
static void
update_drop_state(_GLFWwindow *window, size_t mime_count) {
update_drop_state(_GLFWwindow *window, size_t accepted_count) {
_GLFWDropData *d = &window->ns.drop_data;
for (size_t i = mime_count; i < d->mimes_count; i++) {
if (d->mimes[i]) { free((void*)d->mimes[i]); d->mimes[i] = NULL; }
d->copy_mimes_count = accepted_count;
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;
}
d->mimes_count = mime_count;
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
@@ -1547,14 +1580,17 @@ update_drop_state(_GLFWwindow *window, size_t mime_count) {
window->ns.drop_data.mimes = mime_array;
window->ns.drop_data.mimes_count = mime_count;
bool from_self = ([sender draggingSource] != nil);
mime_count = _glfwInputDropEvent(window, GLFW_DROP_ENTER, xpos, ypos, mime_array, mime_count, from_self);
update_drop_state(window, mime_count);
return mime_count ? NSDragOperationGeneric :NSDragOperationNone;
_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);
}
return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone;
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
if (!window->ns.drop_data.mimes_count) return NSDragOperationNone;
if (!window->ns.drop_data.drag_accepted) return NSDragOperationNone;
const NSRect contentRect = [window->ns.view frame];
const NSPoint pos = [sender draggingLocation];
double xpos = pos.x;
@@ -1562,35 +1598,40 @@ update_drop_state(_GLFWwindow *window, size_t mime_count) {
bool from_self = ([sender draggingSource] != nil);
_GLFWDropData *d = &window->ns.drop_data;
size_t mime_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, d->mimes, d->mimes_count, from_self);
update_drop_state(window, mime_count);
return mime_count ? NSDragOperationGeneric :NSDragOperationNone;
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);
}
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;
size_t mime_count = _glfwInputDropEvent(window, GLFW_DROP_LEAVE, 0, 0, d->mimes, d->mimes_count, from_self);
update_drop_state(window, mime_count);
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);
}
free_drop_data(window);
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
if (!window->ns.drop_data.mimes_count) return NO;
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;
size_t mime_count = _glfwInputDropEvent(window, GLFW_DROP_DROP, xpos, ypos, d->mimes, d->mimes_count, from_self);
if (d->mimes) {
update_drop_state(window, mime_count);
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);
window->ns.drop_data.pasteboard = [[sender draggingPasteboard] retain];
for (size_t i = 0; i < d->mimes_count; i++)
_glfwPlatformRequestDropData(window, d->mimes[i]);
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
@@ -1913,6 +1954,17 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
- (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
@@ -2387,6 +2439,12 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window)
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];
@@ -3313,6 +3371,44 @@ make_window_fullscreen_after_show(unsigned long long timer_id, void* data) {
}
}
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;
@@ -3335,8 +3431,13 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
w->ns.pre_traditional_fullscreen_frame = [window frame];
[window setStyleMask: NSWindowStyleMaskBorderless];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
[window setFrame:[window.screen frame] display:YES];
NSRect screenFrame = [window.screen frame];
if (@available(macOS 12.0, *)) {
screenFrame.size.height -= window.screen.safeAreaInsets.top;
}
[window setFrame:screenFrame display:YES];
w->ns.in_traditional_fullscreen = true;
_glfwUpdateNotchCover(w);
} else {
made_fullscreen = false;
if (sm & NSWindowStyleMaskFullScreen) {
@@ -3354,6 +3455,7 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
[window setStyleMask: w->ns.pre_full_screen_style_mask];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
w->ns.in_traditional_fullscreen = false;
_glfwUpdateNotchCover(w);
}
}
} else {
@@ -3925,6 +4027,9 @@ GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool us
// 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

8
glfw/input.c vendored
View File

@@ -402,7 +402,13 @@ void _glfwInputCursorEnter(_GLFWwindow* window, bool entered)
window->callbacks.cursorEnter((GLFWwindow*) window, entered);
}
// Notifies shared code of a drop event
// Notifies shared code of a drop event.
// The caller is responsible for passing a mutable working-copy of the mimes
// array (reset to the full original list before each call) so that the
// callback can sort/filter in-place without touching the backend's canonical
// storage. The return value is ev.num_mimes after the callback returns,
// i.e. the number of accepted (possibly reordered) mimes starting at
// mimes[0].
size_t _glfwInputDropEvent(_GLFWwindow *window, GLFWDropEventType type, double xpos, double ypos, const char** mimes, size_t num_mimes, bool from_self) {
if (!window->callbacks.drop_event) return 0;
GLFWDropEvent ev = {

10
glfw/wl_init.c vendored
View File

@@ -206,27 +206,35 @@ pointer_handle_frame(void *data UNUSED, struct wl_pointer *pointer UNUSED) {
_GLFWwindow* window = _glfw.wl.pointerFocus;
if (!window) return;
GLFWScrollEvent ev = {.keyboard_modifiers=_glfw.wl.xkb.states.modifiers};
bool found = false;
if (info.discrete.y_axis_type != AXIS_EVENT_UNKNOWN) {
ev.unscaled.y = info.discrete.y;
if (info.discrete.y_axis_type == AXIS_EVENT_VALUE120) ev.offset_type = GLFW_SCROLL_OFFEST_V120;
found = true;
} else if (info.continuous.y_axis_type != AXIS_EVENT_UNKNOWN) {
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
ev.unscaled.y = info.continuous.y;
found = true;
}
if (info.discrete.x_axis_type != AXIS_EVENT_UNKNOWN) {
ev.unscaled.x = info.discrete.x;
if (info.discrete.x_axis_type == AXIS_EVENT_VALUE120) ev.offset_type = GLFW_SCROLL_OFFEST_V120;
found = true;
} else if (info.continuous.x_axis_type != AXIS_EVENT_UNKNOWN) {
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
ev.unscaled.x = info.continuous.x;
found = true;
}
bool stopped = info.y_stop_received || info.x_stop_received;
if (!found && stopped) ev.offset_type = window->wl.prev_frame_offset_type;
ev.unscaled.x *= -1;
const double scale = ev.offset_type == GLFW_SCROLL_OFFEST_HIGHRES ? _glfwWaylandWindowScale(window) : 1;
ev.x_offset = scale * ev.unscaled.x; ev.y_offset = scale * ev.unscaled.y;
glfw_handle_scroll_event_for_momentum(
window, &ev, info.y_stop_received || info.x_stop_received, info.source_type == WL_POINTER_AXIS_SOURCE_FINGER);
window, &ev, stopped, info.source_type == WL_POINTER_AXIS_SOURCE_FINGER);
window->wl.prev_frame_offset_type = ev.offset_type;
/* clear pointer_curr_axis_info for next frame */
memset(&info, 0, sizeof(info));
}

3
glfw/wl_platform.h vendored
View File

@@ -212,6 +212,7 @@ typedef struct _GLFWwindowWayland
uint32_t source_type;
monotonic_t x_start_time, x_stop_time, y_stop_time, y_start_time;
} pointer_curr_axis_info;
GLFWOffsetType prev_frame_offset_type;
_GLFWcursor* currentCursor;
double cursorPosX, cursorPosY, allCursorPosX, allCursorPosY;
@@ -310,6 +311,8 @@ typedef struct _GLFWWaylandDataOffer
struct wl_surface *surface;
const char **mimes;
size_t mimes_capacity, mimes_count;
const char **copy_mimes; // Working copy passed to callbacks; pointers into mimes[]
size_t copy_mimes_count; // Count of entries in copy_mimes (accepted count after callback)
bool drag_accepted, dropped;
uint32_t serial;
struct {

120
glfw/wl_window.c vendored
View File

@@ -1588,6 +1588,14 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
void
_glfwPlatformSetWindowIcon(_GLFWwindow* window, int count, const GLFWimage* images) {
if (is_layer_shell(window)) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Cannot set window icon on layer shell surfaces");
return;
}
if (!window->wl.xdg.toplevel) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Ignoring attempt to set window icon on window without a toplevel");
return;
}
if (!_glfw.wl.xdg_toplevel_icon_manager_v1) {
static bool warned_once = false;
if (!warned_once) {
@@ -2364,10 +2372,25 @@ destroy_data_offer(_GLFWWaylandDataOffer *offer) {
for (size_t i = 0; i < offer->mimes_count; i++) free((char*)offer->mimes[i]);
free(offer->mimes);
}
free(offer->copy_mimes); // pointer array only; strings are owned by mimes[]
if (offer->requested_drop_data) destroy_drop_data(offer);
memset(offer, 0, sizeof(offer[0]));
}
// Reset the working copy of mimes so the next callback sees the full original
// list. Returns false on allocation failure.
static bool
reset_copy_mimes(_GLFWWaylandDataOffer *offer) {
if (offer->mimes_count == 0) { offer->copy_mimes_count = 0; return true; }
if (!offer->copy_mimes) {
offer->copy_mimes = malloc(offer->mimes_count * sizeof(const char*));
if (!offer->copy_mimes) return false;
}
memcpy(offer->copy_mimes, offer->mimes, offer->mimes_count * sizeof(const char*));
offer->copy_mimes_count = offer->mimes_count;
return true;
}
static void
mark_data_offer(_GLFWWaylandDataOffer *ans, void *id) {
if (ans->id) destroy_data_offer(ans);
@@ -2480,15 +2503,12 @@ static void handle_primary_selection_offer(void *data UNUSED, struct zwp_primary
// Helper function to update drop state from callback results
static void
update_drop_state(_GLFWWaylandDataOffer *d, _GLFWwindow* window UNUSED, size_t mime_count) {
for (size_t i = mime_count; i < d->mimes_count; i++) {
if (d->mimes[i]) { free((void*)d->mimes[i]); d->mimes[i] = NULL; }
}
d->mimes_count = mime_count;
bool accepted = mime_count > 0;
update_drop_state(_GLFWWaylandDataOffer *d, _GLFWwindow* window UNUSED, size_t accepted_count) {
d->copy_mimes_count = accepted_count;
bool accepted = accepted_count > 0;
bool acceptance_changed = (accepted != d->drag_accepted);
// The first MIME in the sorted list is the preferred one for drop
const char* new_preferred_mime = (accepted && mime_count > 0) ? d->mimes[0] : NULL;
// The first entry in the accepted (sorted) copy is the preferred MIME.
const char* new_preferred_mime = (accepted && d->copy_mimes) ? d->copy_mimes[0] : NULL;
bool mime_changed = false;
// Check if the preferred MIME changed
@@ -2522,10 +2542,12 @@ drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint
if (window->wl.surface == surface) {
double xpos = wl_fixed_to_double(x);
double ypos = wl_fixed_to_double(y);
size_t mime_count = _glfwInputDropEvent(
window, GLFW_DROP_ENTER, xpos, ypos,
offer->mimes, offer->mimes_count, offer->is_self_offer);
update_drop_state(offer, window, mime_count);
if (reset_copy_mimes(offer)) {
size_t mime_count = _glfwInputDropEvent(
window, GLFW_DROP_ENTER, xpos, ypos,
offer->copy_mimes, offer->copy_mimes_count, offer->is_self_offer);
update_drop_state(offer, window, mime_count);
}
break;
}
window = window->next;
@@ -2563,7 +2585,7 @@ _glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffe
for (size_t o = 0; o < offer->dd_count; o++) {
if (offer->requested_drop_data[o].fd == fd) {
ssize_t ret;
do { ret = read(fd, buffer, sz); } while (ret < 0 && errno == EINTR);
do { ret = read(fd, buffer, sz); } while (ret < 0 && (errno == EINTR || errno == EAGAIN));
if (ret <= 0) removeWatch(&_glfw.wl.eventLoopData, offer->requested_drop_data[o].watch_id);
return ret < 0 ? -errno : ret;
}
@@ -2643,9 +2665,11 @@ drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
_GLFWwindow* window = _glfw.windowListHead;
while (window) {
if (window->wl.surface == offer->surface) {
size_t num_mimes = _glfwInputDropEvent(window, GLFW_DROP_DROP, 0, 0, offer->mimes, offer->mimes_count, offer->is_self_offer);
if (!offer->mimes) { destroy_data_offer(offer); return; }
for (size_t i = 0; i < num_mimes; i++) request_drop_data(offer, offer->mimes[i]);
if (reset_copy_mimes(offer)) {
size_t num_accepted = _glfwInputDropEvent(window, GLFW_DROP_DROP, 0, 0, offer->copy_mimes, offer->copy_mimes_count, offer->is_self_offer);
if (!offer->copy_mimes) { destroy_data_offer(offer); return; }
for (size_t i = 0; i < num_accepted; i++) request_drop_data(offer, offer->copy_mimes[i]);
}
break;
}
window = window->next;
@@ -2661,9 +2685,11 @@ motion(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t
if (window->wl.surface == offer->surface) {
double xpos = wl_fixed_to_double(x);
double ypos = wl_fixed_to_double(y);
size_t mime_count = _glfwInputDropEvent(
window, GLFW_DROP_MOVE, xpos, ypos, offer->mimes, offer->mimes_count, offer->is_self_offer);
update_drop_state(offer, window, mime_count);
if (reset_copy_mimes(offer)) {
size_t mime_count = _glfwInputDropEvent(
window, GLFW_DROP_MOVE, xpos, ypos, offer->copy_mimes, offer->copy_mimes_count, offer->is_self_offer);
update_drop_state(offer, window, mime_count);
}
break;
}
window = window->next;
@@ -2673,8 +2699,8 @@ motion(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t
void
_glfwPlatformRequestDropUpdate(_GLFWwindow* window) {
_GLFWWaylandDataOffer *d = &_glfw.wl.drop_data_offer;
if (d->id) {
size_t mime_count = _glfwInputDropEvent(window, GLFW_DROP_STATUS_UPDATE, 0, 0, d->mimes, d->mimes_count, d->is_self_offer);
if (d->id && reset_copy_mimes(d)) {
size_t mime_count = _glfwInputDropEvent(window, GLFW_DROP_STATUS_UPDATE, 0, 0, d->copy_mimes, d->copy_mimes_count, d->is_self_offer);
update_drop_state(d, window, mime_count);
}
}
@@ -3151,6 +3177,7 @@ finish_drag_write(size_t i) {
dr.watch_id = 0;
if (dr.fd > -1) safe_close(dr.fd);
dr.fd = -1;
free(dr.pending_data); dr.pending_data = NULL; dr.sz = 0; dr.offset = 0;
free((void*)dr.mime_type); dr.mime_type = NULL;
}
@@ -3172,31 +3199,49 @@ write_as_much_as_possible(int fd, const char *data, size_t sz) {
static void
send_drag_data(_GLFWwindow *window, size_t i) {
ssize_t ret;
bool has_preset_data = _glfw.drag.items[i].data_size > 0;
// Find the item matching the mime type for this data request.
// We cannot use i directly to index _glfw.drag.items because the compositor
// may call drag_source_send multiple times (once per target entered), making
// data_requests grow independently of the items array.
size_t item_idx = _glfw.drag.item_count; // sentinel: not found
for (size_t j = 0; j < _glfw.drag.item_count; j++) {
if (dr.mime_type && _glfw.drag.items[j].mime_type &&
strcmp(_glfw.drag.items[j].mime_type, dr.mime_type) == 0) {
item_idx = j; break;
}
}
if (item_idx == _glfw.drag.item_count) {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: compositor requested data for unrecognised MIME type: %s",
dr.mime_type ? dr.mime_type : "(null)");
}
bool has_preset_data = item_idx < _glfw.drag.item_count && _glfw.drag.items[item_idx].data_size > 0;
// On write error, only close this pipe; do NOT destroy the wl_data_source
// since the compositor must send cancelled/dnd_finished before that is safe.
#define on_fail _glfwInputError(\
GLFW_PLATFORM_ERROR, "Wayland: failed to write drag source data to pipe with error: %s", strerror(errno)); \
cancel_drag(GLFW_DRAG_CANCELLED);
finish_drag_write(i); return;
if (dr.sz > dr.offset) {
ret = write_as_much_as_possible(dr.fd, dr.pending_data + dr.offset, dr.sz - dr.offset);
if (ret < 0) { on_fail; } else {
dr.offset += ret;
if (dr.offset >= dr.sz) {
free(dr.pending_data); dr.sz = 0; dr.offset = 0;
if (has_preset_data) finish_drag_write(i);
free(dr.pending_data); dr.pending_data = NULL; dr.sz = 0; dr.offset = 0;
finish_drag_write(i);
}
}
} else if (has_preset_data) {
do { ret = write(dr.fd, _glfw.drag.items[i].optional_data, _glfw.drag.items[i].data_size); } while (ret < 0 && errno == EINTR);
do { ret = write(dr.fd, _glfw.drag.items[item_idx].optional_data, _glfw.drag.items[item_idx].data_size); } while (ret < 0 && errno == EINTR);
if (ret < 0) {
on_fail;
} else {
if ((size_t)ret >= _glfw.drag.items[i].data_size) {
if ((size_t)ret >= _glfw.drag.items[item_idx].data_size) {
finish_drag_write(i);
} else {
void *pending = malloc(_glfw.drag.items[i].data_size - ret);
if (!pending) { on_fail; } else {
dr.pending_data = pending; dr.sz = _glfw.drag.items[i].data_size - ret; dr.offset = 0;
void *pending = malloc(_glfw.drag.items[item_idx].data_size - ret);
if (!pending) { errno = ENOMEM; on_fail; } else {
dr.pending_data = pending; dr.sz = _glfw.drag.items[item_idx].data_size - ret; dr.offset = 0;
}
}
}
@@ -3205,21 +3250,20 @@ send_drag_data(_GLFWwindow *window, size_t i) {
_glfwInputDragSourceRequest(window, &ev);
if (ev.err_num) {
if (ev.err_num == EAGAIN) { removeWatch(&_glfw.wl.eventLoopData, dr.watch_id); dr.watch_id = 0; }
else cancel_drag(GLFW_DRAG_CANCELLED);
else { finish_drag_write(i); return; }
} else {
if (ev.data_sz) {
ret = write_as_much_as_possible(dr.fd, ev.data, ev.data_sz);
if (ret >= 0) {
if ((size_t)ret < ev.data_sz) {
void *pending = malloc(ev.data_sz - ret);
if (!pending) { on_fail; } else {
dr.pending_data = pending; dr.sz = ev.data_sz - ret; dr.offset = 0;
memcpy(pending, ev.data + ret, dr.sz);
}
if (ret >= 0 && (size_t)ret < ev.data_sz) {
void *pending = malloc(ev.data_sz - ret);
if (!pending) { ret = -1; } else {
dr.pending_data = pending; dr.sz = ev.data_sz - ret; dr.offset = 0;
memcpy(pending, ev.data + ret, dr.sz);
}
}
_glfwInputDragSourceRequest(window, &ev);
if (ret < 0) { on_fail; }
else if ((size_t)ret >= ev.data_sz) { finish_drag_write(i); }
} else finish_drag_write(i);
}
}

6
glfw/x11_init.c vendored
View File

@@ -216,8 +216,12 @@ read_xi_scroll_devices(void) {
if (_glfw.x11.xi.num_scroll_devices >= arraysz(_glfw.x11.xi.scroll_devices)) continue;
d = &_glfw.x11.xi.scroll_devices[_glfw.x11.xi.num_scroll_devices++];
*d = (XIScrollDevice){
.is_highres=is_highres, .is_finger_based=is_finger_based, .deviceid=device->deviceid, .sourceid=scroll->sourceid,
.is_finger_based=is_finger_based, .deviceid=device->deviceid, .sourceid=scroll->sourceid,
};
if (is_highres) {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
}
memcpy(d->name, device->name, MIN(sizeof(d->name)-1, strlen(device->name)));
}
if (d->num_valuators >= arraysz(d->valuators)) continue;

11
glfw/x11_platform.h vendored
View File

@@ -239,12 +239,14 @@ typedef struct XIScrollValuator {
} XIScrollValuator;
typedef struct XIScrollDevice {
bool is_highres;
bool is_finger_based;
bool type_detected;
int deviceid, sourceid;
XIScrollValuator valuators[8];
unsigned num_valuators;
char name[32];
unsigned num_events;
GLFWOffsetType offset_type;
} XIScrollDevice;
typedef struct XdndSelectionRequest {
@@ -393,8 +395,11 @@ typedef struct _GLFWlibraryX11
char format[256];
int format_priority;
Window target_window; // For drag events: the window being dragged over
const char** mimes; // Cached MIME types from drag enter
size_t mimes_count; // Current count of MIME types (may be reduced by callback)
const char** mimes; // Cached MIME types from drag enter (original, never reordered)
size_t mimes_count; // Count of MIME types (full original list, never reduced)
const char** copy_mimes; // Working copy passed to callbacks; pointers into mimes[]
size_t copy_mimes_count; // Accepted count after last callback
bool drag_accepted; // Whether the callback accepted at least one MIME type
bool from_self, dropped;
Time drop_time;
XdndSelectionRequest *selection_requests;

128
glfw/x11_window.c vendored
View File

@@ -29,6 +29,7 @@
#define _GNU_SOURCE
#include "internal.h"
#include "math.h"
#include "backend_utils.h"
#include "linux_notify.h"
#include "../kitty/monotonic.h"
@@ -1414,6 +1415,11 @@ handle_mouse_move_event(_GLFWwindow *window, const int x, const int y) {
window->x11.lastCursorPosY = y;
}
static bool
number_has_fractional_part(double x) {
return fabs(x - round(x)) >= 1e-6;
}
static void
handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
XIScrollDevice *d = NULL;
@@ -1441,17 +1447,35 @@ handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
scroll_valuator_found = true;
double delta = value - v->value;
v->value = value;
if (v->is_vertical) delta *= -1;
delta *= -1;
double *off = v->is_vertical ? &yOffset : &xOffset;
*off = delta;
if (!d->is_highres) {
if (v->increment == 120.) type = GLFW_SCROLL_OFFEST_V120;
else {
type = GLFW_SCROLL_OFFSET_LINES;
if (v->increment != 0) *off /= v->increment;
d->num_events++;
if (!d->type_detected) {
if (v->increment == 120.) {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_V120;
} else {
bool delta_is_fractional = number_has_fractional_part(delta);
if (delta_is_fractional) {
if (fabs(delta * 120 - round(delta * 120)) < 0.01) {
d->type_detected = d->num_events > 2;
d->offset_type = GLFW_SCROLL_OFFEST_V120;
} else {
d->type_detected = true;
d->offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
}
} else {
d->type_detected = d->num_events > 2;
d->offset_type = GLFW_SCROLL_OFFSET_LINES;
}
}
}
if (d->offset_type == GLFW_SCROLL_OFFSET_LINES) {
if (v->increment != 0) *off /= v->increment;
}
}
type = d->offset_type;
if (xOffset != 0 || yOffset != 0) {
// Get keyboard modifiers
int mods = translateState(de->mods.effective);
@@ -1465,7 +1489,7 @@ handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
};
// For high-resolution, finger-based scrolling, use timer-based momentum scrolling
if (d->is_highres && d->is_finger_based && type == GLFW_SCROLL_OFFEST_HIGHRES) {
if (d->is_finger_based && type == GLFW_SCROLL_OFFEST_HIGHRES) {
// Reset the timer on each scroll event
x11_cancel_momentum_scroll_timer();
@@ -1498,7 +1522,7 @@ handle_xi_motion_event(_GLFWwindow *window, XIDeviceEvent *de) {
static void
end_drop(_GLFWwindow *window, GLFWDragOperationType op) {
bool accepted = dnd.mimes_count > 0 || dnd.dropped;
bool accepted = dnd.drag_accepted || dnd.dropped;
XEvent reply = { ClientMessage };
reply.xclient.window = dnd.source;
reply.xclient.message_type = _glfw.x11.XdndFinished;
@@ -1520,14 +1544,12 @@ end_drop(_GLFWwindow *window, GLFWDragOperationType op) {
static void
update_drop_state(_GLFWwindow* window, size_t mime_count) {
for (size_t i = mime_count; i < dnd.mimes_count; i++) {
if (dnd.mimes[i]) { XFree((void*)dnd.mimes[i]); dnd.mimes[i] = NULL; }
}
dnd.mimes_count = mime_count;
bool accepted = mime_count > 0;
// The first MIME in the sorted list is the preferred one for drop
const char* new_preferred_mime = (accepted && mime_count > 0) ? dnd.mimes[0] : NULL;
update_drop_state(_GLFWwindow* window, size_t accepted_count) {
dnd.copy_mimes_count = accepted_count;
bool accepted = accepted_count > 0;
dnd.drag_accepted = accepted;
// The first entry in the accepted (sorted) copy is the preferred MIME.
const char* new_preferred_mime = (accepted && dnd.copy_mimes) ? dnd.copy_mimes[0] : NULL;
// Check if the preferred MIME changed
bool mime_changed = strncmp(new_preferred_mime ? new_preferred_mime : "", dnd.format, arraysz(dnd.format)) != 0;
if (mime_changed) {
@@ -1566,12 +1588,30 @@ free_dnd_mimes(void) {
dnd.mimes = NULL;
dnd.mimes_count = 0;
}
free(dnd.copy_mimes); // pointer array only; strings are owned by mimes[]
dnd.copy_mimes = NULL;
dnd.copy_mimes_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_dnd_copy_mimes(void) {
if (dnd.mimes_count == 0) { dnd.copy_mimes_count = 0; return true; }
if (!dnd.copy_mimes) {
dnd.copy_mimes = malloc(dnd.mimes_count * sizeof(const char*));
if (!dnd.copy_mimes) return false;
}
memcpy(dnd.copy_mimes, dnd.mimes, dnd.mimes_count * sizeof(const char*));
dnd.copy_mimes_count = dnd.mimes_count;
return true;
}
void
free_dnd_data(void) {
dnd.source = None;
dnd.target_window = None;
dnd.drag_accepted = false;
free_dnd_mimes();
if (dnd.selection_requests) {
for (size_t i = 0; i < dnd.selection_requests_count; i++) {
@@ -1640,14 +1680,13 @@ drop_start(_GLFWwindow *window, XEvent *event) {
update_dnd_mimes(event);
dnd.from_self = _glfw.x11.drag.source_window != None && dnd.source == _glfw.x11.drag.source_window;
// Position is not known yet at enter time, will be updated with XdndPosition
size_t mimes_count = _glfwInputDropEvent(
window, GLFW_DROP_ENTER, 0, 0, dnd.mimes, dnd.mimes_count, dnd.from_self);
update_drop_state(window, mimes_count);
// Update cached mime count with callback result
if (dnd.mimes_count > 0) {
// The first MIME type in the reordered list is the preferred one
strncpy(dnd.format, dnd.mimes[0], arraysz(dnd.format) - 1);
dnd.format_priority = 1;
if (reset_dnd_copy_mimes()) {
size_t accepted_count = _glfwInputDropEvent(
window, GLFW_DROP_ENTER, 0, 0, dnd.copy_mimes, dnd.copy_mimes_count, dnd.from_self);
update_drop_state(window, accepted_count);
// Set format_priority when the callback accepted at least one MIME type.
// dnd.format has already been updated by update_drop_state.
if (accepted_count > 0) dnd.format_priority = 1;
}
}
@@ -1674,8 +1713,10 @@ drop_move(_GLFWwindow *window, XEvent *event) {
_glfwReleaseErrorHandlerX11();
if (_glfw.x11.errorCode != Success) _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to get DND event position");
_glfwInputCursorPos(window, xpos, ypos);
size_t mimes_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, dnd.mimes, dnd.mimes_count, dnd.from_self);
update_drop_state(window, mimes_count);
if (reset_dnd_copy_mimes()) {
size_t mimes_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, dnd.copy_mimes, dnd.copy_mimes_count, dnd.from_self);
update_drop_state(window, mimes_count);
}
}
void
@@ -1684,8 +1725,10 @@ _glfwPlatformRequestDropUpdate(_GLFWwindow* window) {
if (dnd.source == None || dnd.target_window != window->x11.handle) return;
// Call the drag callback with STATUS_UPDATE event to get updated state
// Position values are not valid for this event type
size_t mimes_count = _glfwInputDropEvent(window, GLFW_DROP_STATUS_UPDATE, 0, 0, dnd.mimes, dnd.mimes_count, dnd.from_self);
update_drop_state(window, mimes_count);
if (reset_dnd_copy_mimes()) {
size_t mimes_count = _glfwInputDropEvent(window, GLFW_DROP_STATUS_UPDATE, 0, 0, dnd.copy_mimes, dnd.copy_mimes_count, dnd.from_self);
update_drop_state(window, mimes_count);
}
}
@@ -1695,9 +1738,10 @@ drop(_GLFWwindow *window, XEvent *event) {
if (dnd.version > _GLFW_XDND_VERSION || dnd.version < 2) return;
dnd.dropped = true;
dnd.drop_time = (unsigned long)event->xclient.data.l[2];
size_t mimes_count = _glfwInputDropEvent(window, GLFW_DROP_DROP, 0, 0, dnd.mimes, dnd.mimes_count, dnd.from_self);
if (!dnd.mimes) return;
for (size_t i = 0; i < mimes_count; i++) _glfwPlatformRequestDropData(window, dnd.mimes[i]);
if (!reset_dnd_copy_mimes()) return;
size_t num_accepted = _glfwInputDropEvent(window, GLFW_DROP_DROP, 0, 0, dnd.copy_mimes, dnd.copy_mimes_count, dnd.from_self);
if (!dnd.copy_mimes) return;
for (size_t i = 0; i < num_accepted; i++) _glfwPlatformRequestDropData(window, dnd.copy_mimes[i]);
}
static void
@@ -4004,8 +4048,11 @@ send_xdnd_enter(Window target, int version) {
}
}
_glfwGrabErrorHandlerX11();
XSendEvent(_glfw.x11.display, target, False, NoEventMask, &event);
XFlush(_glfw.x11.display);
_glfwReleaseErrorHandlerX11();
if (_glfw.x11.errorCode == BadWindow) _glfw.x11.drag.current_target = None;
}
// Send XdndPosition message to target window
@@ -4024,9 +4071,12 @@ send_xdnd_position(Window target, int root_x, int root_y, Time timestamp) {
event.xclient.data.l[3] = timestamp;
event.xclient.data.l[4] = _glfw.x11.drag.action_atom;
_glfwGrabErrorHandlerX11();
XSendEvent(_glfw.x11.display, target, False, NoEventMask, &event);
XFlush(_glfw.x11.display);
_glfw.x11.drag.waiting_for_status = true;
_glfwReleaseErrorHandlerX11();
if (_glfw.x11.errorCode == BadWindow) _glfw.x11.drag.current_target = None;
else _glfw.x11.drag.waiting_for_status = true;
}
// Send XdndLeave message to target window
@@ -4041,8 +4091,11 @@ send_xdnd_leave(Window target) {
event.xclient.format = 32;
event.xclient.data.l[0] = _glfw.x11.drag.source_window;
_glfwGrabErrorHandlerX11();
XSendEvent(_glfw.x11.display, target, False, NoEventMask, &event);
XFlush(_glfw.x11.display);
_glfwReleaseErrorHandlerX11();
// BadWindow on leave is benign the target window is already gone
}
// Send XdndDrop message to target window
@@ -4059,8 +4112,19 @@ send_xdnd_drop(Window target, Time timestamp) {
event.xclient.data.l[1] = 0; // Reserved
event.xclient.data.l[2] = timestamp;
_glfwGrabErrorHandlerX11();
XSendEvent(_glfw.x11.display, target, False, NoEventMask, &event);
XFlush(_glfw.x11.display);
_glfwReleaseErrorHandlerX11();
if (_glfw.x11.errorCode == BadWindow) {
// Target window was destroyed; cancel the drag gracefully
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
if (window) {
GLFWDragEvent ev = {.type = GLFW_DRAG_CANCELLED};
_glfwInputDragSourceRequest(window, &ev);
}
_glfwFreeDragSourceData();
}
}
// Render thumbnail pixels into _glfw.x11.drag.thumbnail_pixmap / thumbnail_gc.