gtk: avoid physical fallback for XKB modifiers

This commit is contained in:
Joseph Corneli
2026-08-22 08:20:19 +01:00
parent 36676c5728
commit ef01d5fda7
2 changed files with 93 additions and 13 deletions

View File

@@ -1378,19 +1378,11 @@ pub const Surface = extern struct {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// Consult the pre-remapped XKB keyval/keysym to get the (possibly)
// remapped key. If the W3C key or the remapped key
// is eligible for remapping, we use it.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (gtk_key.keyFromKeyval(keyval)) |remapped| {
if (w3c_key.shouldBeRemappable() or remapped.shouldBeRemappable())
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
break :keycode gtk_key.remapKey(
w3c_key,
keyval,
key_event.isModifier() != 0,
);
};
// Get our modifier for the event

View File

@@ -227,6 +227,94 @@ pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
return null;
}
/// Returns the logical key after applying any eligible XKB remapping.
///
/// Modifier keyvals that Ghostty doesn't represent must not fall back to the
/// physical key. XKB can turn any physical key into a modifier, and using the
/// original key in that case would encode an unrelated key press.
pub fn remapKey(
physical_key: input.Key,
keyval: c_uint,
is_modifier: bool,
) input.Key {
if (keyFromKeyval(keyval)) |remapped| {
if (physical_key.shouldBeRemappable() or remapped.shouldBeRemappable())
return remapped;
}
if (is_modifier or isModifierKeyval(keyval)) return .unidentified;
return physical_key;
}
/// Returns whether a keyval has modifier semantics that GDK may not expose
/// through `gdk_key_event_is_modifier`.
///
/// In particular, XKB can report ISO level and group modifiers as keyvals
/// such as `Mode_switch` while GDK reports that the event is not a modifier.
fn isModifierKeyval(keyval: c_uint) bool {
return switch (keyval) {
gdk.KEY_Shift_L,
gdk.KEY_Shift_R,
gdk.KEY_Shift_Lock,
gdk.KEY_Control_L,
gdk.KEY_Control_R,
gdk.KEY_Caps_Lock,
gdk.KEY_Meta_L,
gdk.KEY_Meta_R,
gdk.KEY_Alt_L,
gdk.KEY_Alt_R,
gdk.KEY_Super_L,
gdk.KEY_Super_R,
gdk.KEY_Hyper_L,
gdk.KEY_Hyper_R,
gdk.KEY_Num_Lock,
gdk.KEY_Mode_switch,
gdk.KEY_ISO_Lock,
gdk.KEY_ISO_Level2_Latch,
gdk.KEY_ISO_Level3_Shift,
gdk.KEY_ISO_Level3_Latch,
gdk.KEY_ISO_Level3_Lock,
gdk.KEY_ISO_Level5_Shift,
gdk.KEY_ISO_Level5_Latch,
gdk.KEY_ISO_Level5_Lock,
gdk.KEY_ISO_Group_Latch,
gdk.KEY_ISO_Group_Lock,
=> true,
else => false,
};
}
test "remap key" {
const testing = std.testing;
// XKB remaps between non-writing-system keys are honored.
try testing.expectEqual(
input.Key.escape,
remapKey(.caps_lock, gdk.KEY_Escape, false),
);
// Writing-system keys retain their physical identity.
try testing.expectEqual(
input.Key.key_a,
remapKey(.key_a, gdk.KEY_b, false),
);
// GDK doesn't identify Mode_switch as a modifier, but it must not fall
// back to the physical key.
try testing.expectEqual(
input.Key.unidentified,
remapKey(.delete, gdk.KEY_Mode_switch, false),
);
// GDK's modifier flag also covers custom modifier actions whose keyval
// Ghostty doesn't represent.
try testing.expectEqual(
input.Key.unidentified,
remapKey(.delete, gdk.KEY_VoidSymbol, true),
);
}
/// Returns a keyval from an input key or null if we don't have a mapping.
pub fn keyvalFromKey(key: input.Key) ?c_uint {
switch (key) {