mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
renderer/metal: support multi-codepoint preedit text
This commit is contained in:
@ -1025,25 +1025,50 @@ fn resize(self: *Surface, size: renderer.ScreenSize) !void {
|
||||
/// The core surface will NOT reset the preedit state on charCallback or
|
||||
/// keyCallback and we rely completely on the apprt implementation to track
|
||||
/// the preedit state correctly.
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?u21) !void {
|
||||
// log.debug("preedit cp={any}", .{preedit_});
|
||||
|
||||
const preedit: ?renderer.State.Preedit = if (preedit_) |cp| preedit: {
|
||||
const width = ziglyph.display_width.codePointWidth(cp, .half);
|
||||
|
||||
// This shouldn't ever happen in well-behaved programs because
|
||||
// preedit text must be visible, but we want to protect against it
|
||||
// at this point.
|
||||
if (width <= 0) break :preedit null;
|
||||
|
||||
break :preedit .{
|
||||
.codepoint = cp,
|
||||
.wide = width >= 2,
|
||||
};
|
||||
} else null;
|
||||
|
||||
///
|
||||
/// The preedit input must be UTF-8 encoded.
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// We always clear our prior preedit
|
||||
self.renderer_state.preedit = null;
|
||||
|
||||
// If we have no text, we're done. We queue a render in case we cleared
|
||||
// a prior preedit (likely).
|
||||
const text = preedit_ orelse {
|
||||
try self.queueRender();
|
||||
return;
|
||||
};
|
||||
|
||||
// We convert the UTF-8 text to codepoints.
|
||||
const view = try std.unicode.Utf8View.init(text);
|
||||
var it = view.iterator();
|
||||
|
||||
// Allocate the codepoints slice
|
||||
var preedit: renderer.State.Preedit = .{};
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
const width = ziglyph.display_width.codePointWidth(cp, .half);
|
||||
|
||||
// I've never seen a preedit text with a zero-width character. In
|
||||
// theory its possible but we can't really handle it right now.
|
||||
// Let's just ignore it.
|
||||
if (width <= 0) continue;
|
||||
|
||||
preedit.codepoints[preedit.len] = .{ .codepoint = cp, .wide = width >= 2 };
|
||||
preedit.len += 1;
|
||||
|
||||
// This is a strange edge case. We have a generous buffer for
|
||||
// preedit text but if we exceed it, we just truncate.
|
||||
if (preedit.len >= preedit.codepoints.len) {
|
||||
log.warn("preedit text is longer than our buffer, truncating", .{});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have no codepoints, then we're done.
|
||||
if (preedit.len == 0) return;
|
||||
|
||||
self.renderer_state.preedit = preedit;
|
||||
try self.queueRender();
|
||||
}
|
||||
@ -1055,7 +1080,7 @@ pub fn keyCallback(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
) !bool {
|
||||
// log.debug("keyCallback event={}", .{event});
|
||||
log.debug("text keyCallback event={}", .{event});
|
||||
|
||||
// Setup our inspector event if we have an inspector.
|
||||
var insp_ev: ?inspector.key.Event = if (self.inspector != null) ev: {
|
||||
|
@ -702,14 +702,7 @@ pub const Surface = struct {
|
||||
// If this is a dead key, then we're composing a character and
|
||||
// we need to set our proper preedit state.
|
||||
if (result.composing) {
|
||||
const view = std.unicode.Utf8View.init(result.text) catch |err| {
|
||||
log.warn("cannot build utf8 view over input: {}", .{err});
|
||||
return;
|
||||
};
|
||||
var it = view.iterator();
|
||||
|
||||
const cp: u21 = it.nextCodepoint() orelse 0;
|
||||
self.core_surface.preeditCallback(cp) catch |err| {
|
||||
self.core_surface.preeditCallback(result.text) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
@ -1095,6 +1095,19 @@ fn rebuildCells(
|
||||
(screen.rows * screen.cols * 2) + 1,
|
||||
);
|
||||
|
||||
// Determine our x/y range for preedit. We don't want to render anything
|
||||
// here because we will render the preedit separately.
|
||||
const preedit_range: ?struct {
|
||||
y: usize,
|
||||
x: [2]usize,
|
||||
} = if (preedit) |preedit_v| preedit: {
|
||||
var x = screen.cursor.x;
|
||||
break :preedit .{
|
||||
.y = screen.cursor.y,
|
||||
.x = .{ x, x + preedit_v.width() },
|
||||
};
|
||||
} else null;
|
||||
|
||||
// This is the cell that has [mode == .fg] and is underneath our cursor.
|
||||
// We keep track of it so that we can invert the colors so the character
|
||||
// remains visible.
|
||||
@ -1175,6 +1188,18 @@ fn rebuildCells(
|
||||
);
|
||||
while (try iter.next(self.alloc)) |run| {
|
||||
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
||||
// If this cell falls within our preedit range then we skip it.
|
||||
// We do this so we don't have conflicting data on the same
|
||||
// cell.
|
||||
if (preedit_range) |range| {
|
||||
if (range.y == y and
|
||||
shaper_cell.x >= range.x[0] and
|
||||
shaper_cell.x <= range.x[1])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.updateCell(
|
||||
term_selection,
|
||||
screen,
|
||||
@ -1202,30 +1227,29 @@ fn rebuildCells(
|
||||
// Add the cursor at the end so that it overlays everything. If we have
|
||||
// a cursor cell then we invert the colors on that and add it in so
|
||||
// that we can always see it.
|
||||
if (cursor_style_) |cursor_style| {
|
||||
const real_cursor_cell = self.addCursor(screen, cursor_style, preedit);
|
||||
|
||||
if (cursor_style_) |cursor_style| cursor_style: {
|
||||
// If we have a preedit, we try to render the preedit text on top
|
||||
// of the cursor.
|
||||
if (preedit) |preedit_v| preedit: {
|
||||
if (preedit_v.codepoint > 0) {
|
||||
// We try to base on the cursor cell but if its not there
|
||||
// we use the actual cursor and if thats not there we give
|
||||
// up on preedit rendering.
|
||||
var cell: mtl_shaders.Cell = cursor_cell orelse
|
||||
(real_cursor_cell orelse break :preedit).*;
|
||||
cell.color = .{ 0, 0, 0, 255 };
|
||||
cell.cell_width = if (preedit_v.wide) 2 else 1;
|
||||
if (preedit) |preedit_v| {
|
||||
const range = preedit_range.?;
|
||||
var x = range.x[0];
|
||||
for (preedit_v.codepoints[0..preedit_v.len]) |cp| {
|
||||
self.addPreeditCell(cp, x, range.y) catch |err| {
|
||||
log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
|
||||
x,
|
||||
range.y,
|
||||
err,
|
||||
});
|
||||
};
|
||||
|
||||
// If preedit rendering succeeded then we don't want to
|
||||
// re-render the underlying cell fg
|
||||
if (self.updateCellChar(&cell, preedit_v.codepoint)) {
|
||||
cursor_cell = null;
|
||||
self.cells.appendAssumeCapacity(cell);
|
||||
}
|
||||
x += if (cp.wide) 2 else 1;
|
||||
}
|
||||
|
||||
// Preedit hides the cursor
|
||||
break :cursor_style;
|
||||
}
|
||||
|
||||
_ = self.addCursor(screen, cursor_style);
|
||||
if (cursor_cell) |*cell| {
|
||||
if (cell.mode == .fg) {
|
||||
cell.color = if (self.config.cursor_text) |txt|
|
||||
@ -1428,18 +1452,10 @@ fn addCursor(
|
||||
self: *Metal,
|
||||
screen: *terminal.Screen,
|
||||
cursor_style: renderer.CursorStyle,
|
||||
preedit: ?renderer.State.Preedit,
|
||||
) ?*const mtl_shaders.Cell {
|
||||
// Add the cursor. We render the cursor over the wide character if
|
||||
// we're on the wide characer tail.
|
||||
const wide, const x = cell: {
|
||||
// If we have preedit text, our width is based on that.
|
||||
if (preedit) |p| {
|
||||
if (p.codepoint > 0) {
|
||||
break :cell .{ p.wide, screen.cursor.x };
|
||||
}
|
||||
}
|
||||
|
||||
// The cursor goes over the screen cursor position.
|
||||
const cell = screen.getCell(
|
||||
.active,
|
||||
@ -1497,25 +1513,32 @@ fn addCursor(
|
||||
return &self.cells.items[self.cells.items.len - 1];
|
||||
}
|
||||
|
||||
/// Updates cell with the the given character. This returns true if the
|
||||
/// cell was successfully updated.
|
||||
fn updateCellChar(self: *Metal, cell: *mtl_shaders.Cell, cp: u21) bool {
|
||||
// Get the font index for this codepoint
|
||||
fn addPreeditCell(
|
||||
self: *Metal,
|
||||
cp: renderer.State.Preedit.Codepoint,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) !void {
|
||||
// Preedit is rendered inverted
|
||||
const bg = self.foreground_color;
|
||||
const fg = self.background_color;
|
||||
|
||||
// Get the font for this codepoint.
|
||||
const font_index = if (self.font_group.indexForCodepoint(
|
||||
self.alloc,
|
||||
@intCast(cp),
|
||||
@intCast(cp.codepoint),
|
||||
.regular,
|
||||
.text,
|
||||
)) |index| index orelse return false else |_| return false;
|
||||
)) |index| index orelse return else |_| return;
|
||||
|
||||
// Get the font face so we can get the glyph
|
||||
const face = self.font_group.group.faceFromIndex(font_index) catch |err| {
|
||||
log.warn("error getting face for font_index={} err={}", .{ font_index, err });
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
|
||||
// Use the face to now get the glyph index
|
||||
const glyph_index = face.glyphIndex(@intCast(cp)) orelse return false;
|
||||
const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return;
|
||||
|
||||
// Render the glyph for our preedit text
|
||||
const glyph = self.font_group.renderGlyph(
|
||||
@ -1525,14 +1548,27 @@ fn updateCellChar(self: *Metal, cell: *mtl_shaders.Cell, cp: u21) bool {
|
||||
.{},
|
||||
) catch |err| {
|
||||
log.warn("error rendering preedit glyph err={}", .{err});
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
|
||||
// Update the cell glyph
|
||||
cell.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y };
|
||||
cell.glyph_size = .{ glyph.width, glyph.height };
|
||||
cell.glyph_offset = .{ glyph.offset_x, glyph.offset_y };
|
||||
return true;
|
||||
// Add our opaque background cell
|
||||
self.cells_bg.appendAssumeCapacity(.{
|
||||
.mode = .bg,
|
||||
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
|
||||
.cell_width = if (cp.wide) 2 else 1,
|
||||
.color = .{ bg.r, bg.g, bg.b, 1 },
|
||||
});
|
||||
|
||||
// Add our text
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
|
||||
.cell_width = if (cp.wide) 2 else 1,
|
||||
.color = .{ fg.r, fg.g, fg.b, 255 },
|
||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||
.glyph_size = .{ glyph.width, glyph.height },
|
||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
||||
});
|
||||
}
|
||||
|
||||
/// Sync the atlas data to the given texture. This copies the bytes
|
||||
|
@ -27,14 +27,25 @@ preedit: ?Preedit = null,
|
||||
|
||||
/// The pre-edit state. See Surface.preeditCallback for more information.
|
||||
pub const Preedit = struct {
|
||||
/// The codepoint to render as preedit text. We only support single
|
||||
/// codepoint for now. In theory this can be multiple codepoints but
|
||||
/// that is left as a future exercise.
|
||||
///
|
||||
/// This can also be "0" in which case we can know we're in a preedit
|
||||
/// mode but we don't have any preedit text to render.
|
||||
codepoint: u21 = 0,
|
||||
/// The codepoints to render as preedit text. We allow up to 16 codepoints
|
||||
/// as a sort of arbitrary limit. If we experience a realisitic use case
|
||||
/// where we need more please open an issue.
|
||||
codepoints: [16]Codepoint = undefined,
|
||||
len: u8 = 0,
|
||||
|
||||
/// True if the preedit text should be rendered "wide" (two cells)
|
||||
wide: bool = false,
|
||||
/// A single codepoint to render as preedit text.
|
||||
pub const Codepoint = struct {
|
||||
codepoint: u21,
|
||||
wide: bool = false,
|
||||
};
|
||||
|
||||
/// The width in cells of all codepoints in the preedit.
|
||||
pub fn width(self: *const Preedit) usize {
|
||||
var result: usize = 0;
|
||||
for (self.codepoints[0..self.len]) |cp| {
|
||||
result += if (cp.wide) 2 else 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user