mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
apprt/gtk: ensure modifier state matches current keypress under X11
This fixes an issue in that when running under X11, when a modifier key is pressed, the modifier state will "lag" behind what should be current. This is due to how X11 sends modifiers in events, i.e. it sends the state from right before the key press, and does not include the effects of the key press itself. This is corrected by checking the X event queue directly for a pending XkbStateNotify event (we mask this on modifiers), and setting the modifiers off of that if we find one. If not, we fall back to the GDK call.
This commit is contained in:
@ -28,6 +28,7 @@ const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const c = @import("c.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -170,7 +171,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
}
|
||||
|
||||
const display = c.gdk_display_get_default();
|
||||
if (c.g_type_check_instance_is_a(@ptrCast(@alignCast(display)), c.gdk_x11_display_get_type()) != 0) {
|
||||
if (x11.x11_is_display(display)) {
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
|
@ -19,6 +19,7 @@ const Window = @import("Window.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const c = @import("c.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
@ -253,6 +254,9 @@ im_composing: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
|
||||
/// Xkb state (X11 only). Will be null on Wayland.
|
||||
x11_xkb: ?x11.X11Xkb = null,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface {
|
||||
var surface = try alloc.create(Surface);
|
||||
errdefer alloc.destroy(surface);
|
||||
@ -355,6 +359,12 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
};
|
||||
errdefer self.* = undefined;
|
||||
|
||||
// Set up Xkb if we are running under X11.
|
||||
const display = c.gdk_display_get_default();
|
||||
if (x11.x11_is_display(display)) {
|
||||
self.x11_xkb = try x11.X11Xkb.init(display);
|
||||
}
|
||||
|
||||
// Set our default mouse shape
|
||||
try self.setMouseShape(.text);
|
||||
|
||||
@ -1222,6 +1232,7 @@ fn keyEvent(
|
||||
const self = userdataSelf(ud.?);
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||
const display = c.gtk_widget_get_display(@ptrCast(self.gl_area));
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
@ -1232,7 +1243,6 @@ fn keyEvent(
|
||||
|
||||
// Get all the possible keyboard mappings for this keycode. A keycode
|
||||
// is the physical key pressed.
|
||||
const display = c.gtk_widget_get_display(@ptrCast(self.gl_area));
|
||||
var keys: [*]c.GdkKeymapKey = undefined;
|
||||
var keyvals: [*]c.guint = undefined;
|
||||
var n_keys: c_int = 0;
|
||||
@ -1335,7 +1345,19 @@ fn keyEvent(
|
||||
_ = gtk_mods;
|
||||
|
||||
const device = c.gdk_event_get_device(event);
|
||||
var mods = translateMods(c.gdk_device_get_modifier_state(device));
|
||||
var mods = if (self.x11_xkb) |x11_xkb| init_mods: {
|
||||
// Add any modifier state events from Xkb if we have them (X11 only).
|
||||
if (x11_xkb.modifier_state_from_notify(display)) |xkb_mods| {
|
||||
break :init_mods xkb_mods;
|
||||
} else {
|
||||
// Null back from the Xkb call means there was no modifier
|
||||
// event to read. This likely means that the key event did not
|
||||
// result in a modifier change and we can safely rely on the
|
||||
// GDK state.
|
||||
break :init_mods translateMods(c.gdk_device_get_modifier_state(device));
|
||||
}
|
||||
} else translateMods(c.gdk_device_get_modifier_state(device));
|
||||
|
||||
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
|
||||
|
||||
switch (physical_key) {
|
||||
|
@ -5,6 +5,8 @@ const c = @cImport({
|
||||
// Add in X11-specific GDK backend which we use for specific things (e.g.
|
||||
// X11 window class).
|
||||
@cInclude("gdk/x11/gdkx.h");
|
||||
// Xkb for X11 state handling
|
||||
@cInclude("X11/XKBlib.h");
|
||||
});
|
||||
|
||||
pub usingnamespace c;
|
||||
|
120
src/apprt/gtk/x11.zig
Normal file
120
src/apprt/gtk/x11.zig
Normal file
@ -0,0 +1,120 @@
|
||||
/// Utility functions for X11 handling.
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const input = @import("../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
// X11 Function types. We load these dynamically at runtime to avoid having to
|
||||
// link against X11.
|
||||
const XkbQueryExtensionType = *const fn (?*c.struct__XDisplay, [*c]c_int, [*c]c_int, [*c]c_int, [*c]c_int, [*c]c_int) callconv(.C) c_int;
|
||||
const XkbSelectEventDetailsType = *const fn (?*c.struct__XDisplay, c_uint, c_uint, c_ulong, c_ulong) callconv(.C) c_int;
|
||||
const XEventsQueuedType = *const fn (?*c.struct__XDisplay, c_int) callconv(.C) c_int;
|
||||
const XPeekEventType = *const fn (?*c.struct__XDisplay, [*c]c.union__XEvent) callconv(.C) c_int;
|
||||
|
||||
/// Returns true if the passed in display is an X11 display.
|
||||
pub fn x11_is_display(display_: ?*c.GdkDisplay) bool {
|
||||
const display = display_ orelse return false;
|
||||
return (c.g_type_check_instance_is_a(@ptrCast(@alignCast(display)), c.gdk_x11_display_get_type()) != 0);
|
||||
}
|
||||
|
||||
pub const X11Xkb = struct {
|
||||
opcode: c_int,
|
||||
base_event_code: c_int,
|
||||
base_error_code: c_int,
|
||||
|
||||
// Dynamic functions
|
||||
XkbQueryExtension: XkbQueryExtensionType = undefined,
|
||||
XkbSelectEventDetails: XkbSelectEventDetailsType = undefined,
|
||||
XEventsQueued: XEventsQueuedType = undefined,
|
||||
XPeekEvent: XPeekEventType = undefined,
|
||||
|
||||
pub fn init(display_: ?*c.GdkDisplay) !X11Xkb {
|
||||
log.debug("X11: X11Xkb.init: initializing Xkb", .{});
|
||||
const display = display_ orelse {
|
||||
log.err("Fatal: error initializing Xkb extension: display is null", .{});
|
||||
return error.XkbInitializationError;
|
||||
};
|
||||
|
||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
||||
var result: X11Xkb = .{ .opcode = 0, .base_event_code = 0, .base_error_code = 0 };
|
||||
|
||||
// Load in the X11 calls we need.
|
||||
log.debug("X11: X11Xkb.init: loading libX11.so dynamically", .{});
|
||||
var libX11 = try std.DynLib.open("libX11.so");
|
||||
defer libX11.close();
|
||||
result.XkbQueryExtension = libX11.lookup(XkbQueryExtensionType, "XkbQueryExtension") orelse {
|
||||
log.err("Fatal: error dynamic loading libX11: missing symbol XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
};
|
||||
result.XkbSelectEventDetails = libX11.lookup(XkbSelectEventDetailsType, "XkbSelectEventDetails") orelse {
|
||||
log.err("Fatal: error dynamic loading libX11: missing symbol XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
};
|
||||
result.XEventsQueued = libX11.lookup(XEventsQueuedType, "XEventsQueued") orelse {
|
||||
log.err("Fatal: error dynamic loading libX11: missing symbol XEventsQueued", .{});
|
||||
return error.XkbInitializationError;
|
||||
};
|
||||
result.XPeekEvent = libX11.lookup(XPeekEventType, "XPeekEvent") orelse {
|
||||
log.err("Fatal: error dynamic loading libX11: missing symbol XPeekEvent", .{});
|
||||
return error.XkbInitializationError;
|
||||
};
|
||||
|
||||
log.debug("X11: X11Xkb.init: running XkbQueryExtension", .{});
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (result.XkbQueryExtension(xdisplay, &result.opcode, &result.base_event_code, &result.base_error_code, &major, &minor) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
log.debug("X11: X11Xkb.init: running XkbSelectEventDetails", .{});
|
||||
if (result.XkbSelectEventDetails(xdisplay, c.XkbUseCoreKbd, c.XkbStateNotify, c.XkbModifierStateMask, c.XkbModifierStateMask) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
/// keyboard state based on if it finds any. This is necessary as the
|
||||
/// standard GTK X11 API (and X11 in general) does not include the current
|
||||
/// key pressed in any modifier state snapshot for that event (e.g. if the
|
||||
/// pressed key is a modifier, that is not necessarily reflected in the
|
||||
/// modifiers).
|
||||
///
|
||||
/// Returns null if there is no event. In this case, the caller should fall
|
||||
/// back to the standard GDK modifier state (this likely means the key
|
||||
/// event did not result in a modifier change).
|
||||
pub fn modifier_state_from_notify(self: X11Xkb, display_: ?*c.GdkDisplay) ?input.Mods {
|
||||
const display = display_ orelse return null;
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
||||
if (self.XEventsQueued(xdisplay, c.QueuedAfterReading) != 0) {
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = self.XPeekEvent(xdisplay, &nextEvent);
|
||||
if (nextEvent.type == self.base_event_code) {
|
||||
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
|
||||
if (xkb_event.any.xkb_type == c.XkbStateNotify) {
|
||||
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
|
||||
// Check the state according to XKB masks.
|
||||
const lookup_mods = xkb_state_notify_event.lookup_mods;
|
||||
var mods: input.Mods = .{};
|
||||
|
||||
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
|
||||
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
|
||||
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
|
||||
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
|
||||
if (lookup_mods & c.Mod2Mask != 0) mods.super = true;
|
||||
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
|
||||
|
||||
return mods;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user