mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Ligatures
This introduces a naive first pass at integrating ligatures. The basic ligatures (such as "==" in some fonts) work great. Skin-toned emoji are struggling a bit. This isn't the most performant way to do this, either, and I plan on improving that.
This commit is contained in:
@ -37,6 +37,9 @@ layout (location = 5) in vec4 bg_color_in;
|
||||
// the entire terminal grid in a single GPU pass.
|
||||
layout (location = 6) in uint mode_in;
|
||||
|
||||
// The width in cells of this item.
|
||||
layout (location = 7) in uint grid_width;
|
||||
|
||||
// The background or foreground color for the fragment, depending on
|
||||
// whether this is a background or foreground pass.
|
||||
flat out vec4 color;
|
||||
@ -133,7 +136,7 @@ void main() {
|
||||
// The "+ 3" here is to give some wiggle room for fonts that are
|
||||
// BARELY over it.
|
||||
vec2 glyph_size_downsampled = glyph_size;
|
||||
if (glyph_size.x > (cell_size.x + 3)) {
|
||||
if (glyph_size.y > cell_size.y + 2) {
|
||||
glyph_size_downsampled.x = cell_size_scaled.x;
|
||||
glyph_size_downsampled.y = glyph_size.y * (glyph_size_downsampled.x / glyph_size.x);
|
||||
glyph_offset_calc.y = glyph_offset.y * (glyph_size_downsampled.x / glyph_size.x);
|
||||
|
75
src/Grid.zig
75
src/Grid.zig
@ -109,6 +109,9 @@ const GPUCell = struct {
|
||||
|
||||
/// uint mode
|
||||
mode: GPUCellMode,
|
||||
|
||||
/// The width in grid cells that a rendering takes.
|
||||
grid_width: u16 = 1,
|
||||
};
|
||||
|
||||
const GPUCellMode = enum(u8) {
|
||||
@ -224,6 +227,8 @@ pub fn init(
|
||||
try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset);
|
||||
offset += 4 * @sizeOf(u8);
|
||||
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
||||
offset += 1 * @sizeOf(u8);
|
||||
try vbobind.attributeAdvanced(7, 1, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset);
|
||||
try vbobind.enableAttribArray(0);
|
||||
try vbobind.enableAttribArray(1);
|
||||
try vbobind.enableAttribArray(2);
|
||||
@ -231,6 +236,7 @@ pub fn init(
|
||||
try vbobind.enableAttribArray(4);
|
||||
try vbobind.enableAttribArray(5);
|
||||
try vbobind.enableAttribArray(6);
|
||||
try vbobind.enableAttribArray(7);
|
||||
try vbobind.attributeDivisor(0, 1);
|
||||
try vbobind.attributeDivisor(1, 1);
|
||||
try vbobind.attributeDivisor(2, 1);
|
||||
@ -238,6 +244,7 @@ pub fn init(
|
||||
try vbobind.attributeDivisor(4, 1);
|
||||
try vbobind.attributeDivisor(5, 1);
|
||||
try vbobind.attributeDivisor(6, 1);
|
||||
try vbobind.attributeDivisor(7, 1);
|
||||
|
||||
// Build our texture
|
||||
const tex = try gl.Texture.create();
|
||||
@ -341,17 +348,31 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
|
||||
// We've written no data to the GPU, refresh it all
|
||||
self.gl_cells_written = 0;
|
||||
|
||||
// Create a text shaper we'll use for the screen
|
||||
var shape_buf = try self.alloc.alloc(font.Shaper.Cell, term.screen.cols * 2);
|
||||
defer self.alloc.free(shape_buf);
|
||||
var shaper = try font.Shaper.init(&self.font_group, shape_buf);
|
||||
defer shaper.deinit();
|
||||
|
||||
// Build each cell
|
||||
var rowIter = term.screen.rowIterator(.viewport);
|
||||
var y: usize = 0;
|
||||
while (rowIter.next()) |row| {
|
||||
defer y += 1;
|
||||
|
||||
var cellIter = row.cellIterator();
|
||||
var x: usize = 0;
|
||||
while (cellIter.next()) |cell| {
|
||||
defer x += 1;
|
||||
assert(try self.updateCell(term, cell, x, y));
|
||||
// Split our row into runs and shape each one.
|
||||
var iter = shaper.runIterator(row);
|
||||
while (try iter.next(self.alloc)) |run| {
|
||||
for (try shaper.shape(run)) |shaper_cell| {
|
||||
assert(try self.updateCell(
|
||||
term,
|
||||
row[shaper_cell.x],
|
||||
shaper_cell,
|
||||
run,
|
||||
shaper_cell.x,
|
||||
y,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,6 +419,7 @@ fn addCursor(self: *Grid, term: *Terminal) void {
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, term.screen.cursor.x),
|
||||
.grid_row = @intCast(u16, term.screen.cursor.y),
|
||||
.grid_width = if (cell.attrs.wide) 2 else 1,
|
||||
.fg_r = 0,
|
||||
.fg_g = 0,
|
||||
.fg_b = 0,
|
||||
@ -417,6 +439,8 @@ pub fn updateCell(
|
||||
self: *Grid,
|
||||
term: *Terminal,
|
||||
cell: terminal.Screen.Cell,
|
||||
shaper_cell: font.Shaper.Cell,
|
||||
shaper_run: font.Shaper.TextRun,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) !bool {
|
||||
@ -471,9 +495,6 @@ pub fn updateCell(
|
||||
break :colors res;
|
||||
};
|
||||
|
||||
// If we are a trailing spacer, we never render anything.
|
||||
if (cell.attrs.wide_spacer_tail) return true;
|
||||
|
||||
// Calculate the amount of space we need in the cells list.
|
||||
const needed = needed: {
|
||||
var i: usize = 0;
|
||||
@ -496,6 +517,7 @@ pub fn updateCell(
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.grid_width = shaper_cell.width,
|
||||
.glyph_x = 0,
|
||||
.glyph_y = 0,
|
||||
.glyph_width = 0,
|
||||
@ -515,37 +537,16 @@ pub fn updateCell(
|
||||
|
||||
// If the cell is empty then we draw nothing in the box.
|
||||
if (!cell.empty()) {
|
||||
// Determine our glyph styling
|
||||
const style: font.Style = if (cell.attrs.bold)
|
||||
.bold
|
||||
else
|
||||
.regular;
|
||||
|
||||
var mode: GPUCellMode = .fg;
|
||||
|
||||
// Get the glyph that we're going to use. We first try what the cell
|
||||
// wants, then the Unicode replacement char, then finally a space.
|
||||
const FontInfo = struct { index: font.Group.FontIndex, ch: u32 };
|
||||
const font_info: FontInfo = font_info: {
|
||||
var chars = [_]u32{ @intCast(u32, cell.char), 0xFFFD, ' ' };
|
||||
for (chars) |char| {
|
||||
if (try self.font_group.indexForCodepoint(self.alloc, style, char)) |idx| {
|
||||
break :font_info FontInfo{
|
||||
.index = idx,
|
||||
.ch = char,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@panic("all fonts require at least space");
|
||||
};
|
||||
|
||||
// Render
|
||||
const face = self.font_group.group.faceFromIndex(font_info.index);
|
||||
const glyph_index = face.glyphIndex(font_info.ch).?;
|
||||
const glyph = try self.font_group.renderGlyph(self.alloc, font_info.index, glyph_index);
|
||||
const face = self.font_group.group.faceFromIndex(shaper_run.font_index);
|
||||
const glyph = try self.font_group.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index,
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
var mode: GPUCellMode = .fg;
|
||||
if (face.hasColor()) mode = .fg_color;
|
||||
|
||||
// If the cell is wide, we need to note that in the mode
|
||||
@ -555,6 +556,7 @@ pub fn updateCell(
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.grid_width = shaper_cell.width,
|
||||
.glyph_x = glyph.atlas_x,
|
||||
.glyph_y = glyph.atlas_y,
|
||||
.glyph_width = glyph.width,
|
||||
@ -580,6 +582,7 @@ pub fn updateCell(
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.grid_width = shaper_cell.width,
|
||||
.glyph_x = 0,
|
||||
.glyph_y = 0,
|
||||
.glyph_width = 0,
|
||||
|
@ -22,10 +22,16 @@ group: *GroupCache,
|
||||
/// calls to prevent allocations.
|
||||
hb_buf: harfbuzz.Buffer,
|
||||
|
||||
pub fn init(group: *GroupCache) !Shaper {
|
||||
/// The shared memory used for shaping results.
|
||||
cell_buf: []Cell,
|
||||
|
||||
/// The cell_buf argument is the buffer to use for storing shaped results.
|
||||
/// This should be at least the number of columns in the terminal.
|
||||
pub fn init(group: *GroupCache, cell_buf: []Cell) !Shaper {
|
||||
return Shaper{
|
||||
.group = group,
|
||||
.hb_buf = try harfbuzz.Buffer.create(),
|
||||
.cell_buf = cell_buf,
|
||||
};
|
||||
}
|
||||
|
||||
@ -44,27 +50,85 @@ pub fn runIterator(self: *Shaper, row: terminal.Screen.Row) RunIterator {
|
||||
/// text run that was iterated since the text run does share state with the
|
||||
/// Shaper struct.
|
||||
///
|
||||
/// NOTE: there is no return value here yet because its still WIP
|
||||
pub fn shape(self: Shaper, run: TextRun) void {
|
||||
/// The return value is only valid until the next shape call is called.
|
||||
///
|
||||
/// If there is not enough space in the cell buffer, an error is returned.
|
||||
pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
||||
const face = self.group.group.faceFromIndex(run.font_index);
|
||||
harfbuzz.shape(face.hb_font, self.hb_buf, null);
|
||||
|
||||
// If our buffer is empty, we short-circuit the rest of the work
|
||||
// return nothing.
|
||||
if (self.hb_buf.getLength() == 0) return self.cell_buf[0..0];
|
||||
const info = self.hb_buf.getGlyphInfos();
|
||||
const pos = self.hb_buf.getGlyphPositions() orelse return;
|
||||
const pos = self.hb_buf.getGlyphPositions() orelse return error.HarfbuzzFailed;
|
||||
|
||||
// This is perhaps not true somewhere, but we currently assume it is true.
|
||||
// If it isn't true, I'd like to catch it and learn more.
|
||||
assert(info.len == pos.len);
|
||||
|
||||
// log.warn("info={} pos={}", .{ info.len, pos.len });
|
||||
// for (info) |v, i| {
|
||||
// log.warn("info {} = {}", .{ i, v });
|
||||
// }
|
||||
// Convert all our info/pos to cells and set it.
|
||||
if (info.len > self.cell_buf.len) return error.OutOfMemory;
|
||||
// log.debug("info={} pos={}", .{ info.len, pos.len });
|
||||
|
||||
// x is the column that we currently occupy. We start at the offset.
|
||||
var x: u16 = run.offset;
|
||||
|
||||
for (info) |v, i| {
|
||||
// The number of codepoints is used as the cell "width". If
|
||||
// we're the last cell, this is remaining otherwise we use cluster numbers
|
||||
// to detect since we set the cluster number to the column it
|
||||
// originated.
|
||||
const cp_width = if (i == info.len - 1)
|
||||
run.max_cluster - v.cluster
|
||||
else width: {
|
||||
const next_cluster = info[i + 1].cluster;
|
||||
break :width next_cluster - v.cluster;
|
||||
};
|
||||
|
||||
self.cell_buf[i] = .{
|
||||
.x = x,
|
||||
.glyph_index = v.codepoint,
|
||||
.width = if (cp_width > 2) 2 else @intCast(u8, cp_width),
|
||||
};
|
||||
|
||||
// Increase x by the amount of codepoints we replaced so that
|
||||
// we retain the grid.
|
||||
x += @intCast(u16, cp_width);
|
||||
|
||||
// log.debug("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] });
|
||||
}
|
||||
|
||||
return self.cell_buf[0..info.len];
|
||||
}
|
||||
|
||||
pub const Cell = struct {
|
||||
/// The column that this cell occupies. Since a set of shaper cells is
|
||||
/// always on the same line, only the X is stored. It is expected the
|
||||
/// caller has access to the original screen cell.
|
||||
x: u16,
|
||||
|
||||
/// The glyph index for this cell. The font index to use alongside
|
||||
/// this cell is available in the text run.
|
||||
glyph_index: u32,
|
||||
|
||||
/// The width that this cell consumes.
|
||||
width: u8,
|
||||
};
|
||||
|
||||
/// A single text run. A text run is only valid for one Shaper and
|
||||
/// until the next run is created.
|
||||
pub const TextRun = struct {
|
||||
/// The offset in the row where this run started
|
||||
offset: u16,
|
||||
|
||||
/// The total number of cells produced by this run.
|
||||
cells: u16,
|
||||
|
||||
/// The maximum cluster value used
|
||||
max_cluster: u16,
|
||||
|
||||
/// The font index to use for the glyphs of this run.
|
||||
font_index: Group.FontIndex,
|
||||
};
|
||||
|
||||
@ -85,6 +149,7 @@ pub const RunIterator = struct {
|
||||
|
||||
// Go through cell by cell and accumulate while we build our run.
|
||||
var j: usize = self.i;
|
||||
var max_cluster: usize = j;
|
||||
while (j < self.row.lenCells()) : (j += 1) {
|
||||
const cell = self.row.getCell(j);
|
||||
|
||||
@ -96,8 +161,19 @@ pub const RunIterator = struct {
|
||||
else
|
||||
.regular;
|
||||
|
||||
// Determine the font for this cell
|
||||
const font_idx_opt = try self.shaper.group.indexForCodepoint(alloc, style, cell.char);
|
||||
// Determine the font for this cell. We'll use fallbacks
|
||||
// manually here to try replacement chars and then a space
|
||||
// for unknown glyphs.
|
||||
const font_idx_opt = (try self.shaper.group.indexForCodepoint(
|
||||
alloc,
|
||||
style,
|
||||
cell.char,
|
||||
)) orelse (try self.shaper.group.indexForCodepoint(
|
||||
alloc,
|
||||
style,
|
||||
0xFFFD,
|
||||
)) orelse
|
||||
try self.shaper.group.indexForCodepoint(alloc, style, ' ');
|
||||
const font_idx = font_idx_opt.?;
|
||||
//log.warn("char={x} idx={}", .{ cell.char, font_idx });
|
||||
if (j == self.i) current_font = font_idx;
|
||||
@ -116,15 +192,22 @@ pub const RunIterator = struct {
|
||||
self.shaper.hb_buf.add(cp, @intCast(u32, j));
|
||||
}
|
||||
}
|
||||
|
||||
max_cluster = j;
|
||||
}
|
||||
|
||||
// Finalize our buffer
|
||||
self.shaper.hb_buf.guessSegmentProperties();
|
||||
|
||||
// Move our cursor
|
||||
self.i = j;
|
||||
// Move our cursor. Must defer since we use self.i below.
|
||||
defer self.i = j;
|
||||
|
||||
return TextRun{ .font_index = current_font };
|
||||
return TextRun{
|
||||
.offset = @intCast(u16, self.i),
|
||||
.cells = @intCast(u16, j - self.i),
|
||||
.max_cluster = @intCast(u16, max_cluster),
|
||||
.font_index = current_font,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -194,7 +277,7 @@ test "shape" {
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength());
|
||||
shaper.shape(run);
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
@ -204,11 +287,13 @@ const TestShaper = struct {
|
||||
shaper: Shaper,
|
||||
cache: *GroupCache,
|
||||
lib: Library,
|
||||
cell_buf: []Cell,
|
||||
|
||||
pub fn deinit(self: *TestShaper) void {
|
||||
self.shaper.deinit();
|
||||
self.cache.deinit(self.alloc);
|
||||
self.alloc.destroy(self.cache);
|
||||
self.alloc.free(self.cell_buf);
|
||||
self.lib.deinit();
|
||||
}
|
||||
};
|
||||
@ -230,7 +315,10 @@ fn testShaper(alloc: Allocator) !TestShaper {
|
||||
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
|
||||
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
|
||||
|
||||
var shaper = try init(cache_ptr);
|
||||
var cell_buf = try alloc.alloc(Cell, 80);
|
||||
errdefer alloc.free(cell_buf);
|
||||
|
||||
var shaper = try init(cache_ptr, cell_buf);
|
||||
errdefer shaper.deinit();
|
||||
|
||||
return TestShaper{
|
||||
@ -238,5 +326,6 @@ fn testShaper(alloc: Allocator) !TestShaper {
|
||||
.shaper = shaper,
|
||||
.cache = cache_ptr,
|
||||
.lib = lib,
|
||||
.cell_buf = cell_buf,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user