macOS: firstRect should return full rect width/height (#8492)

Fixes #2473

This commit changes `ghostty_surface_ime_point` to return a full rect
with the width/height calculated for the preedit.

The `firstRect` function, which calls `ghostty_surface_ime_point` was
previously setting the width/height to zero. macOS didn't like this. We
then changed it to just hardcode it to width/height of one cell. This
worked but made it so the IME cursor didn't follow the preedit.

The result is shown in the video below. Notice the dictation icon
follows the text properly:



https://github.com/user-attachments/assets/81be8c63-9f0a-49b7-ac30-2db930beb238
pull/8462/head
Mitchell Hashimoto 2025-09-02 13:28:08 -07:00 committed by GitHub
commit ce94bb9f6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 56 additions and 8 deletions

View File

@ -964,7 +964,7 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t,
double,
ghostty_input_scroll_mods_t);
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
void ghostty_surface_split_focus(ghostty_surface_t,

View File

@ -1683,8 +1683,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
}
// Ghostty will tell us where it thinks an IME keyboard should render.
var x: Double = 0;
var y: Double = 0;
var x: Double = 0
var y: Double = 0
var width: Double = cellSize.width
var height: Double = cellSize.height
// QuickLook never gives us a matching range to our selection so if we detect
// this then we return the top-left selection point rather than the cursor point.
@ -1702,15 +1704,19 @@ extension Ghostty.SurfaceView: NSTextInputClient {
// Free our text
ghostty_surface_free_text(surface, &text)
} else {
ghostty_surface_ime_point(surface, &x, &y)
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
}
} else {
ghostty_surface_ime_point(surface, &x, &y)
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
}
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
// bottom-left since that is what UIKit expects
let viewRect = NSMakeRect(x, frame.size.height - y, cellSize.width, cellSize.height)
let viewRect = NSMakeRect(
x,
frame.size.height - y,
max(width, cellSize.width),
max(height, cellSize.height))
// Convert the point to the window coordinates
let winRect = self.convert(viewRect, to: nil)

View File

@ -1730,6 +1730,7 @@ pub fn pwd(
pub fn imePoint(self: *const Surface) apprt.IMEPos {
self.renderer_state.mutex.lock();
const cursor = self.renderer_state.terminal.screen.cursor;
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
self.renderer_state.mutex.unlock();
// TODO: need to handle when scrolling and the cursor is not
@ -1764,7 +1765,38 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
break :y y;
};
return .{ .x = x, .y = y };
// Our height for now is always just the cell height because our preedit
// rendering only renders in a single line.
const height: f64 = height: {
var height: f64 = @floatFromInt(self.size.cell.height);
height /= content_scale.y;
break :height height;
};
const width: f64 = width: {
var width: f64 = @floatFromInt(preedit_width * self.size.cell.width);
// Our max width is the remaining screen width after the cursor.
// We don't have to deal with wrapping because the preedit doesn't
// wrap right now.
const screen_width: f64 = @floatFromInt(self.size.terminal().width);
const x_offset: f64 = @floatFromInt((cursor.x + 1) * self.size.cell.width);
const max = screen_width - x_offset;
width = @min(width, max);
// Note: we don't apply content scale here because it looks like
// for some reason in macOS its already scaled. I'm not sure why
// that is so I'm going to just leave this comment here so its known
// that I left this out on purpose pending more investigation.
break :width width;
};
return .{
.x = x,
.y = y,
.width = width,
.height = height,
};
}
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {

View File

@ -1822,10 +1822,18 @@ pub const CAPI = struct {
surface.mousePressureCallback(stage, pressure);
}
export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void {
export fn ghostty_surface_ime_point(
surface: *Surface,
x: *f64,
y: *f64,
width: *f64,
height: *f64,
) void {
const pos = surface.core_surface.imePoint();
x.* = pos.x;
y.* = pos.y;
width.* = pos.width;
height.* = pos.height;
}
/// Request that the surface become closed. This will go through the

View File

@ -24,6 +24,8 @@ pub const CursorPos = struct {
pub const IMEPos = struct {
x: f64,
y: f64,
width: f64,
height: f64,
};
/// The clipboard type.