mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
support preedit text rendering in core and metal
This commit is contained in:
@ -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();
|
||||
|
@ -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});
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user