mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
apprt/gtk: use manual translation, handle dead key states
This commit is contained in:
@ -675,6 +675,13 @@ pub const Surface = struct {
|
||||
cursor_pos: apprt.CursorPos,
|
||||
clipboard: c.GValue,
|
||||
|
||||
/// Key input states. See gtkKeyPressed for detailed descriptions.
|
||||
in_keypress: bool = false,
|
||||
im_context: *c.GtkIMContext,
|
||||
im_composing: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
const widget = @as(*c.GtkWidget, @ptrCast(opts.gl_area));
|
||||
c.gtk_gl_area_set_required_version(opts.gl_area, 3, 3);
|
||||
@ -694,15 +701,6 @@ pub const Surface = struct {
|
||||
c.gtk_widget_add_controller(widget, ec_focus);
|
||||
errdefer c.gtk_widget_remove_controller(widget, ec_focus);
|
||||
|
||||
// Tell the key controller that we're interested in getting a full
|
||||
// input method so raw characters/strings are given too.
|
||||
const im_context = c.gtk_im_multicontext_new();
|
||||
errdefer c.g_object_unref(im_context);
|
||||
c.gtk_event_controller_key_set_im_context(
|
||||
@ptrCast(ec_key),
|
||||
im_context,
|
||||
);
|
||||
|
||||
// Create a second key controller so we can receive the raw
|
||||
// key-press events BEFORE the input method gets them.
|
||||
const ec_key_press = c.gtk_event_controller_key_new();
|
||||
@ -729,6 +727,12 @@ pub const Surface = struct {
|
||||
errdefer c.g_object_unref(ec_scroll);
|
||||
c.gtk_widget_add_controller(widget, ec_scroll);
|
||||
|
||||
// The input method context that we use to translate key events into
|
||||
// characters. This doesn't have an event key controller attached because
|
||||
// we call it manually from our own key controller.
|
||||
const im_context = c.gtk_im_multicontext_new();
|
||||
errdefer c.g_object_unref(im_context);
|
||||
|
||||
// The GL area has to be focusable so that it can receive events
|
||||
c.gtk_widget_set_focusable(widget, 1);
|
||||
c.gtk_widget_set_focus_on_click(widget, 1);
|
||||
@ -748,6 +752,7 @@ pub const Surface = struct {
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.clipboard = std.mem.zeroes(c.GValue),
|
||||
.im_context = im_context,
|
||||
};
|
||||
errdefer self.* = undefined;
|
||||
|
||||
@ -761,11 +766,14 @@ pub const Surface = struct {
|
||||
_ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "preedit-start", c.G_CALLBACK(>kInputPreeditStart), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, G_CONNECT_DEFAULT);
|
||||
}
|
||||
|
||||
fn realize(self: *Surface) !void {
|
||||
@ -792,8 +800,6 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
c.g_value_unset(&self.clipboard);
|
||||
|
||||
// We don't allocate anything if we aren't realized.
|
||||
if (!self.realized) return;
|
||||
|
||||
@ -803,6 +809,10 @@ pub const Surface = struct {
|
||||
// Clean up our core surface so that all the rendering and IO stop.
|
||||
self.core_surface.deinit();
|
||||
self.core_surface = undefined;
|
||||
|
||||
// Free all our GTK stuff
|
||||
c.g_object_unref(self.im_context);
|
||||
c.g_value_unset(&self.clipboard);
|
||||
}
|
||||
|
||||
fn render(self: *Surface) !void {
|
||||
@ -1123,57 +1133,129 @@ pub const Surface = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Key press event. This is where we do ALL of our key handling,
|
||||
/// translation to keyboard layouts, dead key handling, etc. Key handling
|
||||
/// is complicated so this comment will explain what's going on.
|
||||
///
|
||||
/// At a high level, we want to do the following:
|
||||
///
|
||||
/// 1. Emit a keyCallback for the key press with the right keys.
|
||||
/// 2. Emit a charCallback if a unicode char was generated from the
|
||||
/// keypresses, but only if keyCallback didn't consume the input.
|
||||
///
|
||||
/// This callback will first set the "in_keypress" flag to true. This
|
||||
/// lets our IM callbacks know that we're in a keypress event so they don't
|
||||
/// emit a charCallback since this function will do it after the keyCallback
|
||||
/// (remember, the order matters!).
|
||||
///
|
||||
/// Next, we run the keypress through the input method context in order
|
||||
/// to determine if we're in a dead key state, completed unicode char, etc.
|
||||
/// This all happens through various callbacks: preedit, commit, etc.
|
||||
/// These inspect "in_keypress" if they have to and set some instance
|
||||
/// state.
|
||||
///
|
||||
/// Finally, we map our keys to input.Keys, emit the keyCallback, then
|
||||
/// emit the charCallback if we have to.
|
||||
///
|
||||
/// 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 gtkKeyPressed(
|
||||
_: *c.GtkEventControllerKey,
|
||||
keyval_event: c.guint,
|
||||
ec_key: *c.GtkEventControllerKey,
|
||||
_: c.guint,
|
||||
keycode: c.guint,
|
||||
state: c.GdkModifierType,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
const self = userdataSelf(ud.?);
|
||||
const display = c.gtk_widget_get_display(@ptrCast(self.gl_area)).?;
|
||||
const mods = translateMods(gtk_mods);
|
||||
|
||||
// We want to use only the key that corresponds to the hardware key.
|
||||
// I suspect this logic is actually wrong for customized keyboards,
|
||||
// maybe international keyboards, but I don't have an easy way to
|
||||
// test that that I know of... sorry!
|
||||
var keys: [*c]c.GdkKeymapKey = undefined;
|
||||
var keyvals: [*c]c.guint = undefined;
|
||||
var keys_len: c_int = undefined;
|
||||
const found = c.gdk_display_map_keycode(display, keycode, &keys, &keyvals, &keys_len);
|
||||
defer if (found > 0) {
|
||||
c.g_free(keys);
|
||||
c.g_free(keyvals);
|
||||
};
|
||||
// We mark that we're in a keypress event. We use this in our
|
||||
// IM commit callback to determine if we need to send a char callback
|
||||
// to the core surface or not.
|
||||
self.in_keypress = true;
|
||||
defer self.in_keypress = false;
|
||||
|
||||
// We look for the keyval corresponding to this key pressed with
|
||||
// zero modifiers. We're assuming this always exist but unsure if
|
||||
// that assumption is true.
|
||||
const keyval = keyval: {
|
||||
if (found > 0) {
|
||||
for (keys[0..@intCast(keys_len)], 0..) |key, i| {
|
||||
if (key.group == 0 and key.level == 0)
|
||||
break :keyval keyvals[i];
|
||||
}
|
||||
}
|
||||
// We always reset our committed text when ending a keypress so that
|
||||
// future keypresses don't think we have a commit event.
|
||||
defer self.im_len = 0;
|
||||
|
||||
log.warn("key-press with unknown key keyval={} keycode={}", .{
|
||||
keyval_event,
|
||||
keycode,
|
||||
});
|
||||
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;
|
||||
|
||||
const key = translateKey(keyval);
|
||||
const mods = translateMods(state);
|
||||
log.debug("key-press code={} key={} mods={}", .{ keycode, key, mods });
|
||||
const processed = self.core_surface.keyCallback(.press, key, key, mods) catch |err| {
|
||||
// Pass the event through the IM controller to handle dead key states.
|
||||
// Filter is true if the event was handled by the IM controller.
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
|
||||
_ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
|
||||
|
||||
// If we're not in a dead key state, we want to translate our text
|
||||
// to some input.Key.
|
||||
const key = if (!self.im_composing) key: {
|
||||
if (self.im_len != 1) break :key physical_key;
|
||||
break :key input.Key.fromASCII(self.im_buf[0]) orelse physical_key;
|
||||
} else .invalid;
|
||||
|
||||
// If both keys are invalid then we won't call the key callback. But
|
||||
// if either one is valid, we want to give it a chance.
|
||||
if (key != .invalid or physical_key != .invalid) {
|
||||
const consumed = self.core_surface.keyCallback(
|
||||
.press,
|
||||
key,
|
||||
physical_key,
|
||||
mods,
|
||||
) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
// If we processed the key, we say we handled it.
|
||||
return if (processed) 1 else 0;
|
||||
// If we consume the key then we want to reset the dead key state.
|
||||
if (consumed) {
|
||||
c.gtk_im_context_reset(self.im_context);
|
||||
|
||||
// This is kloodge right now to reset the surface ignore_char
|
||||
// state. We should refactor the API contract with the surface
|
||||
// to be that if we consume a key then we don't call the char
|
||||
// callback.
|
||||
//
|
||||
// If you don't do this, then after a consumed char a pure
|
||||
// char event will be ignored. i.e. an emoji keyboard entry.
|
||||
self.core_surface.charCallback(0) catch {};
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a dead key, then we're composing a character and
|
||||
// we end processing here. We don't process keybinds for dead keys.
|
||||
if (self.im_composing) {
|
||||
// TODO: we ultimately want to update some surface state so that
|
||||
// we can show the user that we're in dead key mode and the
|
||||
// precomposed character. For now, we can just ignore and that
|
||||
// is not incorrect behavior.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Next, we want to call the char callback with each codepoint.
|
||||
if (self.im_len > 0) {
|
||||
const text = self.im_buf[0..self.im_len];
|
||||
const view = std.unicode.Utf8View.init(text) catch |err| {
|
||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn gtkKeyReleased(
|
||||
@ -1188,12 +1270,52 @@ pub const Surface = struct {
|
||||
const key = translateKey(keyval);
|
||||
const mods = translateMods(state);
|
||||
const self = userdataSelf(ud.?);
|
||||
self.core_surface.keyCallback(.release, key, key, mods) catch |err| {
|
||||
const consumed = self.core_surface.keyCallback(.release, key, key, mods) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
return 0;
|
||||
return if (consumed) 1 else 0;
|
||||
}
|
||||
|
||||
fn gtkInputPreeditStart(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit start", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Mark that we are now composing a string with a dead key state.
|
||||
// We'll record the string in the preedit-changed callback.
|
||||
self.im_composing = true;
|
||||
}
|
||||
|
||||
fn gtkInputPreeditChanged(
|
||||
ctx: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Get our pre-edit string that we'll use to show the user.
|
||||
var buf: [*c]u8 = undefined;
|
||||
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
|
||||
defer c.g_free(buf);
|
||||
const str = std.mem.sliceTo(buf, 0);
|
||||
log.debug("preedit str={s}", .{str});
|
||||
|
||||
// TODO: actually use this string.
|
||||
}
|
||||
|
||||
fn gtkInputPreeditEnd(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit end", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
self.im_composing = false;
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
@ -1201,13 +1323,30 @@ pub const Surface = struct {
|
||||
bytes: [*:0]u8,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud.?);
|
||||
const str = std.mem.sliceTo(bytes, 0);
|
||||
|
||||
// If we're in a key event, then we want to buffer the commit so
|
||||
// that we can send the proper keycallback followed by the char
|
||||
// callback.
|
||||
if (self.in_keypress) {
|
||||
if (str.len <= self.im_buf.len) {
|
||||
@memcpy(self.im_buf[0..str.len], str);
|
||||
self.im_len = @intCast(str.len);
|
||||
} else {
|
||||
log.warn("not enough buffer space for input method commit", .{});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We're not in a keypress, so this was sent from an on-screen emoji
|
||||
// keyboard or someting like that. Send the characters directly to
|
||||
// the surface.
|
||||
const view = std.unicode.Utf8View.init(str) catch |err| {
|
||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
|
Reference in New Issue
Block a user