renderer/metal: support multi-codepoint preedit text

This commit is contained in:
Mitchell Hashimoto
2023-11-15 09:49:28 -08:00
parent 4cff8d972c
commit 50f0aaf26b
4 changed files with 140 additions and 75 deletions

View File

@ -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: {

View File

@ -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;
};

View File

@ -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

View File

@ -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)
/// 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;
}
};