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:
* Strikethrough
* Bell
* Mac:
- 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),
));
}
/// 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);
}

View File

@ -1,2 +1,3 @@
#include <ft2build.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_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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -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.

View File

@ -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 = .{

View File

@ -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 } };