font: unify metrics calculations & separate sprite metrics

Unify grid metrics calculations by relying on shared logic mostly based
on values directly from the font tables, this deduplicates a lot of code
and gives us more control over how we interpret various metrics.

Also separate metrics for underlined, strikethrough, and overline
thickness and position, and box drawing thickness, so that they can
individually be adjusted as the user desires.
This commit is contained in:
Qwerasd
2024-12-11 16:30:40 -05:00
parent 7e5a164be8
commit bd18452310
8 changed files with 691 additions and 508 deletions

View File

@ -215,6 +215,8 @@ pub const SfntTag = enum(c_int) {
pub fn DataType(comptime self: SfntTag) type {
return switch (self) {
.os2 => c.TT_OS2,
.head => c.TT_Header,
.post => c.TT_Postscript,
else => unreachable, // As-needed...
};
}

View File

@ -520,7 +520,14 @@ test "getIndex box glyph" {
var r: CodepointResolver = .{
.collection = c,
.sprite = .{ .width = 18, .height = 36, .thickness = 2 },
.sprite = .{
.metrics = font.Metrics.calc(.{
.cell_width = 18.0,
.ascent = 30.0,
.descent = -6.0,
.line_gap = 0.0,
}),
},
};
defer r.deinit(alloc);

View File

@ -122,13 +122,7 @@ fn reloadMetrics(self: *SharedGrid) !void {
self.metrics = face.metrics;
// Setup our sprite font.
self.resolver.sprite = .{
.width = self.metrics.cell_width,
.height = self.metrics.cell_height,
.thickness = self.metrics.underline_thickness,
.underline_position = self.metrics.underline_position,
.strikethrough_position = self.metrics.strikethrough_position,
};
self.resolver.sprite = .{ .metrics = self.metrics };
}
/// Returns the grid cell size.

View File

@ -6,21 +6,28 @@ const std = @import("std");
cell_width: u32,
cell_height: u32,
/// 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.
/// Distance in pixels from the bottom of the cell to the text baseline.
cell_baseline: u32,
/// The position of the underline from the top of the cell and the
/// thickness in pixels.
/// Distance in pixels from the top of the cell to the top of the underline.
underline_position: u32,
/// Thickness in pixels of the underline.
underline_thickness: u32,
/// The position and thickness of a strikethrough. Same units/style
/// as the underline fields.
/// Distance in pixels from the top of the cell to the top of the strikethrough.
strikethrough_position: u32,
/// Thickness in pixels of the strikethrough.
strikethrough_thickness: u32,
/// Distance in pixels from the top of the cell to the top of the overline.
/// Can be negative to adjust the position above the top of the cell.
overline_position: i32,
/// Thickness in pixels of the overline.
overline_thickness: u32,
/// Thickness in pixels of box drawing characters.
box_thickness: u32,
/// The thickness in pixels of the cursor sprite. This has a default value
/// because it is not determined by fonts but rather by user configuration.
cursor_thickness: u32 = 1,
@ -30,6 +37,143 @@ cursor_thickness: u32 = 1,
original_cell_width: ?u32 = null,
original_cell_height: ?u32 = null,
/// Minimum acceptable values for some fields to prevent modifiers
/// from being able to, for example, cause 0-thickness underlines.
const Minimums = struct {
const cell_width = 1;
const cell_height = 1;
const underline_thickness = 1;
const strikethrough_thickness = 1;
const overline_thickness = 1;
const box_thickness = 1;
const cursor_thickness = 1;
};
const CalcOpts = struct {
cell_width: f64,
/// The typographic ascent metric from the font.
/// This represents the maximum vertical position of the highest ascender.
///
/// Relative to the baseline, in px, +Y=up
ascent: f64,
/// The typographic descent metric from the font.
/// This represents the minimum vertical position of the lowest descender.
///
/// Relative to the baseline, in px, +Y=up
///
/// Note:
/// As this value is generally below the baseline, it is typically negative.
descent: f64,
/// The typographic line gap (aka "leading") metric from the font.
/// This represents the additional space to be added between lines in
/// addition to the space defined by the ascent and descent metrics.
///
/// Positive value in px
line_gap: f64,
/// The TOP of the underline stroke.
///
/// Relative to the baseline, in px, +Y=up
underline_position: ?f64 = null,
/// The thickness of the underline stroke in px.
underline_thickness: ?f64 = null,
/// The TOP of the strikethrough stroke.
///
/// Relative to the baseline, in px, +Y=up
strikethrough_position: ?f64 = null,
/// The thickness of the strikethrough stroke in px.
strikethrough_thickness: ?f64 = null,
/// The height of capital letters in the font, either derived from
/// a provided cap height metric or measured from the height of the
/// capital H glyph.
cap_height: ?f64 = null,
/// The height of lowercase letters in the font, either derived from
/// a provided ex height metric or measured from the height of the
/// lowercase x glyph.
ex_height: ?f64 = null,
};
/// Calculate our metrics based on values extracted from a font.
///
/// Try to pass values with as much precision as possible,
/// do not round them before using them for this function.
///
/// For any nullable options that are not provided, estimates will be used.
pub fn calc(opts: CalcOpts) Metrics {
// We use the ceiling of the provided cell width and height to ensure
// that the cell is large enough for the provided size, since we cast
// it to an integer later.
const cell_width = @ceil(opts.cell_width);
const cell_height = @ceil(opts.ascent - opts.descent + opts.line_gap);
// We split our line gap in two parts, and put half of it on the top
// of the cell and the other half on the bottom, so that our text never
// bumps up against either edge of the cell vertically.
const half_line_gap = opts.line_gap / 2;
// Unlike all our other metrics, `cell_baseline` is relative to the
// BOTTOM of the cell.
const cell_baseline = @round(half_line_gap - opts.descent);
// We calculate a top_to_baseline to make following calculations simpler.
const top_to_baseline = cell_height - cell_baseline;
// If we don't have a provided cap height,
// we estimate it as 75% of the ascent.
const cap_height = opts.cap_height orelse opts.ascent * 0.75;
// If we don't have a provided ex height,
// we estimate it as 75% of the cap height.
const ex_height = opts.ex_height orelse cap_height * 0.75;
// If we don't have a provided underline thickness,
// we estimate it as 15% of the ex height.
const underline_thickness = @max(1, @ceil(opts.underline_thickness orelse 0.15 * ex_height));
// If we don't have a provided strikethrough thickness
// then we just use the underline thickness for it.
const strikethrough_thickness = @max(1, @ceil(opts.strikethrough_thickness orelse underline_thickness));
// If we don't have a provided underline position then
// we place it 1 underline-thickness below the baseline.
const underline_position = @round(top_to_baseline -
(opts.underline_position orelse
-underline_thickness));
// If we don't have a provided strikethrough position
// then we center the strikethrough stroke at half the
// ex height, so that it's perfectly centered on lower
// case text.
const strikethrough_position = @round(top_to_baseline -
(opts.strikethrough_position orelse
ex_height * 0.5 + strikethrough_thickness * 0.5));
const result: Metrics = .{
.cell_width = @intFromFloat(cell_width),
.cell_height = @intFromFloat(cell_height),
.cell_baseline = @intFromFloat(cell_baseline),
.underline_position = @intFromFloat(underline_position),
.underline_thickness = @intFromFloat(underline_thickness),
.strikethrough_position = @intFromFloat(strikethrough_position),
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
.overline_position = 0,
.overline_thickness = @intFromFloat(underline_thickness),
.box_thickness = @intFromFloat(underline_thickness),
};
// std.log.debug("metrics={}", .{result});
return result;
}
/// Apply a set of modifiers.
pub fn apply(self: *Metrics, mods: ModifierSet) void {
var it = mods.iterator();
@ -76,7 +220,13 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
},
inline else => |tag| {
@field(self, @tagName(tag)) = entry.value_ptr.apply(@field(self, @tagName(tag)));
var new = entry.value_ptr.apply(@field(self, @tagName(tag)));
// If we have a minimum acceptable value
// for this metric, clamp the new value.
if (@hasDecl(Minimums, @tagName(tag))) {
new = @max(new, @field(Minimums, @tagName(tag)));
}
@field(self, @tagName(tag)) = new;
},
}
}
@ -152,23 +302,26 @@ pub const Modifier = union(enum) {
}
/// Apply a modifier to a numeric value.
pub fn apply(self: Modifier, v: u32) u32 {
pub fn apply(self: Modifier, v: anytype) @TypeOf(v) {
const T = @TypeOf(v);
const signed = @typeInfo(T).Int.signedness == .signed;
return switch (self) {
.percent => |p| percent: {
const p_clamped: f64 = @max(0, p);
const v_f64: f64 = @floatFromInt(v);
const applied_f64: f64 = @round(v_f64 * p_clamped);
const applied_u32: u32 = @intFromFloat(applied_f64);
break :percent applied_u32;
const applied_T: T = @intFromFloat(applied_f64);
break :percent applied_T;
},
.absolute => |abs| absolute: {
const v_i64: i64 = @intCast(v);
const abs_i64: i64 = @intCast(abs);
const applied_i64: i64 = @max(0, v_i64 +| abs_i64);
const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse
std.math.maxInt(u32);
break :absolute applied_u32;
const applied_i64: i64 = v_i64 +| abs_i64;
const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64);
const applied_T: T = std.math.cast(T, clamped_i64) orelse
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
break :absolute applied_T;
},
};
}
@ -215,7 +368,7 @@ pub const Key = key: {
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
var count: usize = 0;
for (field_infos, 0..) |field, i| {
if (field.type != u32) continue;
if (field.type != u32 and field.type != i32) continue;
enumFields[i] = .{ .name = field.name, .value = i };
count += 1;
}
@ -242,6 +395,9 @@ fn init() Metrics {
.underline_thickness = 0,
.strikethrough_position = 0,
.strikethrough_thickness = 0,
.overline_position = 0,
.overline_thickness = 0,
.box_thickness = 0,
};
}
@ -337,12 +493,12 @@ test "Modifier: percent" {
{
const m: Modifier = .{ .percent = 0.8 };
const v: u32 = m.apply(100);
const v: u32 = m.apply(@as(u32, 100));
try testing.expectEqual(@as(u32, 80), v);
}
{
const m: Modifier = .{ .percent = 1.8 };
const v: u32 = m.apply(100);
const v: u32 = m.apply(@as(u32, 100));
try testing.expectEqual(@as(u32, 180), v);
}
}
@ -352,17 +508,17 @@ test "Modifier: absolute" {
{
const m: Modifier = .{ .absolute = -100 };
const v: u32 = m.apply(100);
const v: u32 = m.apply(@as(u32, 100));
try testing.expectEqual(@as(u32, 0), v);
}
{
const m: Modifier = .{ .absolute = -120 };
const v: u32 = m.apply(100);
const v: u32 = m.apply(@as(u32, 100));
try testing.expectEqual(@as(u32, 0), v);
}
{
const m: Modifier = .{ .absolute = 100 };
const v: u32 = m.apply(100);
const v: u32 = m.apply(@as(u32, 100));
try testing.expectEqual(@as(u32, 200), v);
}
}

View File

@ -533,10 +533,91 @@ pub const Face = struct {
}
fn calcMetrics(ct_font: *macos.text.Font) !font.face.Metrics {
// Read the 'head' table out of the font data.
const head: opentype.Head = head: {
const tag = macos.text.FontTableTag.init("head");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
defer data.release();
const ptr = data.getPointer();
const len = data.getLength();
break :head try opentype.Head.init(ptr[0..len]);
};
// Read the 'post' table out of the font data.
const post: opentype.Post = post: {
const tag = macos.text.FontTableTag.init("post");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
defer data.release();
const ptr = data.getPointer();
const len = data.getLength();
break :post try opentype.Post.init(ptr[0..len]);
};
// Read the 'OS/2' table out of the font data.
const os2: opentype.OS2 = os2: {
const tag = macos.text.FontTableTag.init("OS/2");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
defer data.release();
const ptr = data.getPointer();
const len = data.getLength();
break :os2 try opentype.OS2.init(ptr[0..len]);
};
const units_per_em = head.unitsPerEm;
const px_per_em = ct_font.getSize();
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;
// Some fonts have degenerate 'post' tables where the underline
// thickness (and often position) are 0. We consider them null
// if this is the case and use our own fallbacks when we calculate.
const has_broken_underline = post.underlineThickness == 0;
// If the underline position isn't 0 then we do use it,
// even if the thickness is't properly specified.
const underline_position = if (has_broken_underline and post.underlinePosition == 0)
null
else
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
const underline_thickness = if (has_broken_underline)
null
else
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
// Similar logic to the underline above.
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
null
else
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const strikethrough_thickness = if (has_broken_strikethrough)
null
else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
// We fall back to whatever CoreText does if
// the OS/2 table doesn't specify a cap height.
const cap_height = if (os2.sCapHeight) |sCapHeight|
@as(f64, @floatFromInt(sCapHeight)) * px_per_unit
else
ct_font.getCapHeight();
// Ditto for ex height.
const ex_height = if (os2.sxHeight) |sxHeight|
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
else
ct_font.getXHeight();
// Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take
// whatever is widest.
const cell_width: f32 = cell_width: {
const cell_width: f64 = cell_width: {
// Build a comptime array of all the ASCII chars
const unichars = comptime unichars: {
const len = 127 - 32;
@ -564,89 +645,25 @@ pub const Face = struct {
max = @max(advances[i].width, max);
}
break :cell_width @floatCast(@ceil(max));
break :cell_width max;
};
// Calculate the layout metrics for height/ascent by just asking
// the font. I also tried Kitty's approach at one point which is to
// use the CoreText layout engine but this led to some glyphs being
// set incorrectly.
const layout_metrics: struct {
height: f32,
ascent: f32,
leading: f32,
} = metrics: {
const ascent = ct_font.getAscent();
const descent = ct_font.getDescent();
return font.face.Metrics.calc(.{
.cell_width = cell_width,
// Leading is the value between lines at the TOP of a line.
// Because we are rendering a fixed size terminal grid, we
// want the leading to be split equally between the top and bottom.
const leading = ct_font.getLeading();
.ascent = ascent,
.descent = descent,
.line_gap = line_gap,
// We ceil the metrics below because we don't want to cut off any
// potential used pixels. This tends to only make a one pixel
// difference but at small font sizes this can be noticeable.
break :metrics .{
.height = @floatCast(@ceil(ascent + descent + leading)),
.ascent = @floatCast(@ceil(ascent + (leading / 2))),
.leading = @floatCast(leading),
};
};
.underline_position = underline_position,
.underline_thickness = underline_thickness,
// All of these metrics are based on our layout above.
const cell_height = @ceil(layout_metrics.height);
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
.strikethrough_position = strikethrough_position,
.strikethrough_thickness = strikethrough_thickness,
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
const strikethrough_thickness = underline_thickness;
const strikethrough_position = strikethrough_position: {
// This is the height of lower case letters in our font.
const ex_height = ct_font.getXHeight();
// We want to position the strikethrough so that it's
// vertically centered on any lower case text. This is
// a fairly standard choice for strikethrough positioning.
//
// Because our `strikethrough_position` is relative to the
// top of the cell we start with the ascent metric, which
// is the distance from the top down to the baseline, then
// we subtract half of the ex height to go back up to the
// correct height that should evenly split lowercase text.
const pos = layout_metrics.ascent -
ex_height * 0.5 -
strikethrough_thickness * 0.5;
break :strikethrough_position @ceil(pos);
};
// Underline position reported is usually something like "-1" to
// represent the amount under the baseline. We add this to our real
// baseline to get the actual value from the bottom (+y is up).
// The final underline position is +y from the TOP (confusing)
// so we have to subtract from the cell height.
const underline_position = @ceil(layout_metrics.ascent -
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
// Note: is this useful?
// const units_per_em = ct_font.getUnitsPerEm();
// const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize();
const result = font.face.Metrics{
.cell_width = @intFromFloat(cell_width),
.cell_height = @intFromFloat(cell_height),
.cell_baseline = @intFromFloat(cell_baseline),
.underline_position = @intFromFloat(underline_position),
.underline_thickness = @intFromFloat(underline_thickness),
.strikethrough_position = @intFromFloat(strikethrough_position),
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
};
// std.log.warn("font size size={d}", .{ct_font.getSize()});
// std.log.warn("font metrics={}", .{result});
return result;
.cap_height = cap_height,
.ex_height = ex_height,
});
}
/// Copy the font table data for the given tag.

View File

@ -16,6 +16,7 @@ const font = @import("../main.zig");
const Glyph = font.Glyph;
const Library = font.Library;
const convert = @import("freetype_convert.zig");
const opentype = @import("../opentype.zig");
const fastmem = @import("../../fastmem.zig");
const quirks = @import("../../quirks.zig");
const config = @import("../../config.zig");
@ -85,7 +86,7 @@ pub const Face = struct {
.lib = lib.lib,
.face = face,
.hb_font = hb_font,
.metrics = calcMetrics(face, opts.metric_modifiers),
.metrics = try calcMetrics(face, opts.metric_modifiers),
.load_flags = opts.freetype_load_flags,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@ -185,7 +186,7 @@ pub const Face = struct {
/// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, opts: font.face.Options) !void {
try setSize_(self.face, opts.size);
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
}
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
@ -258,7 +259,7 @@ pub const Face = struct {
try self.face.setVarDesignCoordinates(coords);
// We need to recalculate font metrics which may have changed.
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
}
/// Returns the glyph index for the given Unicode code point. If this
@ -593,6 +594,10 @@ pub const Face = struct {
return @floatFromInt(v >> 6);
}
fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 {
return @as(opentype.sfnt.F26Dot6, @bitCast(@as(u32, @intCast(v)))).to(f64);
}
/// 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
@ -605,138 +610,136 @@ pub const Face = struct {
fn calcMetrics(
face: freetype.Face,
modifiers: ?*const font.face.Metrics.ModifierSet,
) font.face.Metrics {
) !font.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| {
// This code relies on this assumption, and it should always be
// true since we don't do any non-uniform scaling on the font ever.
assert(size_metrics.x_ppem == size_metrics.y_ppem);
// Read the 'head' table out of the font data.
const head = face.getSfntTable(.head) orelse return error.CannotGetTable;
// Read the 'post' table out of the font data.
const post = face.getSfntTable(.post) orelse return error.CannotGetTable;
// Read the 'OS/2' table out of the font data.
const os2 = face.getSfntTable(.os2) orelse return error.CannotGetTable;
// 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
if (os2.version == 0xFFFF) return error.MissingTable;
const units_per_em = head.Units_Per_EM;
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;
// Some fonts have degenerate 'post' tables where the underline
// thickness (and often position) are 0. We consider them null
// if this is the case and use our own fallbacks when we calculate.
const has_broken_underline = post.underlineThickness == 0;
// If the underline position isn't 0 then we do use it,
// even if the thickness is't properly specified.
const underline_position = if (has_broken_underline and post.underlinePosition == 0)
null
else
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
const underline_thickness = if (has_broken_underline)
null
else
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
// Similar logic to the underline above.
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
null
else
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const strikethrough_thickness = if (has_broken_strikethrough)
null
else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
// Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take
// whatever is widest.
//
// If we fail to load any visible ASCII we just use max_advance from
// the metrics provided by FreeType.
const cell_width: f64 = cell_width: {
var c: u8 = ' ';
while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) {
break :cell_width f26dot6ToFloat(face.handle.*.glyph.*.advance.x);
break :cell_width f26dot6ToF64(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);
break :cell_width f26dot6ToF64(size_metrics.max_advance);
};
// Ex height is calculated by measuring the height of the `x` glyph.
// If that fails then we just pretend it's 65% of the ascent height.
const ex_height: f32 = ex_height: {
// The OS/2 table does not include sCapHeight or sxHeight in version 1.
const has_os2_height_metrics = os2.version >= 2;
// We use the cap height specified by the font if it's
// available, otherwise we try to measure the `H` glyph.
const cap_height: ?f64 = cap_height: {
if (has_os2_height_metrics) {
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit;
}
if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) {
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {}
}
break :cap_height null;
};
// We use the ex height specified by the font if it's
// available, otherwise we try to measure the `x` glyph.
const ex_height: ?f64 = ex_height: {
if (has_os2_height_metrics) {
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit;
}
if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) {
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
} else |_| {
// Ignore the error since we just fall back to 65% of the ascent below
}
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {}
}
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
break :ex_height null;
};
// 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);
var result = font.face.Metrics.calc(.{
.cell_width = cell_width,
// The maximum height a glyph can take in the font
const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) -
f26dot6ToFloat(size_metrics.descender);
.ascent = ascent,
.descent = descent,
.line_gap = line_gap,
// 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 -= @floatFromInt(face.handle.*.glyph.*.bitmap_top);
res += @floatFromInt(face.handle.*.glyph.*.bitmap.rows);
break :underscore res;
} else |_| {
// Ignore the error since we just fall back below
}
}
.underline_position = underline_position,
.underline_thickness = underline_thickness,
break :underscore 0;
};
.strikethrough_position = strikethrough_position,
.strikethrough_thickness = strikethrough_thickness,
break :cell_height @max(
face_height,
@max(max_glyph_height, underscore_height),
);
};
.cap_height = cap_height,
.ex_height = ex_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);
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
face,
face.handle.*.underline_thickness,
));
// The underline position. This is a value from the top where the
// underline should go.
const underline_position: f32 = underline_pos: {
// From the FreeType docs:
// > `underline_position`
// > The position, in font units, of the underline line for
// > this face. It is the center of the underlining stem.
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
face.handle.*.underline_position,
@intCast(face.handle.*.size.*.metrics.y_scale),
))) / 64;
// We use the declared underline position if its available.
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
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;
};
// 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| st: {
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
const pos = @as(f32, @floatFromInt(freetype.mulFix(
os2.yStrikeoutPosition,
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
))) / 64;
break :st .{
.pos = @ceil(cell_height - cell_baseline - pos),
.thickness = thickness,
};
} else .{
// Exactly 50% of the ex height so that our strikethrough is
// centered through lowercase text. This is a common choice.
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
.thickness = underline_thickness,
};
var result = font.face.Metrics{
.cell_width = @intFromFloat(cell_width),
.cell_height = @intFromFloat(cell_height),
.cell_baseline = @intFromFloat(cell_baseline),
.underline_position = @intFromFloat(underline_position),
.underline_thickness = @intFromFloat(underline_thickness),
.strikethrough_position = @intFromFloat(strikethrough.pos),
.strikethrough_thickness = @intFromFloat(strikethrough.thickness),
};
if (modifiers) |m| result.apply(m.*);
// std.log.warn("font metrics={}", .{result});
@ -744,13 +747,6 @@ pub const Face = struct {
return result;
}
/// Convert freetype "font units" to pixels using the Y scale.
fn fontUnitsToPxY(face: freetype.Face, x: i32) f32 {
const mul = freetype.mulFix(x, @as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)));
const div = @as(f32, @floatFromInt(mul)) / 64;
return @ceil(div);
}
/// Copy the font table data for the given tag.
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag));
@ -828,6 +824,9 @@ test "color emoji" {
.underline_thickness = 0,
.strikethrough_position = 0,
.strikethrough_thickness = 0,
.overline_position = 0,
.overline_thickness = 0,
.box_thickness = 0,
},
});
try testing.expectEqual(@as(u32, 24), glyph.height);
@ -853,24 +852,42 @@ test "metrics" {
try testing.expectEqual(font.face.Metrics{
.cell_width = 8,
.cell_height = 1.8e1,
.cell_baseline = 4,
.underline_position = 18,
// The cell height is 17 px because the calculation is
//
// ascender - descender + gap
//
// which, for inconsolata is
//
// 859 - -190 + 0
//
// font units, at 1000 units per em that works out to 1.049 em,
// and 1em should be the point size * dpi scale, so 12 * (96/72)
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
// to 17.
.cell_height = 17,
.cell_baseline = 3,
.underline_position = 17,
.underline_thickness = 1,
.strikethrough_position = 10,
.strikethrough_thickness = 1,
.overline_position = 0,
.overline_thickness = 1,
.box_thickness = 1,
}, ft_font.metrics);
// Resize should change metrics
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
try testing.expectEqual(font.face.Metrics{
.cell_width = 16,
.cell_height = 35,
.cell_baseline = 7,
.underline_position = 35,
.cell_height = 34,
.cell_baseline = 6,
.underline_position = 34,
.underline_thickness = 2,
.strikethrough_position = 20,
.strikethrough_position = 19,
.strikethrough_thickness = 2,
.overline_position = 0,
.overline_thickness = 2,
.box_thickness = 2,
}, ft_font.metrics);
}

View File

@ -27,14 +27,8 @@ const Sprite = @import("../sprite.zig").Sprite;
const log = std.log.scoped(.box_font);
/// The cell width and height because the boxes are fit perfectly
/// into a cell so that they all properly connect with zero spacing.
width: u32,
height: u32,
/// Base thickness value for lines of the box. This is in pixels. If you
/// want to do any DPI scaling, it is expected to be done earlier.
thickness: u32,
/// Grid metrics for the rendering.
metrics: font.Metrics,
/// The thickness of a line.
const Thickness = enum {
@ -218,8 +212,29 @@ pub fn renderGlyph(
atlas: *font.Atlas,
cp: u32,
) !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, self.width, self.height);
var canvas = try font.sprite.Canvas.init(
alloc,
metrics.cell_width,
height,
);
defer canvas.deinit(alloc);
// Perform the actual drawing
@ -231,16 +246,20 @@ 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.
const offset_y = @as(i32, @intCast(self.height));
//
// 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));
return font.Glyph{
.width = self.width,
.height = self.height,
.width = metrics.cell_width,
.height = metrics.cell_height,
.offset_x = 0,
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatFromInt(self.width),
.advance_x = @floatFromInt(metrics.cell_width),
};
}
@ -1652,16 +1671,16 @@ fn draw_lines(
canvas: *font.sprite.Canvas,
lines: Lines,
) void {
const light_px = Thickness.light.height(self.thickness);
const heavy_px = Thickness.heavy.height(self.thickness);
const light_px = Thickness.light.height(self.metrics.box_thickness);
const heavy_px = Thickness.heavy.height(self.metrics.box_thickness);
// Top of light horizontal strokes
const h_light_top = (self.height -| light_px) / 2;
const h_light_top = (self.metrics.cell_height -| light_px) / 2;
// Bottom of light horizontal strokes
const h_light_bottom = h_light_top +| light_px;
// Top of heavy horizontal strokes
const h_heavy_top = (self.height -| heavy_px) / 2;
const h_heavy_top = (self.metrics.cell_height -| heavy_px) / 2;
// Bottom of heavy horizontal strokes
const h_heavy_bottom = h_heavy_top +| heavy_px;
@ -1671,12 +1690,12 @@ fn draw_lines(
const h_double_bottom = h_light_bottom +| light_px;
// Left of light vertical strokes
const v_light_left = (self.width -| light_px) / 2;
const v_light_left = (self.metrics.cell_width -| light_px) / 2;
// Right of light vertical strokes
const v_light_right = v_light_left +| light_px;
// Left of heavy vertical strokes
const v_heavy_left = (self.width -| heavy_px) / 2;
const v_heavy_left = (self.metrics.cell_width -| heavy_px) / 2;
// Right of heavy vertical strokes
const v_heavy_right = v_heavy_left +| heavy_px;
@ -1752,27 +1771,27 @@ fn draw_lines(
switch (lines.right) {
.none => {},
.light => self.rect(canvas, right_left, h_light_top, self.width, h_light_bottom),
.heavy => self.rect(canvas, right_left, h_heavy_top, self.width, h_heavy_bottom),
.light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom),
.heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom),
.double => {
const top_left = if (lines.up == .double) v_light_right else right_left;
const bottom_left = if (lines.down == .double) v_light_right else right_left;
self.rect(canvas, top_left, h_double_top, self.width, h_light_top);
self.rect(canvas, bottom_left, h_light_bottom, self.width, h_double_bottom);
self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top);
self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom);
},
}
switch (lines.down) {
.none => {},
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.height),
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.height),
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height),
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height),
.double => {
const left_top = if (lines.left == .double) h_light_bottom else down_top;
const right_top = if (lines.right == .double) h_light_bottom else down_top;
self.rect(canvas, v_double_left, left_top, v_light_left, self.height);
self.rect(canvas, v_light_right, right_top, v_double_right, self.height);
self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height);
self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height);
},
}
@ -1794,8 +1813,8 @@ fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
self.draw_dash_horizontal(
canvas,
3,
Thickness.light.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.light.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1803,8 +1822,8 @@ fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
self.draw_dash_horizontal(
canvas,
3,
Thickness.heavy.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.heavy.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1812,8 +1831,8 @@ fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
self.draw_dash_vertical(
canvas,
3,
Thickness.light.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.light.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1821,8 +1840,8 @@ fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
self.draw_dash_vertical(
canvas,
3,
Thickness.heavy.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.heavy.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1830,8 +1849,8 @@ fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
self.draw_dash_horizontal(
canvas,
4,
Thickness.light.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.light.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1839,8 +1858,8 @@ fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
self.draw_dash_horizontal(
canvas,
4,
Thickness.heavy.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.heavy.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1848,8 +1867,8 @@ fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
self.draw_dash_vertical(
canvas,
4,
Thickness.light.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.light.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1857,8 +1876,8 @@ fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
self.draw_dash_vertical(
canvas,
4,
Thickness.heavy.height(self.thickness),
@max(4, Thickness.light.height(self.thickness)),
Thickness.heavy.height(self.metrics.box_thickness),
@max(4, Thickness.light.height(self.metrics.box_thickness)),
);
}
@ -1866,8 +1885,8 @@ fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
self.draw_dash_horizontal(
canvas,
2,
Thickness.light.height(self.thickness),
Thickness.light.height(self.thickness),
Thickness.light.height(self.metrics.box_thickness),
Thickness.light.height(self.metrics.box_thickness),
);
}
@ -1875,8 +1894,8 @@ fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
self.draw_dash_horizontal(
canvas,
2,
Thickness.heavy.height(self.thickness),
Thickness.heavy.height(self.thickness),
Thickness.heavy.height(self.metrics.box_thickness),
Thickness.heavy.height(self.metrics.box_thickness),
);
}
@ -1884,8 +1903,8 @@ fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
self.draw_dash_vertical(
canvas,
2,
Thickness.light.height(self.thickness),
Thickness.heavy.height(self.thickness),
Thickness.light.height(self.metrics.box_thickness),
Thickness.heavy.height(self.metrics.box_thickness),
);
}
@ -1893,26 +1912,26 @@ fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
self.draw_dash_vertical(
canvas,
2,
Thickness.heavy.height(self.thickness),
Thickness.heavy.height(self.thickness),
Thickness.heavy.height(self.metrics.box_thickness),
Thickness.heavy.height(self.metrics.box_thickness),
);
}
fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void {
canvas.line(.{
.p0 = .{ .x = @floatFromInt(self.width), .y = 0 },
.p1 = .{ .x = 0, .y = @floatFromInt(self.height) },
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
.p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 },
.p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) },
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
}
fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void {
canvas.line(.{
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{
.x = @floatFromInt(self.width),
.y = @floatFromInt(self.height),
.x = @floatFromInt(self.metrics.cell_width),
.y = @floatFromInt(self.metrics.cell_height),
},
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
}
fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void {
@ -1938,21 +1957,21 @@ fn draw_block_shade(
comptime height: f64,
comptime shade: Shade,
) void {
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const w: u32 = @intFromFloat(@round(float_width * width));
const h: u32 = @intFromFloat(@round(float_height * height));
const x = switch (alignment.horizontal) {
.left => 0,
.right => self.width - w,
.center => (self.width - w) / 2,
.right => self.metrics.cell_width - w,
.center => (self.metrics.cell_width - w) / 2,
};
const y = switch (alignment.vertical) {
.top => 0,
.bottom => self.height - h,
.middle => (self.height - h) / 2,
.bottom => self.metrics.cell_height - h,
.middle => (self.metrics.cell_height - h) / 2,
};
canvas.rect(.{
@ -1970,10 +1989,10 @@ fn draw_corner_triangle_shade(
comptime shade: Shade,
) void {
const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) {
.tl => .{ 0, 0, 0, self.height, self.width, 0 },
.tr => .{ 0, 0, self.width, self.height, self.width, 0 },
.bl => .{ 0, 0, 0, self.height, self.width, self.height },
.br => .{ 0, self.height, self.width, self.height, self.width, 0 },
.tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 },
.tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
.bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height },
.br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
};
canvas.triangle(.{
@ -1984,26 +2003,26 @@ fn draw_corner_triangle_shade(
}
fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void {
self.rect(canvas, 0, 0, self.width, self.height);
self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height);
}
fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.width)) / 8)));
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 8)));
self.rect(canvas, x, 0, x + w, self.height);
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
self.rect(canvas, x, 0, x + w, self.metrics.cell_height);
}
fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void {
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const x_size: usize = 4;
const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width)));
for (0..x_size) |x| {
const x0 = (self.width * x) / x_size;
const x1 = (self.width * (x + 1)) / x_size;
const x0 = (self.metrics.cell_width * x) / x_size;
const x1 = (self.metrics.cell_width * (x + 1)) / x_size;
for (0..y_size) |y| {
const y0 = (self.height * y) / y_size;
const y1 = (self.height * (y + 1)) / y_size;
const y0 = (self.metrics.cell_height * y) / y_size;
const y1 = (self.metrics.cell_height * (y + 1)) / y_size;
if ((x + y) % 2 == parity) {
canvas.rect(.{
.x = @intCast(x0),
@ -2017,11 +2036,11 @@ fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) vo
}
fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void {
const thick_px = Thickness.light.height(self.thickness);
const line_count = self.width / (2 * thick_px);
const thick_px = Thickness.light.height(self.metrics.box_thickness);
const line_count = self.metrics.cell_width / (2 * thick_px);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
@ -2037,11 +2056,11 @@ fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) v
}
fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void {
const thick_px = Thickness.light.height(self.thickness);
const line_count = self.width / (2 * thick_px);
const thick_px = Thickness.light.height(self.metrics.box_thickness);
const line_count = self.metrics.cell_width / (2 * thick_px);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
@ -2061,13 +2080,13 @@ fn draw_corner_diagonal_lines(
canvas: *font.sprite.Canvas,
comptime corners: Quads,
) void {
const thick_px = Thickness.light.height(self.thickness);
const thick_px = Thickness.light.height(self.metrics.box_thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const center_x: f64 = @floatFromInt(self.width / 2 + self.width % 2);
const center_y: f64 = @floatFromInt(self.height / 2 + self.height % 2);
const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2);
const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2);
if (corners.tl) canvas.line(.{
.p0 = .{ .x = center_x, .y = 0 },
@ -2096,8 +2115,8 @@ fn draw_cell_diagonal(
comptime from: Alignment,
comptime to: Alignment,
) void {
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const x0: f64 = switch (from.horizontal) {
.left => 0,
@ -2134,16 +2153,16 @@ fn draw_fading_line(
comptime to: Edge,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const thick_px = thickness.height(self.metrics.box_thickness);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
// Top of horizontal strokes
const h_top = (self.height -| thick_px) / 2;
const h_top = (self.metrics.cell_height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (self.width -| thick_px) / 2;
const v_left = (self.metrics.cell_width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
@ -2163,7 +2182,7 @@ fn draw_fading_line(
switch (to) {
.top, .bottom => {
for (0..self.height) |y| {
for (0..self.metrics.cell_height) |y| {
for (v_left..v_right) |x| {
canvas.pixel(
@intCast(x),
@ -2175,7 +2194,7 @@ fn draw_fading_line(
}
},
.left, .right => {
for (0..self.width) |x| {
for (0..self.metrics.cell_width) |x| {
for (h_top..h_bottom) |y| {
canvas.pixel(
@intCast(x),
@ -2195,17 +2214,17 @@ fn draw_branch_node(
node: BranchNode,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const thick_px = thickness.height(self.metrics.box_thickness);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
// Top of horizontal strokes
const h_top = (self.height -| thick_px) / 2;
const h_top = (self.metrics.cell_height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (self.width -| thick_px) / 2;
const v_left = (self.metrics.cell_width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
@ -2240,9 +2259,9 @@ fn draw_branch_node(
if (node.up)
self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r)));
if (node.right)
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.width, h_bottom);
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom);
if (node.down)
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.height);
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height);
if (node.left)
self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom);
@ -2263,8 +2282,8 @@ fn draw_circle(
comptime position: Alignment,
comptime filled: bool,
) void {
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const x: f64 = switch (position.horizontal) {
.left => 0,
@ -2285,7 +2304,7 @@ fn draw_circle(
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
},
},
.line_width = @floatFromInt(Thickness.light.height(self.thickness)),
.line_width = @floatFromInt(Thickness.light.height(self.metrics.box_thickness)),
};
var path = z2d.Path.init(canvas.alloc);
@ -2311,7 +2330,7 @@ fn draw_line(
) !void {
canvas.line(
.{ .p0 = p0, .p1 = p1 },
@floatFromInt(thickness.height(self.thickness)),
@floatFromInt(thickness.height(self.metrics.box_thickness)),
.on,
) catch {};
}
@ -2320,8 +2339,8 @@ fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void {
canvas.rect((font.sprite.Box(u32){
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{
.x = self.width,
.y = self.height,
.x = self.metrics.cell_width,
.y = self.metrics.cell_height,
},
}).rect(), @as(font.sprite.Color, @enumFromInt(v)));
}
@ -2339,12 +2358,12 @@ fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void {
}
fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.height)) / 8)));
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8)));
const y = @min(
self.height -| h,
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.height)) / 8))),
self.metrics.cell_height -| h,
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))),
);
self.rect(canvas, 0, y, self.width, y + h);
self.rect(canvas, 0, y, self.metrics.cell_width, y + h);
}
fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void {
@ -2355,24 +2374,24 @@ fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas)
}
fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void {
const center_x = self.width / 2 + self.width % 2;
const center_y = self.height / 2 + self.height % 2;
const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2;
const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2;
if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y);
if (quads.tr) self.rect(canvas, center_x, 0, self.width, center_y);
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.height);
if (quads.br) self.rect(canvas, center_x, center_y, self.width, self.height);
if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y);
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height);
if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height);
}
fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
var w: u32 = @min(self.width / 4, self.height / 8);
var x_spacing: u32 = self.width / 4;
var y_spacing: u32 = self.height / 8;
var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8);
var x_spacing: u32 = self.metrics.cell_width / 4;
var y_spacing: u32 = self.metrics.cell_height / 8;
var x_margin: u32 = x_spacing / 2;
var y_margin: u32 = y_spacing / 2;
var x_px_left: u32 = self.width - 2 * x_margin - x_spacing - 2 * w;
var y_px_left: u32 = self.height - 2 * y_margin - 3 * y_spacing - 4 * w;
var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w;
var y_px_left: u32 = self.metrics.cell_height - 2 * y_margin - 3 * y_spacing - 4 * w;
// First, try hard to ensure the DOT width is non-zero
if (x_px_left >= 2 and y_px_left >= 4 and w == 0) {
@ -2419,8 +2438,8 @@ fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
}
assert(x_px_left <= 1 or y_px_left <= 1);
assert(2 * x_margin + 2 * w + x_spacing <= self.width);
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.height);
assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width);
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height);
const x = [2]u32{ x_margin, x_margin + w + x_spacing };
const y = y: {
@ -2479,25 +2498,25 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
const y_thirds = self.yThirds();
if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]);
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.width, y_thirds[0]);
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]);
if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]);
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.width, y_thirds[1]);
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.height);
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.width, self.height);
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.metrics.cell_width, y_thirds[1]);
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.metrics.cell_height);
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height);
}
fn xHalfs(self: Box) [2]u32 {
return .{
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 2))),
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.width)) / 2)),
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))),
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2)),
};
}
fn yThirds(self: Box) [2]u32 {
return switch (@mod(self.height, 3)) {
0 => .{ self.height / 3, 2 * self.height / 3 },
1 => .{ self.height / 3, 2 * self.height / 3 + 1 },
2 => .{ self.height / 3 + 1, 2 * self.height / 3 },
return switch (@mod(self.metrics.cell_height, 3)) {
0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 },
1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 },
2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 },
else => unreachable,
};
}
@ -2511,10 +2530,10 @@ fn draw_smooth_mosaic(
const top: f64 = 0.0;
const upper: f64 = @floatFromInt(y_thirds[0]);
const lower: f64 = @floatFromInt(y_thirds[1]);
const bottom: f64 = @floatFromInt(self.height);
const bottom: f64 = @floatFromInt(self.metrics.cell_height);
const left: f64 = 0.0;
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
const right: f64 = @floatFromInt(self.width);
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
const right: f64 = @floatFromInt(self.metrics.cell_width);
var path = z2d.Path.init(canvas.alloc);
defer path.deinit();
@ -2549,11 +2568,11 @@ fn draw_edge_triangle(
comptime edge: Edge,
) !void {
const upper: f64 = 0.0;
const middle: f64 = @round(@as(f64, @floatFromInt(self.height)) / 2);
const lower: f64 = @floatFromInt(self.height);
const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2);
const lower: f64 = @floatFromInt(self.metrics.cell_height);
const left: f64 = 0.0;
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
const right: f64 = @floatFromInt(self.width);
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
const right: f64 = @floatFromInt(self.metrics.cell_width);
var path = z2d.Path.init(canvas.alloc);
defer path.deinit();
@ -2588,12 +2607,12 @@ fn draw_arc(
comptime corner: Corner,
comptime thickness: Thickness,
) !void {
const thick_px = thickness.height(self.thickness);
const float_width: f64 = @floatFromInt(self.width);
const float_height: f64 = @floatFromInt(self.height);
const thick_px = thickness.height(self.metrics.box_thickness);
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const center_x: f64 = @as(f64, @floatFromInt((self.width -| thick_px) / 2)) + float_thick / 2;
const center_y: f64 = @as(f64, @floatFromInt((self.height -| thick_px) / 2)) + float_thick / 2;
const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
const center_y: f64 = @as(f64, @floatFromInt((self.metrics.cell_height -| thick_px) / 2)) + float_thick / 2;
const r = @min(float_width, float_height) / 2;
@ -2703,23 +2722,23 @@ fn draw_dash_horizontal(
// We need at least 1 pixel for each gap and each dash, if we don't
// have that then we can't draw our dashed line correctly so we just
// draw a solid line and return.
if (self.width < count + gap_count) {
if (self.metrics.cell_width < count + gap_count) {
self.hline_middle(canvas, .light);
return;
}
// We never want the gaps to take up more than 50% of the space,
// because if they do the dashes are too small and look wrong.
const gap_width = @min(desired_gap, self.width / (2 * count));
const gap_width = @min(desired_gap, self.metrics.cell_width / (2 * count));
const total_gap_width = gap_count * gap_width;
const total_dash_width = self.width - total_gap_width;
const total_dash_width = self.metrics.cell_width - total_gap_width;
const dash_width = total_dash_width / count;
const remaining = total_dash_width % count;
assert(dash_width * count + gap_width * gap_count + remaining == self.width);
assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width);
// Our dashes should be centered vertically.
const y: u32 = (self.height -| thick_px) / 2;
const y: u32 = (self.metrics.cell_height -| thick_px) / 2;
// We start at half a gap from the left edge, in order to center
// our dashes properly.
@ -2782,23 +2801,23 @@ fn draw_dash_vertical(
// We need at least 1 pixel for each gap and each dash, if we don't
// have that then we can't draw our dashed line correctly so we just
// draw a solid line and return.
if (self.height < count + gap_count) {
if (self.metrics.cell_height < count + gap_count) {
self.vline_middle(canvas, .light);
return;
}
// We never want the gaps to take up more than 50% of the space,
// because if they do the dashes are too small and look wrong.
const gap_height = @min(desired_gap, self.height / (2 * count));
const gap_height = @min(desired_gap, self.metrics.cell_height / (2 * count));
const total_gap_height = gap_count * gap_height;
const total_dash_height = self.height - total_gap_height;
const total_dash_height = self.metrics.cell_height - total_gap_height;
const dash_height = total_dash_height / count;
const remaining = total_dash_height % count;
assert(dash_height * count + gap_height * gap_count + remaining == self.height);
assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height);
// Our dashes should be centered horizontally.
const x: u32 = (self.width -| thick_px) / 2;
const x: u32 = (self.metrics.cell_width -| thick_px) / 2;
// We start at the top of the cell.
var y: u32 = 0;
@ -2824,32 +2843,32 @@ fn draw_dash_vertical(
}
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
self.rect(canvas, 0, 0, self.width, self.height);
self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height);
}
fn draw_cursor_hollow_rect(self: Box, canvas: *font.sprite.Canvas) void {
const thick_px = Thickness.super_light.height(self.thickness);
const thick_px = Thickness.super_light.height(self.metrics.cursor_thickness);
self.vline(canvas, 0, self.height, 0, thick_px);
self.vline(canvas, 0, self.height, self.width -| thick_px, thick_px);
self.hline(canvas, 0, self.width, 0, thick_px);
self.hline(canvas, 0, self.width, self.height -| thick_px, thick_px);
self.vline(canvas, 0, self.metrics.cell_height, 0, thick_px);
self.vline(canvas, 0, self.metrics.cell_height, self.metrics.cell_width -| thick_px, thick_px);
self.hline(canvas, 0, self.metrics.cell_width, 0, thick_px);
self.hline(canvas, 0, self.metrics.cell_width, self.metrics.cell_height -| thick_px, thick_px);
}
fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void {
const thick_px = Thickness.light.height(self.thickness);
const thick_px = Thickness.light.height(self.metrics.cursor_thickness);
self.vline(canvas, 0, self.height, 0, thick_px);
self.vline(canvas, 0, self.metrics.cell_height, 0, thick_px);
}
fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
const thick_px = thickness.height(self.thickness);
self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px);
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);
}
fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
const thick_px = thickness.height(self.thickness);
self.hline(canvas, 0, self.width, (self.height -| thick_px) / 2, thick_px);
const thick_px = thickness.height(self.metrics.box_thickness);
self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px);
}
fn vline(
@ -2861,11 +2880,11 @@ fn vline(
thickness_px: u32,
) void {
canvas.rect((font.sprite.Box(u32){ .p0 = .{
.x = @min(@max(x, 0), self.width),
.y = @min(@max(y1, 0), self.height),
.x = @min(@max(x, 0), self.metrics.cell_width),
.y = @min(@max(y1, 0), self.metrics.cell_height),
}, .p1 = .{
.x = @min(@max(x + thickness_px, 0), self.width),
.y = @min(@max(y2, 0), self.height),
.x = @min(@max(x + thickness_px, 0), self.metrics.cell_width),
.y = @min(@max(y2, 0), self.metrics.cell_height),
} }).rect(), .on);
}
@ -2878,11 +2897,11 @@ fn hline(
thickness_px: u32,
) void {
canvas.rect((font.sprite.Box(u32){ .p0 = .{
.x = @min(@max(x1, 0), self.width),
.y = @min(@max(y, 0), self.height),
.x = @min(@max(x1, 0), self.metrics.cell_width),
.y = @min(@max(y, 0), self.metrics.cell_height),
}, .p1 = .{
.x = @min(@max(x2, 0), self.width),
.y = @min(@max(y + thickness_px, 0), self.height),
.x = @min(@max(x2, 0), self.metrics.cell_width),
.y = @min(@max(y + thickness_px, 0), self.metrics.cell_height),
} }).rect(), .on);
}
@ -2895,11 +2914,11 @@ fn rect(
y2: u32,
) void {
canvas.rect((font.sprite.Box(u32){ .p0 = .{
.x = @min(@max(x1, 0), self.width),
.y = @min(@max(y1, 0), self.height),
.x = @min(@max(x1, 0), self.metrics.cell_width),
.y = @min(@max(y1, 0), self.metrics.cell_height),
}, .p1 = .{
.x = @min(@max(x2, 0), self.width),
.y = @min(@max(y2, 0), self.height),
.x = @min(@max(x2, 0), self.metrics.cell_width),
.y = @min(@max(y2, 0), self.metrics.cell_height),
} }).rect(), .on);
}
@ -2913,14 +2932,21 @@ test "all" {
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
const face: Box = .{ .width = 18, .height = 36, .thickness = 2 };
const face: Box = .{
.metrics = font.Metrics.calc(.{
.cell_width = 18.0,
.ascent = 30.0,
.descent = -6.0,
.line_gap = 0.0,
}),
};
const glyph = try face.renderGlyph(
alloc,
&atlas_grayscale,
cp,
);
try testing.expectEqual(@as(u32, face.width), glyph.width);
try testing.expectEqual(@as(u32, face.height), glyph.height);
try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width);
try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height);
}
}
@ -3037,18 +3063,28 @@ test "render all sprites" {
var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale);
defer atlas_grayscale.deinit(alloc);
// Even cell size and thickness
// Even cell size and thickness (18 x 36)
try (Box{
.width = 18,
.height = 36,
.thickness = 2,
.metrics = font.Metrics.calc(.{
.cell_width = 18.0,
.ascent = 30.0,
.descent = -6.0,
.line_gap = 0.0,
.underline_thickness = 2.0,
.strikethrough_thickness = 2.0,
}),
}).testRenderAll(alloc, &atlas_grayscale);
// Odd cell size and thickness
// Odd cell size and thickness (9 x 15)
try (Box{
.width = 9,
.height = 15,
.thickness = 1,
.metrics = font.Metrics.calc(.{
.cell_width = 9.0,
.ascent = 12.0,
.descent = -3.0,
.line_gap = 0.0,
.underline_thickness = 1.0,
.strikethrough_thickness = 1.0,
}),
}).testRenderAll(alloc, &atlas_grayscale);
const ground_truth = @embedFile("./testdata/Box.ppm");

View File

@ -24,22 +24,8 @@ const underline = @import("underline.zig");
const log = std.log.scoped(.font_sprite);
/// The cell width and height.
width: u32,
height: u32,
/// Base thickness value for lines of sprites. This is in pixels. If you
/// want to do any DPI scaling, it is expected to be done earlier.
thickness: u32 = 1,
/// The position of the underline.
underline_position: u32 = 0,
/// The position of the strikethrough.
// NOTE(mitchellh): We don't use a dedicated strikethrough thickness
// setting yet but fonts can in theory set this. If this becomes an
// issue in practice we can add it here.
strikethrough_position: u32 = 0,
/// Grid metrics for rendering sprites.
metrics: font.Metrics,
/// Returns true if the codepoint exists in our sprite font.
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
@ -65,10 +51,12 @@ pub fn renderGlyph(
}
}
const metrics = opts.grid_metrics orelse self.metrics;
// We adjust our sprite width based on the cell width.
const width = switch (opts.cell_width orelse 1) {
0, 1 => self.width,
else => |width| self.width * width,
0, 1 => metrics.cell_width,
else => |width| metrics.cell_width * width,
};
// It should be impossible for this to be null and we assert that
@ -86,58 +74,16 @@ pub fn renderGlyph(
// Safe to ".?" because of the above assertion.
return switch (kind) {
.box => box: {
const thickness = switch (cp) {
@intFromEnum(Sprite.cursor_rect),
@intFromEnum(Sprite.cursor_hollow_rect),
@intFromEnum(Sprite.cursor_bar),
=> if (opts.grid_metrics) |m| m.cursor_thickness else self.thickness,
else => self.thickness,
};
const f: Box, const y_offset: u32 = face: {
// Expected, usual values.
var f: Box = .{
.width = width,
.height = self.height,
.thickness = thickness,
};
// If the codepoint is unadjusted then we want to adjust
// (heh) the width/height to the proper size and also record
// an offset to apply to our final glyph so it renders in the
// correct place because renderGlyph assumes full size.
var y_offset: u32 = 0;
if (Box.unadjustedCodepoint(cp)) unadjust: {
const metrics = opts.grid_metrics orelse break :unadjust;
const height = metrics.original_cell_height orelse break :unadjust;
// If our height shrunk, then we use the original adjusted
// height because we don't want to overflow the cell.
if (height >= self.height) break :unadjust;
// The offset is divided by two because it is vertically
// centered.
y_offset = (self.height - height) / 2;
f.height = height;
}
break :face .{ f, y_offset };
};
var g = try f.renderGlyph(alloc, atlas, cp);
g.offset_y += @intCast(y_offset);
break :box g;
},
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
.underline => try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
self.height,
self.underline_position,
self.thickness,
metrics.cell_height,
metrics.underline_position,
metrics.underline_thickness,
),
.strikethrough => try underline.renderGlyph(
@ -145,26 +91,34 @@ pub fn renderGlyph(
atlas,
@enumFromInt(cp),
width,
self.height,
self.strikethrough_position,
self.thickness,
metrics.cell_height,
metrics.strikethrough_position,
metrics.strikethrough_thickness,
),
.overline => try underline.renderGlyph(
.overline => overline: {
var g = try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
self.height,
metrics.cell_height,
0,
self.thickness,
),
metrics.overline_thickness,
);
// We have to manually subtract the overline position
// on the rendered glyph since it can be negative.
g.offset_y -= metrics.overline_position;
break :overline g;
},
.powerline => powerline: {
const f: Powerline = .{
.width = width,
.height = self.height,
.thickness = self.thickness,
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.box_thickness,
};
break :powerline try f.renderGlyph(alloc, atlas, cp);