diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index 8bbc75616..e3bcd5292 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -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... }; } diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index f3be843c5..326ca0186 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -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); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 8af385b84..f907b59ad 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -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. diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index a1eb50bdd..17a03d497 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -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); } } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 363dbacd8..6a77ee159 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -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. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 683f80cc8..d7fb2e4a3 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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| { - 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 - } - } + // 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); - break :cell_width f26dot6ToFloat(size_metrics.max_advance); - }; + // Read the 'head' table out of the font data. + const head = face.getSfntTable(.head) orelse return error.CannotGetTable; - // 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: { - 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 - } - } + // Read the 'post' table out of the font data. + const post = face.getSfntTable(.post) orelse return error.CannotGetTable; - break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65; - }; + // Read the 'OS/2' table out of the font data. + const os2 = face.getSfntTable(.os2) orelse return error.CannotGetTable; - // 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); + // 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; - // The maximum height a glyph can take in the font - const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) - - f26dot6ToFloat(size_metrics.descender); + 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)); - // The height of the underscore character - const underscore_height = underscore: { - if (face.getCharIndex('_')) |glyph_index| { + 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 })) { - var res: f32 = f26dot6ToFloat(size_metrics.ascender); - res -= @floatFromInt(face.handle.*.glyph.*.bitmap_top); - res += @floatFromInt(face.handle.*.glyph.*.bitmap.rows); - break :underscore res; + break :cell_width f26dot6ToF64(face.handle.*.glyph.*.advance.x); } else |_| { - // Ignore the error since we just fall back below + // Ignore the error since we just fall back to max_advance below } } + } - break :underscore 0; - }; - - break :cell_height @max( - face_height, - @max(max_glyph_height, underscore_height), - ); + break :cell_width f26dot6ToF64(size_metrics.max_advance); }; - // 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 OS/2 table does not include sCapHeight or sxHeight in version 1. + const has_os2_height_metrics = os2.version >= 2; - const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY( - face, - face.handle.*.underline_thickness, - )); + // 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 |_| {} + } - // 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; + break :cap_height null; }; - // 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)); + // 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 f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + } else |_| {} + } - 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, + break :ex_height null; }; - 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), - }; + var result = font.face.Metrics.calc(.{ + .cell_width = cell_width, + + .ascent = ascent, + .descent = descent, + .line_gap = line_gap, + + .underline_position = underline_position, + .underline_thickness = underline_thickness, + + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, + + .cap_height = cap_height, + .ex_height = ex_height, + }); + 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); } diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index 382aa4206..03fa8fb1e 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -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"); diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index e1cd12f00..b8c89c74e 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -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( - alloc, - atlas, - @enumFromInt(cp), - width, - self.height, - 0, - self.thickness, - ), + .overline => overline: { + var g = try underline.renderGlyph( + alloc, + atlas, + @enumFromInt(cp), + width, + metrics.cell_height, + 0, + 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);