Merge pull request #856 from mitchellh/asian-input

macos: handle preedit in AppKit, enables Korean input
This commit is contained in:
Mitchell Hashimoto
2023-11-10 10:08:34 -08:00
committed by GitHub
3 changed files with 137 additions and 29 deletions

View File

@ -292,6 +292,14 @@ typedef enum {
GHOSTTY_KEY_RIGHT_SUPER,
} ghostty_input_key_e;
typedef struct {
ghostty_input_action_e action;
ghostty_input_mods_e mods;
uint32_t keycode;
const char *text;
bool composing;
} ghostty_input_key_s;
typedef struct {
ghostty_input_key_e key;
ghostty_input_mods_e mods;
@ -414,7 +422,7 @@ void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
void ghostty_surface_set_focus(ghostty_surface_t, bool);
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, uint32_t, ghostty_input_mods_e);
void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
void ghostty_surface_text(ghostty_surface_t, const char *, uintptr_t);
void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e);
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);

View File

@ -285,6 +285,9 @@ extension Ghostty {
private var cursor: NSCursor = .iBeam
private var cursorVisible: CursorVisibility = .visible
// This is set to non-null during keyDown to accumulate insertText contents
private var keyTextAccumulator: [String]? = nil
// We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true }
@ -722,18 +725,36 @@ extension Ghostty {
}
override func keyDown(with event: NSEvent) {
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
keyAction(action, event: event)
// By setting this to non-nil, we note that we'rein a keyDown event. From here,
// we call interpretKeyEvents so that we can handle complex input such as Korean
// language.
keyTextAccumulator = []
defer { keyTextAccumulator = nil }
self.interpretKeyEvents([event])
// We specifically DO NOT call interpretKeyEvents because ghostty_surface_key
// automatically handles all key translation, and we don't handle any commands
// currently.
//
// It is possible that in the future we'll have to modify ghostty_surface_key
// and the embedding API so that we can call this because macOS needs to do
// some things with certain keys. I'm not sure. For now this works.
//
// self.interpretKeyEvents([event])
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
// If we have text, then we've composed a character, send that down. We do this
// first because if we completed a preedit, the text will be available here
// AND we'll have a preedit.
var handled: Bool = false
if let list = keyTextAccumulator, list.count > 0 {
handled = true
for text in list {
keyAction(action, event: event, text: text)
}
}
// If we have marked text, we're in a preedit state. Send that down.
if (markedText.length > 0) {
handled = true
keyAction(action, event: event, preedit: markedText.string)
}
if (!handled) {
// No text or anything, we want to handle this manually.
keyAction(action, event: event)
}
}
override func keyUp(with event: NSEvent) {
@ -764,8 +785,41 @@ extension Ghostty {
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_key(surface, action, UInt32(event.keyCode), mods)
var key_ev = ghostty_input_key_s()
key_ev.action = action
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
key_ev.keycode = UInt32(event.keyCode)
key_ev.text = nil
key_ev.composing = false
ghostty_surface_key(surface, key_ev)
}
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
guard let surface = self.surface else { return }
preedit.withCString { ptr in
var key_ev = ghostty_input_key_s()
key_ev.action = action
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
key_ev.keycode = UInt32(event.keyCode)
key_ev.text = ptr
key_ev.composing = true
ghostty_surface_key(surface, key_ev)
}
}
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, text: String) {
guard let surface = self.surface else { return }
text.withCString { ptr in
var key_ev = ghostty_input_key_s()
key_ev.action = action
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
key_ev.keycode = UInt32(event.keyCode)
key_ev.text = ptr
ghostty_surface_key(surface, key_ev)
}
}
// MARK: Menu Handlers
@ -873,6 +927,17 @@ extension Ghostty {
return
}
// If insertText is called, our preedit must be over.
unmarkText()
// If we have an accumulator we're in another key event so we just
// accumulate and return.
if var acc = keyTextAccumulator {
acc.append(chars)
keyTextAccumulator = acc
return
}
let len = chars.utf8CString.count
if (len == 0) { return }

View File

@ -244,6 +244,20 @@ pub const Surface = struct {
working_directory: [*:0]const u8 = "",
};
/// This is the key event sent for ghostty_surface_key.
pub const KeyEvent = struct {
/// The three below are absolutely required.
action: input.Action,
mods: input.Mods,
keycode: u32,
/// Optionally, the embedder can handle text translation and send
/// the text value here. If text is non-nil, it is assumed that the
/// embedder also handles dead key states and sets composing as necessary.
text: ?[:0]const u8,
composing: bool,
};
pub fn init(self: *Surface, app: *App, opts: Options) !void {
const nsview = objc.Object.fromId(opts.nsview orelse
return error.NSViewMustBeSet);
@ -625,10 +639,12 @@ pub const Surface = struct {
pub fn keyCallback(
self: *Surface,
action: input.Action,
keycode: u32,
mods: input.Mods,
event: KeyEvent,
) !void {
const action = event.action;
const keycode = event.keycode;
const mods = event.mods;
// True if this is a key down event
const is_down = action == .press or action == .repeat;
@ -666,7 +682,12 @@ pub const Surface = struct {
// the raw keycode.
var buf: [128]u8 = undefined;
const result: input.Keymap.Translation = if (is_down) translate: {
const result = try self.app.keymap.translate(
// If the event provided us with text, then we use this as a result
// and do not do manual translation.
const result: input.Keymap.Translation = if (event.text) |text| .{
.text = text,
.composing = event.composing,
} else try self.app.keymap.translate(
&buf,
&self.keymap_state,
@intCast(keycode),
@ -1165,6 +1186,29 @@ pub const Inspector = struct {
pub const CAPI = struct {
const global = &@import("../main.zig").state;
/// This is the same as Surface.KeyEvent but this is the raw C API version.
const KeyEvent = extern struct {
action: input.Action,
mods: c_int,
keycode: u32,
text: ?[*:0]const u8,
composing: bool,
/// Convert to surface key event.
fn keyEvent(self: KeyEvent) Surface.KeyEvent {
return .{
.action = self.action,
.mods = @bitCast(@as(
input.Mods.Backing,
@truncate(@as(c_uint, @bitCast(self.mods))),
)),
.keycode = self.keycode,
.text = if (self.text) |ptr| std.mem.sliceTo(ptr, 0) else null,
.composing = self.composing,
};
}
};
/// Create a new app.
export fn ghostty_app_new(
opts: *const apprt.runtime.App.Options,
@ -1307,18 +1351,9 @@ pub const CAPI = struct {
/// with a keypress, i.e. IME keyboard.
export fn ghostty_surface_key(
surface: *Surface,
action: input.Action,
keycode: u32,
c_mods: c_int,
event: KeyEvent,
) void {
surface.keyCallback(
action,
keycode,
@bitCast(@as(
input.Mods.Backing,
@truncate(@as(c_uint, @bitCast(c_mods))),
)),
) catch |err| {
surface.keyCallback(event.keyEvent()) catch |err| {
log.err("error processing key event err={}", .{err});
return;
};