Merge pull request #2406 from ghostty-org/gtk-key

gtk: handle key press events at the window level if necessary
This commit is contained in:
Mitchell Hashimoto
2024-10-07 10:20:54 -10:00
committed by GitHub
3 changed files with 179 additions and 99 deletions

View File

@ -1268,7 +1268,7 @@ fn gtkMouseDown(
const gtk_mods = c.gdk_event_get_modifier_state(event); const gtk_mods = c.gdk_event_get_modifier_state(event);
const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
const mods = translateMods(gtk_mods); const mods = gtk_key.translateMods(gtk_mods);
// If we don't have focus, grab it. // If we don't have focus, grab it.
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
@ -1300,7 +1300,7 @@ fn gtkMouseUp(
const gtk_mods = c.gdk_event_get_modifier_state(event); const gtk_mods = c.gdk_event_get_modifier_state(event);
const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
const mods = translateMods(gtk_mods); const mods = gtk_key.translateMods(gtk_mods);
const self = userdataSelf(ud.?); const self = userdataSelf(ud.?);
_ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| {
@ -1342,7 +1342,7 @@ fn gtkMouseMotion(
// Get our modifiers // Get our modifiers
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
const gtk_mods = c.gdk_event_get_modifier_state(event); const gtk_mods = c.gdk_event_get_modifier_state(event);
const mods = translateMods(gtk_mods); const mods = gtk_key.translateMods(gtk_mods);
self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
log.err("error in cursor pos callback err={}", .{err}); log.err("error in cursor pos callback err={}", .{err});
@ -1359,7 +1359,7 @@ fn gtkMouseLeave(
// Get our modifiers // Get our modifiers
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
const gtk_mods = c.gdk_event_get_modifier_state(event); const gtk_mods = c.gdk_event_get_modifier_state(event);
const mods = translateMods(gtk_mods); const mods = gtk_key.translateMods(gtk_mods);
self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| { self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| {
log.err("error in cursor pos callback err={}", .{err}); log.err("error in cursor pos callback err={}", .{err});
return; return;
@ -1395,7 +1395,14 @@ fn gtkKeyPressed(
gtk_mods: c.GdkModifierType, gtk_mods: c.GdkModifierType,
ud: ?*anyopaque, ud: ?*anyopaque,
) callconv(.C) c.gboolean { ) callconv(.C) c.gboolean {
return if (keyEvent(.press, ec_key, keyval, keycode, gtk_mods, ud)) 1 else 0; const self = userdataSelf(ud.?);
return if (self.keyEvent(
.press,
ec_key,
keyval,
keycode,
gtk_mods,
)) 1 else 0;
} }
fn gtkKeyReleased( fn gtkKeyReleased(
@ -1405,7 +1412,14 @@ fn gtkKeyReleased(
state: c.GdkModifierType, state: c.GdkModifierType,
ud: ?*anyopaque, ud: ?*anyopaque,
) callconv(.C) c.gboolean { ) callconv(.C) c.gboolean {
return if (keyEvent(.release, ec_key, keyval, keycode, state, ud)) 1 else 0; const self = userdataSelf(ud.?);
return if (self.keyEvent(
.release,
ec_key,
keyval,
keycode,
state,
)) 1 else 0;
} }
/// Key press event. This is where we do ALL of our key handling, /// Key press event. This is where we do ALL of our key handling,
@ -1432,64 +1446,26 @@ fn gtkKeyReleased(
/// Note we ALSO have an IMContext attached directly to the widget /// Note we ALSO have an IMContext attached directly to the widget
/// which can emit preedit and commit callbacks. But, if we're not /// which can emit preedit and commit callbacks. But, if we're not
/// in a keypress, we let those automatically work. /// in a keypress, we let those automatically work.
fn keyEvent( pub fn keyEvent(
self: *Surface,
action: input.Action, action: input.Action,
ec_key: *c.GtkEventControllerKey, ec_key: *c.GtkEventControllerKey,
keyval: c.guint, keyval: c.guint,
keycode: c.guint, keycode: c.guint,
gtk_mods: c.GdkModifierType, gtk_mods: c.GdkModifierType,
ud: ?*anyopaque,
) bool { ) bool {
const self = userdataSelf(ud.?);
const keyval_unicode = c.gdk_keyval_to_unicode(keyval); const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); const event = c.gtk_event_controller_get_current_event(
const display = c.gtk_widget_get_display(@ptrCast(self.gl_area)); @ptrCast(ec_key),
) orelse return false;
// Get the unshifted unicode value of the keyval. This is used // Get the unshifted unicode value of the keyval. This is used
// by the Kitty keyboard protocol. // by the Kitty keyboard protocol.
const keyval_unicode_unshifted: u21 = unshifted: { const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
// We need to get the currently active keyboard layout so we know @ptrCast(self.gl_area),
// what group to look at. event,
const layout = c.gdk_key_event_get_layout(@ptrCast(event)); keycode,
);
// Get all the possible keyboard mappings for this keycode. A keycode
// is the physical key pressed.
var keys: [*]c.GdkKeymapKey = undefined;
var keyvals: [*]c.guint = undefined;
var n_keys: c_int = 0;
if (c.gdk_display_map_keycode(
display,
keycode,
@ptrCast(&keys),
@ptrCast(&keyvals),
&n_keys,
) == 0) break :unshifted 0;
defer c.g_free(keys);
defer c.g_free(keyvals);
// debugging:
// log.debug("layout={}", .{layout});
// for (0..@intCast(n_keys)) |i| {
// log.debug("keymap key={} codepoint={x}", .{
// keys[i],
// c.gdk_keyval_to_unicode(keyvals[i]),
// });
// }
for (0..@intCast(n_keys)) |i| {
if (keys[i].group == layout and
keys[i].level == 0)
{
break :unshifted std.math.cast(
u21,
c.gdk_keyval_to_unicode(keyvals[i]),
) orelse 0;
}
}
break :unshifted 0;
};
// We always reset our committed text when ending a keypress so that // We always reset our committed text when ending a keypress so that
// future keypresses don't think we have a commit event. // future keypresses don't think we have a commit event.
@ -1549,44 +1525,20 @@ fn keyEvent(
if (entry.native == keycode) break :keycode entry.key; if (entry.native == keycode) break :keycode entry.key;
} else .invalid; } else .invalid;
const mods = mods: { // Get our modifier for the event
const device = c.gdk_event_get_device(event); const mods: input.Mods = gtk_key.eventMods(
@ptrCast(self.gl_area),
var mods = if (self.app.x11_xkb) |xkb| event,
// Add any modifier state events from Xkb if we have them (X11 physical_key,
// only). Null back from the Xkb call means there was no modifier gtk_mods,
// event to read. This likely means that the key event did not if (self.app.x11_xkb) |*xkb| xkb else null,
// result in a modifier change and we can safely rely on the GDK );
// state.
xkb.modifier_state_from_notify(display) orelse translateMods(gtk_mods)
else
// On Wayland, we have to use the GDK device because the mods sent
// to this event do not have the modifier key applied if it was
// presssed (i.e. left control).
translateMods(c.gdk_device_get_modifier_state(device));
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
switch (physical_key) {
.left_shift => mods.sides.shift = .left,
.right_shift => mods.sides.shift = .right,
.left_control => mods.sides.ctrl = .left,
.right_control => mods.sides.ctrl = .right,
.left_alt => mods.sides.alt = .left,
.right_alt => mods.sides.alt = .right,
.left_super => mods.sides.super = .left,
.right_super => mods.sides.super = .right,
else => {},
}
break :mods mods;
};
// Get our consumed modifiers // Get our consumed modifiers
const consumed_mods: input.Mods = consumed: { const consumed_mods: input.Mods = consumed: {
const raw = c.gdk_key_event_get_consumed_modifiers(event); const raw = c.gdk_key_event_get_consumed_modifiers(event);
const masked = raw & c.GDK_MODIFIER_MASK; const masked = raw & c.GDK_MODIFIER_MASK;
break :consumed translateMods(masked); break :consumed gtk_key.translateMods(masked);
}; };
// If we're not in a dead key state, we want to translate our text // If we're not in a dead key state, we want to translate our text
@ -1908,18 +1860,6 @@ fn translateMouseButton(button: c.guint) input.MouseButton {
}; };
} }
fn translateMods(state: c.GdkModifierType) input.Mods {
var mods: input.Mods = .{};
if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true;
if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true;
if (state & c.GDK_ALT_MASK != 0) mods.alt = true;
if (state & c.GDK_SUPER_MASK != 0) mods.super = true;
// Lock is dependent on the X settings but we just assume caps lock.
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;
return mods;
}
pub fn present(self: *Surface) void { pub fn present(self: *Surface) void {
if (self.container.window()) |window| { if (self.container.window()) |window| {
if (self.container.tab()) |tab| { if (self.container.tab()) |tab| {

View File

@ -21,6 +21,7 @@ const Surface = @import("Surface.zig");
const Tab = @import("Tab.zig"); const Tab = @import("Tab.zig");
const c = @import("c.zig").c; const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig"); const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook; const Notebook = @import("notebook.zig").Notebook;
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -255,10 +256,18 @@ pub fn init(self: *Window, app: *App) !void {
// If we are in fullscreen mode, new windows start fullscreen. // If we are in fullscreen mode, new windows start fullscreen.
if (app.config.fullscreen) c.gtk_window_fullscreen(self.window); if (app.config.fullscreen) c.gtk_window_fullscreen(self.window);
// We register a key event controller with the window so
// we can catch key events when our surface may not be
// focused (i.e. when the libadw tab overview is shown).
const ec_key_press = c.gtk_event_controller_key_new();
errdefer c.g_object_unref(ec_key_press);
c.gtk_widget_add_controller(window, ec_key_press);
// All of our events // All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(&gtkKeyPressed), self, null, c.G_CONNECT_DEFAULT);
// Our actions for the menu // Our actions for the menu
initActions(self); initActions(self);
@ -662,6 +671,40 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
alloc.destroy(self); alloc.destroy(self);
} }
fn gtkKeyPressed(
ec_key: *c.GtkEventControllerKey,
keyval: c.guint,
keycode: c.guint,
gtk_mods: c.GdkModifierType,
ud: ?*anyopaque,
) callconv(.C) c.gboolean {
const self = userdataSelf(ud.?);
// We only process window-level events currently for the tab
// overview. This is primarily defensive programming because
// I'm not 100% certain how our logic below will interact with
// other parts of the application but I know for sure we must
// handle this during the tab overview.
//
// If someone can confidently show or explain that this is not
// necessary, please remove this check.
if (comptime adwaita.versionAtLeast(1, 4, 0)) {
if (self.tab_overview) |tab_overview_widget| {
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0;
}
}
const surface = self.app.core_app.focusedSurface() orelse return 0;
return if (surface.rt_surface.keyEvent(
.press,
ec_key,
keyval,
keycode,
gtk_mods,
)) 1 else 0;
}
fn gtkActionAbout( fn gtkActionAbout(
_: *c.GSimpleAction, _: *c.GSimpleAction,
_: *c.GVariant, _: *c.GVariant,

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const input = @import("../../input.zig"); const input = @import("../../input.zig");
const c = @import("c.zig").c; const c = @import("c.zig").c;
const x11 = @import("x11.zig");
/// Returns a GTK accelerator string from a trigger. /// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 { pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
@ -47,6 +48,102 @@ pub fn translateMods(state: c.GdkModifierType) input.Mods {
return mods; return mods;
} }
// Get the unshifted unicode value of the keyval. This is used
// by the Kitty keyboard protocol.
pub fn keyvalUnicodeUnshifted(
widget: *c.GtkWidget,
event: *c.GdkEvent,
keycode: c.guint,
) u21 {
const display = c.gtk_widget_get_display(widget);
// We need to get the currently active keyboard layout so we know
// what group to look at.
const layout = c.gdk_key_event_get_layout(@ptrCast(event));
// Get all the possible keyboard mappings for this keycode. A keycode
// is the physical key pressed.
var keys: [*]c.GdkKeymapKey = undefined;
var keyvals: [*]c.guint = undefined;
var n_keys: c_int = 0;
if (c.gdk_display_map_keycode(
display,
keycode,
@ptrCast(&keys),
@ptrCast(&keyvals),
&n_keys,
) == 0) return 0;
defer c.g_free(keys);
defer c.g_free(keyvals);
// debugging:
// log.debug("layout={}", .{layout});
// for (0..@intCast(n_keys)) |i| {
// log.debug("keymap key={} codepoint={x}", .{
// keys[i],
// c.gdk_keyval_to_unicode(keyvals[i]),
// });
// }
for (0..@intCast(n_keys)) |i| {
if (keys[i].group == layout and
keys[i].level == 0)
{
return std.math.cast(
u21,
c.gdk_keyval_to_unicode(keyvals[i]),
) orelse 0;
}
}
return 0;
}
/// Returns the mods to use a key event from a GTK event.
/// This requires a lot of context because the GdkEvent
/// doesn't contain enough on its own.
pub fn eventMods(
widget: *c.GtkWidget,
event: *c.GdkEvent,
physical_key: input.Key,
gtk_mods: c.GdkModifierType,
x11_xkb: ?*x11.Xkb,
) input.Mods {
const device = c.gdk_event_get_device(event);
const display = c.gtk_widget_get_display(widget);
var mods = if (x11_xkb) |xkb|
// Add any modifier state events from Xkb if we have them (X11
// only). 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.
xkb.modifier_state_from_notify(display) orelse
translateMods(gtk_mods)
else
// On Wayland, we have to use the GDK device because the mods sent
// to this event do not have the modifier key applied if it was
// presssed (i.e. left control).
translateMods(c.gdk_device_get_modifier_state(device));
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
switch (physical_key) {
.left_shift => mods.sides.shift = .left,
.right_shift => mods.sides.shift = .right,
.left_control => mods.sides.ctrl = .left,
.right_control => mods.sides.ctrl = .right,
.left_alt => mods.sides.alt = .left,
.right_alt => mods.sides.alt = .right,
.left_super => mods.sides.super = .left,
.right_super => mods.sides.super = .right,
else => {},
}
return mods;
}
/// Returns an input key from a keyval or null if we don't have a mapping. /// Returns an input key from a keyval or null if we don't have a mapping.
pub fn keyFromKeyval(keyval: c.guint) ?input.Key { pub fn keyFromKeyval(keyval: c.guint) ?input.Key {
for (keymap) |entry| { for (keymap) |entry| {