mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +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_KEY_RIGHT_SUPER,
|
||||||
} ghostty_input_key_e;
|
} 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 {
|
typedef struct {
|
||||||
ghostty_input_key_e key;
|
ghostty_input_key_e key;
|
||||||
ghostty_input_mods_e mods;
|
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_content_scale(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
||||||
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
|
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_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_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);
|
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
||||||
|
@ -285,6 +285,9 @@ extension Ghostty {
|
|||||||
private var cursor: NSCursor = .iBeam
|
private var cursor: NSCursor = .iBeam
|
||||||
private var cursorVisible: CursorVisibility = .visible
|
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
|
// We need to support being a first responder so that we can get input events
|
||||||
override var acceptsFirstResponder: Bool { return true }
|
override var acceptsFirstResponder: Bool { return true }
|
||||||
|
|
||||||
@ -722,18 +725,36 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
// By setting this to non-nil, we note that we'rein a keyDown event. From here,
|
||||||
keyAction(action, event: event)
|
// 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
|
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||||
// automatically handles all key translation, and we don't handle any commands
|
|
||||||
// currently.
|
// 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
|
||||||
// It is possible that in the future we'll have to modify ghostty_surface_key
|
// AND we'll have a preedit.
|
||||||
// and the embedding API so that we can call this because macOS needs to do
|
var handled: Bool = false
|
||||||
// some things with certain keys. I'm not sure. For now this works.
|
if let list = keyTextAccumulator, list.count > 0 {
|
||||||
//
|
handled = true
|
||||||
// self.interpretKeyEvents([event])
|
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) {
|
override func keyUp(with event: NSEvent) {
|
||||||
@ -764,8 +785,41 @@ extension Ghostty {
|
|||||||
|
|
||||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||||
guard let surface = self.surface else { return }
|
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
|
// MARK: Menu Handlers
|
||||||
@ -873,6 +927,17 @@ extension Ghostty {
|
|||||||
return
|
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
|
let len = chars.utf8CString.count
|
||||||
if (len == 0) { return }
|
if (len == 0) { return }
|
||||||
|
|
||||||
|
@ -244,6 +244,20 @@ pub const Surface = struct {
|
|||||||
working_directory: [*:0]const u8 = "",
|
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 {
|
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||||
const nsview = objc.Object.fromId(opts.nsview orelse
|
const nsview = objc.Object.fromId(opts.nsview orelse
|
||||||
return error.NSViewMustBeSet);
|
return error.NSViewMustBeSet);
|
||||||
@ -625,10 +639,12 @@ pub const Surface = struct {
|
|||||||
|
|
||||||
pub fn keyCallback(
|
pub fn keyCallback(
|
||||||
self: *Surface,
|
self: *Surface,
|
||||||
action: input.Action,
|
event: KeyEvent,
|
||||||
keycode: u32,
|
|
||||||
mods: input.Mods,
|
|
||||||
) !void {
|
) !void {
|
||||||
|
const action = event.action;
|
||||||
|
const keycode = event.keycode;
|
||||||
|
const mods = event.mods;
|
||||||
|
|
||||||
// True if this is a key down event
|
// True if this is a key down event
|
||||||
const is_down = action == .press or action == .repeat;
|
const is_down = action == .press or action == .repeat;
|
||||||
|
|
||||||
@ -666,7 +682,12 @@ pub const Surface = struct {
|
|||||||
// the raw keycode.
|
// the raw keycode.
|
||||||
var buf: [128]u8 = undefined;
|
var buf: [128]u8 = undefined;
|
||||||
const result: input.Keymap.Translation = if (is_down) translate: {
|
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,
|
&buf,
|
||||||
&self.keymap_state,
|
&self.keymap_state,
|
||||||
@intCast(keycode),
|
@intCast(keycode),
|
||||||
@ -1165,6 +1186,29 @@ pub const Inspector = struct {
|
|||||||
pub const CAPI = struct {
|
pub const CAPI = struct {
|
||||||
const global = &@import("../main.zig").state;
|
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.
|
/// Create a new app.
|
||||||
export fn ghostty_app_new(
|
export fn ghostty_app_new(
|
||||||
opts: *const apprt.runtime.App.Options,
|
opts: *const apprt.runtime.App.Options,
|
||||||
@ -1307,18 +1351,9 @@ pub const CAPI = struct {
|
|||||||
/// with a keypress, i.e. IME keyboard.
|
/// with a keypress, i.e. IME keyboard.
|
||||||
export fn ghostty_surface_key(
|
export fn ghostty_surface_key(
|
||||||
surface: *Surface,
|
surface: *Surface,
|
||||||
action: input.Action,
|
event: KeyEvent,
|
||||||
keycode: u32,
|
|
||||||
c_mods: c_int,
|
|
||||||
) void {
|
) void {
|
||||||
surface.keyCallback(
|
surface.keyCallback(event.keyEvent()) catch |err| {
|
||||||
action,
|
|
||||||
keycode,
|
|
||||||
@bitCast(@as(
|
|
||||||
input.Mods.Backing,
|
|
||||||
@truncate(@as(c_uint, @bitCast(c_mods))),
|
|
||||||
)),
|
|
||||||
) catch |err| {
|
|
||||||
log.err("error processing key event err={}", .{err});
|
log.err("error processing key event err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user