From b18309187e7acfd7a489004f90308424d2c28b7f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 6 Oct 2022 15:03:19 -0700 Subject: [PATCH] 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. --- TODO.md | 1 - pkg/freetype/face.zig | 34 ++++++++++++++++++++++++++++++++++ pkg/freetype/freetype-zig.h | 1 + shaders/cell.f.glsl | 6 +++++- shaders/cell.v.glsl | 19 +++++++++++++++++++ src/Grid.zig | 27 +++++++++++++++++++++++++++ src/font/Face.zig | 26 +++++++++++++++++++++++++- src/terminal/Screen.zig | 1 + src/terminal/Terminal.zig | 8 ++++++++ src/terminal/sgr.zig | 20 ++++++++++++++++++++ 10 files changed, 140 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 436ff324e..d0ca4bfe0 100644 --- a/TODO.md +++ b/TODO.md @@ -36,7 +36,6 @@ Improvements: Major Features: -* Strikethrough * Bell * Mac: - Switch to raw Cocoa and Metal instead of glfw and libuv (major!) diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index c5cb413c0..ef70e344d 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -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); } diff --git a/pkg/freetype/freetype-zig.h b/pkg/freetype/freetype-zig.h index c069c19b7..fc15c5941 100644 --- a/pkg/freetype/freetype-zig.h +++ b/pkg/freetype/freetype-zig.h @@ -1,2 +1,3 @@ #include #include FT_FREETYPE_H +#include FT_TRUETYPE_TABLES_H diff --git a/shaders/cell.f.glsl b/shaders/cell.f.glsl index 9fdcea9fc..79ab8186c 100644 --- a/shaders/cell.f.glsl +++ b/shaders/cell.f.glsl @@ -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; } } diff --git a/shaders/cell.v.glsl b/shaders/cell.v.glsl index a282981f5..d608b98c7 100644 --- a/shaders/cell.v.glsl +++ b/shaders/cell.v.glsl @@ -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; diff --git a/src/Grid.zig b/src/Grid.zig index ee5efa7d2..54f74fd0c 100644 --- a/src/Grid.zig +++ b/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; } diff --git a/src/font/Face.zig b/src/font/Face.zig index 6f21d5920..5266af719 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -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, }; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7652a7546..349ada767 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -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. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9fb89a877..b7d275bb2 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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 = .{ diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index 6d03bb66c..950cbf328 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -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 } };