mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
apprt/gtk-ng: port keyEvent
This commit is contained in:
@ -221,20 +221,20 @@ pub const Application = extern struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Setup our windowing protocol logic
|
// Setup our windowing protocol logic
|
||||||
var winproto: winprotopkg.App = winprotopkg.App.init(
|
var wp: winprotopkg.App = winprotopkg.App.init(
|
||||||
alloc,
|
alloc,
|
||||||
display,
|
display,
|
||||||
app_id,
|
app_id,
|
||||||
&config,
|
&config,
|
||||||
) catch |err| winproto: {
|
) catch |err| wp: {
|
||||||
// If we fail to detect or setup the windowing protocol
|
// If we fail to detect or setup the windowing protocol
|
||||||
// specifies, we fallback to a noop implementation so we can
|
// specifies, we fallback to a noop implementation so we can
|
||||||
// still launch.
|
// still launch.
|
||||||
log.warn("error initializing windowing protocol err={}", .{err});
|
log.warn("error initializing windowing protocol err={}", .{err});
|
||||||
break :winproto .{ .none = .{} };
|
break :wp .{ .none = .{} };
|
||||||
};
|
};
|
||||||
errdefer winproto.deinit(alloc);
|
errdefer wp.deinit(alloc);
|
||||||
log.debug("windowing protocol={s}", .{@tagName(winproto)});
|
log.debug("windowing protocol={s}", .{@tagName(wp)});
|
||||||
|
|
||||||
// Create our GTK Application which encapsulates our process.
|
// Create our GTK Application which encapsulates our process.
|
||||||
log.debug("creating GTK application id={s} single-instance={}", .{
|
log.debug("creating GTK application id={s} single-instance={}", .{
|
||||||
@ -265,7 +265,7 @@ pub const Application = extern struct {
|
|||||||
.rt_app = rt_app,
|
.rt_app = rt_app,
|
||||||
.core_app = core_app,
|
.core_app = core_app,
|
||||||
.config = config_obj,
|
.config = config_obj,
|
||||||
.winproto = winproto,
|
.winproto = wp,
|
||||||
};
|
};
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@ -520,6 +520,11 @@ pub const Application = extern struct {
|
|||||||
return self.private().rt_app;
|
return self.private().rt_app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the app winproto implementation.
|
||||||
|
pub fn winproto(self: *Self) *winprotopkg.App {
|
||||||
|
return &self.private().winproto;
|
||||||
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
// Libghostty Callbacks
|
// Libghostty Callbacks
|
||||||
|
|
||||||
|
@ -156,12 +156,202 @@ pub const Surface = extern struct {
|
|||||||
gtk_mods: gdk.ModifierType,
|
gtk_mods: gdk.ModifierType,
|
||||||
) bool {
|
) bool {
|
||||||
log.warn("keyEvent action={}", .{action});
|
log.warn("keyEvent action={}", .{action});
|
||||||
|
const event = ec_key.as(gtk.EventController).getCurrentEvent() orelse return false;
|
||||||
|
const key_event = gobject.ext.cast(gdk.KeyEvent, event) orelse return false;
|
||||||
|
const priv = self.private();
|
||||||
|
|
||||||
|
// The block below is all related to input method handling. See the function
|
||||||
|
// comment for some high level details and then the comments within
|
||||||
|
// the block for more specifics.
|
||||||
|
if (priv.im_context) |im_context| {
|
||||||
|
// This can trigger an input method so we need to notify the im context
|
||||||
|
// where the cursor is so it can render the dropdowns in the correct
|
||||||
|
// place.
|
||||||
|
if (priv.core_surface) |surface| {
|
||||||
|
const ime_point = surface.imePoint();
|
||||||
|
im_context.as(gtk.IMContext).setCursorLocation(&.{
|
||||||
|
.f_x = @intFromFloat(ime_point.x),
|
||||||
|
.f_y = @intFromFloat(ime_point.y),
|
||||||
|
.f_width = 1,
|
||||||
|
.f_height = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We note that we're in a keypress because we want some logic to
|
||||||
|
// depend on this. For example, we don't want to send character events
|
||||||
|
// like "a" via the input "commit" event if we're actively processing
|
||||||
|
// a keypress because we'd lose access to the keycode information.
|
||||||
|
//
|
||||||
|
// We have to maintain some additional state here of whether we
|
||||||
|
// were composing because different input methods call the callbacks
|
||||||
|
// in different orders. For example, ibus calls commit THEN preedit
|
||||||
|
// end but simple calls preedit end THEN commit.
|
||||||
|
priv.in_keyevent = if (priv.im_composing) .composing else .not_composing;
|
||||||
|
defer priv.in_keyevent = .false;
|
||||||
|
|
||||||
|
// Pass the event through the input method which returns true if handled.
|
||||||
|
// Confusingly, not all events handled by the input method result
|
||||||
|
// in this returning true so we have to maintain some additional
|
||||||
|
// state about whether we were composing or not to determine if
|
||||||
|
// we should proceed with key encoding.
|
||||||
|
//
|
||||||
|
// Cases where the input method does not mark the event as handled:
|
||||||
|
//
|
||||||
|
// - If we change the input method via keypress while we have preedit
|
||||||
|
// text, the input method will commit the pending text but will not
|
||||||
|
// mark it as handled. We use the `.composing` state to detect
|
||||||
|
// this case.
|
||||||
|
//
|
||||||
|
// - If we switch input methods (i.e. via ctrl+shift with fcitx),
|
||||||
|
// the input method will handle the key release event but will not
|
||||||
|
// mark it as handled. I don't know any way to detect this case so
|
||||||
|
// it will result in a key event being sent to the key callback.
|
||||||
|
// For Kitty text encoding, this will result in modifiers being
|
||||||
|
// triggered despite being technically consumed. At the time of
|
||||||
|
// writing, both Kitty and Alacritty have the same behavior. I
|
||||||
|
// know of no way to fix this.
|
||||||
|
const im_handled = im_context.as(gtk.IMContext).filterKeypress(event) != 0;
|
||||||
|
// log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
|
||||||
|
// im_handled,
|
||||||
|
// self.im_len,
|
||||||
|
// self.im_composing,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// If the input method handled the event, you would think we would
|
||||||
|
// never proceed with key encoding for Ghostty but that is not the
|
||||||
|
// case. Input methods will handle basic character encoding like
|
||||||
|
// typing "a" and we want to associate that with the key event.
|
||||||
|
// So we have to check additional state to determine if we exit.
|
||||||
|
if (im_handled) {
|
||||||
|
// If we are composing then we're in a preedit state and do
|
||||||
|
// not want to encode any keys. For example: type a deadkey
|
||||||
|
// such as single quote on a US international keyboard layout.
|
||||||
|
if (priv.im_composing) return true;
|
||||||
|
|
||||||
|
// If we were composing and now we're not it means that we committed
|
||||||
|
// the text. We also don't want to encode a key event for this.
|
||||||
|
// Example: enable Japanese input method, press "konn" and then
|
||||||
|
// press enter. The final enter should not be encoded and "konn"
|
||||||
|
// (in hiragana) should be written as "こん".
|
||||||
|
if (priv.in_keyevent == .composing) return true;
|
||||||
|
|
||||||
|
// Not composing and our input method buffer is empty. This could
|
||||||
|
// mean that the input method reacted to this event by activating
|
||||||
|
// an onscreen keyboard or something equivalent. We don't know.
|
||||||
|
// But the input method handled it and didn't give us text so
|
||||||
|
// we will just assume we should not encode this. This handles a
|
||||||
|
// real scenario when ibus starts the emoji input method
|
||||||
|
// (super+.).
|
||||||
|
if (priv.im_len == 0) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, for the sake of explanation of internal state:
|
||||||
|
// it is possible that im_len > 0 and im_composing == false. This
|
||||||
|
// means that we received a commit event from the input method that
|
||||||
|
// we want associated with the key event. This is common: its how
|
||||||
|
// basic character translation for simple inputs like "a" work.
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always reset the length of the im buffer. There's only one scenario
|
||||||
|
// we reach this point with im_len > 0 and that's if we received a commit
|
||||||
|
// event from the input method. We don't want to keep that state around
|
||||||
|
// since we've handled it here.
|
||||||
|
defer priv.im_len = 0;
|
||||||
|
|
||||||
|
// Get the keyvals for this event.
|
||||||
|
const keyval_unicode = gdk.keyvalToUnicode(keyval);
|
||||||
|
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||||
|
priv.gl_area.as(gtk.Widget),
|
||||||
|
key_event,
|
||||||
|
keycode,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 .unidentified;
|
||||||
|
|
||||||
|
// Get our modifier for the event
|
||||||
|
const mods: input.Mods = gtk_key.eventMods(
|
||||||
|
event,
|
||||||
|
physical_key,
|
||||||
|
gtk_mods,
|
||||||
|
action,
|
||||||
|
Application.default().winproto(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get our consumed modifiers
|
||||||
|
const consumed_mods: input.Mods = consumed: {
|
||||||
|
const T = @typeInfo(gdk.ModifierType);
|
||||||
|
std.debug.assert(T.@"struct".layout == .@"packed");
|
||||||
|
const I = T.@"struct".backing_integer.?;
|
||||||
|
|
||||||
|
const masked = @as(I, @bitCast(key_event.getConsumedModifiers())) & @as(I, gdk.MODIFIER_MASK);
|
||||||
|
break :consumed gtk_key.translateMods(@bitCast(masked));
|
||||||
|
};
|
||||||
|
|
||||||
|
// log.debug("key pressed key={} keyval={x} physical_key={} composing={} text_len={} mods={}", .{
|
||||||
|
// key,
|
||||||
|
// keyval,
|
||||||
|
// physical_key,
|
||||||
|
// priv.im_composing,
|
||||||
|
// priv.im_len,
|
||||||
|
// mods,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// If we have no UTF-8 text, we try to convert our keyval to
|
||||||
|
// a text value. We have to do this because GTK will not process
|
||||||
|
// "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "".
|
||||||
|
// But the keyval is set correctly so we can at least extract that.
|
||||||
|
if (priv.im_len == 0 and keyval_unicode > 0) im: {
|
||||||
|
if (std.math.cast(u21, keyval_unicode)) |cp| {
|
||||||
|
// We don't want to send control characters as IM
|
||||||
|
// text. Control characters are handled already by
|
||||||
|
// the encoder directly.
|
||||||
|
if (cp < 0x20) break :im;
|
||||||
|
|
||||||
|
if (std.unicode.utf8Encode(cp, &priv.im_buf)) |len| {
|
||||||
|
priv.im_len = len;
|
||||||
|
} else |_| {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke the core Ghostty logic to handle this input.
|
||||||
|
const surface = priv.core_surface orelse return false;
|
||||||
|
const effect = surface.keyCallback(.{
|
||||||
|
.action = action,
|
||||||
|
.key = physical_key,
|
||||||
|
.mods = mods,
|
||||||
|
.consumed_mods = consumed_mods,
|
||||||
|
.composing = priv.im_composing,
|
||||||
|
.utf8 = priv.im_buf[0..priv.im_len],
|
||||||
|
.unshifted_codepoint = keyval_unicode_unshifted,
|
||||||
|
}) catch |err| {
|
||||||
|
log.err("error in key callback err={}", .{err});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (effect) {
|
||||||
|
.closed => return true,
|
||||||
|
.ignored => {},
|
||||||
|
.consumed => if (action == .press or action == .repeat) {
|
||||||
|
// If we were in the composing state then we reset our context.
|
||||||
|
// We do NOT want to reset if we're not in the composing state
|
||||||
|
// because there is other IME state that we want to preserve,
|
||||||
|
// such as quotation mark ordering for Chinese input.
|
||||||
|
if (priv.im_composing) {
|
||||||
|
if (priv.im_context) |im_context| {
|
||||||
|
im_context.as(gtk.IMContext).reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.preeditCallback(null) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
_ = self;
|
|
||||||
_ = ec_key;
|
|
||||||
_ = keyval;
|
|
||||||
_ = keycode;
|
|
||||||
_ = gtk_mods;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user