diff --git a/TODO.md b/TODO.md index 608e1b1f9..436ff324e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ Bugs: -* Underline should use freetype underline thickness hint * Glyph baseline is using the main font, but it can vary font to font Performance: diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index c7648b3ca..c5cb413c0 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -24,6 +24,13 @@ pub const Face = struct { return c.FT_HAS_COLOR(self.handle); } + /// A macro that returns true whenever a face object contains a scalable + /// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF, + /// and PFR font formats). + pub fn isScalable(self: Face) bool { + return c.FT_IS_SCALABLE(self.handle); + } + /// Select a given charmap by its encoding tag (as listed in freetype.h). pub fn selectCharmap(self: Face, encoding: Encoding) Error!void { return intToError(c.FT_Select_Charmap(self.handle, @enumToInt(encoding))); diff --git a/shaders/cell.v.glsl b/shaders/cell.v.glsl index 5460e4195..a282981f5 100644 --- a/shaders/cell.v.glsl +++ b/shaders/cell.v.glsl @@ -57,7 +57,8 @@ uniform sampler2D text; uniform sampler2D text_color; uniform vec2 cell_size; uniform mat4 projection; -uniform float glyph_baseline; +uniform float underline_position; +uniform float underline_thickness; /******************************************************************** * Modes @@ -138,9 +139,8 @@ void main() { // The glyph_offset.y is the y bearing, a y value that when added // to the baseline is the offset (+y is up). Our grid goes down. - // So we flip it with `cell_size.y - glyph_offset.y`. The glyph_baseline - // uniform sets our line baseline where characters "sit". - glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y - glyph_baseline; + // So we flip it with `cell_size.y - glyph_offset.y`. + glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y; // Calculate the final position of the cell. cell_pos = cell_pos + glyph_size_downsampled * position + glyph_offset_calc; @@ -197,18 +197,16 @@ void main() { break; case MODE_UNDERLINE: - // Make the underline a smaller version of our cell - // TODO: use real font underline thickness - vec2 underline_size = vec2(cell_size_scaled.x, cell_size_scaled.y*0.05); + // Underline Y value is just our thickness + vec2 underline_size = vec2(cell_size_scaled.x, underline_thickness); - // Position our underline so that it is midway between the glyph - // baseline and the bottom of the cell. - vec2 underline_offset = vec2(cell_size_scaled.x, cell_size_scaled.y - (glyph_baseline / 2)); + // Position the underline where we are told to + vec2 underline_offset = vec2(cell_size_scaled.x, underline_position) ; // Go to the bottom of the cell, take away the size of the // underline, and that is our position. We also float it slightly // 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; diff --git a/src/Grid.zig b/src/Grid.zig index de0eea79d..ee5efa7d2 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -158,9 +158,14 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid { var shaper = try font.Shaper.init(shape_buf); errdefer shaper.deinit(); - // Load all visible ASCII characters and build our cell width based on - // the widest character that we see. - const metrics = try font_group.metrics(alloc); + // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? + // Doesn't matter, any normal ASCII will do we're just trying to make + // sure we use the regular font. + const metrics = metrics: { + const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; + const face = try font_group.group.faceFromIndex(index); + break :metrics face.metrics; + }; log.debug("cell dimensions={}", .{metrics}); // Create our shader @@ -173,7 +178,8 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid { const pbind = try program.use(); defer pbind.unbind(); try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height }); - try program.setUniform("glyph_baseline", metrics.cell_baseline); + try program.setUniform("underline_position", metrics.underline_position); + try program.setUniform("underline_thickness", metrics.underline_thickness); // Set all of our texture indexes try program.setUniform("text", 0); diff --git a/src/font/Face.zig b/src/font/Face.zig index 3068b8cbc..6f21d5920 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -29,6 +29,9 @@ hb_font: harfbuzz.Font, /// a way to declare this. We just assume a font with color is an emoji font. presentation: Presentation, +/// Metrics for this font face. These are useful for renderers. +metrics: Metrics, + /// If a DPI can't be calculated, this DPI is used. This is probably /// wrong on modern devices so it is highly recommended you get the DPI /// using whatever platform method you can. @@ -64,6 +67,7 @@ pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: DesiredSize) .face = face, .hb_font = hb_font, .presentation = if (face.hasColor()) .emoji else .text, + .metrics = calcMetrics(face), }; } @@ -81,6 +85,7 @@ pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face { .face = face, .hb_font = hb_font, .presentation = if (face.hasColor()) .emoji else .text, + .metrics = calcMetrics(face), }; } @@ -90,12 +95,6 @@ pub fn deinit(self: *Face) void { self.* = undefined; } -/// Change the size of the loaded font face. If you're using a texture -/// atlas, you should invalidate all the previous values if cached. -pub fn setSize(self: Face, size: DesiredSize) !void { - return try setSize_(self.face, size); -} - fn setSize_(face: freetype.Face, size: DesiredSize) !void { // If we have fixed sizes, we just have to try to pick the one closest // to what the user requested. Otherwise, we can choose an arbitrary @@ -210,12 +209,32 @@ pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32 atlas.set(region, buffer); } + const offset_y = offset_y: { + // For non-scalable colorized fonts, we assume they are pictographic + // and just center the glyph. So far this has only applied to emoji + // fonts. Emoji fonts don't always report a correct ascender/descender + // (mainly Apple Emoji) so we just center them. Also, since emoji font + // aren't scalable, cell_baseline is incorrect anyways. + // + // NOTE(mitchellh): I don't know if this is right, this doesn't + // _feel_ right, but it makes all my limited test cases work. + if (self.face.hasColor() and !self.face.isScalable()) { + break :offset_y @intCast(c_int, tgt_h); + } + + // The Y offset is the offset of the top of our bitmap PLUS our + // baseline calculation. The baseline calculation is so that everything + // is properly centered when we render it out into a monospace grid. + // Note: we add here because our X/Y is actually reversed, adding goes UP. + break :offset_y glyph.*.bitmap_top + @floatToInt(c_int, self.metrics.cell_baseline); + }; + // Store glyph metadata return Glyph{ .width = tgt_w, .height = tgt_h, .offset_x = glyph.*.bitmap_left, - .offset_y = glyph.*.bitmap_top, + .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, .advance_x = f26dot6ToFloat(glyph.*.advance.x), @@ -224,7 +243,7 @@ pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32 /// Convert 16.6 pixel format to pixels based on the scale factor of the /// current font size. -pub fn unitsToPxY(self: Face, units: i32) i32 { +fn unitsToPxY(self: Face, units: i32) i32 { return @intCast(i32, freetype.mulFix( units, @intCast(i32, self.face.handle.*.size.*.metrics.y_scale), @@ -236,6 +255,134 @@ fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 { return @intToFloat(f32, v >> 6); } +/// Metrics associated with the font that are useful for renderers to know. +pub const Metrics = struct { + /// Recommended cell width and height for a monospace grid using this font. + cell_width: f32, + cell_height: f32, + + /// For monospace grids, the recommended y-value from the bottom to set + /// the baseline for font rendering. This is chosen so that things such + /// as the bottom of a "g" or "y" do not drop below the cell. + cell_baseline: f32, + + /// The position of the underline from the top of the cell and the + /// thickness in pixels. + underline_position: f32, + underline_thickness: f32, +}; + +/// Calculate the metrics associated with a face. This is not public because +/// the metrics are calculated for every face and cached since they're +/// frequently required for renderers and take up next to little memory space +/// in the grand scheme of things. +/// +/// An aside: the proper way to limit memory usage due to faces is to limit +/// the faces with DeferredFaces and reload on demand. A Face can't be converted +/// into a DeferredFace but a Face that comes from a DeferredFace can be +/// deinitialized anytime and reloaded with the deferred face. +fn calcMetrics(face: freetype.Face) Metrics { + const size_metrics = face.handle.*.size.*.metrics; + + // Cell width is calculated by preferring to use 'M' as the width of a + // cell since 'M' is generally the widest ASCII character. If loading 'M' + // fails then we use the max advance of the font face size metrics. + const cell_width: f32 = cell_width: { + if (face.getCharIndex('M')) |glyph_index| { + if (face.loadGlyph(glyph_index, .{ .render = true })) { + break :cell_width f26dot6ToFloat(face.handle.*.glyph.*.advance.x); + } else |_| { + // Ignore the error since we just fall back to max_advance below + } + } + + break :cell_width f26dot6ToFloat(size_metrics.max_advance); + }; + + // Cell height is calculated as the maximum of multiple things in order + // to handle edge cases in fonts: (1) the height as reported in metadata + // by the font designer (2) the maximum glyph height as measured in the + // font and (3) the height from the ascender to an underscore. + const cell_height: f32 = cell_height: { + // The height as reported by the font designer. + const face_height = f26dot6ToFloat(size_metrics.height); + + // The maximum height a glyph can take in the font + const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) - + f26dot6ToFloat(size_metrics.descender); + + // The height of the underscore character + const underscore_height = underscore: { + if (face.getCharIndex('_')) |glyph_index| { + if (face.loadGlyph(glyph_index, .{ .render = true })) { + var res: f32 = f26dot6ToFloat(size_metrics.ascender); + res -= @intToFloat(f32, face.handle.*.glyph.*.bitmap_top); + res += @intToFloat(f32, face.handle.*.glyph.*.bitmap.rows); + break :underscore res; + } else |_| { + // Ignore the error since we just fall back below + } + } + + break :underscore 0; + }; + + break :cell_height @maximum( + face_height, + @maximum(max_glyph_height, underscore_height), + ); + }; + + // The baseline is the descender amount for the font. This is the maximum + // that a font may go down. We switch signs because our coordinate system + // is reversed. + const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender); + + // The underline position. This is a value from the top where the + // underline should go. + const underline_position = underline_pos: { + // We use the declared underline position if its available + const declared = fontUnitsToPxY( + face, + @intCast(i32, size_metrics.ascender) - face.handle.*.underline_position, + ); + if (declared > 0) + break :underline_pos declared; + + // If we have no declared underline position, we go slightly under the + // cell height (mainly: non-scalable fonts, i.e. emoji) + break :underline_pos cell_height - 1; + }; + const underline_thickness = @maximum(1, fontUnitsToPxY( + face, + face.handle.*.underline_thickness, + )); + + // log.warn("METRICS={} width={d} height={d} baseline={d} underline_pos={d} underline_thickness={d}", .{ + // size_metrics, + // cell_width, + // cell_height, + // cell_height - cell_baseline, + // underline_position, + // underline_thickness, + // }); + + return .{ + .cell_width = cell_width, + .cell_height = cell_height, + .cell_baseline = cell_baseline, + .underline_position = underline_position, + .underline_thickness = underline_thickness, + }; +} + +/// Convert freetype "font units" to pixels using the Y scale. +fn fontUnitsToPxY(face: freetype.Face, x: i32) f32 { + const mul = freetype.mulFix(x, @intCast(i32, face.handle.*.size.*.metrics.y_scale)); + const div = @intToFloat(f32, mul) / 64; + return @ceil(div); +} + test { const testFont = @import("test.zig").fontRegular; const alloc = testing.allocator; diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 86bbbd520..688d018e3 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -12,7 +12,6 @@ const Library = @import("main.zig").Library; const Glyph = @import("main.zig").Glyph; const Style = @import("main.zig").Style; const Group = @import("main.zig").Group; -const Metrics = @import("main.zig").Metrics; const Presentation = @import("main.zig").Presentation; const log = std.log.scoped(.font_groupcache); @@ -84,66 +83,6 @@ pub fn reset(self: *GroupCache) void { self.glyphs.clearRetainingCapacity(); } -/// Calculate the metrics for this group. This also warms the cache -/// since this preloads all the ASCII characters. -pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics { - // Load all visible ASCII characters and build our cell width based on - // the widest character that we see. - const cell_width: f32 = cell_width: { - var cell_width: f32 = 0; - var i: u32 = 32; - while (i <= 126) : (i += 1) { - const index = (try self.indexForCodepoint(alloc, i, .regular, .text)).?; - const face = try self.group.faceFromIndex(index); - const glyph_index = face.glyphIndex(i).?; - const glyph = try self.renderGlyph(alloc, index, glyph_index); - if (glyph.advance_x > cell_width) { - cell_width = @ceil(glyph.advance_x); - } - } - - break :cell_width cell_width; - }; - - // The cell height is the vertical height required to render underscore - // '_' which should live at the bottom of a cell. - const cell_height: f32 = cell_height: { - // Get the '_' char for height - const index = (try self.indexForCodepoint(alloc, '_', .regular, .text)).?; - const face = try self.group.faceFromIndex(index); - const glyph_index = face.glyphIndex('_').?; - const glyph = try self.renderGlyph(alloc, index, glyph_index); - - // This is the height reported by the font face - const face_height: i32 = face.unitsToPxY(face.face.handle.*.height); - - // Determine the height of the underscore char - var res: i32 = face.unitsToPxY(face.face.handle.*.ascender); - res -= glyph.offset_y; - res += @intCast(i32, glyph.height); - - // We take whatever is larger to account for some fonts that - // put the underscore outside f the rectangle. - if (res < face_height) res = face_height; - - break :cell_height @intToFloat(f32, res); - }; - - const cell_baseline = cell_baseline: { - const face = self.group.faces.get(.regular).items[0].face.?; - break :cell_baseline cell_height - @intToFloat( - f32, - face.unitsToPxY(face.face.handle.*.ascender), - ); - }; - - return Metrics{ - .cell_width = cell_width, - .cell_height = cell_height, - .cell_baseline = cell_baseline, - }; -} - /// Get the font index for a given codepoint. This is cached. pub fn indexForCodepoint( self: *GroupCache, diff --git a/src/font/main.zig b/src/font/main.zig index d7463a2e5..201eb0e68 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -49,16 +49,6 @@ pub const Presentation = enum(u1) { emoji = 1, // U+FEOF }; -/// Font metrics useful for things such as grid calculation. -pub const Metrics = struct { - /// The width and height of a monospace cell. - cell_width: f32, - cell_height: f32, - - /// The baseline offset that can be used to place underlines. - cell_baseline: f32, -}; - test { @import("std").testing.refAllDecls(@This()); }