mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Calculate grid_width properly, use that instead of wide mask in shader
This commit is contained in:
@ -11,7 +11,6 @@ const uint MODE_CURSOR_RECT = 3u;
|
|||||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||||
const uint MODE_CURSOR_BAR = 5u;
|
const uint MODE_CURSOR_BAR = 5u;
|
||||||
const uint MODE_UNDERLINE = 6u;
|
const uint MODE_UNDERLINE = 6u;
|
||||||
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
|
||||||
|
|
||||||
// The grid coordinates (x, y) where x < columns and y < rows
|
// The grid coordinates (x, y) where x < columns and y < rows
|
||||||
layout (location = 0) in vec2 grid_coord;
|
layout (location = 0) in vec2 grid_coord;
|
||||||
@ -81,12 +80,9 @@ uniform float glyph_baseline;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Remove any masks from our mode
|
|
||||||
uint mode_unmasked = mode_in & ~MODE_WIDE_MASK;
|
|
||||||
|
|
||||||
// We always forward our mode unmasked because the fragment
|
// We always forward our mode unmasked because the fragment
|
||||||
// shader doesn't use any of the masks.
|
// shader doesn't use any of the masks.
|
||||||
mode = mode_unmasked;
|
mode = mode_in;
|
||||||
|
|
||||||
// Top-left cell coordinates converted to world space
|
// Top-left cell coordinates converted to world space
|
||||||
// Example: (1,0) with a 30 wide cell is converted to (30,0)
|
// Example: (1,0) with a 30 wide cell is converted to (30,0)
|
||||||
@ -113,9 +109,7 @@ void main() {
|
|||||||
|
|
||||||
// Scaled for wide chars
|
// Scaled for wide chars
|
||||||
vec2 cell_size_scaled = cell_size;
|
vec2 cell_size_scaled = cell_size;
|
||||||
if ((mode_in & MODE_WIDE_MASK) == MODE_WIDE_MASK) {
|
cell_size_scaled.x = cell_size_scaled.x * grid_width;
|
||||||
cell_size_scaled.x = cell_size_scaled.x * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case MODE_BG:
|
case MODE_BG:
|
||||||
@ -136,7 +130,8 @@ void main() {
|
|||||||
// The "+ 3" here is to give some wiggle room for fonts that are
|
// The "+ 3" here is to give some wiggle room for fonts that are
|
||||||
// BARELY over it.
|
// BARELY over it.
|
||||||
vec2 glyph_size_downsampled = glyph_size;
|
vec2 glyph_size_downsampled = glyph_size;
|
||||||
if (glyph_size.y > cell_size.y + 2) {
|
if (glyph_size.y > cell_size.y + 2 ||
|
||||||
|
glyph_size.x > cell_size_scaled.x + 2) {
|
||||||
glyph_size_downsampled.x = cell_size_scaled.x;
|
glyph_size_downsampled.x = cell_size_scaled.x;
|
||||||
glyph_size_downsampled.y = glyph_size.y * (glyph_size_downsampled.x / glyph_size.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);
|
glyph_offset_calc.y = glyph_offset.y * (glyph_size_downsampled.x / glyph_size.x);
|
||||||
|
20
src/Grid.zig
20
src/Grid.zig
@ -78,7 +78,9 @@ pub const CursorStyle = enum(u8) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// The raw structure that maps directly to the buffer sent to the vertex shader.
|
/// The raw structure that maps directly to the buffer sent to the vertex shader.
|
||||||
const GPUCell = struct {
|
/// This must be "extern" so that the field order is not reordered by the
|
||||||
|
/// Zig compiler.
|
||||||
|
const GPUCell = extern struct {
|
||||||
/// vec2 grid_coord
|
/// vec2 grid_coord
|
||||||
grid_col: u16,
|
grid_col: u16,
|
||||||
grid_row: u16,
|
grid_row: u16,
|
||||||
@ -111,7 +113,7 @@ const GPUCell = struct {
|
|||||||
mode: GPUCellMode,
|
mode: GPUCellMode,
|
||||||
|
|
||||||
/// The width in grid cells that a rendering takes.
|
/// The width in grid cells that a rendering takes.
|
||||||
grid_width: u16 = 1,
|
grid_width: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GPUCellMode = enum(u8) {
|
const GPUCellMode = enum(u8) {
|
||||||
@ -123,8 +125,6 @@ const GPUCellMode = enum(u8) {
|
|||||||
cursor_bar = 5,
|
cursor_bar = 5,
|
||||||
underline = 6,
|
underline = 6,
|
||||||
|
|
||||||
wide_mask = 0b1000_0000,
|
|
||||||
|
|
||||||
// Non-exhaustive because masks change it
|
// Non-exhaustive because masks change it
|
||||||
_,
|
_,
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ pub fn init(
|
|||||||
offset += 4 * @sizeOf(u8);
|
offset += 4 * @sizeOf(u8);
|
||||||
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
||||||
offset += 1 * @sizeOf(u8);
|
offset += 1 * @sizeOf(u8);
|
||||||
try vbobind.attributeAdvanced(7, 1, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset);
|
try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
||||||
try vbobind.enableAttribArray(0);
|
try vbobind.enableAttribArray(0);
|
||||||
try vbobind.enableAttribArray(1);
|
try vbobind.enableAttribArray(1);
|
||||||
try vbobind.enableAttribArray(2);
|
try vbobind.enableAttribArray(2);
|
||||||
@ -413,7 +413,6 @@ fn addCursor(self: *Grid, term: *Terminal) void {
|
|||||||
GPUCellMode,
|
GPUCellMode,
|
||||||
@enumToInt(self.cursor_style),
|
@enumToInt(self.cursor_style),
|
||||||
);
|
);
|
||||||
if (cell.attrs.wide) mode = mode.mask(.wide_mask);
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = mode,
|
.mode = mode,
|
||||||
@ -511,7 +510,6 @@ pub fn updateCell(
|
|||||||
// If the cell has a background, we always draw it.
|
// If the cell has a background, we always draw it.
|
||||||
if (colors.bg) |rgb| {
|
if (colors.bg) |rgb| {
|
||||||
var mode: GPUCellMode = .bg;
|
var mode: GPUCellMode = .bg;
|
||||||
if (cell.attrs.wide) mode = mode.mask(.wide_mask);
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = mode,
|
.mode = mode,
|
||||||
@ -549,9 +547,6 @@ pub fn updateCell(
|
|||||||
var mode: GPUCellMode = .fg;
|
var mode: GPUCellMode = .fg;
|
||||||
if (face.hasColor()) mode = .fg_color;
|
if (face.hasColor()) mode = .fg_color;
|
||||||
|
|
||||||
// If the cell is wide, we need to note that in the mode
|
|
||||||
if (cell.attrs.wide) mode = mode.mask(.wide_mask);
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = mode,
|
.mode = mode,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
@ -575,11 +570,8 @@ pub fn updateCell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cell.attrs.underline) {
|
if (cell.attrs.underline) {
|
||||||
var mode: GPUCellMode = .underline;
|
|
||||||
if (cell.attrs.wide) mode = mode.mask(.wide_mask);
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = mode,
|
.mode = .underline,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
.grid_row = @intCast(u16, y),
|
.grid_row = @intCast(u16, y),
|
||||||
.grid_width = shaper_cell.width,
|
.grid_width = shaper_cell.width,
|
||||||
|
@ -54,8 +54,14 @@ pub fn runIterator(self: *Shaper, row: terminal.Screen.Row) RunIterator {
|
|||||||
///
|
///
|
||||||
/// If there is not enough space in the cell buffer, an error is returned.
|
/// If there is not enough space in the cell buffer, an error is returned.
|
||||||
pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
||||||
|
// TODO: we do not want to hardcode these
|
||||||
|
const hb_feats = &[_]harfbuzz.Feature{
|
||||||
|
harfbuzz.Feature.fromString("dlig").?,
|
||||||
|
harfbuzz.Feature.fromString("liga").?,
|
||||||
|
};
|
||||||
|
|
||||||
const face = self.group.group.faceFromIndex(run.font_index);
|
const face = self.group.group.faceFromIndex(run.font_index);
|
||||||
harfbuzz.shape(face.hb_font, self.hb_buf, null);
|
harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats);
|
||||||
|
|
||||||
// If our buffer is empty, we short-circuit the rest of the work
|
// If our buffer is empty, we short-circuit the rest of the work
|
||||||
// return nothing.
|
// return nothing.
|
||||||
@ -69,7 +75,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
|||||||
|
|
||||||
// Convert all our info/pos to cells and set it.
|
// Convert all our info/pos to cells and set it.
|
||||||
if (info.len > self.cell_buf.len) return error.OutOfMemory;
|
if (info.len > self.cell_buf.len) return error.OutOfMemory;
|
||||||
// log.debug("info={} pos={}", .{ info.len, pos.len });
|
//log.warn("info={} pos={} run={}", .{ info.len, pos.len, run });
|
||||||
|
|
||||||
// x is the column that we currently occupy. We start at the offset.
|
// x is the column that we currently occupy. We start at the offset.
|
||||||
var x: u16 = run.offset;
|
var x: u16 = run.offset;
|
||||||
@ -80,7 +86,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
|||||||
// to detect since we set the cluster number to the column it
|
// to detect since we set the cluster number to the column it
|
||||||
// originated.
|
// originated.
|
||||||
const cp_width = if (i == info.len - 1)
|
const cp_width = if (i == info.len - 1)
|
||||||
run.max_cluster - v.cluster
|
(run.max_cluster - v.cluster) + 1 // + 1 because we're zero indexed
|
||||||
else width: {
|
else width: {
|
||||||
const next_cluster = info[i + 1].cluster;
|
const next_cluster = info[i + 1].cluster;
|
||||||
break :width next_cluster - v.cluster;
|
break :width next_cluster - v.cluster;
|
||||||
@ -89,14 +95,14 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
|||||||
self.cell_buf[i] = .{
|
self.cell_buf[i] = .{
|
||||||
.x = x,
|
.x = x,
|
||||||
.glyph_index = v.codepoint,
|
.glyph_index = v.codepoint,
|
||||||
.width = if (cp_width > 2) 2 else @intCast(u8, cp_width),
|
.width = @intCast(u8, cp_width),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Increase x by the amount of codepoints we replaced so that
|
// Increase x by the amount of codepoints we replaced so that
|
||||||
// we retain the grid.
|
// we retain the grid.
|
||||||
x += @intCast(u16, cp_width);
|
x += @intCast(u16, cp_width);
|
||||||
|
|
||||||
// log.debug("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] });
|
//log.warn("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] });
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.cell_buf[0..info.len];
|
return self.cell_buf[0..info.len];
|
||||||
@ -153,8 +159,15 @@ pub const RunIterator = struct {
|
|||||||
while (j < self.row.lenCells()) : (j += 1) {
|
while (j < self.row.lenCells()) : (j += 1) {
|
||||||
const cell = self.row.getCell(j);
|
const cell = self.row.getCell(j);
|
||||||
|
|
||||||
// Ignore tailing wide spacers, this will get fixed up by the shaper
|
// Ignore empty cells
|
||||||
if (cell.empty() or cell.attrs.wide_spacer_tail) continue;
|
if (cell.empty()) continue;
|
||||||
|
|
||||||
|
// If we're a spacer, then we ignore it but increase the max cluster
|
||||||
|
// size so that the width calculation is correct.
|
||||||
|
if (cell.attrs.wide_spacer_tail) {
|
||||||
|
max_cluster = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const style: Style = if (cell.attrs.bold)
|
const style: Style = if (cell.attrs.bold)
|
||||||
.bold
|
.bold
|
||||||
@ -232,6 +245,19 @@ test "run iterator" {
|
|||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spaces should be part of a run
|
||||||
|
{
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString("ABCD EFG");
|
||||||
|
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |_| count += 1;
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Make a screen with some data
|
// Make a screen with some data
|
||||||
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
@ -282,6 +308,76 @@ test "shape" {
|
|||||||
try testing.expectEqual(@as(usize, 1), count);
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "shape inconsolata ligs" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var testdata = try testShaper(alloc);
|
||||||
|
defer testdata.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString(">=");
|
||||||
|
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |run| {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const cells = try shaper.shape(run);
|
||||||
|
try testing.expectEqual(@as(usize, 1), cells.len);
|
||||||
|
try testing.expectEqual(@as(u8, 2), cells[0].width);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString("===");
|
||||||
|
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |run| {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const cells = try shaper.shape(run);
|
||||||
|
try testing.expectEqual(@as(usize, 1), cells.len);
|
||||||
|
try testing.expectEqual(@as(u8, 3), cells[0].width);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shape emoji width" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var testdata = try testShaper(alloc);
|
||||||
|
defer testdata.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||||
|
defer screen.deinit(alloc);
|
||||||
|
screen.testWriteString("👍");
|
||||||
|
|
||||||
|
var shaper = testdata.shaper;
|
||||||
|
var it = shaper.runIterator(screen.getRow(.{ .screen = 0 }));
|
||||||
|
var count: usize = 0;
|
||||||
|
while (try it.next(alloc)) |run| {
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
const cells = try shaper.shape(run);
|
||||||
|
try testing.expectEqual(@as(usize, 1), cells.len);
|
||||||
|
try testing.expectEqual(@as(u8, 2), cells[0].width);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 1), count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const TestShaper = struct {
|
const TestShaper = struct {
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
shaper: Shaper,
|
shaper: Shaper,
|
||||||
|
Reference in New Issue
Block a user