core: send key release events on focus loss

Related to #1284

This is highly GUI toolkit specific, but it is impossible to receive
events for some key releases when focus is lost while the keys are still
behind held. This commit always sends a release event for the last
pressed key when focus is lost, including each individual modifier.

On macOS, AppKit sends a key release event to a view if a prior press
event was sent, but only for non-modifier keys. This means that with
this commit (1) the full key release event is repeated but (2) modifier
release events are now properly sent.

On Linux with GTK, GTK sends modifier release events but not key release
events. This means that the behavior is inverted from macOS!

The result of this commit is that key release events _may be repeated_
on focus loss, but it ensures that all prior key+modifiers for the most
recent press event are released. This will require that TUI apps
handling release apps are idempotent in their release handling but I
don't think thats unrealistic to expect and I've already been able to
demonstrate at least Kitty sending duplicate release events in some
scenarios so this seems like a safe assumption.
This commit is contained in:
Mitchell Hashimoto
2024-01-26 22:04:08 -08:00
parent 894554f1a0
commit 1f4c8f3aa5

View File

@ -73,6 +73,23 @@ renderer_thr: std.Thread,
/// Mouse state.
mouse: Mouse,
/// A currently pressed key. This is used so that we can send a keyboard
/// release event when the surface is unfocused. Note that when the surface
/// is refocused, a key press event may not be sent again -- this depends
/// on the apprt (UI framework) in use, but we want to consistently send
/// a release.
///
/// This is only sent when a keypress event results in a key event being
/// sent to the pty. If it is consumed by a keybinding or other action,
/// this is not set.
///
/// Also note the utf8 value is not valid for this event so some unfocused
/// release events may not send exactly the right data within Kitty keyboard
/// events. This seems unspecificed in the spec so for now I'm okay with
/// this. Plus, its only for release events where the key text is far
/// less important.
pressed_key: ?input.KeyEvent = null,
/// The hash value of the last keybinding trigger that we performed. This
/// is only set if the last key input matched a keybinding, consumed it,
/// and performed it. This is used to prevent sending release/repeat events
@ -1393,6 +1410,29 @@ pub fn keyCallback(
};
};
// We've processed a key event that produced some data so we want to
// track the last pressed key.
self.pressed_key = event: {
// We need to unset the allocated fields that will become invalid
var copy = event;
copy.utf8 = "";
// If we have a previous pressed key and we're releasing it
// then we set it to invalid to prevent repeating the release event.
if (event.action == .release) {
// if we didn't have a previous event and this is a release
// event then we just want to set it to null.
const prev = self.pressed_key orelse break :event null;
if (prev.key == copy.key) copy.key = .invalid;
}
// If our key is invalid and we have no mods, then we're done!
// This helps catch the state that we naturally released all keys.
if (copy.key == .invalid and copy.mods.empty()) break :event null;
break :event copy;
};
var data: termio.Message.WriteReq.Small.Array = undefined;
const seq = try enc.encode(&data);
if (seq.len == 0) return .ignored;
@ -1440,8 +1480,53 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
.focus = focused,
}, .{ .forever = {} });
// Notify our app if we gained focus.
if (focused) self.app.focusSurface(self);
if (focused) {
// Notify our app if we gained focus.
self.app.focusSurface(self);
} else unfocused: {
// If we lost focus and we have a keypress, then we want to send a key
// release event for it. Depending on the apprt, this CAN result in
// duplicate key release events, but that is better than not sending
// a key release event at all.
var pressed_key = self.pressed_key orelse break :unfocused;
self.pressed_key = null;
// All our actions will be releases
pressed_key.action = .release;
// Release the full key first
if (pressed_key.key != .invalid) {
assert(self.keyCallback(pressed_key) catch |err| err: {
log.warn("error releasing key on focus loss err={}", .{err});
break :err .ignored;
} != .closed);
}
// Release any modifiers if set
if (pressed_key.mods.empty()) break :unfocused;
// This is kind of nasty comptime meta programming but all we're doing
// here is going through all the modifiers and if they're set, releasing
// both the left and right sides of the modifier. This may not match
// the exact input event but it ensures a full reset.
const keys = &.{ "shift", "ctrl", "alt", "super" };
const original_key = pressed_key.key;
inline for (keys) |key| {
if (@field(pressed_key.mods, key)) {
@field(pressed_key.mods, key) = false;
inline for (&.{ "right", "left" }) |side| {
const keyname = if (comptime std.mem.eql(u8, key, "ctrl")) "control" else key;
pressed_key.key = @field(input.Key, side ++ "_" ++ keyname);
if (pressed_key.key != original_key) {
assert(self.keyCallback(pressed_key) catch |err| err: {
log.warn("error releasing key on focus loss err={}", .{err});
break :err .ignored;
} != .closed);
}
}
}
}
}
// Schedule render which also drains our mailbox
try self.queueRender();