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:
Mitchell Hashimoto
2022-10-06 15:03:19 -07:00
committed by GitHub
parent 4909e0117c
commit b18309187e
10 changed files with 140 additions and 3 deletions

View File

@ -36,7 +36,6 @@ Improvements:
Major Features: Major Features:
* Strikethrough
* Bell * Bell
* Mac: * Mac:
- Switch to raw Cocoa and Metal instead of glfw and libuv (major!) - Switch to raw Cocoa and Metal instead of glfw and libuv (major!)

View File

@ -75,6 +75,36 @@ pub const Face = struct {
@bitCast(i32, load_flags), @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 /// An enumeration to specify character sets supported by charmaps. Used in the
@ -158,4 +188,8 @@ test "loading memory font" {
// Try loading // Try loading
const idx = face.getCharIndex('A').?; const idx = face.getCharIndex('A').?;
try face.loadGlyph(idx, .{}); try face.loadGlyph(idx, .{});
// Try getting a truetype table
const os2 = face.getSfntTable(.os2);
try testing.expect(os2 != null);
} }

View File

@ -1,2 +1,3 @@
#include <ft2build.h> #include <ft2build.h>
#include FT_FREETYPE_H #include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H

View File

@ -31,7 +31,7 @@ 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 const uint MODE_STRIKETHROUGH = 8u;
void main() { void main() {
float a; float a;
@ -97,5 +97,9 @@ void main() {
case MODE_UNDERLINE: case MODE_UNDERLINE:
out_FragColor = color; out_FragColor = color;
break; break;
case MODE_STRIKETHROUGH:
out_FragColor = color;
break;
} }
} }

View File

@ -11,6 +11,7 @@ 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_STRIKETHROUGH = 8u;
// 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;
@ -59,6 +60,8 @@ uniform vec2 cell_size;
uniform mat4 projection; uniform mat4 projection;
uniform float underline_position; uniform float underline_position;
uniform float underline_thickness; uniform float underline_thickness;
uniform float strikethrough_position;
uniform float strikethrough_thickness;
/******************************************************************** /********************************************************************
* Modes * Modes
@ -208,6 +211,22 @@ void main() {
// above the bottom. // above the bottom.
cell_pos = cell_pos + underline_offset - (underline_size * position); 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); gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
color = fg_color_in / 255.0; color = fg_color_in / 255.0;
break; break;

View File

@ -138,6 +138,7 @@ const GPUCellMode = enum(u8) {
cursor_rect_hollow = 4, cursor_rect_hollow = 4,
cursor_bar = 5, cursor_bar = 5,
underline = 6, underline = 6,
strikethrough = 8,
// Non-exhaustive because masks change it // 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("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height });
try program.setUniform("underline_position", metrics.underline_position); try program.setUniform("underline_position", metrics.underline_position);
try program.setUniform("underline_thickness", metrics.underline_thickness); 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 // Set all of our texture indexes
try program.setUniform("text", 0); try program.setUniform("text", 0);
@ -538,6 +541,7 @@ pub fn updateCell(
if (colors.bg != null) i += 1; if (colors.bg != null) i += 1;
if (!cell.empty()) i += 1; if (!cell.empty()) i += 1;
if (cell.attrs.underline) i += 1; if (cell.attrs.underline) i += 1;
if (cell.attrs.strikethrough) i += 1;
break :needed i; break :needed i;
}; };
if (self.cells.items.len + needed > self.cells.capacity) return false; 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; return true;
} }

View File

@ -270,6 +270,11 @@ pub const Metrics = struct {
/// thickness in pixels. /// thickness in pixels.
underline_position: f32, underline_position: f32,
underline_thickness: 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 /// 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, 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, // size_metrics,
// cell_width, // cell_width,
// cell_height, // cell_height,
// cell_height - cell_baseline, // cell_height - cell_baseline,
// underline_position, // underline_position,
// underline_thickness, // underline_thickness,
// strikethrough,
// }); // });
return .{ return .{
@ -373,6 +395,8 @@ fn calcMetrics(face: freetype.Face) Metrics {
.cell_baseline = cell_baseline, .cell_baseline = cell_baseline,
.underline_position = underline_position, .underline_position = underline_position,
.underline_thickness = underline_thickness, .underline_thickness = underline_thickness,
.strikethrough_position = strikethrough.pos,
.strikethrough_thickness = strikethrough.thickness,
}; };
} }

View File

@ -164,6 +164,7 @@ pub const Cell = struct {
faint: bool = false, faint: bool = false,
underline: bool = false, underline: bool = false,
inverse: bool = false, inverse: bool = false,
strikethrough: bool = false,
/// True if this is a wide character. This char takes up /// True if this is a wide character. This char takes up
/// two cells. The following cell ALWAYS is a space. /// two cells. The following cell ALWAYS is a space.

View File

@ -365,6 +365,14 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
self.screen.cursor.pen.attrs.inverse = false; 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| { .direct_color_fg => |rgb| {
self.screen.cursor.pen.attrs.has_fg = true; self.screen.cursor.pen.attrs.has_fg = true;
self.screen.cursor.pen.fg = .{ self.screen.cursor.pen.fg = .{

View File

@ -34,6 +34,10 @@ pub const Attribute = union(enum) {
inverse: void, inverse: void,
reset_inverse: void, reset_inverse: void,
/// Strikethrough the text.
strikethrough: void,
reset_strikethrough: void,
/// Set foreground color as RGB values. /// Set foreground color as RGB values.
direct_color_fg: RGB, direct_color_fg: RGB,
@ -99,8 +103,12 @@ pub const Parser = struct {
7 => return Attribute{ .inverse = {} }, 7 => return Attribute{ .inverse = {} },
9 => return Attribute{ .strikethrough = {} },
27 => return Attribute{ .reset_inverse = {} }, 27 => return Attribute{ .reset_inverse = {} },
29 => return Attribute{ .reset_strikethrough = {} },
30...37 => return Attribute{ 30...37 => return Attribute{
.@"8_fg" = @intToEnum(color.Name, slice[0] - 30), .@"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" { test "sgr: 8 color" {
var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } }; var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } };