mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
font/shaper: split ligature around cell style change
This commit is contained in:
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
|
Reference in New Issue
Block a user