From 55778a049b2cc16c1fd68447f959bba89bf3a3a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Aug 2023 12:37:27 -0700 Subject: [PATCH] apprt/gtk, opengl: render preedit --- src/apprt/gtk.zig | 30 +++++++++--- src/renderer/Metal.zig | 1 - src/renderer/OpenGL.zig | 100 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 6966cf9ee..90782765a 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -1191,6 +1191,11 @@ pub const Surface = struct { const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); _ = 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 // to some input.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 (consumed) { c.gtk_im_context_reset(self.im_context); + self.core_surface.preeditCallback(null) catch {}; return 1; } } @@ -1221,10 +1227,19 @@ pub const Surface = struct { // 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 (self.im_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. + const text = self.im_buf[0..self.im_len]; + const view = std.unicode.Utf8View.init(text) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + 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; } @@ -1294,9 +1309,11 @@ pub const Surface = struct { _ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null); defer c.g_free(buf); 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( @@ -1307,6 +1324,7 @@ pub const Surface = struct { const self = userdataSelf(ud.?); if (!self.in_keypress) return; self.im_composing = false; + self.im_len = 0; } fn gtkInputCommit( diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index bf9d50bd9..f116e94fd 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1275,7 +1275,6 @@ fn updateCellChar(self: *Metal, cell: *GPUCell, cp: u21) bool { 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; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 186451e3f..a0c4f4ec4 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -721,6 +721,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. @@ -733,6 +734,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; @@ -740,10 +744,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 @@ -796,13 +807,17 @@ 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 .{ .gl_bg = self.config.background, .devmode_data = devmode_data, .active_screen = state.terminal.active_screen, .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(); @@ -821,6 +836,7 @@ pub fn render( critical.selection, &critical.screen, critical.draw_cursor, + critical.preedit, ); } @@ -858,6 +874,7 @@ pub fn rebuildCells( term_selection: ?terminal.Selection, screen: *terminal.Screen, draw_cursor: bool, + preedit: ?renderer.State.Preedit, ) !void { const t = trace(@src()); 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 // 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.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| { cell.fg_r = 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 const cell = screen.getCell( .active, @@ -1050,7 +1091,7 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { .{}, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); - return; + return null; }; self.cells.appendAssumeCapacity(.{ @@ -1073,6 +1114,49 @@ fn addCursor(self: *OpenGL, screen: *terminal.Screen) void { .glyph_offset_x = glyph.offset_x, .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