diff --git a/src/font/Group.zig b/src/font/Group.zig index ba08ad53f..81ccbc066 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -515,6 +515,7 @@ pub fn renderGlyph( alloc, atlas, glyph_index, + opts, ), }; diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 511ec8137..6e8ee9856 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -41,6 +41,7 @@ const CodepointKey = struct { const GlyphKey = struct { index: Group.FontIndex, glyph: u32, + opts: font.face.RenderOptions, }; /// The GroupCache takes ownership of Group and will free it. @@ -124,7 +125,7 @@ pub fn renderGlyph( glyph_index: u32, opts: font.face.RenderOptions, ) !Glyph { - const key: GlyphKey = .{ .index = index, .glyph = glyph_index }; + const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts }; const gop = try self.glyphs.getOrPut(alloc, key); // If it is in the cache, use it. diff --git a/src/font/face.zig b/src/font/face.zig index e6dd746b2..2087c864f 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -76,6 +76,10 @@ pub const RenderOptions = struct { /// is typically naive, but ultimately up to the rasterizer. max_height: ?u16 = null, + /// The number of grid cells this glyph will take up. This can be used + /// optionally by the rasterizer to better layout the glyph. + cell_width: ?u2 = null, + /// Thicken the glyph. This draws the glyph with a thicker stroke width. /// This is purely an aesthetic setting. /// diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 3b170b439..1e59adebd 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -49,6 +49,7 @@ pub fn renderGlyph( alloc: Allocator, atlas: *font.Atlas, cp: u32, + opts: font.face.RenderOptions, ) !font.Glyph { if (std.debug.runtime_safety) { if (!self.hasCodepoint(cp, null)) { @@ -57,11 +58,17 @@ pub fn renderGlyph( } } + // We adjust our sprite width based on the cell width. + const width = switch (opts.cell_width orelse 1) { + 0, 1 => self.width, + else => |width| self.width * width, + }; + // Safe to ".?" because of the above assertion. return switch (Kind.init(cp).?) { .box => box: { const f: Box = .{ - .width = self.width, + .width = width, .height = self.height, .thickness = self.thickness, }; @@ -73,7 +80,7 @@ pub fn renderGlyph( alloc, atlas, @enumFromInt(cp), - self.width, + width, self.height, self.underline_position, self.thickness, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 28182f8d1..bee7327c4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1364,7 +1364,7 @@ pub fn updateCell( self.alloc, font.sprite_index, @intFromEnum(sprite), - .{}, + .{ .cell_width = if (cell.attrs.wide) 2 else 1 }, ); const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; @@ -1397,12 +1397,26 @@ fn addCursor( screen: *terminal.Screen, cursor_style: renderer.CursorStyle, ) ?*const mtl_shaders.Cell { - // Add the cursor - const cell = screen.getCell( - .active, - screen.cursor.y, - screen.cursor.x, - ); + // Add the cursor. We render the cursor over the wide character if + // we're on the wide characer tail. + const cell, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.getCell( + .active, + screen.cursor.y, + screen.cursor.x, + ); + if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0) + break :cell .{ cell, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + break :cell .{ screen.getCell( + .active, + screen.cursor.y, + screen.cursor.x - 1, + ), screen.cursor.x - 1 }; + }; const color = self.config.cursor_color orelse terminal.color.RGB{ .r = 0xFF, @@ -1421,7 +1435,7 @@ fn addCursor( self.alloc, font.sprite_index, @intFromEnum(sprite), - .{}, + .{ .cell_width = if (cell.attrs.wide) 2 else 1 }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return null; @@ -1430,7 +1444,7 @@ fn addCursor( self.cells.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ - @as(f32, @floatFromInt(screen.cursor.x)), + @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(screen.cursor.y)), }, .cell_width = if (cell.attrs.wide) 2 else 1, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index ff4dab542..8bc935559 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -848,12 +848,26 @@ fn addCursor( screen: *terminal.Screen, cursor_style: renderer.CursorStyle, ) ?*const GPUCell { - // Add the cursor - const cell = screen.getCell( - .active, - screen.cursor.y, - screen.cursor.x, - ); + // Add the cursor. We render the cursor over the wide character if + // we're on the wide characer tail. + const cell, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.getCell( + .active, + screen.cursor.y, + screen.cursor.x, + ); + if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0) + break :cell .{ cell, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + break :cell .{ screen.getCell( + .active, + screen.cursor.y, + screen.cursor.x - 1, + ), screen.cursor.x - 1 }; + }; const color = self.config.cursor_color orelse terminal.color.RGB{ .r = 0xFF, @@ -872,7 +886,7 @@ fn addCursor( self.alloc, font.sprite_index, @intFromEnum(sprite), - .{}, + .{ .cell_width = if (cell.attrs.wide) 2 else 1 }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return null; @@ -880,7 +894,7 @@ fn addCursor( self.cells.appendAssumeCapacity(.{ .mode = .fg, - .grid_col = @intCast(screen.cursor.x), + .grid_col = @intCast(x), .grid_row = @intCast(screen.cursor.y), .grid_width = if (cell.attrs.wide) 2 else 1, .fg_r = color.r, @@ -1136,7 +1150,7 @@ pub fn updateCell( self.alloc, font.sprite_index, @intFromEnum(sprite), - .{}, + .{ .cell_width = if (cell.attrs.wide) 2 else 1 }, ); const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8a0e0c7f2..d27e5c54f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -821,9 +821,10 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { const x = self.screen.cursor.x - 1; const wide_cell = row.getCellPtr(x); + wide_cell.char = 0; wide_cell.attrs.wide = false; - if (self.screen.cursor.x <= 1) { + if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) { self.clearWideSpacerHead(); } } @@ -2013,6 +2014,47 @@ test "Terminal: print over wide char at 0,0" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + + const row = t.screen.getRow(.{ .screen = 0 }); + { + const cell = row.getCell(0); + try testing.expectEqual(@as(u32, 'A'), cell.char); + try testing.expect(!cell.attrs.wide); + try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); + } + { + const cell = row.getCell(1); + try testing.expect(!cell.attrs.wide_spacer_tail); + } +} + +test "Terminal: print over wide spacer tail" { + var t = try init(testing.allocator, 5, 5); + defer t.deinit(testing.allocator); + + try t.print('橋'); + t.setCursorPos(1, 2); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" X", str); + } + + const row = t.screen.getRow(.{ .screen = 0 }); + { + const cell = row.getCell(0); + try testing.expectEqual(@as(u32, 0), cell.char); + try testing.expect(!cell.attrs.wide); + try testing.expectEqual(@as(usize, 1), row.codepointLen(0)); + } + { + const cell = row.getCell(1); + try testing.expectEqual(@as(u32, 'X'), cell.char); + try testing.expect(!cell.attrs.wide_spacer_tail); + try testing.expectEqual(@as(usize, 1), row.codepointLen(1)); + } } test "Terminal: print multicodepoint grapheme, disabled mode 2027" {