Merge pull request #2417 from qwerasd205/misc-shaper-fixes

Renderer `rebuildCells` rework
This commit is contained in:
Mitchell Hashimoto
2024-10-12 09:39:08 -07:00
committed by GitHub
7 changed files with 1040 additions and 812 deletions

View File

@ -44,10 +44,7 @@ pub const Cell = struct {
/// this cell is available in the text run. This glyph index is only /// this cell is available in the text run. This glyph index is only
/// valid for a given GroupCache and FontIndex that was used to create /// valid for a given GroupCache and FontIndex that was used to create
/// the runs. /// the runs.
/// glyph_index: u32,
/// If this is null then this is an empty cell. If there are styles
/// then those should be applied but there is no glyph to render.
glyph_index: ?u32,
}; };
/// Options for shapers. /// Options for shapers.

View File

@ -361,6 +361,12 @@ pub const Shaper = struct {
self.cell_buf.clearRetainingCapacity(); self.cell_buf.clearRetainingCapacity();
try self.cell_buf.ensureTotalCapacity(self.alloc, state.codepoints.items.len); try self.cell_buf.ensureTotalCapacity(self.alloc, state.codepoints.items.len);
for (state.codepoints.items) |entry| { for (state.codepoints.items) |entry| {
// We use null codepoints to pad out our list so indices match
// the UTF-16 string we constructed for CoreText. We don't want
// to emit these if this is a special font, since they're not
// part of the original run.
if (entry.codepoint == 0) continue;
self.cell_buf.appendAssumeCapacity(.{ self.cell_buf.appendAssumeCapacity(.{
.x = @intCast(entry.cluster), .x = @intCast(entry.cluster),
.glyph_index = @intCast(entry.codepoint), .glyph_index = @intCast(entry.codepoint),
@ -437,15 +443,6 @@ pub const Shaper = struct {
// wait for that. // wait for that.
if (cell_offset.cluster > cluster) break :pad; if (cell_offset.cluster > cluster) break :pad;
// If we have a gap between clusters then we need to
// add empty cells to the buffer.
for (cell_offset.cluster + 1..cluster) |x| {
try self.cell_buf.append(self.alloc, .{
.x = @intCast(x),
.glyph_index = null,
});
}
cell_offset = .{ .cluster = cluster }; cell_offset = .{ .cluster = cluster };
} }
@ -463,25 +460,6 @@ pub const Shaper = struct {
} }
} }
// If our last cell doesn't match our last cluster then we have
// a left-replaced ligature that needs to have spaces appended
// so that cells retain their background colors.
if (self.cell_buf.items.len > 0) pad: {
const last_cell = self.cell_buf.items[self.cell_buf.items.len - 1];
const last_cp = state.codepoints.items[state.codepoints.items.len - 1];
if (last_cell.x == last_cp.cluster) break :pad;
assert(last_cell.x < last_cp.cluster);
// We need to go back to the last matched cluster and add
// padding up to there.
for (last_cell.x + 1..last_cp.cluster + 1) |x| {
try self.cell_buf.append(self.alloc, .{
.x = @intCast(x),
.glyph_index = null,
});
}
}
return self.cell_buf.items; return self.cell_buf.items;
} }
@ -865,10 +843,10 @@ test "shape inconsolata ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 2), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -890,11 +868,10 @@ test "shape inconsolata ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 3), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -924,11 +901,10 @@ test "shape monaspace ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 3), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -959,11 +935,10 @@ test "shape left-replaced lig in last run" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 3), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -992,12 +967,11 @@ test "shape left-replaced lig in early run" {
); );
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
try testing.expectEqual(@as(usize, 4), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 4), cells.len); try testing.expectEqual(@as(usize, 2), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
try testing.expect(cells[3].glyph_index != null);
} }
} }
@ -1100,8 +1074,7 @@ test "shape emoji width long" {
count += 1; count += 1;
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
// screen.testWriteString isn't grapheme aware, otherwise this is one try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expectEqual(@as(usize, 5), cells.len);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -1282,9 +1255,9 @@ test "shape box glyphs" {
count += 1; count += 1;
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(usize, 2), cells.len);
try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index.?); try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index);
try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 0), cells[0].x);
try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index.?); try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index);
try testing.expectEqual(@as(u16, 1), cells[1].x); try testing.expectEqual(@as(u16, 1), cells[1].x);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -1703,6 +1676,73 @@ test "shape cell attribute change" {
} }
} }
test "shape high plane sprite font codepoint" {
// While creating runs, the CoreText shaper uses `0` codepoints to
// pad its codepoint list to account for high plane characters which
// use two UTF-16 code units. This is so that, after shaping, the string
// indices can be used to find the originating codepoint / cluster.
//
// This is a problem for special (sprite) fonts, which need to be "shaped"
// by simply returning the input codepoints verbatim. We include logic to
// skip `0` codepoints when constructing the shaped run for sprite fonts,
// this test verifies that it works correctly.
const testing = std.testing;
const alloc = testing.allocator;
var testdata = try testShaper(alloc);
defer testdata.deinit();
var screen = try terminal.Screen.init(alloc, 10, 3, 0);
defer screen.deinit();
// U+1FB70: Vertical One Eighth Block-2
try screen.testWriteString("\u{1FB70}");
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
null,
);
// We should get one run
const run = (try it.next(alloc)).?;
// The run state should have the UTF-16 encoding of the character.
try testing.expectEqualSlices(
u16,
&.{ 0xD83E, 0xDF70 },
shaper.run_state.unichars.items,
);
// The codepoint list should be padded.
try testing.expectEqualSlices(
Shaper.Codepoint,
&.{
.{ .codepoint = 0x1FB70, .cluster = 0 },
.{ .codepoint = 0, .cluster = 0 },
},
shaper.run_state.codepoints.items,
);
// And when shape it
const cells = try shaper.shape(run);
// we should have
// - 1 cell
try testing.expectEqual(1, run.cells);
// - at position 0
try testing.expectEqual(0, run.offset);
// - with 1 glyph in it
try testing.expectEqual(1, cells.len);
// - at position 0
try testing.expectEqual(0, cells[0].x);
// - the glyph index should be equal to the codepoint
try testing.expectEqual(0x1FB70, cells[0].glyph_index);
// - it should be a sprite font
try testing.expect(run.font_index.special() != null);
// And we should get a null run after that
try testing.expectEqual(null, try it.next(alloc));
}
const TestShaper = struct { const TestShaper = struct {
alloc: Allocator, alloc: Allocator,
shaper: Shaper, shaper: Shaper,

View File

@ -150,7 +150,7 @@ pub const Shaper = struct {
// Convert all our info/pos to cells and set it. // Convert all our info/pos to cells and set it.
self.cell_buf.clearRetainingCapacity(); self.cell_buf.clearRetainingCapacity();
for (info, pos, 0..) |info_v, pos_v, i| { for (info, pos) |info_v, pos_v| {
// If our cluster changed then we've moved to a new cell. // If our cluster changed then we've moved to a new cell.
if (info_v.cluster != cell_offset.cluster) cell_offset = .{ if (info_v.cluster != cell_offset.cluster) cell_offset = .{
.cluster = info_v.cluster, .cluster = info_v.cluster,
@ -174,48 +174,6 @@ pub const Shaper = struct {
cell_offset.y += pos_v.y_advance; cell_offset.y += pos_v.y_advance;
} }
// Determine the width of the cell. To do this, we have to
// find the next cluster that has been shaped. This tells us how
// many cells this glyph replaced (i.e. for ligatures). For example
// in some fonts "!=" turns into a single glyph from the component
// parts "!" and "=" so this cell width would be "2" despite
// only having a single glyph.
//
// Many fonts replace ligature cells with space so that this always
// is one (e.g. Fira Code, JetBrains Mono, etc). Some do not
// (e.g. Monaspace).
const cell_width = width: {
if (i + 1 < info.len) {
// We may have to go through multiple glyphs because
// multiple can be replaced. e.g. "==="
for (info[i + 1 ..]) |next_info_v| {
if (next_info_v.cluster != info_v.cluster) {
// We do a saturating sub here because for RTL
// text, the next cluster can be less than the
// current cluster. We don't really support RTL
// currently so we do this to prevent an underflow
// but it isn't correct generally.
break :width next_info_v.cluster -| info_v.cluster;
}
}
}
// If we reached the end then our width is our max cluster
// minus this one.
const max = run.offset + run.cells;
break :width max - info_v.cluster;
};
if (cell_width > 1) {
// To make the renderer implementations simpler, we convert
// the extra spaces for width to blank cells.
for (1..cell_width) |j| {
try self.cell_buf.append(self.alloc, .{
.x = @intCast(info_v.cluster + j),
.glyph_index = null,
});
}
}
// const i = self.cell_buf.items.len - 1; // const i = self.cell_buf.items.len - 1;
// log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] }); // log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] });
} }
@ -428,10 +386,10 @@ test "shape inconsolata ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 2), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -453,11 +411,10 @@ test "shape inconsolata ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 3), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -487,11 +444,10 @@ test "shape monaspace ligs" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 3), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expect(cells[0].glyph_index != null);
try testing.expect(cells[1].glyph_index == null);
try testing.expect(cells[2].glyph_index == null);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -521,8 +477,10 @@ test "shape emoji width" {
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
try testing.expectEqual(@as(usize, 2), run.cells);
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(usize, 1), cells.len);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -564,8 +522,7 @@ test "shape emoji width long" {
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
// screen.testWriteString isn't grapheme aware, otherwise this is two try testing.expectEqual(@as(usize, 1), cells.len);
try testing.expectEqual(@as(usize, 5), cells.len);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
@ -751,9 +708,9 @@ test "shape box glyphs" {
try testing.expectEqual(@as(u32, 2), shaper.hb_buf.getLength()); try testing.expectEqual(@as(u32, 2), shaper.hb_buf.getLength());
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
try testing.expectEqual(@as(usize, 2), cells.len); try testing.expectEqual(@as(usize, 2), cells.len);
try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index.?); try testing.expectEqual(@as(u32, 0x2500), cells[0].glyph_index);
try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 0), cells[0].x);
try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index.?); try testing.expectEqual(@as(u32, 0x2501), cells[1].glyph_index);
try testing.expectEqual(@as(u16, 1), cells[1].x); try testing.expectEqual(@as(u16, 1), cells[1].x);
} }
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);

View File

@ -111,7 +111,13 @@ pub const Shaper = struct {
// expose a public API for this. // expose a public API for this.
const face = try run.grid.resolver.collection.getFace(run.font_index); const face = try run.grid.resolver.collection.getFace(run.font_index);
for (state.codepoints.items) |entry| { for (state.codepoints.items) |entry| {
const glyph_index = face.glyphIndex(entry.codepoint); const glyph_index = face.glyphIndex(entry.codepoint) orelse {
// The run iterator shared logic should guarantee that
// there is a glyph index for all codepoints in the run.
// This is not well tested because we don't use the noop
// shaper in any release builds.
unreachable;
};
try self.cell_buf.append(self.alloc, .{ try self.cell_buf.append(self.alloc, .{
.x = @intCast(entry.cluster), .x = @intCast(entry.cluster),
.glyph_index = glyph_index, .glyph_index = glyph_index,

View File

@ -55,6 +55,14 @@ pub const RunIterator = struct {
break :max 0; break :max 0;
}; };
// Invisible cells don't have any glyphs rendered,
// so we explicitly skip them in the shaping process.
while (self.i < max and
self.row.style(&cells[self.i]).flags.invisible)
{
self.i += 1;
}
// We're over at the max // We're over at the max
if (self.i >= max) return null; if (self.i >= max) return null;

View File

@ -2209,7 +2209,7 @@ fn rebuildCells(
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
var y: terminal.size.CellCountInt = screen.pages.rows; var y: terminal.size.CellCountInt = screen.pages.rows;
while (row_it.next()) |row| { while (row_it.next()) |row| {
y = y - 1; y -= 1;
if (!rebuild) { if (!rebuild) {
// Only rebuild if we are doing a full rebuild or this row is dirty. // Only rebuild if we are doing a full rebuild or this row is dirty.
@ -2255,82 +2255,327 @@ fn rebuildCells(
}, },
} }
// Split our row into runs and shape each one. // Iterator of runs for shaping.
var iter = self.font_shaper.runIterator( var run_iter = self.font_shaper.runIterator(
self.font_grid, self.font_grid,
screen, screen,
row, row,
row_selection, row_selection,
if (shape_cursor) screen.cursor.x else null, if (shape_cursor) screen.cursor.x else null,
); );
while (try iter.next(self.alloc)) |run| { var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
// Try to read the cells from the shaping cache if we can. var shaper_cells: ?[]const font.shape.Cell = null;
const shaper_cells = self.font_shaper_cache.get(run) orelse cache: { var shaper_cells_i: usize = 0;
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we continue const row_cells = row.cells(.all);
// because it is just a performance optimization, not a correctness
// issue.
self.font_shaper_cache.put(self.alloc, run, cells) catch |err| {
log.warn("error caching font shaping results err={}", .{err});
};
// The cells we get from direct shaping are always owned by for (row_cells, 0..) |*cell, x| {
// the shaper and valid until the next shaping call so we can // If this cell falls within our preedit range then we
// just return them. // skip this because preedits are setup separately.
break :cache cells; if (preedit_range) |range| preedit: {
}; // We're not on the preedit line, no actions necessary.
if (range.y != y) break :preedit;
// We're before the preedit range, no actions necessary.
if (x < range.x[0]) break :preedit;
// We're in the preedit range, skip this cell.
if (x <= range.x[1]) continue;
// After exiting the preedit range we need to catch
// the run position up because of the missed cells.
// In all other cases, no action is necessary.
if (x != range.x[1] + 1) break :preedit;
for (shaper_cells) |shaper_cell| { // Step the run iterator until we find a run that ends
// The shaper can emit null glyphs representing the right half // after the current cell, which will be the soonest run
// of wide characters, we don't need to do anything with them. // that might contain glyphs for our cell.
if (shaper_cell.glyph_index == null) continue; while (shaper_run) |run| {
if (run.offset + run.cells > x) break;
const coord: terminal.Coordinate = .{ shaper_run = try run_iter.next(self.alloc);
.x = shaper_cell.x, shaper_cells = null;
.y = y, shaper_cells_i = 0;
};
// If this cell falls within our preedit range then we
// skip this because preedits are setup separately.
if (preedit_range) |range| {
if (range.y == coord.y and
coord.x >= range.x[0] and
coord.x <= range.x[1])
{
continue;
}
} }
// It this cell is within our hint range then we need to const run = shaper_run orelse break :preedit;
// underline it.
const cell: terminal.Pin = cell: { // If we haven't shaped this run, do so now.
var copy = row; shaper_cells = shaper_cells orelse
copy.x = coord.x; // Try to read the cells from the shaping cache if we can.
break :cell copy; self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
}; };
if (self.updateCell( // Advance our index until we reach or pass
screen, // our current x position in the shaper cells.
cell, while (shaper_cells.?[shaper_cells_i].x < x) {
if (link_match_set.contains(screen, cell)) shaper_cells_i += 1;
.single
else
null,
color_palette,
shaper_cell,
run,
coord,
)) |update| {
assert(update);
} else |err| {
log.warn("error building cell, will be invalid x={} y={}, err={}", .{
coord.x,
coord.y,
err,
});
} }
} }
const wide = cell.wide;
const style = row.style(cell);
const cell_pin: terminal.Pin = cell: {
var copy = row;
copy.x = @intCast(x);
break :cell copy;
};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, .{
.page = row.page,
.y = row.y,
.x = @intCast(
// Spacer tails should show the selection
// state of the wide cell they belong to.
if (wide == .spacer_tail)
x -| 1
else
x,
),
})
else
false;
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
// The final background color for the cell.
const bg = bg: {
if (selected) {
break :bg if (self.config.invert_selection_fg_bg)
if (style.flags.inverse)
// Cell is selected with invert selection fg/bg
// enabled, and the cell has the inverse style
// flag, so they cancel out and we get the normal
// bg color.
bg_style
else
// If it doesn't have the inverse style
// flag then we use the fg color instead.
fg_style
else
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color;
}
// Not selected
break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
// Two cases cause us to invert (use the fg color as the bg)
// - The "inverse" style flag.
// - A "covering" glyph; we use fg for bg in that case to
// help make sure that padding extension works correctly.
// If one of these is true (but not the other)
// then we use the fg style color for the bg.
fg_style
else
// Otherwise they cancel out.
bg_style;
};
const fg = fg: {
if (selected and !self.config.invert_selection_fg_bg) {
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color;
}
// Whether we need to use the bg color as our fg color:
// - Cell is inverted and not selected
// - Cell is selected and not inverted
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color
else
fg_style;
};
// Foreground alpha for this cell.
const alpha: u8 = if (style.flags.faint) 175 else 255;
// If the cell has a background color, set it.
if (bg) |rgb| {
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
// situations. See inline comments.
const bg_alpha: u8 = bg_alpha: {
const default: u8 = 255;
if (self.config.background_opacity >= 1) break :bg_alpha default;
// If we're selected, we do not apply background opacity
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (style.flags.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
// We apply background opacity.
var bg_alpha: f64 = @floatFromInt(default);
bg_alpha *= self.config.background_opacity;
bg_alpha = @ceil(bg_alpha);
break :bg_alpha @intFromFloat(bg_alpha);
};
self.cells.bgCell(y, x).* = .{
rgb.r, rgb.g, rgb.b, bg_alpha,
};
}
// If the invisible flag is set on this cell then we
// don't need to render any foreground elements, so
// we just skip all glyphs with this x coordinate.
//
// NOTE: This behavior matches xterm. Some other terminal
// emulators, e.g. Alacritty, still render text decorations
// and only make the text itself invisible. The decision
// has been made here to match xterm's behavior for this.
if (style.flags.invisible) {
continue;
}
// Give links a single underline, unless they already have
// an underline, in which case use a double underline to
// distinguish them.
const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
if (style.flags.underline == .single)
.double
else
.single
else
style.flags.underline;
// We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used
// which intersects parts of the text (descenders).
if (underline != .none) self.addUnderline(
@intCast(x),
@intCast(y),
underline,
style.underlineColor(color_palette) orelse fg,
alpha,
) catch |err| {
log.warn(
"error adding underline to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
// If we're at or past the end of our shaper run then
// we need to get the next run from the run iterator.
if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
shaper_run = try run_iter.next(self.alloc);
shaper_cells = null;
shaper_cells_i = 0;
}
if (shaper_run) |run| glyphs: {
// If we haven't shaped this run yet, do so.
shaper_cells = shaper_cells orelse
// Try to read the cells from the shaping cache if we can.
self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
};
const cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine.
if (cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
assert(cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1;
}) {
self.addGlyph(
@intCast(x),
@intCast(y),
cell_pin,
cells[shaper_cells_i],
shaper_run.?,
fg,
alpha,
) catch |err| {
log.warn(
"error adding glyph to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
}
}
// Finally, draw a strikethrough if necessary.
if (style.flags.strikethrough) self.addStrikethrough(
@intCast(x),
@intCast(y),
fg,
alpha,
) catch |err| {
log.warn(
"error adding strikethrough to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
} }
} }
@ -2422,252 +2667,133 @@ fn rebuildCells(
// }); // });
} }
fn updateCell( /// Add an underline decoration to the specified cell
fn addUnderline(
self: *Metal, self: *Metal,
screen: *const terminal.Screen, x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
style: terminal.Attribute.Underline,
color: terminal.color.RGB,
alpha: u8,
) !void {
const sprite: font.Sprite = switch (style) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
try self.cells.add(self.alloc, .underline, .{
.mode = .fg,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.bearings = .{
@intCast(render.glyph.offset_x),
@intCast(render.glyph.offset_y),
},
});
}
/// Add a strikethrough decoration to the specified cell
fn addStrikethrough(
self: *Metal,
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
color: terminal.color.RGB,
alpha: u8,
) !void {
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(font.Sprite.strikethrough),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
try self.cells.add(self.alloc, .strikethrough, .{
.mode = .fg,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.bearings = .{
@intCast(render.glyph.offset_x),
@intCast(render.glyph.offset_y),
},
});
}
// Add a glyph to the specified cell.
fn addGlyph(
self: *Metal,
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
cell_pin: terminal.Pin, cell_pin: terminal.Pin,
cell_underline: ?terminal.Attribute.Underline,
palette: *const terminal.color.Palette,
shaper_cell: font.shape.Cell, shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun, shaper_run: font.shape.TextRun,
coord: terminal.Coordinate, color: terminal.color.RGB,
) !bool { alpha: u8,
const BgFg = struct { ) !void {
/// Background is optional because in un-inverted mode
/// it may just be equivalent to the default background in
/// which case we do nothing to save on GPU render time.
bg: ?terminal.color.RGB,
/// Fg is always set to some color, though we may not render
/// any fg if the cell is empty or has no attributes like
/// underline.
fg: terminal.color.RGB,
};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, cell_pin)
else
false;
const rac = cell_pin.rowAndCell(); const rac = cell_pin.rowAndCell();
const cell = rac.cell; const cell = rac.cell;
const style = cell_pin.style(cell);
const underline = cell_underline orelse style.flags.underline;
// The colors for the cell. // Render
const colors: BgFg = colors: { const render = try self.font_grid.renderGlyph(
// The normal cell result self.alloc,
const cell_res: BgFg = if (!style.flags.inverse) .{ shaper_run.font_index,
// In normal mode, background and fg match the cell. We shaper_cell.glyph_index,
// un-optionalize the fg by defaulting to our fg color. .{
.bg = style.bg(cell, palette), .grid_metrics = self.grid_metrics,
.fg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, .thicken = self.config.font_thicken,
} else .{ },
// In inverted mode, the background MUST be set to something );
// (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background.
.bg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color,
.fg = style.bg(cell, palette) orelse self.background_color,
};
// If we are selected, we our colors are just inverted fg/bg // If the glyph is 0 width or height, it will be invisible
const selection_res: ?BgFg = if (selected) .{ // when drawn, so don't bother adding it to the buffer.
.bg = if (self.config.invert_selection_fg_bg) if (render.glyph.width == 0 or render.glyph.height == 0) {
cell_res.fg return;
else }
self.config.selection_background orelse self.foreground_color,
.fg = if (self.config.invert_selection_fg_bg)
cell_res.bg orelse self.background_color
else
self.config.selection_foreground orelse self.background_color,
} else null;
// If the cell is "invisible" then we just make fg = bg so that const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
// the cell is transparent but still copy-able. render.presentation,
const res: BgFg = selection_res orelse cell_res; cell_pin,
if (style.flags.invisible) { )) {
break :colors .{ .normal => .fg,
.bg = res.bg, .color => .fg_color,
.fg = res.bg orelse self.background_color, .constrained => .fg_constrained,
}; .powerline => .fg_powerline,
}
// If our cell has a covering glyph, then our bg is set to our fg
// so that padding extension works correctly.
if (!selected and isCovering(cell.codepoint())) {
break :colors .{
.bg = res.fg,
.fg = res.fg,
};
}
break :colors res;
}; };
// Alpha multiplier try self.cells.add(self.alloc, .text, .{
const alpha: u8 = if (style.flags.faint) 175 else 255; .mode = mode,
.grid_pos = .{ @intCast(x), @intCast(y) },
// If the cell has a background, we always draw it. .constraint_width = cell.gridWidth(),
if (colors.bg) |rgb| { .color = .{ color.r, color.g, color.b, alpha },
// Determine our background alpha. If we have transparency configured .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
// then this is dynamic depending on some situations. This is all .glyph_size = .{ render.glyph.width, render.glyph.height },
// in an attempt to make transparency look the best for various .bearings = .{
// situations. See inline comments. @intCast(render.glyph.offset_x + shaper_cell.x_offset),
const bg_alpha: u8 = bg_alpha: { @intCast(render.glyph.offset_y + shaper_cell.y_offset),
const default: u8 = 255; },
});
if (self.config.background_opacity >= 1) break :bg_alpha default;
// If we're selected, we do not apply background opacity
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (style.flags.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
// We apply background opacity.
var bg_alpha: f64 = @floatFromInt(default);
bg_alpha *= self.config.background_opacity;
bg_alpha = @ceil(bg_alpha);
break :bg_alpha @intFromFloat(bg_alpha);
};
self.cells.bgCell(coord.y, coord.x).* = .{
rgb.r, rgb.g, rgb.b, bg_alpha,
};
if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) {
self.cells.bgCell(coord.y, coord.x + 1).* = .{
rgb.r, rgb.g, rgb.b, bg_alpha,
};
}
}
// If the cell has an underline, draw it before the character glyph,
// so that it layers underneath instead of overtop, since that can
// make text difficult to read.
if (underline != .none) {
const sprite: font.Sprite = switch (underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
const color = style.underlineColor(palette) orelse colors.fg;
var gpu_cell: mtl_cell.Key.underline.CellType() = .{
.mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.bearings = .{
@intCast(render.glyph.offset_x),
@intCast(render.glyph.offset_y),
},
};
try self.cells.add(self.alloc, .underline, gpu_cell);
// If it's a wide cell we need to underline the right half as well.
if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) {
gpu_cell.grid_pos[0] = @intCast(coord.x + 1);
try self.cells.add(self.alloc, .underline, gpu_cell);
}
}
// If the shaper cell has a glyph, draw it.
if (shaper_cell.glyph_index) |glyph_index| glyph: {
// Render
const render = try self.font_grid.renderGlyph(
self.alloc,
shaper_run.font_index,
glyph_index,
.{
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
},
);
// If the glyph is 0 width or height, it will be invisible
// when drawn, so don't bother adding it to the buffer.
if (render.glyph.width == 0 or render.glyph.height == 0) {
break :glyph;
}
const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
try self.cells.add(self.alloc, .text, .{
.mode = mode,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.constraint_width = cell.gridWidth(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.bearings = .{
@intCast(render.glyph.offset_x + shaper_cell.x_offset),
@intCast(render.glyph.offset_y + shaper_cell.y_offset),
},
});
}
if (style.flags.strikethrough) {
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(font.Sprite.strikethrough),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
var gpu_cell: mtl_cell.Key.strikethrough.CellType() = .{
.mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.constraint_width = 1,
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.bearings = .{
@intCast(render.glyph.offset_x),
@intCast(render.glyph.offset_y),
},
};
try self.cells.add(self.alloc, .strikethrough, gpu_cell);
// If it's a wide cell we need to strike through the right half as well.
if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) {
gpu_cell.grid_pos[0] = @intCast(coord.x + 1);
try self.cells.add(self.alloc, .strikethrough, gpu_cell);
}
}
return true;
} }
fn addCursor( fn addCursor(

View File

@ -1255,29 +1255,10 @@ pub fn rebuildCells(
} }
// Build each cell // Build each cell
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
var y: terminal.size.CellCountInt = 0; var y: terminal.size.CellCountInt = screen.pages.rows;
while (row_it.next()) |row| { while (row_it.next()) |row| {
defer y += 1; y -= 1;
// Our selection value is only non-null if this selection happens
// to contain this row. This selection value will be set to only be
// the selection that contains this row. This way, if the selection
// changes but not for this line, we don't invalidate the cache.
const selection = sel: {
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// See Metal.zig
const cursor_row = if (cursor_style_) |cursor_style|
cursor_style == .block and
screen.viewportIsBottom() and
y == screen.cursor.y
else
false;
// True if we want to do font shaping around the cursor. We want to // True if we want to do font shaping around the cursor. We want to
// do font shaping as long as the cursor is enabled. // do font shaping as long as the cursor is enabled.
@ -1287,7 +1268,7 @@ pub fn rebuildCells(
// If this is the row with our cursor, then we may have to modify // If this is the row with our cursor, then we may have to modify
// the cell with the cursor. // the cell with the cursor.
const start_i: usize = self.cells.items.len; const start_i: usize = self.cells.items.len;
defer if (cursor_row) { defer if (shape_cursor and cursor_style_ == .block) {
const x = screen.cursor.x; const x = screen.cursor.x;
const wide = row.cells(.all)[x].wide; const wide = row.cells(.all)[x].wide;
const min_x = switch (wide) { const min_x = switch (wide) {
@ -1310,6 +1291,15 @@ pub fn rebuildCells(
} }
}; };
// We need to get this row's selection if there is one for proper
// run splitting.
const row_selection = sel: {
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// On primary screen, we still apply vertical padding extension // On primary screen, we still apply vertical padding extension
// under certain conditions we feel are safe. This helps make some // under certain conditions we feel are safe. This helps make some
// scenarios look better while avoiding scenarios we know do NOT look // scenarios look better while avoiding scenarios we know do NOT look
@ -1332,79 +1322,356 @@ pub fn rebuildCells(
}, },
} }
// Split our row into runs and shape each one. // Iterator of runs for shaping.
var iter = self.font_shaper.runIterator( var run_iter = self.font_shaper.runIterator(
self.font_grid, self.font_grid,
screen, screen,
row, row,
selection, row_selection,
if (shape_cursor) screen.cursor.x else null, if (shape_cursor) screen.cursor.x else null,
); );
while (try iter.next(self.alloc)) |run| { var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
// Try to read the cells from the shaping cache if we can. var shaper_cells: ?[]const font.shape.Cell = null;
const shaper_cells = self.font_shaper_cache.get(run) orelse cache: { var shaper_cells_i: usize = 0;
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we continue const row_cells = row.cells(.all);
// because it is just a performance optimization, not a correctness
// issue.
self.font_shaper_cache.put(self.alloc, run, cells) catch |err| {
log.warn("error caching font shaping results err={}", .{err});
};
// The cells we get from direct shaping are always owned by for (row_cells, 0..) |*cell, x| {
// the shaper and valid until the next shaping call so we can // If this cell falls within our preedit range then we
// just return them. // skip this because preedits are setup separately.
break :cache cells; if (preedit_range) |range| preedit: {
}; // We're not on the preedit line, no actions necessary.
if (range.y != y) break :preedit;
// We're before the preedit range, no actions necessary.
if (x < range.x[0]) break :preedit;
// We're in the preedit range, skip this cell.
if (x <= range.x[1]) continue;
// After exiting the preedit range we need to catch
// the run position up because of the missed cells.
// In all other cases, no action is necessary.
if (x != range.x[1] + 1) break :preedit;
for (shaper_cells) |shaper_cell| { // Step the run iterator until we find a run that ends
// The shaper can emit null glyphs representing the right half // after the current cell, which will be the soonest run
// of wide characters, we don't need to do anything with them. // that might contain glyphs for our cell.
if (shaper_cell.glyph_index == null) continue; while (shaper_run) |run| {
if (run.offset + run.cells > x) break;
// If this cell falls within our preedit range then we skip it. shaper_run = try run_iter.next(self.alloc);
// We do this so we don't have conflicting data on the same shaper_cells = null;
// cell. shaper_cells_i = 0;
if (preedit_range) |range| {
if (range.y == y and
shaper_cell.x >= range.x[0] and
shaper_cell.x <= range.x[1])
{
continue;
}
} }
// It this cell is within our hint range then we need to const run = shaper_run orelse break :preedit;
// underline it.
const cell: terminal.Pin = cell: { // If we haven't shaped this run, do so now.
var copy = row; shaper_cells = shaper_cells orelse
copy.x = shaper_cell.x; // Try to read the cells from the shaping cache if we can.
break :cell copy; self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
}; };
if (self.updateCell( // Advance our index until we reach or pass
screen, // our current x position in the shaper cells.
cell, while (shaper_cells.?[shaper_cells_i].x < x) {
if (link_match_set.orderedContains(screen, cell)) shaper_cells_i += 1;
.single
else
null,
color_palette,
shaper_cell,
run,
shaper_cell.x,
y,
)) |update| {
assert(update);
} else |err| {
log.warn("error building cell, will be invalid x={} y={}, err={}", .{
shaper_cell.x,
y,
err,
});
} }
} }
const wide = cell.wide;
const style = row.style(cell);
const cell_pin: terminal.Pin = cell: {
var copy = row;
copy.x = @intCast(x);
break :cell copy;
};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, .{
.page = row.page,
.y = row.y,
.x = @intCast(
// Spacer tails should show the selection
// state of the wide cell they belong to.
if (wide == .spacer_tail)
x -| 1
else
x,
),
})
else
false;
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
// The final background color for the cell.
const bg = bg: {
if (selected) {
break :bg if (self.config.invert_selection_fg_bg)
if (style.flags.inverse)
// Cell is selected with invert selection fg/bg
// enabled, and the cell has the inverse style
// flag, so they cancel out and we get the normal
// bg color.
bg_style
else
// If it doesn't have the inverse style
// flag then we use the fg color instead.
fg_style
else
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color;
}
// Not selected
break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
// Two cases cause us to invert (use the fg color as the bg)
// - The "inverse" style flag.
// - A "covering" glyph; we use fg for bg in that case to
// help make sure that padding extension works correctly.
// If one of these is true (but not the other)
// then we use the fg style color for the bg.
fg_style
else
// Otherwise they cancel out.
bg_style;
};
const fg = fg: {
if (selected and !self.config.invert_selection_fg_bg) {
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color;
}
// Whether we need to use the bg color as our fg color:
// - Cell is inverted and not selected
// - Cell is selected and not inverted
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color
else
fg_style;
};
// Foreground alpha for this cell.
const alpha: u8 = if (style.flags.faint) 175 else 255;
// If the cell has a background color, set it.
const bg_color: [4]u8 = if (bg) |rgb| bg: {
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
// situations. See inline comments.
const bg_alpha: u8 = bg_alpha: {
const default: u8 = 255;
if (self.config.background_opacity >= 1) break :bg_alpha default;
// If we're selected, we do not apply background opacity
if (selected) break :bg_alpha default;
// If we're reversed, do not apply background opacity
if (style.flags.inverse) break :bg_alpha default;
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
// We apply background opacity.
var bg_alpha: f64 = @floatFromInt(default);
bg_alpha *= self.config.background_opacity;
bg_alpha = @ceil(bg_alpha);
break :bg_alpha @intFromFloat(bg_alpha);
};
try self.cells_bg.append(self.alloc, .{
.mode = .bg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
.a = bg_alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
break :bg .{
rgb.r, rgb.g, rgb.b, bg_alpha,
};
} else .{
self.draw_background.r,
self.draw_background.g,
self.draw_background.b,
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
};
// If the invisible flag is set on this cell then we
// don't need to render any foreground elements, so
// we just skip all glyphs with this x coordinate.
//
// NOTE: This behavior matches xterm. Some other terminal
// emulators, e.g. Alacritty, still render text decorations
// and only make the text itself invisible. The decision
// has been made here to match xterm's behavior for this.
if (style.flags.invisible) {
continue;
}
// Give links a single underline, unless they already have
// an underline, in which case use a double underline to
// distinguish them.
const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
if (style.flags.underline == .single)
.double
else
.single
else
style.flags.underline;
// We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used
// which intersects parts of the text (descenders).
if (underline != .none) self.addUnderline(
@intCast(x),
@intCast(y),
underline,
style.underlineColor(color_palette) orelse fg,
alpha,
bg_color,
) catch |err| {
log.warn(
"error adding underline to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
// If we're at or past the end of our shaper run then
// we need to get the next run from the run iterator.
if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
shaper_run = try run_iter.next(self.alloc);
shaper_cells = null;
shaper_cells_i = 0;
}
if (shaper_run) |run| glyphs: {
// If we haven't shaped this run yet, do so.
shaper_cells = shaper_cells orelse
// Try to read the cells from the shaping cache if we can.
self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
// not a correctness issue.
self.font_shaper_cache.put(
self.alloc,
run,
cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
.{err},
);
};
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
};
const cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine.
if (cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
assert(cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1;
}) {
self.addGlyph(
@intCast(x),
@intCast(y),
cell_pin,
cells[shaper_cells_i],
shaper_run.?,
fg,
alpha,
bg_color,
) catch |err| {
log.warn(
"error adding glyph to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
}
}
// Finally, draw a strikethrough if necessary.
if (style.flags.strikethrough) self.addStrikethrough(
@intCast(x),
@intCast(y),
fg,
alpha,
bg_color,
) catch |err| {
log.warn(
"error adding strikethrough to cell, will be invalid x={} y={}, err={}",
.{ x, y, err },
);
};
} }
} }
@ -1637,334 +1904,161 @@ fn addCursor(
return &self.cells.items[self.cells.items.len - 1]; return &self.cells.items[self.cells.items.len - 1];
} }
/// Update a single cell. The bool returns whether the cell was updated /// Add an underline decoration to the specified cell
/// or not. If the cell wasn't updated, a full refreshCells call is fn addUnderline(
/// needed.
fn updateCell(
self: *OpenGL, self: *OpenGL,
screen: *terminal.Screen, x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
style: terminal.Attribute.Underline,
color: terminal.color.RGB,
alpha: u8,
bg: [4]u8,
) !void {
const sprite: font.Sprite = switch (style) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = color.r,
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
/// Add a strikethrough decoration to the specified cell
fn addStrikethrough(
self: *OpenGL,
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
color: terminal.color.RGB,
alpha: u8,
bg: [4]u8,
) !void {
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(font.Sprite.strikethrough),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = color.r,
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
// Add a glyph to the specified cell.
fn addGlyph(
self: *OpenGL,
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
cell_pin: terminal.Pin, cell_pin: terminal.Pin,
cell_underline: ?terminal.Attribute.Underline,
palette: *const terminal.color.Palette,
shaper_cell: font.shape.Cell, shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun, shaper_run: font.shape.TextRun,
x: usize, color: terminal.color.RGB,
y: usize, alpha: u8,
) !bool { bg: [4]u8,
const BgFg = struct { ) !void {
/// Background is optional because in un-inverted mode
/// it may just be equivalent to the default background in
/// which case we do nothing to save on GPU render time.
bg: ?terminal.color.RGB,
/// Fg is always set to some color, though we may not render
/// any fg if the cell is empty or has no attributes like
/// underline.
fg: terminal.color.RGB,
};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, cell_pin)
else
false;
const rac = cell_pin.rowAndCell(); const rac = cell_pin.rowAndCell();
const cell = rac.cell; const cell = rac.cell;
const style = cell_pin.style(cell);
const underline = cell_underline orelse style.flags.underline;
// The colors for the cell. // Render
const colors: BgFg = colors: { const render = try self.font_grid.renderGlyph(
// The normal cell result self.alloc,
const cell_res: BgFg = if (!style.flags.inverse) .{ shaper_run.font_index,
// In normal mode, background and fg match the cell. We shaper_cell.glyph_index,
// un-optionalize the fg by defaulting to our fg color. .{
.bg = style.bg(cell, palette), .grid_metrics = self.grid_metrics,
.fg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, .thicken = self.config.font_thicken,
} else .{ },
// In inverted mode, the background MUST be set to something );
// (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background.
.bg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color,
.fg = style.bg(cell, palette) orelse self.background_color,
};
// If we are selected, we our colors are just inverted fg/bg // If the glyph is 0 width or height, it will be invisible
const selection_res: ?BgFg = if (selected) .{ // when drawn, so don't bother adding it to the buffer.
.bg = if (self.config.invert_selection_fg_bg) if (render.glyph.width == 0 or render.glyph.height == 0) {
cell_res.fg return;
else }
self.config.selection_background orelse self.foreground_color,
.fg = if (self.config.invert_selection_fg_bg)
cell_res.bg orelse self.background_color
else
self.config.selection_foreground orelse self.background_color,
} else null;
// If the cell is "invisible" then we just make fg = bg so that // If we're rendering a color font, we use the color atlas
// the cell is transparent but still copy-able. const mode: CellProgram.CellMode = switch (try fgMode(
const res: BgFg = selection_res orelse cell_res; render.presentation,
if (style.flags.invisible) { cell_pin,
break :colors BgFg{ )) {
.bg = res.bg, .normal => .fg,
.fg = res.bg orelse self.background_color, .color => .fg_color,
}; .constrained => .fg_constrained,
} .powerline => .fg_powerline,
// If our cell has a covering glyph, then our bg is set to our fg
// so that padding extension works correctly.
if (!selected and isCovering(cell.codepoint())) {
break :colors .{
.bg = res.fg,
.fg = res.fg,
};
}
break :colors res;
}; };
// Alpha multiplier try self.cells.append(self.alloc, .{
const alpha: u8 = if (style.flags.faint) 175 else 255; .mode = mode,
.grid_col = @intCast(x),
// If the cell has a background, we always draw it. .grid_row = @intCast(y),
const bg: [4]u8 = if (colors.bg) |rgb| bg: { .grid_width = cell.gridWidth(),
// Determine our background alpha. If we have transparency configured .glyph_x = render.glyph.atlas_x,
// then this is dynamic depending on some situations. This is all .glyph_y = render.glyph.atlas_y,
// in an attempt to make transparency look the best for various .glyph_width = render.glyph.width,
// situations. See inline comments. .glyph_height = render.glyph.height,
const bg_alpha: u8 = bg_alpha: { .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
const default: u8 = 255; .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
.r = color.r,
if (self.config.background_opacity >= 1) break :bg_alpha default; .g = color.g,
.b = color.b,
// If we're selected, we do not apply background opacity .a = alpha,
if (selected) break :bg_alpha default; .bg_r = bg[0],
.bg_g = bg[1],
// If we're reversed, do not apply background opacity .bg_b = bg[2],
if (style.flags.inverse) break :bg_alpha default; .bg_a = bg[3],
});
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) {
break :bg_alpha default;
}
// We apply background opacity.
var bg_alpha: f64 = @floatFromInt(default);
bg_alpha *= self.config.background_opacity;
bg_alpha = @ceil(bg_alpha);
break :bg_alpha @intFromFloat(bg_alpha);
};
try self.cells_bg.append(self.alloc, .{
.mode = .bg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
.glyph_x = 0,
.glyph_y = 0,
.glyph_width = 0,
.glyph_height = 0,
.glyph_offset_x = 0,
.glyph_offset_y = 0,
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
.a = bg_alpha,
.bg_r = 0,
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
});
break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha };
} else .{
self.draw_background.r,
self.draw_background.g,
self.draw_background.b,
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
};
// If the cell has an underline, draw it before the character glyph,
// so that it layers underneath instead of overtop, since that can
// make text difficult to read.
if (underline != .none) {
const sprite: font.Sprite = switch (underline) {
.none => unreachable,
.single => .underline,
.double => .underline_double,
.dotted => .underline_dotted,
.dashed => .underline_dashed,
.curly => .underline_curly,
};
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
const color = style.underlineColor(palette) orelse colors.fg;
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = color.r,
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
// If it's a wide cell we need to underline the right half as well.
if (cell.gridWidth() > 1 and x < self.grid_size.columns - 1) {
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x + 1),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = color.r,
.g = color.g,
.b = color.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
}
// If the shaper cell has a glyph, draw it.
if (shaper_cell.glyph_index) |glyph_index| glyph: {
// Render
const render = try self.font_grid.renderGlyph(
self.alloc,
shaper_run.font_index,
glyph_index,
.{
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
},
);
// If the glyph is 0 width or height, it will be invisible
// when drawn, so don't bother adding it to the buffer.
if (render.glyph.width == 0 or render.glyph.height == 0) {
break :glyph;
}
// If we're rendering a color font, we use the color atlas
const mode: CellProgram.CellMode = switch (try fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
try self.cells.append(self.alloc, .{
.mode = mode,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
.r = colors.fg.r,
.g = colors.fg.g,
.b = colors.fg.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
if (style.flags.strikethrough) {
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(font.Sprite.strikethrough),
.{
.cell_width = 1,
.grid_metrics = self.grid_metrics,
},
);
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = colors.fg.r,
.g = colors.fg.g,
.b = colors.fg.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
// If it's a wide cell we need to strike through the right half as well.
if (cell.gridWidth() > 1 and x < self.grid_size.columns - 1) {
try self.cells.append(self.alloc, .{
.mode = .fg,
.grid_col = @intCast(x + 1),
.grid_row = @intCast(y),
.grid_width = 1,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = colors.fg.r,
.g = colors.fg.g,
.b = colors.fg.b,
.a = alpha,
.bg_r = bg[0],
.bg_g = bg[1],
.bg_b = bg[2],
.bg_a = bg[3],
});
}
}
return true;
} }
/// Returns the grid size for a given screen size. This is safe to call /// Returns the grid size for a given screen size. This is safe to call