mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
renderer, shaper: don't use null cells, handle bg and decorations separately
Significant rework that also removes a lot of unnecessarily duplicated work while rebuilding cells in both renderers. Fixes multiple issues with decorations and bg colors on wide chars and ligatures, while reducing the amount of special case handling required.
This commit is contained in:
@ -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.
|
||||||
|
@ -437,15 +437,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 +454,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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] });
|
||||||
}
|
}
|
||||||
|
@ -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,317 @@ fn rebuildCells(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split our row into runs and shape each one.
|
// Iterator of 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| {
|
||||||
};
|
if (range.y == y) {
|
||||||
|
if (x >= range.x[0] and
|
||||||
for (shaper_cells) |shaper_cell| {
|
x <= range.x[1])
|
||||||
// The shaper can emit null glyphs representing the right half
|
|
||||||
// of wide characters, we don't need to do anything with them.
|
|
||||||
if (shaper_cell.glyph_index == null) continue;
|
|
||||||
|
|
||||||
const coord: terminal.Coordinate = .{
|
|
||||||
.x = shaper_cell.x,
|
|
||||||
.y = y,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// It this cell is within our hint range then we need to
|
// After exiting the preedit range we need to catch
|
||||||
// underline it.
|
// the run position up because of the missed cells.
|
||||||
const cell: terminal.Pin = cell: {
|
if (x == range.x[1] + 1) {
|
||||||
var copy = row;
|
while (shaper_run) |run| {
|
||||||
copy.x = coord.x;
|
if (run.offset + run.cells > x) break;
|
||||||
break :cell copy;
|
shaper_run = try run_iter.next(self.alloc);
|
||||||
};
|
shaper_cells = null;
|
||||||
|
shaper_cells_i = 0;
|
||||||
|
}
|
||||||
|
if (shaper_run) |run| {
|
||||||
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
shaper_cells = self.font_shaper_cache.get(run) orelse cache: {
|
||||||
|
// Otherwise we have to shape them.
|
||||||
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
if (self.updateCell(
|
// Try to cache them. If caching fails for any reason we
|
||||||
screen,
|
// continue because it is just a performance optimization,
|
||||||
cell,
|
// not a correctness issue.
|
||||||
if (link_match_set.contains(screen, cell))
|
self.font_shaper_cache.put(
|
||||||
.single
|
self.alloc,
|
||||||
else
|
run,
|
||||||
null,
|
cells,
|
||||||
color_palette,
|
) catch |err| {
|
||||||
shaper_cell,
|
log.warn(
|
||||||
run,
|
"error caching font shaping results err={}",
|
||||||
coord,
|
.{err},
|
||||||
)) |update| {
|
);
|
||||||
assert(update);
|
};
|
||||||
} else |err| {
|
|
||||||
log.warn("error building cell, will be invalid x={} y={}, err={}", .{
|
// The cells we get from direct shaping are always owned
|
||||||
coord.x,
|
// by the shaper and valid until the next shaping call so
|
||||||
coord.y,
|
// we can safely use them.
|
||||||
err,
|
break :cache cells;
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
if (shaper_cells) |cells| {
|
||||||
|
while (cells[shaper_cells_i].x < x) {
|
||||||
|
shaper_cells_i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 we haven't shaped this run yet, do so.
|
||||||
|
if (shaper_cells == null) if (shaper_run) |run| {
|
||||||
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
shaper_cells = 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
if (shaper_cells) |cells| 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);
|
||||||
|
|
||||||
|
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 +2657,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(
|
||||||
|
@ -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,346 @@ pub fn rebuildCells(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split our row into runs and shape each one.
|
// Iterator of 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| {
|
||||||
};
|
if (range.y == y) {
|
||||||
|
if (x >= range.x[0] and
|
||||||
for (shaper_cells) |shaper_cell| {
|
x <= range.x[1])
|
||||||
// The shaper can emit null glyphs representing the right half
|
|
||||||
// of wide characters, we don't need to do anything with them.
|
|
||||||
if (shaper_cell.glyph_index == null) continue;
|
|
||||||
|
|
||||||
// If this cell falls within our preedit range then we skip it.
|
|
||||||
// We do this so we don't have conflicting data on the same
|
|
||||||
// cell.
|
|
||||||
if (preedit_range) |range| {
|
|
||||||
if (range.y == y and
|
|
||||||
shaper_cell.x >= range.x[0] and
|
|
||||||
shaper_cell.x <= range.x[1])
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// It this cell is within our hint range then we need to
|
// After exiting the preedit range we need to catch
|
||||||
// underline it.
|
// the run position up because of the missed cells.
|
||||||
const cell: terminal.Pin = cell: {
|
if (x == range.x[1] + 1) {
|
||||||
var copy = row;
|
while (shaper_run) |run| {
|
||||||
copy.x = shaper_cell.x;
|
if (run.offset + run.cells > x) break;
|
||||||
break :cell copy;
|
shaper_run = try run_iter.next(self.alloc);
|
||||||
};
|
shaper_cells = null;
|
||||||
|
shaper_cells_i = 0;
|
||||||
|
}
|
||||||
|
if (shaper_run) |run| {
|
||||||
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
shaper_cells = self.font_shaper_cache.get(run) orelse cache: {
|
||||||
|
// Otherwise we have to shape them.
|
||||||
|
const cells = try self.font_shaper.shape(run);
|
||||||
|
|
||||||
if (self.updateCell(
|
// Try to cache them. If caching fails for any reason we
|
||||||
screen,
|
// continue because it is just a performance optimization,
|
||||||
cell,
|
// not a correctness issue.
|
||||||
if (link_match_set.orderedContains(screen, cell))
|
self.font_shaper_cache.put(
|
||||||
.single
|
self.alloc,
|
||||||
else
|
run,
|
||||||
null,
|
cells,
|
||||||
color_palette,
|
) catch |err| {
|
||||||
shaper_cell,
|
log.warn(
|
||||||
run,
|
"error caching font shaping results err={}",
|
||||||
shaper_cell.x,
|
.{err},
|
||||||
y,
|
);
|
||||||
)) |update| {
|
};
|
||||||
assert(update);
|
|
||||||
} else |err| {
|
// The cells we get from direct shaping are always owned
|
||||||
log.warn("error building cell, will be invalid x={} y={}, err={}", .{
|
// by the shaper and valid until the next shaping call so
|
||||||
shaper_cell.x,
|
// we can safely use them.
|
||||||
y,
|
break :cache cells;
|
||||||
err,
|
};
|
||||||
});
|
}
|
||||||
|
if (shaper_cells) |cells| {
|
||||||
|
while (cells[shaper_cells_i].x < x) {
|
||||||
|
shaper_cells_i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 we haven't shaped this run yet, do so.
|
||||||
|
if (shaper_cells == null) if (shaper_run) |run| {
|
||||||
|
// Try to read the cells from the shaping cache if we can.
|
||||||
|
shaper_cells = 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
if (shaper_cells) |cells| 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);
|
||||||
|
|
||||||
|
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 +1894,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
|
||||||
|
Reference in New Issue
Block a user