diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index e3bcd5292..eea3c6851 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -217,6 +217,7 @@ pub const SfntTag = enum(c_int) { .os2 => c.TT_OS2, .head => c.TT_Header, .post => c.TT_Postscript, + .hhea => c.TT_HoriHeader, else => unreachable, // As-needed... }; } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 8749f9092..756d1ae6a 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -536,6 +536,7 @@ pub const Face = struct { InvalidPostTable, InvalidOS2Table, OS2VersionNotSupported, + InvalidHheaTable, }; fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics { @@ -563,7 +564,7 @@ pub const Face = struct { const len = data.getLength(); break :post opentype.Post.init(ptr[0..len]) catch |err| { return switch (err) { - error.EndOfStream => error.InvalidOS2Table, + error.EndOfStream => error.InvalidPostTable, }; }; }; @@ -583,13 +584,73 @@ pub const Face = struct { }; }; + // Read the 'hhea' table out of the font data. + const hhea: opentype.Hhea = hhea: { + const tag = macos.text.FontTableTag.init("hhea"); + const data = ct_font.copyTable(tag) orelse return error.CopyTableError; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :hhea opentype.Hhea.init(ptr[0..len]) catch |err| { + return switch (err) { + error.EndOfStream => error.InvalidHheaTable, + }; + }; + }; + const units_per_em: f64 = @floatFromInt(head.unitsPerEm); const px_per_em: f64 = ct_font.getSize(); const px_per_unit: f64 = px_per_em / units_per_em; - const ascent = @as(f64, @floatFromInt(os2.sTypoAscender)) * px_per_unit; - const descent = @as(f64, @floatFromInt(os2.sTypoDescender)) * px_per_unit; - const line_gap = @as(f64, @floatFromInt(os2.sTypoLineGap)) * px_per_unit; + const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { + const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); + const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); + const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); + + // If the font says to use typo metrics, trust it. + if (os2.fsSelection.use_typo_metrics) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + // Otherwise we prefer the height metrics from 'hhea' if they + // are available, or else OS/2 sTypo* metrics, and if all else + // fails then we use OS/2 usWin* metrics. + // + // This is not "standard" behavior, but it's our best bet to + // account for fonts being... just weird. It's pretty much what + // FreeType does to get its generic ascent and descent metrics. + + if (hhea.ascender != 0 or hhea.descender != 0) { + const hhea_ascent: f64 = @floatFromInt(hhea.ascender); + const hhea_descent: f64 = @floatFromInt(hhea.descender); + const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap); + break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + } + + if (os2_ascent != 0 or os2_descent != 0) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + const win_ascent: f64 = @floatFromInt(os2.usWinAscent); + const win_descent: f64 = @floatFromInt(os2.usWinDescent); + break :vertical_metrics .{ + win_ascent * px_per_unit, + win_descent * px_per_unit, + 0.0, + }; + }; // Some fonts have degenerate 'post' tables where the underline // thickness (and often position) are 0. We consider them null diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index c3d4a449b..e9f8d3207 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -631,6 +631,9 @@ pub const Face = struct { // Read the 'OS/2' table out of the font data. const os2 = face.getSfntTable(.os2) orelse return error.CopyTableError; + // Read the 'hhea' table out of the font data. + const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError; + // Some fonts don't actually have an OS/2 table, which // we need in order to do the metrics calculations, in // such cases FreeType sets the version to 0xFFFF @@ -640,9 +643,56 @@ pub const Face = struct { const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem); const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em)); - const ascent = @as(f64, @floatFromInt(os2.sTypoAscender)) * px_per_unit; - const descent = @as(f64, @floatFromInt(os2.sTypoDescender)) * px_per_unit; - const line_gap = @as(f64, @floatFromInt(os2.sTypoLineGap)) * px_per_unit; + const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { + const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); + const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); + const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); + + // If the font says to use typo metrics, trust it. + // (The USE_TYPO_METRICS bit is bit 7) + if (os2.fsSelection & (1 << 7) != 0) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + // Otherwise we prefer the height metrics from 'hhea' if they + // are available, or else OS/2 sTypo* metrics, and if all else + // fails then we use OS/2 usWin* metrics. + // + // This is not "standard" behavior, but it's our best bet to + // account for fonts being... just weird. It's pretty much what + // FreeType does to get its generic ascent and descent metrics. + + if (hhea.Ascender != 0 or hhea.Descender != 0) { + const hhea_ascent: f64 = @floatFromInt(hhea.Ascender); + const hhea_descent: f64 = @floatFromInt(hhea.Descender); + const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap); + break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + } + + if (os2_ascent != 0 or os2_descent != 0) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + const win_ascent: f64 = @floatFromInt(os2.usWinAscent); + const win_descent: f64 = @floatFromInt(os2.usWinDescent); + break :vertical_metrics .{ + win_ascent * px_per_unit, + win_descent * px_per_unit, + 0.0, + }; + }; // Some fonts have degenerate 'post' tables where the underline // thickness (and often position) are 0. We consider them null diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index a6317196f..cf929eb67 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -214,26 +214,11 @@ pub fn renderGlyph( ) !font.Glyph { const metrics = self.metrics; - // Some codepoints (such as a few cursors) should not - // grow when the cell height is adjusted to be larger. - // And we also will need to adjust the vertical position. - const height, const dy = adjust: { - const h = metrics.cell_height; - if (unadjustedCodepoint(cp)) { - if (metrics.original_cell_height) |original| { - if (h > original) { - break :adjust .{ original, (h - original) / 2 }; - } - } - } - break :adjust .{ h, 0 }; - }; - // Create the canvas we'll use to draw var canvas = try font.sprite.Canvas.init( alloc, metrics.cell_width, - height, + metrics.cell_height, ); defer canvas.deinit(alloc); @@ -246,15 +231,11 @@ pub fn renderGlyph( // Our coordinates start at the BOTTOM for our renderers so we have to // specify an offset of the full height because we rendered a full size // cell. - // - // If we have an adjustment (see above) to the cell height that we need - // to account for, we subtract half the difference (dy) to keep the glyph - // centered. - const offset_y = @as(i32, @intCast(metrics.cell_height - dy)); + const offset_y = @as(i32, @intCast(metrics.cell_height)); return font.Glyph{ .width = metrics.cell_width, - .height = height, + .height = metrics.cell_height, .offset_x = 0, .offset_y = offset_y, .atlas_x = region.x, @@ -263,19 +244,6 @@ pub fn renderGlyph( }; } -/// Returns true if this codepoint should be rendered with the -/// width/height set to unadjusted values. -pub fn unadjustedCodepoint(cp: u32) bool { - return switch (cp) { - @intFromEnum(Sprite.cursor_rect), - @intFromEnum(Sprite.cursor_hollow_rect), - @intFromEnum(Sprite.cursor_bar), - => true, - - else => false, - }; -} - fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { _ = alloc; switch (cp) { @@ -1656,12 +1624,6 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void .right = true, }, .light), - // Not official box characters but special characters we hide - // in the high bits of a unicode codepoint. - @intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas), - @intFromEnum(Sprite.cursor_hollow_rect) => self.draw_cursor_hollow_rect(canvas), - @intFromEnum(Sprite.cursor_bar) => self.draw_cursor_bar(canvas), - else => return error.InvalidCodepoint, } } @@ -2842,42 +2804,6 @@ fn draw_dash_vertical( } } -fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void { - // The cursor should fit itself to the canvas it's given, since if - // the cell height is adjusted upwards it will be given a canvas - // with the original un-adjusted height, so we can't use the height - // from the metrics. - const height: u32 = @intCast(canvas.sfc.getHeight()); - self.rect(canvas, 0, 0, self.metrics.cell_width, height); -} - -fn draw_cursor_hollow_rect(self: Box, canvas: *font.sprite.Canvas) void { - // The cursor should fit itself to the canvas it's given, since if - // the cell height is adjusted upwards it will be given a canvas - // with the original un-adjusted height, so we can't use the height - // from the metrics. - const height: u32 = @intCast(canvas.sfc.getHeight()); - - const thick_px = Thickness.super_light.height(self.metrics.cursor_thickness); - - self.rect(canvas, 0, 0, self.metrics.cell_width, thick_px); - self.rect(canvas, 0, 0, thick_px, height); - self.rect(canvas, self.metrics.cell_width -| thick_px, 0, self.metrics.cell_width, height); - self.rect(canvas, 0, height -| thick_px, self.metrics.cell_width, height); -} - -fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void { - // The cursor should fit itself to the canvas it's given, since if - // the cell height is adjusted upwards it will be given a canvas - // with the original un-adjusted height, so we can't use the height - // from the metrics. - const height: u32 = @intCast(canvas.sfc.getHeight()); - - const thick_px = Thickness.light.height(self.metrics.cursor_thickness); - - self.rect(canvas, 0, 0, thick_px, height); -} - fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { const thick_px = thickness.height(self.metrics.box_thickness); self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px); diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index b8c89c74e..ede67d00d 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -21,6 +21,7 @@ const Sprite = font.sprite.Sprite; const Box = @import("Box.zig"); const Powerline = @import("Powerline.zig"); const underline = @import("underline.zig"); +const cursor = @import("cursor.zig"); const log = std.log.scoped(.font_sprite); @@ -123,6 +124,35 @@ pub fn renderGlyph( break :powerline try f.renderGlyph(alloc, atlas, cp); }, + + .cursor => cursor: { + // Cursors should be drawn with the original cell height if + // it has been adjusted larger, so they don't get stretched. + const height, const dy = adjust: { + const h = metrics.cell_height; + if (metrics.original_cell_height) |original| { + if (h > original) { + break :adjust .{ original, (h - original) / 2 }; + } + } + break :adjust .{ h, 0 }; + }; + + var g = try cursor.renderGlyph( + alloc, + atlas, + @enumFromInt(cp), + width, + height, + metrics.cursor_thickness, + ); + + // Keep the cursor centered in the cell if it's shorter. + g.offset_y += @intCast(dy); + + break :cursor g; + }, + }; } @@ -133,6 +163,7 @@ const Kind = enum { overline, strikethrough, powerline, + cursor, pub fn init(cp: u32) ?Kind { return switch (cp) { @@ -153,7 +184,7 @@ const Kind = enum { .cursor_rect, .cursor_hollow_rect, .cursor_bar, - => .box, + => .cursor, }, // == Box fonts == diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig new file mode 100644 index 000000000..b20b6c531 --- /dev/null +++ b/src/font/sprite/cursor.zig @@ -0,0 +1,61 @@ +//! This file renders cursor sprites. +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const font = @import("../main.zig"); +const Sprite = font.sprite.Sprite; + +/// Draw a cursor. +pub fn renderGlyph( + alloc: Allocator, + atlas: *font.Atlas, + sprite: Sprite, + width: u32, + height: u32, + thickness: u32, +) !font.Glyph { + // Make a canvas of the desired size + var canvas = try font.sprite.Canvas.init(alloc, width, height); + defer canvas.deinit(alloc); + + // Draw the appropriate sprite + switch (sprite) { + Sprite.cursor_rect => canvas.rect(.{ + .x = 0, + .y = 0, + .width = width, + .height = height, + }, .on), + Sprite.cursor_hollow_rect => { + // left + canvas.rect(.{ .x = 0, .y = 0, .width = thickness, .height = height }, .on); + // right + canvas.rect(.{ .x = width -| thickness, .y = 0, .width = thickness, .height = height }, .on); + // top + canvas.rect(.{ .x = 0, .y = 0, .width = width, .height = thickness }, .on); + // bottom + canvas.rect(.{ .x = 0, .y = height -| thickness, .width = width, .height = thickness }, .on); + }, + Sprite.cursor_bar => canvas.rect(.{ + .x = 0, + .y = 0, + .width = thickness, + .height = height, + }, .on), + else => unreachable, + } + + // Write the drawing to the atlas + const region = try canvas.writeAtlas(alloc, atlas); + + return font.Glyph{ + .width = width, + .height = height, + .offset_x = 0, + .offset_y = @intCast(height), + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = @floatFromInt(width), + }; +}