diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index 732876aef..fd7514d8f 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -83,6 +83,18 @@ pub const Face = struct { @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 diff --git a/pkg/freetype/freetype-zig.h b/pkg/freetype/freetype-zig.h index fc15c5941..ddb242be8 100644 --- a/pkg/freetype/freetype-zig.h +++ b/pkg/freetype/freetype-zig.h @@ -1,3 +1,5 @@ #include #include FT_FREETYPE_H #include FT_TRUETYPE_TABLES_H +#include +#include diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 990fcb12c..e8fe1873f 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -87,6 +87,19 @@ pub const Face = struct { 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 /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index ac0c525f2..c292e05b6 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -70,6 +70,27 @@ pub const Face = struct { 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 /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index e6c6127c5..2e3d20ca8 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -12,6 +12,7 @@ const Library = font.Library; const Style = font.Style; const Presentation = font.Presentation; const terminal = @import("../../terminal/main.zig"); +const quirks = @import("../../quirks.zig"); const log = std.log.scoped(.font_shaper); @@ -29,15 +30,15 @@ pub const Shaper = struct { 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. /// This should be at least the number of columns in the terminal. pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { // Parse all the features we want to use. We use 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); 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. if (run.font_index.special() == null) { 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 diff --git a/src/quirks.zig b/src/quirks.zig new file mode 100644 index 000000000..26e508710 --- /dev/null +++ b/src/quirks.zig @@ -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"); +}