mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Strikethrough (#19)
Not as straightforward as it sounds, but not hard either: * Read OS/2 sfnt tables from TrueType fonts * Calculate strikethrough position/thickness (prefer font-advertised if possible, calculate if not) * Plumb the SGR code through the terminal state -- does not increase cell memory size * Modify the shader to support it The shaders are getting pretty nasty after this... there's tons of room for improvement. I chose to follow the existing shader style for this to keep it straightforward but will likely soon refactor the shaders.
This commit is contained in:

committed by
GitHub

parent
4909e0117c
commit
b18309187e
1
TODO.md
1
TODO.md
@ -36,7 +36,6 @@ Improvements:
|
||||
|
||||
Major Features:
|
||||
|
||||
* Strikethrough
|
||||
* Bell
|
||||
* Mac:
|
||||
- Switch to raw Cocoa and Metal instead of glfw and libuv (major!)
|
||||
|
@ -75,6 +75,36 @@ pub const Face = struct {
|
||||
@bitCast(i32, load_flags),
|
||||
));
|
||||
}
|
||||
|
||||
/// Return a pointer to a given SFNT table stored within a face.
|
||||
pub fn getSfntTable(self: Face, comptime tag: SfntTag) ?*tag.DataType() {
|
||||
const T = tag.DataType();
|
||||
return @ptrCast(?*T, @alignCast(@alignOf(T), c.FT_Get_Sfnt_Table(
|
||||
self.handle,
|
||||
@enumToInt(tag),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
/// An enumeration to specify indices of SFNT tables loaded and parsed by
|
||||
/// FreeType during initialization of an SFNT font. Used in the
|
||||
/// FT_Get_Sfnt_Table API function.
|
||||
pub const SfntTag = enum(c_int) {
|
||||
head = c.FT_SFNT_HEAD,
|
||||
maxp = c.FT_SFNT_MAXP,
|
||||
os2 = c.FT_SFNT_OS2,
|
||||
hhea = c.FT_SFNT_HHEA,
|
||||
vhea = c.FT_SFNT_VHEA,
|
||||
post = c.FT_SFNT_POST,
|
||||
pclt = c.FT_SFNT_PCLT,
|
||||
|
||||
/// The data type for a given sfnt tag.
|
||||
pub fn DataType(self: SfntTag) type {
|
||||
return switch (self) {
|
||||
.os2 => c.TT_OS2,
|
||||
else => unreachable, // As-needed...
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// An enumeration to specify character sets supported by charmaps. Used in the
|
||||
@ -158,4 +188,8 @@ test "loading memory font" {
|
||||
// Try loading
|
||||
const idx = face.getCharIndex('A').?;
|
||||
try face.loadGlyph(idx, .{});
|
||||
|
||||
// Try getting a truetype table
|
||||
const os2 = face.getSfntTable(.os2);
|
||||
try testing.expect(os2 != null);
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_TRUETYPE_TABLES_H
|
||||
|
@ -31,7 +31,7 @@ const uint MODE_CURSOR_RECT = 3u;
|
||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||
const uint MODE_CURSOR_BAR = 5u;
|
||||
const uint MODE_UNDERLINE = 6u;
|
||||
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
||||
const uint MODE_STRIKETHROUGH = 8u;
|
||||
|
||||
void main() {
|
||||
float a;
|
||||
@ -97,5 +97,9 @@ void main() {
|
||||
case MODE_UNDERLINE:
|
||||
out_FragColor = color;
|
||||
break;
|
||||
|
||||
case MODE_STRIKETHROUGH:
|
||||
out_FragColor = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ const uint MODE_CURSOR_RECT = 3u;
|
||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||
const uint MODE_CURSOR_BAR = 5u;
|
||||
const uint MODE_UNDERLINE = 6u;
|
||||
const uint MODE_STRIKETHROUGH = 8u;
|
||||
|
||||
// The grid coordinates (x, y) where x < columns and y < rows
|
||||
layout (location = 0) in vec2 grid_coord;
|
||||
@ -59,6 +60,8 @@ uniform vec2 cell_size;
|
||||
uniform mat4 projection;
|
||||
uniform float underline_position;
|
||||
uniform float underline_thickness;
|
||||
uniform float strikethrough_position;
|
||||
uniform float strikethrough_thickness;
|
||||
|
||||
/********************************************************************
|
||||
* Modes
|
||||
@ -208,6 +211,22 @@ void main() {
|
||||
// above the bottom.
|
||||
cell_pos = cell_pos + underline_offset - (underline_size * position);
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = fg_color_in / 255.0;
|
||||
break;
|
||||
|
||||
case MODE_STRIKETHROUGH:
|
||||
// Strikethrough Y value is just our thickness
|
||||
vec2 strikethrough_size = vec2(cell_size_scaled.x, strikethrough_thickness);
|
||||
|
||||
// Position the strikethrough where we are told to
|
||||
vec2 strikethrough_offset = vec2(cell_size_scaled.x, strikethrough_position) ;
|
||||
|
||||
// Go to the bottom of the cell, take away the size of the
|
||||
// strikethrough, and that is our position. We also float it slightly
|
||||
// above the bottom.
|
||||
cell_pos = cell_pos + strikethrough_offset - (strikethrough_size * position);
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = fg_color_in / 255.0;
|
||||
break;
|
||||
|
27
src/Grid.zig
27
src/Grid.zig
@ -138,6 +138,7 @@ const GPUCellMode = enum(u8) {
|
||||
cursor_rect_hollow = 4,
|
||||
cursor_bar = 5,
|
||||
underline = 6,
|
||||
strikethrough = 8,
|
||||
|
||||
// Non-exhaustive because masks change it
|
||||
_,
|
||||
@ -180,6 +181,8 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid {
|
||||
try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height });
|
||||
try program.setUniform("underline_position", metrics.underline_position);
|
||||
try program.setUniform("underline_thickness", metrics.underline_thickness);
|
||||
try program.setUniform("strikethrough_position", metrics.strikethrough_position);
|
||||
try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness);
|
||||
|
||||
// Set all of our texture indexes
|
||||
try program.setUniform("text", 0);
|
||||
@ -538,6 +541,7 @@ pub fn updateCell(
|
||||
if (colors.bg != null) i += 1;
|
||||
if (!cell.empty()) i += 1;
|
||||
if (cell.attrs.underline) i += 1;
|
||||
if (cell.attrs.strikethrough) i += 1;
|
||||
break :needed i;
|
||||
};
|
||||
if (self.cells.items.len + needed > self.cells.capacity) return false;
|
||||
@ -630,6 +634,29 @@ pub fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
if (cell.attrs.strikethrough) {
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = .strikethrough,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.grid_width = cell.widthLegacy(),
|
||||
.glyph_x = 0,
|
||||
.glyph_y = 0,
|
||||
.glyph_width = 0,
|
||||
.glyph_height = 0,
|
||||
.glyph_offset_x = 0,
|
||||
.glyph_offset_y = 0,
|
||||
.fg_r = colors.fg.r,
|
||||
.fg_g = colors.fg.g,
|
||||
.fg_b = colors.fg.b,
|
||||
.fg_a = alpha,
|
||||
.bg_r = 0,
|
||||
.bg_g = 0,
|
||||
.bg_b = 0,
|
||||
.bg_a = 0,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -270,6 +270,11 @@ pub const Metrics = struct {
|
||||
/// thickness in pixels.
|
||||
underline_position: f32,
|
||||
underline_thickness: f32,
|
||||
|
||||
/// The position and thickness of a strikethrough. Same units/style
|
||||
/// as the underline fields.
|
||||
strikethrough_position: f32,
|
||||
strikethrough_thickness: f32,
|
||||
};
|
||||
|
||||
/// Calculate the metrics associated with a face. This is not public because
|
||||
@ -358,13 +363,30 @@ fn calcMetrics(face: freetype.Face) Metrics {
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{
|
||||
// The strikethrough position. We use the position provided by the
|
||||
// font if it exists otherwise we calculate a best guess.
|
||||
const strikethrough: struct {
|
||||
pos: f32,
|
||||
thickness: f32,
|
||||
} = if (face.getSfntTable(.os2)) |os2| .{
|
||||
.pos = fontUnitsToPxY(face, @maximum(
|
||||
0,
|
||||
@intCast(i32, size_metrics.ascender) - os2.yStrikeoutPosition,
|
||||
)),
|
||||
.thickness = @maximum(1, fontUnitsToPxY(face, os2.yStrikeoutSize)),
|
||||
} else .{
|
||||
.pos = cell_baseline * 0.6,
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
// log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d} strikethrough={}", .{
|
||||
// size_metrics,
|
||||
// cell_width,
|
||||
// cell_height,
|
||||
// cell_height - cell_baseline,
|
||||
// underline_position,
|
||||
// underline_thickness,
|
||||
// strikethrough,
|
||||
// });
|
||||
|
||||
return .{
|
||||
@ -373,6 +395,8 @@ fn calcMetrics(face: freetype.Face) Metrics {
|
||||
.cell_baseline = cell_baseline,
|
||||
.underline_position = underline_position,
|
||||
.underline_thickness = underline_thickness,
|
||||
.strikethrough_position = strikethrough.pos,
|
||||
.strikethrough_thickness = strikethrough.thickness,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -164,6 +164,7 @@ pub const Cell = struct {
|
||||
faint: bool = false,
|
||||
underline: bool = false,
|
||||
inverse: bool = false,
|
||||
strikethrough: bool = false,
|
||||
|
||||
/// True if this is a wide character. This char takes up
|
||||
/// two cells. The following cell ALWAYS is a space.
|
||||
|
@ -365,6 +365,14 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
||||
self.screen.cursor.pen.attrs.inverse = false;
|
||||
},
|
||||
|
||||
.strikethrough => {
|
||||
self.screen.cursor.pen.attrs.strikethrough = true;
|
||||
},
|
||||
|
||||
.reset_strikethrough => {
|
||||
self.screen.cursor.pen.attrs.strikethrough = false;
|
||||
},
|
||||
|
||||
.direct_color_fg => |rgb| {
|
||||
self.screen.cursor.pen.attrs.has_fg = true;
|
||||
self.screen.cursor.pen.fg = .{
|
||||
|
@ -34,6 +34,10 @@ pub const Attribute = union(enum) {
|
||||
inverse: void,
|
||||
reset_inverse: void,
|
||||
|
||||
/// Strikethrough the text.
|
||||
strikethrough: void,
|
||||
reset_strikethrough: void,
|
||||
|
||||
/// Set foreground color as RGB values.
|
||||
direct_color_fg: RGB,
|
||||
|
||||
@ -99,8 +103,12 @@ pub const Parser = struct {
|
||||
|
||||
7 => return Attribute{ .inverse = {} },
|
||||
|
||||
9 => return Attribute{ .strikethrough = {} },
|
||||
|
||||
27 => return Attribute{ .reset_inverse = {} },
|
||||
|
||||
29 => return Attribute{ .reset_strikethrough = {} },
|
||||
|
||||
30...37 => return Attribute{
|
||||
.@"8_fg" = @intToEnum(color.Name, slice[0] - 30),
|
||||
},
|
||||
@ -228,6 +236,18 @@ test "sgr: inverse" {
|
||||
}
|
||||
}
|
||||
|
||||
test "sgr: strikethrough" {
|
||||
{
|
||||
const v = testParse(&[_]u16{9});
|
||||
try testing.expect(v == .strikethrough);
|
||||
}
|
||||
|
||||
{
|
||||
const v = testParse(&[_]u16{29});
|
||||
try testing.expect(v == .reset_strikethrough);
|
||||
}
|
||||
}
|
||||
|
||||
test "sgr: 8 color" {
|
||||
var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } };
|
||||
|
||||
|
Reference in New Issue
Block a user