quirks: Menlo/Monaco should disable ligatures by default (#331)

* font: disable default font features for Menlo and Monaco

Both of these fonts have a default ligature on "fi" which makes terminal
rendering super ugly. The easiest thing to do is special-case these
fonts and disable ligatures. It appears other terminals do the same
thing.
This commit is contained in:
Mitchell Hashimoto
2023-08-25 09:29:15 -07:00
committed by GitHub
parent 63386e4a22
commit ea3b957bc7
6 changed files with 88 additions and 5 deletions

View File

@ -83,6 +83,18 @@ pub const Face = struct {
@intFromEnum(tag), @intFromEnum(tag),
))); )));
} }
/// Retrieve the number of name strings in the SFNT name table.
pub fn getSfntNameCount(self: Face) usize {
return @intCast(c.FT_Get_Sfnt_Name_Count(self.handle));
}
/// Retrieve a string of the SFNT name table for a given index.
pub fn getSfntName(self: Face, i: usize) Error!c.FT_SfntName {
var name: c.FT_SfntName = undefined;
const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name);
return if (intToError(res)) |_| name else |err| err;
}
}; };
/// An enumeration to specify indices of SFNT tables loaded and parsed by /// An enumeration to specify indices of SFNT tables loaded and parsed by

View File

@ -1,3 +1,5 @@
#include <ft2build.h> #include <ft2build.h>
#include FT_FREETYPE_H #include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H #include FT_TRUETYPE_TABLES_H
#include <freetype/ftsnames.h>
#include <freetype/ttnameid.h>

View File

@ -87,6 +87,19 @@ pub const Face = struct {
return try initFontCopy(ct_font, .{ .points = 0 }); return try initFontCopy(ct_font, .{ .points = 0 });
} }
/// Returns the font name. If allocation is required, buf will be used,
/// but sometimes allocation isn't required and a static string is
/// returned.
pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 {
const display_name = self.font.copyDisplayName();
if (display_name.cstringPtr(.utf8)) |str| return str;
// "NULL if the internal storage of theString does not allow
// this to be returned efficiently." In this case, we need
// to allocate.
return display_name.cstring(buf, .utf8) orelse error.OutOfMemory;
}
/// Resize the font in-place. If this succeeds, the caller is responsible /// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc. /// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {

View File

@ -70,6 +70,27 @@ pub const Face = struct {
self.* = undefined; self.* = undefined;
} }
/// Returns the font name. If allocation is required, buf will be used,
/// but sometimes allocation isn't required and a static string is
/// returned.
pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 {
// We don't use this today but its possible the table below
// returns UTF-16 in which case we'd want to use this for conversion.
_ = buf;
const count = self.face.getSfntNameCount();
// We look for the font family entry.
for (0..count) |i| {
const entry = self.face.getSfntName(i) catch continue;
if (entry.name_id == freetype.c.TT_NAME_ID_FONT_FAMILY) {
return entry.string[0..entry.string_len];
}
}
return "";
}
/// Resize the font in-place. If this succeeds, the caller is responsible /// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc. /// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {

View File

@ -12,6 +12,7 @@ const Library = font.Library;
const Style = font.Style; const Style = font.Style;
const Presentation = font.Presentation; const Presentation = font.Presentation;
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const quirks = @import("../../quirks.zig");
const log = std.log.scoped(.font_shaper); const log = std.log.scoped(.font_shaper);
@ -29,15 +30,15 @@ pub const Shaper = struct {
const FeatureList = std.ArrayList(harfbuzz.Feature); const FeatureList = std.ArrayList(harfbuzz.Feature);
// These features are hardcoded to always be on by default. Users
// can turn them off by setting the features to "-liga" for example.
const hardcoded_features = [_][]const u8{ "dlig", "liga" };
/// The cell_buf argument is the buffer to use for storing shaped results. /// The cell_buf argument is the buffer to use for storing shaped results.
/// This should be at least the number of columns in the terminal. /// This should be at least the number of columns in the terminal.
pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper {
// Parse all the features we want to use. We use // Parse all the features we want to use. We use
var hb_feats = hb_feats: { var hb_feats = hb_feats: {
// These features are hardcoded to always be on by default. Users
// can turn them off by setting the features to "-liga" for example.
const hardcoded_features = [_][]const u8{ "dlig", "liga" };
var list = try FeatureList.initCapacity(alloc, opts.features.len + hardcoded_features.len); var list = try FeatureList.initCapacity(alloc, opts.features.len + hardcoded_features.len);
errdefer list.deinit(); errdefer list.deinit();
@ -107,7 +108,14 @@ pub const Shaper = struct {
// fonts, the codepoint == glyph_index so we don't need to run any shaping. // fonts, the codepoint == glyph_index so we don't need to run any shaping.
if (run.font_index.special() == null) { if (run.font_index.special() == null) {
const face = try run.group.group.faceFromIndex(run.font_index); const face = try run.group.group.faceFromIndex(run.font_index);
harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats.items); const i = if (!quirks.disableDefaultFontFeatures(face)) 0 else i: {
// If we are disabling default font features we just offset
// our features by the hardcoded items because always
// add those at the beginning.
break :i hardcoded_features.len;
};
harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats.items[i..]);
} }
// If our buffer is empty, we short-circuit the rest of the work // If our buffer is empty, we short-circuit the rest of the work

27
src/quirks.zig Normal file
View File

@ -0,0 +1,27 @@
//! Inspired by WebKit's quirks.cpp[1], this file centralizes all our
//! sad environment-specific hacks that we have to do to make things work.
//! This is a last resort; if we can find a general solution to a problem,
//! we of course prefer that, but sometimes other software, fonts, etc. are
//! just broken or weird and we have to work around it.
//!
//! [1]: https://github.com/WebKit/WebKit/blob/main/Source/WebCore/page/Quirks.cpp
const std = @import("std");
const font = @import("font/main.zig");
/// If true, the default font features should be disabled for the given face.
pub fn disableDefaultFontFeatures(face: *const font.Face) bool {
var buf: [64]u8 = undefined;
const name = face.name(&buf) catch |err| switch (err) {
// If the name doesn't fit in buf we know this will be false
// because we have no quirks fonts that are longer than buf!
error.OutOfMemory => return false,
};
// Menlo and Monaco both have a default ligature of "fi" that looks
// really bad in terminal grids, so we want to disable ligatures
// by default for these faces.
return std.mem.eql(u8, name, "Menlo") or
std.mem.eql(u8, name, "Monaco");
}