mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #856 from mitchellh/asian-input
macos: handle preedit in AppKit, enables Korean input
This commit is contained in:
@ -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);
|
||||
|
@ -284,6 +284,9 @@ extension Ghostty {
|
||||
private var focused: Bool = true
|
||||
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 }
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
Reference in New Issue
Block a user