From 9e435f050e6d4f5571f2b92af7dca216a1627a55 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Oct 2024 13:01:27 -0700 Subject: [PATCH 1/3] apprt/gtk: setup window key event infrastructure --- src/apprt/gtk/Surface.zig | 115 +++++++------------------------------- src/apprt/gtk/Window.zig | 103 ++++++++++++++++++++++++++++++++++ src/apprt/gtk/key.zig | 97 ++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 94 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index dbe96aa2d..43b36a9eb 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1268,7 +1268,7 @@ fn gtkMouseDown( const gtk_mods = c.gdk_event_get_modifier_state(event); 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. 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 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.?); _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { @@ -1342,7 +1342,7 @@ fn gtkMouseMotion( // Get our modifiers const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); 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| { log.err("error in cursor pos callback err={}", .{err}); @@ -1359,7 +1359,7 @@ fn gtkMouseLeave( // Get our modifiers const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); 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| { log.err("error in cursor pos callback err={}", .{err}); return; @@ -1442,54 +1442,17 @@ fn keyEvent( ) bool { 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)); + const event = c.gtk_event_controller_get_current_event( + @ptrCast(ec_key), + ) orelse return false; // Get the unshifted unicode value of the keyval. This is used // by the Kitty keyboard protocol. - const keyval_unicode_unshifted: u21 = unshifted: { - // 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) 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; - }; + const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted( + @ptrCast(self.gl_area), + event, + keycode, + ); // We always reset our committed text when ending a keypress so that // future keypresses don't think we have a commit event. @@ -1549,44 +1512,20 @@ fn keyEvent( if (entry.native == keycode) break :keycode entry.key; } else .invalid; - const mods = mods: { - const device = c.gdk_event_get_device(event); - - var mods = if (self.app.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 => {}, - } - - break :mods mods; - }; + // Get our modifier for the event + const mods: input.Mods = gtk_key.eventMods( + @ptrCast(self.gl_area), + event, + physical_key, + gtk_mods, + if (self.app.x11_xkb) |*xkb| xkb else null, + ); // Get our consumed modifiers const consumed_mods: input.Mods = consumed: { const raw = c.gdk_key_event_get_consumed_modifiers(event); 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 @@ -1908,18 +1847,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 { if (self.container.window()) |window| { if (self.container.tab()) |tab| { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index ff8735ff9..2931456b7 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -21,6 +21,7 @@ const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); const c = @import("c.zig").c; const adwaita = @import("adwaita.zig"); +const gtk_key = @import("key.zig"); const Notebook = @import("notebook.zig").Notebook; 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 (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 _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); // Our actions for the menu initActions(self); @@ -662,6 +671,100 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { 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.?); + const keyval_unicode = c.gdk_keyval_to_unicode(keyval); + const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)) orelse return 0; + + // We want to get the physical unmapped key to process physical keybinds. + // (These are keybinds explicitly marked as requesting physical mapping). + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == keycode) break :keycode entry.key; + } else .invalid; + + // Get our modifier for the event + const mods: input.Mods = gtk_key.eventMods( + @ptrCast(self.window), + event, + physical_key, + gtk_mods, + if (self.app.x11_xkb) |*xkb| xkb else null, + ); + + // Get our consumed modifiers + const consumed_mods: input.Mods = consumed: { + const raw = c.gdk_key_event_get_consumed_modifiers(event); + const masked = raw & c.GDK_MODIFIER_MASK; + break :consumed gtk_key.translateMods(masked); + }; + + // Get the unshifted unicode value of the keyval. + const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted( + @ptrCast(self.window), + event, + keycode, + ); + + // If we're not in a dead key state, we want to translate our text + // to some input.Key. + const key: input.Key = key: { + // First, try to convert the keyval directly to a key. This allows the + // use of key remapping and identification of keypad numerics (as + // opposed to their ASCII counterparts) + if (gtk_key.keyFromKeyval(keyval)) |key| { + break :key key; + } + + // If that doesn't work then we try to translate the kevval.. + if (keyval_unicode != 0) { + if (std.math.cast(u8, keyval_unicode)) |byte| { + if (input.Key.fromASCII(byte)) |key| { + break :key key; + } + } + } + + // If that doesn't work we use the unshifted value... + if (std.math.cast(u8, keyval_unicode_unshifted)) |ascii| { + if (input.Key.fromASCII(ascii)) |key| { + break :key key; + } + } + + if (keyval_unicode_unshifted != 0) break :key .invalid; + break :key physical_key; + }; + + // Build our final key event + const core_event: input.KeyEvent = .{ + .action = .press, + .key = key, + .physical_key = physical_key, + .mods = mods, + .consumed_mods = consumed_mods, + .composing = false, + .utf8 = "", + .unshifted_codepoint = keyval_unicode_unshifted, + }; + + // log.debug("attempting app-scoped key event={}", .{core_event}); + + // Invoke the core Ghostty logic to handle this input. + const consumed = self.app.core_app.keyEvent(self.app, core_event); + if (consumed) { + log.info("app-scoped key consumed event={}", .{core_event}); + return 1; + } + + return 0; +} + fn gtkActionAbout( _: *c.GSimpleAction, _: *c.GVariant, diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index da4f6ea2b..f8458bd3b 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -1,6 +1,7 @@ const std = @import("std"); const input = @import("../../input.zig"); const c = @import("c.zig").c; +const x11 = @import("x11.zig"); /// Returns a GTK accelerator string from a trigger. 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; } +// 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. pub fn keyFromKeyval(keyval: c.guint) ?input.Key { for (keymap) |entry| { From 9c547dd8cd63e611f0828299d67314cf83e6ac4a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Oct 2024 13:06:25 -0700 Subject: [PATCH 2/3] apprt/gtk: forward window key events to the focused surface --- src/apprt/gtk/Surface.zig | 23 +++++++--- src/apprt/gtk/Window.zig | 90 +++------------------------------------ 2 files changed, 25 insertions(+), 88 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 43b36a9eb..e00304491 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1395,7 +1395,14 @@ fn gtkKeyPressed( gtk_mods: c.GdkModifierType, ud: ?*anyopaque, ) 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( @@ -1405,7 +1412,14 @@ fn gtkKeyReleased( state: c.GdkModifierType, ud: ?*anyopaque, ) 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, @@ -1432,15 +1446,14 @@ fn gtkKeyReleased( /// Note we ALSO have an IMContext attached directly to the widget /// which can emit preedit and commit callbacks. But, if we're not /// in a keypress, we let those automatically work. -fn keyEvent( +pub fn keyEvent( + self: *Surface, action: input.Action, ec_key: *c.GtkEventControllerKey, keyval: c.guint, keycode: c.guint, gtk_mods: c.GdkModifierType, - ud: ?*anyopaque, ) bool { - 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), diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2931456b7..d24542a63 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -679,90 +679,14 @@ fn gtkKeyPressed( ud: ?*anyopaque, ) callconv(.C) c.gboolean { 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)) orelse return 0; - - // We want to get the physical unmapped key to process physical keybinds. - // (These are keybinds explicitly marked as requesting physical mapping). - const physical_key = keycode: for (input.keycodes.entries) |entry| { - if (entry.native == keycode) break :keycode entry.key; - } else .invalid; - - // Get our modifier for the event - const mods: input.Mods = gtk_key.eventMods( - @ptrCast(self.window), - event, - physical_key, - gtk_mods, - if (self.app.x11_xkb) |*xkb| xkb else null, - ); - - // Get our consumed modifiers - const consumed_mods: input.Mods = consumed: { - const raw = c.gdk_key_event_get_consumed_modifiers(event); - const masked = raw & c.GDK_MODIFIER_MASK; - break :consumed gtk_key.translateMods(masked); - }; - - // Get the unshifted unicode value of the keyval. - const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted( - @ptrCast(self.window), - event, + const surface = self.app.core_app.focusedSurface() orelse return 0; + return if (surface.rt_surface.keyEvent( + .press, + ec_key, + keyval, keycode, - ); - - // If we're not in a dead key state, we want to translate our text - // to some input.Key. - const key: input.Key = key: { - // First, try to convert the keyval directly to a key. This allows the - // use of key remapping and identification of keypad numerics (as - // opposed to their ASCII counterparts) - if (gtk_key.keyFromKeyval(keyval)) |key| { - break :key key; - } - - // If that doesn't work then we try to translate the kevval.. - if (keyval_unicode != 0) { - if (std.math.cast(u8, keyval_unicode)) |byte| { - if (input.Key.fromASCII(byte)) |key| { - break :key key; - } - } - } - - // If that doesn't work we use the unshifted value... - if (std.math.cast(u8, keyval_unicode_unshifted)) |ascii| { - if (input.Key.fromASCII(ascii)) |key| { - break :key key; - } - } - - if (keyval_unicode_unshifted != 0) break :key .invalid; - break :key physical_key; - }; - - // Build our final key event - const core_event: input.KeyEvent = .{ - .action = .press, - .key = key, - .physical_key = physical_key, - .mods = mods, - .consumed_mods = consumed_mods, - .composing = false, - .utf8 = "", - .unshifted_codepoint = keyval_unicode_unshifted, - }; - - // log.debug("attempting app-scoped key event={}", .{core_event}); - - // Invoke the core Ghostty logic to handle this input. - const consumed = self.app.core_app.keyEvent(self.app, core_event); - if (consumed) { - log.info("app-scoped key consumed event={}", .{core_event}); - return 1; - } - - return 0; + gtk_mods, + )) 1 else 0; } fn gtkActionAbout( From 1b1589e0c641ccad084233bd05f6705520286f62 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Oct 2024 13:17:14 -0700 Subject: [PATCH 3/3] apprt/gtk: only run window key events with tab overview shown --- src/apprt/gtk/Window.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d24542a63..efde78e61 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -679,6 +679,22 @@ fn gtkKeyPressed( 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,