diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index ce15851c1..4069a526b 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -229,14 +229,14 @@ test "run iterator: empty cells with background set" { // Make a screen with some data var screen = try terminal.Screen.init(alloc, 3, 5, 0); defer screen.deinit(); + screen.cursor.pen.bg = try terminal.color.Name.cyan.default(); + screen.cursor.pen.attrs.has_bg = true; try screen.testWriteString("A"); // Get our first row const row = screen.getRow(.{ .active = 0 }); - row.getCellPtr(1).bg = try terminal.color.Name.cyan.default(); - row.getCellPtr(1).attrs.has_bg = true; - row.getCellPtr(2).fg = try terminal.color.Name.yellow.default(); - row.getCellPtr(2).attrs.has_fg = true; + row.getCellPtr(1).* = screen.cursor.pen; + row.getCellPtr(2).* = screen.cursor.pen; // Get our run iterator var shaper = testdata.shaper; @@ -760,6 +760,107 @@ test "shape cursor boundary and colored emoji" { } } +test "shape cell attribute change" { + const testing = std.testing; + const alloc = testing.allocator; + + var testdata = try testShaper(alloc); + defer testdata.deinit(); + + // Plain >= should shape into 1 run + { + var screen = try terminal.Screen.init(alloc, 3, 10, 0); + defer screen.deinit(); + try screen.testWriteString(">="); + + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + + // Bold vs regular should split + { + var screen = try terminal.Screen.init(alloc, 3, 10, 0); + defer screen.deinit(); + try screen.testWriteString(">"); + screen.cursor.pen.attrs.bold = true; + try screen.testWriteString("="); + + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Changing fg color should split + { + var screen = try terminal.Screen.init(alloc, 3, 10, 0); + defer screen.deinit(); + screen.cursor.pen.attrs.has_fg = true; + screen.cursor.pen.fg = .{ .r = 1, .g = 2, .b = 3 }; + try screen.testWriteString(">"); + screen.cursor.pen.fg = .{ .r = 3, .g = 2, .b = 1 }; + try screen.testWriteString("="); + + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Changing bg color should split + { + var screen = try terminal.Screen.init(alloc, 3, 10, 0); + defer screen.deinit(); + screen.cursor.pen.attrs.has_bg = true; + screen.cursor.pen.bg = .{ .r = 1, .g = 2, .b = 3 }; + try screen.testWriteString(">"); + screen.cursor.pen.bg = .{ .r = 3, .g = 2, .b = 1 }; + try screen.testWriteString("="); + + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + + // Same bg color should not split + { + var screen = try terminal.Screen.init(alloc, 3, 10, 0); + defer screen.deinit(); + screen.cursor.pen.attrs.has_bg = true; + screen.cursor.pen.bg = .{ .r = 1, .g = 2, .b = 3 }; + try screen.testWriteString(">"); + try screen.testWriteString("="); + + var shaper = testdata.shaper; + var it = shaper.runIterator(testdata.cache, screen.getRow(.{ .screen = 0 }), null, null); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } +} + const TestShaper = struct { alloc: Allocator, shaper: Shaper, diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 5cdc8fda2..94a5da3f0 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -77,6 +77,20 @@ pub const RunIterator = struct { // If we're a spacer, then we ignore it if (cell.attrs.wide_spacer_tail) continue; + // If our cell attributes are changing, then we split the run. + // This prevents a single glyph for ">=" to be rendered with + // one color when the two components have different styling. + if (j > self.i) { + const prev_cell = self.row.getCell(j - 1); + const Attrs = @TypeOf(cell.attrs); + const Int = @typeInfo(Attrs).Struct.backing_integer.?; + const prev_attrs: Int = @bitCast(prev_cell.attrs.styleAttrs()); + const attrs: Int = @bitCast(cell.attrs.styleAttrs()); + if (prev_attrs != attrs) break; + if (cell.attrs.has_bg and !cell.bg.eql(prev_cell.bg)) break; + if (cell.attrs.has_fg and !cell.fg.eql(prev_cell.fg)) break; + } + // Text runs break when font styles change so we need to get // the proper style. const style: font.Style = style: { diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 335cf06c8..29ac2bc7c 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -218,6 +218,16 @@ pub const Cell = struct { /// also be true. The grapheme code points can be looked up in the /// screen grapheme map. grapheme: bool = false, + + /// Returns only the attributes related to style. + pub fn styleAttrs(self: @This()) @This() { + var copy = self; + copy.wide = false; + copy.wide_spacer_tail = false; + copy.wide_spacer_head = false; + copy.grapheme = false; + return copy; + } } = .{}, /// True if the cell should be skipped for drawing @@ -2666,6 +2676,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { switch (width) { 1 => { const cell = row.getCellPtr(x); + cell.* = self.cursor.pen; cell.char = @intCast(c); grapheme.x = x; @@ -2691,6 +2702,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { { const cell = row.getCellPtr(x); + cell.* = self.cursor.pen; cell.char = @intCast(c); cell.attrs.wide = true; diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 5ec739bf4..7d651508c 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -99,6 +99,10 @@ pub const RGB = struct { g: u8 = 0, b: u8 = 0, + pub fn eql(self: RGB, other: RGB) bool { + return self.r == other.r and self.g == other.g and self.b == other.b; + } + test "size" { try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB));