mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
calculate font metrics per face
This commit is contained in:
11
src/Grid.zig
11
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);
|
var shaper = try font.Shaper.init(shape_buf);
|
||||||
errdefer shaper.deinit();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
// Load all visible ASCII characters and build our cell width based on
|
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
|
||||||
// the widest character that we see.
|
// Doesn't matter, any normal ASCII will do we're just trying to make
|
||||||
const metrics = try font_group.metrics(alloc);
|
// 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});
|
log.debug("cell dimensions={}", .{metrics});
|
||||||
|
|
||||||
// Create our shader
|
// Create our shader
|
||||||
|
@ -29,6 +29,9 @@ hb_font: harfbuzz.Font,
|
|||||||
/// a way to declare this. We just assume a font with color is an emoji font.
|
/// a way to declare this. We just assume a font with color is an emoji font.
|
||||||
presentation: Presentation,
|
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
|
/// 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
|
/// wrong on modern devices so it is highly recommended you get the DPI
|
||||||
/// using whatever platform method you can.
|
/// 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,
|
.face = face,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.presentation = if (face.hasColor()) .emoji else .text,
|
.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,
|
.face = face,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.presentation = if (face.hasColor()) .emoji else .text,
|
.presentation = if (face.hasColor()) .emoji else .text,
|
||||||
|
.metrics = calcMetrics(face),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,6 +241,86 @@ fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 {
|
|||||||
return @intToFloat(f32, v >> 6);
|
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 top 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.cell_width = cell_width,
|
||||||
|
.cell_height = cell_height,
|
||||||
|
.cell_baseline = cell_height - f26dot6ToFloat(size_metrics.ascender),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
const testFont = @import("test.zig").fontRegular;
|
const testFont = @import("test.zig").fontRegular;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -12,7 +12,6 @@ const Library = @import("main.zig").Library;
|
|||||||
const Glyph = @import("main.zig").Glyph;
|
const Glyph = @import("main.zig").Glyph;
|
||||||
const Style = @import("main.zig").Style;
|
const Style = @import("main.zig").Style;
|
||||||
const Group = @import("main.zig").Group;
|
const Group = @import("main.zig").Group;
|
||||||
const Metrics = @import("main.zig").Metrics;
|
|
||||||
const Presentation = @import("main.zig").Presentation;
|
const Presentation = @import("main.zig").Presentation;
|
||||||
|
|
||||||
const log = std.log.scoped(.font_groupcache);
|
const log = std.log.scoped(.font_groupcache);
|
||||||
@ -84,66 +83,6 @@ pub fn reset(self: *GroupCache) void {
|
|||||||
self.glyphs.clearRetainingCapacity();
|
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.
|
/// Get the font index for a given codepoint. This is cached.
|
||||||
pub fn indexForCodepoint(
|
pub fn indexForCodepoint(
|
||||||
self: *GroupCache,
|
self: *GroupCache,
|
||||||
|
@ -49,16 +49,6 @@ pub const Presentation = enum(u1) {
|
|||||||
emoji = 1, // U+FEOF
|
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 {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user