mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Merge pull request #18 from mitchellh/font-metrics
This improves the way we calculate metrics based on fonts to be more accurate. Two practical impacts: * We now position the underline and underline thickness based on the font metrics * We now correctly position Apple Emoji font And I'm going to follow this up with strikethrough since that should be possible now.
This commit is contained in:
1
TODO.md
1
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:
|
||||
|
@ -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)));
|
||||
|
@ -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;
|
||||
|
14
src/Grid.zig
14
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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
|
Reference in New Issue
Block a user