apprt/gtk, opengl: render preedit

This commit is contained in:
Mitchell Hashimoto
2023-08-11 12:37:27 -07:00
parent d62161e2c3
commit 55778a049b
3 changed files with 116 additions and 15 deletions

View File

@ -1191,6 +1191,11 @@ pub const Surface = struct {
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key));
_ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0; _ = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
// If we aren't composing, then we set our preedit to empty no matter what.
if (!self.im_composing) {
self.core_surface.preeditCallback(null) catch {};
}
// If we're not in a dead key state, we want to translate our text // If we're not in a dead key state, we want to translate our text
// to some input.Key. // to some input.Key.
const key = if (!self.im_composing) key: { const key = if (!self.im_composing) key: {
@ -1214,6 +1219,7 @@ pub const Surface = struct {
// If we consume the key then we want to reset the dead key state. // If we consume the key then we want to reset the dead key state.
if (consumed) { if (consumed) {
c.gtk_im_context_reset(self.im_context); c.gtk_im_context_reset(self.im_context);
self.core_surface.preeditCallback(null) catch {};
return 1; return 1;
} }
} }
@ -1221,10 +1227,19 @@ pub const Surface = struct {
// If this is a dead key, then we're composing a character and // 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. // we end processing here. We don't process keybinds for dead keys.
if (self.im_composing) { if (self.im_composing) {
// TODO: we ultimately want to update some surface state so that const text = self.im_buf[0..self.im_len];
// we can show the user that we're in dead key mode and the const view = std.unicode.Utf8View.init(text) catch |err| {
// precomposed character. For now, we can just ignore and that log.warn("cannot build utf8 view over input: {}", .{err});
// is not incorrect behavior. return 0;
};
var it = view.iterator();
const cp: u21 = it.nextCodepoint() orelse 0;
self.core_surface.preeditCallback(cp) catch |err| {
log.err("error in preedit callback err={}", .{err});
return 0;
};
return 0; return 0;
} }
@ -1294,9 +1309,11 @@ pub const Surface = struct {
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null); _ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
defer c.g_free(buf); defer c.g_free(buf);
const str = std.mem.sliceTo(buf, 0); const str = std.mem.sliceTo(buf, 0);
log.debug("preedit str={s}", .{str});
// TODO: actually use this string. // Copy the preedit string into the im_buf. This is safe because
// commit will always overwrite this.
self.im_len = @intCast(@min(self.im_buf.len, str.len));
@memcpy(self.im_buf[0..self.im_len], str);
} }
fn gtkInputPreeditEnd( fn gtkInputPreeditEnd(
@ -1307,6 +1324,7 @@ pub const Surface = struct {
const self = userdataSelf(ud.?); const self = userdataSelf(ud.?);
if (!self.in_keypress) return; if (!self.in_keypress) return;
self.im_composing = false; self.im_composing = false;
self.im_len = 0;
} }
fn gtkInputCommit( fn gtkInputCommit(

View File

@ -1275,7 +1275,6 @@ fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool {
cell.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }; cell.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y };
cell.glyph_size = .{ glyph.width, glyph.height }; cell.glyph_size = .{ glyph.width, glyph.height };
cell.glyph_offset = .{ glyph.offset_x, glyph.offset_y }; cell.glyph_offset = .{ glyph.offset_x, glyph.offset_y };
cell.cell_width = 1;
return true; return true;
} }

View File

@ -721,6 +721,7 @@ pub fn render(
selection: ?terminal.Selection, selection: ?terminal.Selection,
screen: terminal.Screen, screen: terminal.Screen,
draw_cursor: bool, draw_cursor: bool,
preedit: ?renderer.State.Preedit,
}; };
// Update all our data as tightly as possible within the mutex. // Update all our data as tightly as possible within the mutex.
@ -733,6 +734,9 @@ pub fn render(
// then it is not visible. // then it is not visible.
if (!state.cursor.visible) break :visible false; 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 the cursor isn't a blinking style, then never blink.
if (!state.cursor.style.blinking()) break :visible true; if (!state.cursor.style.blinking()) break :visible true;
@ -740,10 +744,17 @@ pub fn render(
break :visible self.cursor_visible; break :visible self.cursor_visible;
}; };
if (self.focused) { // The cursor style only needs to be set if its visible.
self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; if (self.cursor_visible) {
} else { self.cursor_style = cursor_style: {
self.cursor_style = .box_hollow; // 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 // Swap bg/fg if the terminal is reversed
@ -796,13 +807,17 @@ pub fn render(
else else
null; null;
// Whether to draw our cursor or not.
const draw_cursor = self.cursor_visible and state.terminal.screen.viewportIsBottom();
break :critical .{ break :critical .{
.gl_bg = self.config.background, .gl_bg = self.config.background,
.devmode_data = devmode_data, .devmode_data = devmode_data,
.active_screen = state.terminal.active_screen, .active_screen = state.terminal.active_screen,
.selection = selection, .selection = selection,
.screen = screen_copy, .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(); defer critical.screen.deinit();
@ -821,6 +836,7 @@ pub fn render(
critical.selection, critical.selection,
&critical.screen, &critical.screen,
critical.draw_cursor, critical.draw_cursor,
critical.preedit,
); );
} }
@ -858,6 +874,7 @@ pub fn rebuildCells(
term_selection: ?terminal.Selection, term_selection: ?terminal.Selection,
screen: *terminal.Screen, screen: *terminal.Screen,
draw_cursor: bool, draw_cursor: bool,
preedit: ?renderer.State.Preedit,
) !void { ) !void {
const t = trace(@src()); const t = trace(@src());
defer t.end(); defer t.end();
@ -1006,7 +1023,31 @@ pub fn rebuildCells(
// a cursor cell then we invert the colors on that and add it in so // a cursor cell then we invert the colors on that and add it in so
// that we can always see it. // that we can always see it.
if (draw_cursor) { 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.fg_r = 0;
cell.fg_g = 0;
cell.fg_b = 0;
cell.fg_a = 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| { if (cursor_cell) |*cell| {
cell.fg_r = 0; cell.fg_r = 0;
cell.fg_g = 0; cell.fg_g = 0;
@ -1023,7 +1064,7 @@ pub fn rebuildCells(
} }
} }
fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { fn addCursor(self: *OpenGL, screen: *terminal.Screen) ?*const GPUCell {
// Add the cursor // Add the cursor
const cell = screen.getCell( const cell = screen.getCell(
.active, .active,
@ -1050,7 +1091,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
.{}, .{},
) catch |err| { ) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err}); log.warn("error rendering cursor glyph err={}", .{err});
return; return null;
}; };
self.cells.appendAssumeCapacity(.{ self.cells.appendAssumeCapacity(.{
@ -1073,6 +1114,49 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void {
.glyph_offset_x = glyph.offset_x, .glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y, .glyph_offset_y = 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: *OpenGL, 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_x = glyph.atlas_x;
cell.glyph_y = glyph.atlas_y;
cell.glyph_width = glyph.width;
cell.glyph_height = glyph.height;
cell.glyph_offset_x = glyph.offset_x;
cell.glyph_offset_y = glyph.offset_y;
return true;
} }
/// Update a single cell. The bool returns whether the cell was updated /// Update a single cell. The bool returns whether the cell was updated