support preedit text rendering in core and metal

This commit is contained in:
Mitchell Hashimoto
2023-08-11 12:20:48 -07:00
parent 65c4aada02
commit d62161e2c3
4 changed files with 142 additions and 19 deletions

View File

@ -966,6 +966,22 @@ pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void {
try self.io_thread.wakeup.notify();
}
/// Called to set the preedit state for character input. Preedit is used
/// with dead key states, for example, when typing an accent character.
/// This should be called with null to reset the preedit state.
///
/// 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 {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.preedit = if (preedit) |v| .{
.codepoint = v,
} else null;
try self.queueRender();
}
pub fn charCallback(self: *Surface, codepoint: u21) !void {
const tracy = trace(@src());
defer tracy.end();

View File

@ -399,6 +399,11 @@ pub const Surface = struct {
mods,
);
// If we aren't composing, then we set our preedit to empty no matter what.
if (!result.composing) {
self.core_surface.preeditCallback(null) catch {};
}
// log.warn("TRANSLATE: action={} keycode={x} dead={} key={any} key_str={s} mods={}", .{
// action,
// keycode,
@ -441,26 +446,31 @@ pub const Surface = struct {
// If we consume the key then we want to reset the dead key state.
if (consumed) {
self.keymap_state = .{};
self.core_surface.preeditCallback(null) catch {};
return;
}
}
// If this is a dead key, then we're composing a character and
// we end processing here. We don't process keybinds for dead keys.
if (result.composing) {
// TODO: we ultimately want to update some surface state so that
// we can show the user that we're in dead key mode and the
// precomposed character. For now, we can just ignore and that
// is not incorrect behavior.
return;
}
// Next, we want to call the char callback with each codepoint.
// No matter what happens next we'll want a utf8 view.
const view = std.unicode.Utf8View.init(result.text) catch |err| {
log.warn("cannot build utf8 view over input: {}", .{err});
return;
};
var it = view.iterator();
// If this is a dead key, then we're composing a character and
// we end processing here. We don't process keybinds for dead keys.
if (result.composing) {
const cp: u21 = it.nextCodepoint() orelse 0;
self.core_surface.preeditCallback(cp) catch |err| {
log.err("error in preedit callback err={}", .{err});
return;
};
return;
}
// Next, we want to call the char callback with each codepoint.
while (it.nextCodepoint()) |cp| {
self.core_surface.charCallback(cp) catch |err| {
log.err("error in char callback err={}", .{err});

View File

@ -521,6 +521,7 @@ pub fn render(
selection: ?terminal.Selection,
screen: terminal.Screen,
draw_cursor: bool,
preedit: ?renderer.State.Preedit,
};
// Update all our data as tightly as possible within the mutex.
@ -533,6 +534,9 @@ pub fn render(
// then it is not visible.
if (!state.cursor.visible) break :visible false;
// If we are in preedit, then we always show the cursor
if (state.preedit != null) break :visible true;
// If the cursor isn't a blinking style, then never blink.
if (!state.cursor.style.blinking()) break :visible true;
@ -540,10 +544,17 @@ pub fn render(
break :visible self.cursor_visible;
};
if (self.focused) {
self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box;
} else {
self.cursor_style = .box_hollow;
// The cursor style only needs to be set if its visible.
if (self.cursor_visible) {
self.cursor_style = cursor_style: {
// If we have a dead key preedit then we always use a box style
if (state.preedit != null) break :cursor_style .box;
// If we aren't focused, we use a hollow box
if (!self.focused) break :cursor_style .box_hollow;
break :cursor_style renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box;
};
}
// Swap bg/fg if the terminal is reversed
@ -580,12 +591,16 @@ pub fn render(
else
null;
// Whether to draw our cursor or not.
const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom();
break :critical .{
.bg = self.config.background,
.devmode = if (state.devmode) |dm| dm.visible else false,
.selection = selection,
.screen = screen_copy,
.draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom(),
.draw_cursor = draw_cursor,
.preedit = if (draw_cursor) state.preedit else null,
};
};
defer critical.screen.deinit();
@ -599,6 +614,7 @@ pub fn render(
critical.selection,
&critical.screen,
critical.draw_cursor,
critical.preedit,
);
// Get our drawable (CAMetalDrawable)
@ -848,6 +864,7 @@ fn rebuildCells(
term_selection: ?terminal.Selection,
screen: *terminal.Screen,
draw_cursor: bool,
preedit: ?renderer.State.Preedit,
) !void {
// Bg cells at most will need space for the visible screen size
self.cells_bg.clearRetainingCapacity();
@ -962,8 +979,30 @@ fn rebuildCells(
// a cursor cell then we invert the colors on that and add it in so
// that we can always see it.
if (draw_cursor) {
self.addCursor(screen);
const real_cursor_cell = self.addCursor(screen);
// 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: GPUCell = cursor_cell orelse
(real_cursor_cell orelse break :preedit).*;
cell.color = .{ 0, 0, 0, 255 };
// 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);
}
}
}
if (cursor_cell) |*cell| {
// We always invert the cell color under the cursor.
cell.color = .{ 0, 0, 0, 255 };
self.cells.appendAssumeCapacity(cell.*);
}
@ -1155,7 +1194,7 @@ pub fn updateCell(
return true;
}
fn addCursor(self: *Metal, screen: *terminal.Screen) void {
fn addCursor(self: *Metal, screen: *terminal.Screen) ?*const GPUCell {
// Add the cursor
const cell = screen.getCell(
.active,
@ -1182,7 +1221,7 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
.{},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
return;
return null;
};
self.cells.appendAssumeCapacity(.{
@ -1197,6 +1236,47 @@ fn addCursor(self: *Metal, screen: *terminal.Screen) void {
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
});
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: *GPUCell, cp: u21) bool {
// Get the font index for this codepoint
const font_index = if (self.font_group.indexForCodepoint(
self.alloc,
@intCast(cp),
.regular,
.text,
)) |index| index orelse return false else |_| return false;
// 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;
};
// Use the face to now get the glyph index
const glyph_index = face.glyphIndex(@intCast(cp)) orelse return false;
// Render the glyph for our preedit text
const glyph = self.font_group.renderGlyph(
self.alloc,
font_index,
glyph_index,
.{},
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return false;
};
// 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 };
cell.cell_width = 1;
return true;
}
/// Sync the vertex buffer inputs to the GPU. This will attempt to reuse

View File

@ -18,6 +18,12 @@ cursor: Cursor,
/// The terminal data.
terminal: *terminal.Terminal,
/// Dead key state. This will render the current dead key preedit text
/// over the cursor. This currently only ever renders a single codepoint.
/// Preedit can in theory be multiple codepoints long but that is left as
/// a future exercise.
preedit: ?Preedit = null,
/// The devmode data.
devmode: ?*const DevMode = null,
@ -31,3 +37,14 @@ pub const Cursor = struct {
/// cursor ON or OFF.
visible: bool = true,
};
/// 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,
};