diff --git a/include/ghostty.h b/include/ghostty.h index 5e50cbc95..fa07d657d 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -233,6 +233,7 @@ void ghostty_surface_char(ghostty_surface_t, uint32_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); void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double); +void ghostty_surface_ime_point(ghostty_surface_t, double *, double *); #ifdef __cplusplus } diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift index 1a6e70c34..103c88ceb 100644 --- a/macos/Sources/TerminalSurfaceView.swift +++ b/macos/Sources/TerminalSurfaceView.swift @@ -404,7 +404,22 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { } func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + guard let surface = self.surface else { + return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + } + + // Ghostty will tell us where it thinks an IME keyboard should render. + var x: Double = 0; + var y: Double = 0; + ghostty_surface_ime_point(surface, &x, &y) + + // Ghostty coordinates are in top-left (0, 0) so we have to convert to + // bottom-left since that is what UIKit expects + let rect = NSMakeRect(x, frame.size.height - y, 0, 0) + + // Convert from view to screen coordinates + guard let window = self.window else { return rect } + return window.convertToScreen(rect) } func insertText(_ string: Any, replacementRange: NSRange) { diff --git a/src/App.zig b/src/App.zig index 5857154c0..29d3fc617 100644 --- a/src/App.zig +++ b/src/App.zig @@ -462,4 +462,10 @@ pub const CAPI = struct { export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void { win.window.scrollCallback(x, y); } + + export fn ghostty_surface_ime_point(win: *Window, x: *f64, y: *f64) void { + const pos = win.imePoint(); + x.* = pos.x; + y.* = pos.y; + } }; diff --git a/src/Window.zig b/src/Window.zig index d5bc04eb3..a769343fc 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -553,6 +553,48 @@ pub fn handleMessage(self: *Window, msg: Message) !void { } } +/// Returns the x/y coordinate of where the IME (Input Method Editor) +/// keyboard should be rendered. +pub fn imePoint(self: *const Window) apprt.IMEPos { + self.renderer_state.mutex.lock(); + const cursor = self.renderer_state.terminal.screen.cursor; + self.renderer_state.mutex.unlock(); + + // TODO: need to handle when scrolling and the cursor is not + // in the visible portion of the screen. + + // Our sizes are all scaled so we need to send the unscaled values back. + const content_scale = self.window.getContentScale() catch .{ .x = 1, .y = 1 }; + + const x: f64 = x: { + // Simple x * cell width gives the top-left corner + var x: f64 = @floatCast(f64, @intToFloat(f32, cursor.x) * self.cell_size.width); + + // We want the midpoint + x += self.cell_size.width / 2; + + // And scale it + x /= content_scale.x; + + break :x x; + }; + + const y: f64 = y: { + // Simple x * cell width gives the top-left corner + var y: f64 = @floatCast(f64, @intToFloat(f32, cursor.y) * self.cell_size.height); + + // We want the bottom + y += self.cell_size.height; + + // And scale it + y /= content_scale.y; + + break :y y; + }; + + return .{ .x = x, .y = y }; +} + fn clipboardRead(self: *const Window, kind: u8) !void { if (!self.config.@"clipboard-read") { log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 74fa2fcd1..2cdb8be9e 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -17,3 +17,9 @@ pub const CursorPos = struct { x: f32, y: f32, }; + +/// Input Method Editor (IME) position. +pub const IMEPos = struct { + x: f64, + y: f64, +};