mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Fallback Font Size Adjustment (#7840)
This PR changes `font.Collection` to automagically adjust the sizes of added fonts so that their metrics (specifically their ex height, or their ideograph character width if they have one) match the primary font. This is like [`font-size-adjust`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust) from CSS. This is a big win for users who use mixed writing systems and rely heavily on fallback fonts. For example, in #7774 it's pointed out that CJK characters are not very well harmonized with existing Latin glyphs, well: |Before (`main`)|After (this PR)| |-|-| |<img width="326" alt="image" src="https://github.com/user-attachments/assets/c11d372d-ec69-426d-b008-1f56a7430f23" />|<img width="326" alt="image" src="https://github.com/user-attachments/assets/efcb56ea-0572-481a-b632-a0b5cd170fa9" />| This also improves our handling of the horizontal alignment of fallback glyphs. It's not an ideal solution; it only works for glyphs narrower than the cell width because it messes with ligatures if we include glyphs wider than the cell width; and most things would look better if the center were proportionally remapped based on the ratio from the glyph advance to the cell width, but that messes with glyphs designed to align vertically so it can't be done, instead the original advance width is centered in the cell width.
This commit is contained in:
@ -69,9 +69,13 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
|
|||||||
if (self.load_options) |*v| v.deinit(alloc);
|
if (self.load_options) |*v| v.deinit(alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const AddError = Allocator.Error || error{
|
pub const AddError =
|
||||||
|
Allocator.Error ||
|
||||||
|
AdjustSizeError ||
|
||||||
|
error{
|
||||||
CollectionFull,
|
CollectionFull,
|
||||||
DeferredLoadingUnavailable,
|
DeferredLoadingUnavailable,
|
||||||
|
SetSizeFailed,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Add a face to the collection for the given style. This face will be added
|
/// Add a face to the collection for the given style. This face will be added
|
||||||
@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{
|
|||||||
/// If no error is encountered then the collection takes ownership of the face,
|
/// If no error is encountered then the collection takes ownership of the face,
|
||||||
/// in which case face will be deallocated when the collection is deallocated.
|
/// in which case face will be deallocated when the collection is deallocated.
|
||||||
///
|
///
|
||||||
/// If a loaded face is added to the collection, it should be the same
|
/// If a loaded face is added to the collection, its size will be changed to
|
||||||
/// size as all the other faces in the collection. This function will not
|
/// match the size specified in load_options, adjusted for harmonization with
|
||||||
/// verify or modify the size until the size of the entire collection is
|
/// the primary face.
|
||||||
/// changed.
|
|
||||||
pub fn add(
|
pub fn add(
|
||||||
self: *Collection,
|
self: *Collection,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
@ -103,9 +106,107 @@ pub fn add(
|
|||||||
return error.DeferredLoadingUnavailable;
|
return error.DeferredLoadingUnavailable;
|
||||||
|
|
||||||
try list.append(alloc, face);
|
try list.append(alloc, face);
|
||||||
|
|
||||||
|
var owned: *Entry = list.at(idx);
|
||||||
|
|
||||||
|
// If the face is already loaded, apply font size adjustment
|
||||||
|
// now, otherwise we'll apply it whenever we do load it.
|
||||||
|
if (owned.getLoaded()) |loaded| {
|
||||||
|
if (try self.adjustedSize(loaded)) |opts| {
|
||||||
|
loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return .{ .style = style, .idx = @intCast(idx) };
|
return .{ .style = style, .idx = @intCast(idx) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const AdjustSizeError = font.Face.GetMetricsError;
|
||||||
|
|
||||||
|
// Calculate a size for the provided face that will match it with the primary
|
||||||
|
// font, metrically, to improve consistency with fallback fonts. Right now we
|
||||||
|
// match the font based on the ex height, or the ideograph width if the font
|
||||||
|
// has ideographs in it.
|
||||||
|
//
|
||||||
|
// This returns null if load options is null or if self.load_options is null.
|
||||||
|
//
|
||||||
|
// This is very much like the `font-size-adjust` CSS property in how it works.
|
||||||
|
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
|
||||||
|
//
|
||||||
|
// TODO: In the future, provide config options that allow the user to select
|
||||||
|
// which metric should be matched for fallback fonts, instead of hard
|
||||||
|
// coding it as ex height.
|
||||||
|
pub fn adjustedSize(
|
||||||
|
self: *Collection,
|
||||||
|
face: *Face,
|
||||||
|
) AdjustSizeError!?LoadOptions {
|
||||||
|
const load_options = self.load_options orelse return null;
|
||||||
|
|
||||||
|
// We silently do nothing if we can't get the primary
|
||||||
|
// face, because this might be the primary face itself.
|
||||||
|
const primary_face = self.getFace(.{ .idx = 0 }) catch return null;
|
||||||
|
|
||||||
|
// We do nothing if the primary face and this face are the same.
|
||||||
|
if (@intFromPtr(primary_face) == @intFromPtr(face)) return null;
|
||||||
|
|
||||||
|
const primary_metrics = try primary_face.getMetrics();
|
||||||
|
const face_metrics = try face.getMetrics();
|
||||||
|
|
||||||
|
// We use the ex height to match our font sizes, so that the height of
|
||||||
|
// lower-case letters matches between all fonts in the fallback chain.
|
||||||
|
//
|
||||||
|
// We estimate ex height as 0.75 * cap height if it's not specifically
|
||||||
|
// provided, and we estimate cap height as 0.75 * ascent in the same case.
|
||||||
|
//
|
||||||
|
// If the fallback font has an ic_width we prefer that, for normalization
|
||||||
|
// of CJK font sizes when mixed with latin fonts.
|
||||||
|
//
|
||||||
|
// We estimate the ic_width as twice the cell width if it isn't provided.
|
||||||
|
var primary_cap = primary_metrics.cap_height orelse 0.0;
|
||||||
|
if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75;
|
||||||
|
|
||||||
|
var primary_ex = primary_metrics.ex_height orelse 0.0;
|
||||||
|
if (primary_ex <= 0) primary_ex = primary_cap * 0.75;
|
||||||
|
|
||||||
|
var primary_ic = primary_metrics.ic_width orelse 0.0;
|
||||||
|
if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2;
|
||||||
|
|
||||||
|
var face_cap = face_metrics.cap_height orelse 0.0;
|
||||||
|
if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75;
|
||||||
|
|
||||||
|
var face_ex = face_metrics.ex_height orelse 0.0;
|
||||||
|
if (face_ex <= 0) face_ex = face_cap * 0.75;
|
||||||
|
|
||||||
|
var face_ic = face_metrics.ic_width orelse 0.0;
|
||||||
|
if (face_ic <= 0) face_ic = face_metrics.cell_width * 2;
|
||||||
|
|
||||||
|
// If the line height of the scaled font would be larger than
|
||||||
|
// the line height of the primary font, we don't want that, so
|
||||||
|
// we take the minimum between matching the ic/ex and the line
|
||||||
|
// height.
|
||||||
|
//
|
||||||
|
// NOTE: We actually allow the line height to be up to 1.2
|
||||||
|
// times the primary line height because empirically
|
||||||
|
// this is usually fine and is better for CJK.
|
||||||
|
//
|
||||||
|
// TODO: We should probably provide a config option that lets
|
||||||
|
// the user pick what metric to use for size adjustment.
|
||||||
|
const scale = @min(
|
||||||
|
1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(),
|
||||||
|
if (face_metrics.ic_width != null)
|
||||||
|
primary_ic / face_ic
|
||||||
|
else
|
||||||
|
primary_ex / face_ex,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make a copy of our load options, set the size to the size of
|
||||||
|
// the provided face, and then multiply that by our scaling factor.
|
||||||
|
var opts = load_options;
|
||||||
|
opts.size = face.size;
|
||||||
|
opts.size.points *= @as(f32, @floatCast(scale));
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the Face represented by a given Index. The returned pointer
|
/// Return the Face represented by a given Index. The returned pointer
|
||||||
/// is only valid as long as this collection is not modified.
|
/// is only valid as long as this collection is not modified.
|
||||||
///
|
///
|
||||||
@ -129,21 +230,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
|
|||||||
break :item item;
|
break :item item;
|
||||||
};
|
};
|
||||||
|
|
||||||
return try self.getFaceFromEntry(item);
|
const face = try self.getFaceFromEntry(
|
||||||
|
item,
|
||||||
|
// We only want to adjust the size if this isn't the primary face.
|
||||||
|
index.style != .regular or index.idx > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return face;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the face from an entry.
|
/// Get the face from an entry.
|
||||||
///
|
///
|
||||||
/// This entry must not be an alias.
|
/// This entry must not be an alias.
|
||||||
fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face {
|
fn getFaceFromEntry(
|
||||||
|
self: *Collection,
|
||||||
|
entry: *Entry,
|
||||||
|
/// Whether to adjust the font size to match the primary face after loading.
|
||||||
|
adjust: bool,
|
||||||
|
) !*Face {
|
||||||
assert(entry.* != .alias);
|
assert(entry.* != .alias);
|
||||||
|
|
||||||
return switch (entry.*) {
|
return switch (entry.*) {
|
||||||
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
|
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
|
||||||
const opts = self.load_options orelse
|
const opts = self.load_options orelse
|
||||||
return error.DeferredLoadingUnavailable;
|
return error.DeferredLoadingUnavailable;
|
||||||
const face = try d.load(opts.library, opts.faceOptions());
|
var face = try d.load(opts.library, opts.faceOptions());
|
||||||
d.deinit();
|
d.deinit();
|
||||||
|
|
||||||
|
// If we need to adjust the size, do so.
|
||||||
|
if (adjust) if (try self.adjustedSize(&face)) |new_opts| {
|
||||||
|
try face.setSize(new_opts.faceOptions());
|
||||||
|
};
|
||||||
|
|
||||||
entry.* = switch (tag) {
|
entry.* = switch (tag) {
|
||||||
.deferred => .{ .loaded = face },
|
.deferred => .{ .loaded = face },
|
||||||
.fallback_deferred => .{ .fallback_loaded = face },
|
.fallback_deferred => .{ .fallback_loaded = face },
|
||||||
@ -247,7 +365,7 @@ pub fn completeStyles(
|
|||||||
while (it.next()) |entry| {
|
while (it.next()) |entry| {
|
||||||
// Load our face. If we fail to load it, we just skip it and
|
// Load our face. If we fail to load it, we just skip it and
|
||||||
// continue on to try the next one.
|
// continue on to try the next one.
|
||||||
const face = self.getFaceFromEntry(entry) catch |err| {
|
const face = self.getFaceFromEntry(entry, false) catch |err| {
|
||||||
log.warn("error loading regular entry={d} err={}", .{
|
log.warn("error loading regular entry={d} err={}", .{
|
||||||
it.index - 1,
|
it.index - 1,
|
||||||
err,
|
err,
|
||||||
@ -371,7 +489,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
|
|||||||
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
||||||
|
|
||||||
// Try to bold it.
|
// Try to bold it.
|
||||||
const regular = try self.getFaceFromEntry(entry);
|
const regular = try self.getFaceFromEntry(entry, false);
|
||||||
const face = try regular.syntheticBold(opts.faceOptions());
|
const face = try regular.syntheticBold(opts.faceOptions());
|
||||||
|
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
@ -391,7 +509,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
|
|||||||
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
||||||
|
|
||||||
// Try to italicize it.
|
// Try to italicize it.
|
||||||
const regular = try self.getFaceFromEntry(entry);
|
const regular = try self.getFaceFromEntry(entry, false);
|
||||||
const face = try regular.syntheticItalic(opts.faceOptions());
|
const face = try regular.syntheticItalic(opts.faceOptions());
|
||||||
|
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
@ -420,9 +538,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
|
|||||||
while (it.next()) |array| {
|
while (it.next()) |array| {
|
||||||
var entry_it = array.value.iterator(0);
|
var entry_it = array.value.iterator(0);
|
||||||
while (entry_it.next()) |entry| switch (entry.*) {
|
while (entry_it.next()) |entry| switch (entry.*) {
|
||||||
.loaded, .fallback_loaded => |*f| try f.setSize(
|
.loaded,
|
||||||
opts.faceOptions(),
|
.fallback_loaded,
|
||||||
),
|
=> |*f| {
|
||||||
|
const new_opts = try self.adjustedSize(f) orelse opts.*;
|
||||||
|
try f.setSize(new_opts.faceOptions());
|
||||||
|
},
|
||||||
|
|
||||||
// Deferred aren't loaded so we don't need to set their size.
|
// Deferred aren't loaded so we don't need to set their size.
|
||||||
// The size for when they're loaded is set since `opts` changed.
|
// The size for when they're loaded is set since `opts` changed.
|
||||||
@ -549,6 +670,16 @@ pub const Entry = union(enum) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this face is loaded, or is an alias to a loaded face,
|
||||||
|
/// then this returns the `Face`, otherwise returns null.
|
||||||
|
pub fn getLoaded(self: *Entry) ?*Face {
|
||||||
|
return switch (self.*) {
|
||||||
|
.deferred, .fallback_deferred => null,
|
||||||
|
.loaded, .fallback_loaded => |*face| face,
|
||||||
|
.alias => |v| v.getLoaded(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// True if the entry is deferred.
|
/// True if the entry is deferred.
|
||||||
fn isDeferred(self: Entry) bool {
|
fn isDeferred(self: Entry) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@ -906,12 +1037,13 @@ test "metrics" {
|
|||||||
|
|
||||||
var c = init();
|
var c = init();
|
||||||
defer c.deinit(alloc);
|
defer c.deinit(alloc);
|
||||||
c.load_options = .{ .library = lib };
|
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
|
||||||
|
c.load_options = .{ .library = lib, .size = size };
|
||||||
|
|
||||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||||
lib,
|
lib,
|
||||||
testFont,
|
testFont,
|
||||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
.{ .size = size },
|
||||||
) });
|
) });
|
||||||
|
|
||||||
try c.updateMetrics();
|
try c.updateMetrics();
|
||||||
@ -958,3 +1090,62 @@ test "metrics" {
|
|||||||
.cursor_height = 34,
|
.cursor_height = 34,
|
||||||
}, c.metrics);
|
}, c.metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
|
||||||
|
test "adjusted sizes" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const testFont = font.embedded.inconsolata;
|
||||||
|
const fallback = font.embedded.monaspace_neon;
|
||||||
|
|
||||||
|
var lib = try Library.init(alloc);
|
||||||
|
defer lib.deinit();
|
||||||
|
|
||||||
|
var c = init();
|
||||||
|
defer c.deinit(alloc);
|
||||||
|
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
|
||||||
|
c.load_options = .{ .library = lib, .size = size };
|
||||||
|
|
||||||
|
// Add our primary face.
|
||||||
|
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||||
|
lib,
|
||||||
|
testFont,
|
||||||
|
.{ .size = size },
|
||||||
|
) });
|
||||||
|
|
||||||
|
try c.updateMetrics();
|
||||||
|
|
||||||
|
// Add the fallback face.
|
||||||
|
const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||||
|
lib,
|
||||||
|
fallback,
|
||||||
|
.{ .size = size },
|
||||||
|
) });
|
||||||
|
|
||||||
|
// The ex heights should match.
|
||||||
|
{
|
||||||
|
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||||
|
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
||||||
|
|
||||||
|
try std.testing.expectApproxEqAbs(
|
||||||
|
primary_metrics.ex_height.?,
|
||||||
|
fallback_metrics.ex_height.?,
|
||||||
|
// We accept anything within half a pixel.
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize should keep that relationship.
|
||||||
|
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
|
||||||
|
{
|
||||||
|
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||||
|
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
||||||
|
|
||||||
|
try std.testing.expectApproxEqAbs(
|
||||||
|
primary_metrics.ex_height.?,
|
||||||
|
fallback_metrics.ex_height.?,
|
||||||
|
// We accept anything within half a pixel.
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,9 +17,3 @@ offset_y: i32,
|
|||||||
/// be normalized to be between 0 and 1 prior to use in shaders.
|
/// be normalized to be between 0 and 1 prior to use in shaders.
|
||||||
atlas_x: u32,
|
atlas_x: u32,
|
||||||
atlas_y: u32,
|
atlas_y: u32,
|
||||||
|
|
||||||
/// horizontal position to increase drawing position for strings
|
|
||||||
advance_x: f32,
|
|
||||||
|
|
||||||
/// Whether we drew this glyph ourselves with the sprite font.
|
|
||||||
sprite: bool = false,
|
|
||||||
|
@ -107,6 +107,19 @@ pub const FaceMetrics = struct {
|
|||||||
/// a provided ex height metric or measured from the height of the
|
/// a provided ex height metric or measured from the height of the
|
||||||
/// lowercase x glyph.
|
/// lowercase x glyph.
|
||||||
ex_height: ?f64 = null,
|
ex_height: ?f64 = null,
|
||||||
|
|
||||||
|
/// The width of the character "水" (CJK water ideograph, U+6C34),
|
||||||
|
/// if present. This is used for font size adjustment, to normalize
|
||||||
|
/// the width of CJK fonts mixed with latin fonts.
|
||||||
|
///
|
||||||
|
/// NOTE: IC = Ideograph Character
|
||||||
|
ic_width: ?f64 = null,
|
||||||
|
|
||||||
|
/// Convenience function for getting the line height
|
||||||
|
/// (ascent - descent + line_gap).
|
||||||
|
pub inline fn lineHeight(self: FaceMetrics) f64 {
|
||||||
|
return self.ascent - self.descent + self.line_gap;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Calculate our metrics based on values extracted from a font.
|
/// Calculate our metrics based on values extracted from a font.
|
||||||
|
@ -31,6 +31,9 @@ pub const Face = struct {
|
|||||||
/// tables).
|
/// tables).
|
||||||
color: ?ColorState = null,
|
color: ?ColorState = null,
|
||||||
|
|
||||||
|
/// The current size this font is set to.
|
||||||
|
size: font.face.DesiredSize,
|
||||||
|
|
||||||
/// True if our build is using Harfbuzz. If we're not, we can avoid
|
/// True if our build is using Harfbuzz. If we're not, we can avoid
|
||||||
/// some Harfbuzz-specific code paths.
|
/// some Harfbuzz-specific code paths.
|
||||||
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
|
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
|
||||||
@ -106,6 +109,7 @@ pub const Face = struct {
|
|||||||
.font = ct_font,
|
.font = ct_font,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.color = color,
|
.color = color,
|
||||||
|
.size = opts.size,
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||||
|
|
||||||
@ -333,7 +337,6 @@ pub const Face = struct {
|
|||||||
.offset_y = 0,
|
.offset_y = 0,
|
||||||
.atlas_x = 0,
|
.atlas_x = 0,
|
||||||
.atlas_y = 0,
|
.atlas_y = 0,
|
||||||
.advance_x = 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const metrics = opts.grid_metrics;
|
const metrics = opts.grid_metrics;
|
||||||
@ -482,25 +485,43 @@ pub const Face = struct {
|
|||||||
// This should be the distance from the left of
|
// This should be the distance from the left of
|
||||||
// the cell to the left of the glyph's bounding box.
|
// the cell to the left of the glyph's bounding box.
|
||||||
const offset_x: i32 = offset_x: {
|
const offset_x: i32 = offset_x: {
|
||||||
var result: i32 = @intFromFloat(@round(x));
|
// If the glyph's advance is narrower than the cell width then we
|
||||||
|
// center the advance of the glyph within the cell width. At first
|
||||||
// If our cell was resized then we adjust our glyph's
|
// I implemented this to proportionally scale the center position
|
||||||
// position relative to the new center. This keeps glyphs
|
// of the glyph but that messes up glyphs that are meant to align
|
||||||
// centered in the cell whether it was made wider or narrower.
|
// vertically with others, so this is a compromise.
|
||||||
if (metrics.original_cell_width) |original_width| {
|
//
|
||||||
const before: i32 = @intCast(original_width);
|
// This makes it so that when the `adjust-cell-width` config is
|
||||||
const after: i32 = @intCast(metrics.cell_width);
|
// used, or when a fallback font with a different advance width
|
||||||
// Increase the offset by half of the difference
|
// is used, we don't get weirdly aligned glyphs.
|
||||||
// between the widths to keep things centered.
|
//
|
||||||
result += @divTrunc(after - before, 2);
|
// We don't do this if the constraint has a horizontal alignment,
|
||||||
}
|
// since in that case the position was already calculated with the
|
||||||
|
// new cell width in mind.
|
||||||
break :offset_x result;
|
if (opts.constraint.align_horizontal == .none) {
|
||||||
};
|
|
||||||
|
|
||||||
// Get our advance
|
|
||||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||||
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
||||||
|
const advance = advances[0].width;
|
||||||
|
const new_advance =
|
||||||
|
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
|
||||||
|
// If the original advance is greater than the cell width then
|
||||||
|
// it's possible that this is a ligature or other glyph that is
|
||||||
|
// intended to overflow the cell to one side or the other, and
|
||||||
|
// adjusting the bearings could mess that up, so we just leave
|
||||||
|
// it alone if that's the case.
|
||||||
|
//
|
||||||
|
// We also don't want to do anything if the advance is zero or
|
||||||
|
// less, since this is used for stuff like combining characters.
|
||||||
|
if (advance > new_advance or advance <= 0.0) {
|
||||||
|
break :offset_x @intFromFloat(@round(x));
|
||||||
|
}
|
||||||
|
break :offset_x @intFromFloat(
|
||||||
|
@round(x + (new_advance - advance) / 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
break :offset_x @intFromFloat(@round(x));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.width = px_width,
|
.width = px_width,
|
||||||
@ -509,7 +530,6 @@ pub const Face = struct {
|
|||||||
.offset_y = offset_y,
|
.offset_y = offset_y,
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
.atlas_y = region.y,
|
.atlas_y = region.y,
|
||||||
.advance_x = @floatCast(advances[0].width),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -741,6 +761,20 @@ pub const Face = struct {
|
|||||||
break :cell_width max;
|
break :cell_width max;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||||
|
const ic_width: ?f64 = ic_width: {
|
||||||
|
const glyph = self.glyphIndex('水') orelse break :ic_width null;
|
||||||
|
|
||||||
|
var advances: [1]macos.graphics.Size = undefined;
|
||||||
|
_ = ct_font.getAdvancesForGlyphs(
|
||||||
|
.horizontal,
|
||||||
|
&.{@intCast(glyph)},
|
||||||
|
&advances,
|
||||||
|
);
|
||||||
|
|
||||||
|
break :ic_width advances[0].width;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.cell_width = cell_width,
|
.cell_width = cell_width,
|
||||||
.ascent = ascent,
|
.ascent = ascent,
|
||||||
@ -752,6 +786,7 @@ pub const Face = struct {
|
|||||||
.strikethrough_thickness = strikethrough_thickness,
|
.strikethrough_thickness = strikethrough_thickness,
|
||||||
.cap_height = cap_height,
|
.cap_height = cap_height,
|
||||||
.ex_height = ex_height,
|
.ex_height = ex_height,
|
||||||
|
.ic_width = ic_width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,9 @@ pub const Face = struct {
|
|||||||
bold: bool = false,
|
bold: bool = false,
|
||||||
} = .{},
|
} = .{},
|
||||||
|
|
||||||
|
/// The current size this font is set to.
|
||||||
|
size: font.face.DesiredSize,
|
||||||
|
|
||||||
/// Initialize a new font face with the given source in-memory.
|
/// Initialize a new font face with the given source in-memory.
|
||||||
pub fn initFile(
|
pub fn initFile(
|
||||||
lib: Library,
|
lib: Library,
|
||||||
@ -107,6 +110,7 @@ pub const Face = struct {
|
|||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.ft_mutex = ft_mutex,
|
.ft_mutex = ft_mutex,
|
||||||
.load_flags = opts.freetype_load_flags,
|
.load_flags = opts.freetype_load_flags,
|
||||||
|
.size = opts.size,
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||||
|
|
||||||
@ -203,6 +207,7 @@ pub const Face = struct {
|
|||||||
/// for clearing any glyph caches, font atlas data, etc.
|
/// for clearing any glyph caches, font atlas data, etc.
|
||||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||||
try setSize_(self.face, opts.size);
|
try setSize_(self.face, opts.size);
|
||||||
|
self.size = opts.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
||||||
@ -373,7 +378,6 @@ pub const Face = struct {
|
|||||||
.offset_y = 0,
|
.offset_y = 0,
|
||||||
.atlas_x = 0,
|
.atlas_x = 0,
|
||||||
.atlas_y = 0,
|
.atlas_y = 0,
|
||||||
.advance_x = 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// For synthetic bold, we embolden the glyph.
|
// For synthetic bold, we embolden the glyph.
|
||||||
@ -639,20 +643,40 @@ pub const Face = struct {
|
|||||||
// This should be the distance from the left of
|
// This should be the distance from the left of
|
||||||
// the cell to the left of the glyph's bounding box.
|
// the cell to the left of the glyph's bounding box.
|
||||||
const offset_x: i32 = offset_x: {
|
const offset_x: i32 = offset_x: {
|
||||||
var result: i32 = @intFromFloat(@floor(x));
|
// If the glyph's advance is narrower than the cell width then we
|
||||||
|
// center the advance of the glyph within the cell width. At first
|
||||||
// If our cell was resized then we adjust our glyph's
|
// I implemented this to proportionally scale the center position
|
||||||
// position relative to the new center. This keeps glyphs
|
// of the glyph but that messes up glyphs that are meant to align
|
||||||
// centered in the cell whether it was made wider or narrower.
|
// vertically with others, so this is a compromise.
|
||||||
if (metrics.original_cell_width) |original_width| {
|
//
|
||||||
const before: i32 = @intCast(original_width);
|
// This makes it so that when the `adjust-cell-width` config is
|
||||||
const after: i32 = @intCast(metrics.cell_width);
|
// used, or when a fallback font with a different advance width
|
||||||
// Increase the offset by half of the difference
|
// is used, we don't get weirdly aligned glyphs.
|
||||||
// between the widths to keep things centered.
|
//
|
||||||
result += @divTrunc(after - before, 2);
|
// We don't do this if the constraint has a horizontal alignment,
|
||||||
|
// since in that case the position was already calculated with the
|
||||||
|
// new cell width in mind.
|
||||||
|
if (opts.constraint.align_horizontal == .none) {
|
||||||
|
const advance = f26dot6ToFloat(glyph.*.advance.x);
|
||||||
|
const new_advance =
|
||||||
|
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
|
||||||
|
// If the original advance is greater than the cell width then
|
||||||
|
// it's possible that this is a ligature or other glyph that is
|
||||||
|
// intended to overflow the cell to one side or the other, and
|
||||||
|
// adjusting the bearings could mess that up, so we just leave
|
||||||
|
// it alone if that's the case.
|
||||||
|
//
|
||||||
|
// We also don't want to do anything if the advance is zero or
|
||||||
|
// less, since this is used for stuff like combining characters.
|
||||||
|
if (advance > new_advance or advance <= 0.0) {
|
||||||
|
break :offset_x @intFromFloat(@floor(x));
|
||||||
|
}
|
||||||
|
break :offset_x @intFromFloat(
|
||||||
|
@floor(x + (new_advance - advance) / 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
break :offset_x @intFromFloat(@floor(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
break :offset_x result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Glyph{
|
return Glyph{
|
||||||
@ -662,7 +686,6 @@ pub const Face = struct {
|
|||||||
.offset_y = offset_y,
|
.offset_y = offset_y,
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
.atlas_y = region.y,
|
.atlas_y = region.y,
|
||||||
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -833,7 +856,7 @@ pub const Face = struct {
|
|||||||
while (c < 127) : (c += 1) {
|
while (c < 127) : (c += 1) {
|
||||||
if (face.getCharIndex(c)) |glyph_index| {
|
if (face.getCharIndex(c)) |glyph_index| {
|
||||||
if (face.loadGlyph(glyph_index, .{
|
if (face.loadGlyph(glyph_index, .{
|
||||||
.render = true,
|
.render = false,
|
||||||
.no_svg = true,
|
.no_svg = true,
|
||||||
})) {
|
})) {
|
||||||
max = @max(
|
max = @max(
|
||||||
@ -871,7 +894,7 @@ pub const Face = struct {
|
|||||||
defer self.ft_mutex.unlock();
|
defer self.ft_mutex.unlock();
|
||||||
if (face.getCharIndex('H')) |glyph_index| {
|
if (face.getCharIndex('H')) |glyph_index| {
|
||||||
if (face.loadGlyph(glyph_index, .{
|
if (face.loadGlyph(glyph_index, .{
|
||||||
.render = true,
|
.render = false,
|
||||||
.no_svg = true,
|
.no_svg = true,
|
||||||
})) {
|
})) {
|
||||||
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||||
@ -884,7 +907,7 @@ pub const Face = struct {
|
|||||||
defer self.ft_mutex.unlock();
|
defer self.ft_mutex.unlock();
|
||||||
if (face.getCharIndex('x')) |glyph_index| {
|
if (face.getCharIndex('x')) |glyph_index| {
|
||||||
if (face.loadGlyph(glyph_index, .{
|
if (face.loadGlyph(glyph_index, .{
|
||||||
.render = true,
|
.render = false,
|
||||||
.no_svg = true,
|
.no_svg = true,
|
||||||
})) {
|
})) {
|
||||||
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||||
@ -895,6 +918,21 @@ pub const Face = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||||
|
const ic_width: ?f64 = ic_width: {
|
||||||
|
self.ft_mutex.lock();
|
||||||
|
defer self.ft_mutex.unlock();
|
||||||
|
|
||||||
|
const glyph = face.getCharIndex('水') orelse break :ic_width null;
|
||||||
|
|
||||||
|
face.loadGlyph(glyph, .{
|
||||||
|
.render = false,
|
||||||
|
.no_svg = true,
|
||||||
|
}) catch break :ic_width null;
|
||||||
|
|
||||||
|
break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x);
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.cell_width = cell_width,
|
.cell_width = cell_width,
|
||||||
|
|
||||||
@ -910,6 +948,7 @@ pub const Face = struct {
|
|||||||
|
|
||||||
.cap_height = cap_height,
|
.cap_height = cap_height,
|
||||||
.ex_height = ex_height,
|
.ex_height = ex_height,
|
||||||
|
.ic_width = ic_width,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +235,6 @@ pub const Face = struct {
|
|||||||
.offset_y = 0,
|
.offset_y = 0,
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
.atlas_y = region.y,
|
.atlas_y = region.y,
|
||||||
.advance_x = 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +195,6 @@ pub fn renderGlyph(
|
|||||||
.offset_y = 0,
|
.offset_y = 0,
|
||||||
.atlas_x = 0,
|
.atlas_x = 0,
|
||||||
.atlas_y = 0,
|
.atlas_y = 0,
|
||||||
.advance_x = 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const metrics = self.metrics;
|
const metrics = self.metrics;
|
||||||
@ -227,8 +226,6 @@ pub fn renderGlyph(
|
|||||||
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
|
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
.atlas_y = region.y,
|
.atlas_y = region.y,
|
||||||
.advance_x = @floatFromInt(width),
|
|
||||||
.sprite = true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user