diff --git a/src/Surface.zig b/src/Surface.zig index fff72cdbd..56bab2ec5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 22d205b45..892733d08 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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}); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 9e80aaed9..bf9d50bd9 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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 diff --git a/src/renderer/State.zig b/src/renderer/State.zig index 5b1db42a0..e73a88348 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -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, +};