diff --git a/src/font/Face.zig b/src/font/Face.zig index 6d8c3b9df..f0e89fc29 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -17,9 +17,6 @@ const Library = @import("main.zig").Library; const log = std.log.scoped(.font_face); -/// The core library -library: Library, - /// Our font face. face: freetype.Face, @@ -51,10 +48,7 @@ pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face { try face.selectCharmap(.unicode); try setSize_(face, size); - return Face{ - .library = lib, - .face = face, - }; + return Face{ .face = face }; } pub fn deinit(self: *Face) void { @@ -102,13 +96,21 @@ pub fn glyphIndex(self: Face, cp: u32) ?u32 { return self.face.getCharIndex(cp); } +/// Returns true if this font is colored. This can be used by callers to +/// determine what kind of atlas to pass in. +pub fn hasColor(self: Face) bool { + return self.face.hasColor(); +} + /// Load a glyph for this face. The codepoint can be either a u8 or /// []const u8 depending on if you know it is ASCII or must be UTF-8 decoded. pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph { // We need a UTF32 codepoint for freetype const glyph_index = self.glyphIndex(cp) orelse return error.GlyphNotFound; - //log.warn("glyph index: {}", .{glyph_index}); + return self.renderGlyph(alloc, atlas, glyph_index); +} +pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32) !Glyph { // If our glyph has color, we want to render the color try self.face.loadGlyph(glyph_index, .{ .render = true, diff --git a/src/font/Group.zig b/src/font/Group.zig new file mode 100644 index 000000000..0b3351e3b --- /dev/null +++ b/src/font/Group.zig @@ -0,0 +1,188 @@ +//! A font group is a a set of multiple font faces of potentially different +//! styles that are used together to find glyphs. They usually share sizing +//! properties so that they can be used interchangably with each other in cases +//! a codepoint doesn't map cleanly. For example, if a user requests a bold +//! char and it doesn't exist we can fallback to a regular non-bold char so +//! we show SOMETHING. +const Group = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const Atlas = @import("../Atlas.zig"); +const Face = @import("main.zig").Face; +const Library = @import("main.zig").Library; +const Glyph = @import("main.zig").Glyph; +const Style = @import("main.zig").Style; +const codepoint = @import("main.zig").codepoint; + +const log = std.log.scoped(.font_fallback); + +/// Packed array to map our styles to a set of faces. +// Note: this is not the most efficient way to store these, but there is +// usually only one font group for the entire process so this isn't the +// most important memory efficiency we can look for. This is totally opaque +// to the user so we can change this later. +const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Face)); + +/// The available faces we have. This shouldn't be modified manually. +/// Instead, use the functions available on Group. +faces: StyleArray, + +pub fn init(alloc: Allocator) !Group { + var result = Group{ .faces = undefined }; + + // Initialize all our styles to initially sized lists. + var i: usize = 0; + while (i < StyleArray.len) : (i += 1) { + result.faces.values[i] = .{}; + try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2); + } + + return result; +} + +pub fn deinit(self: *Group, alloc: Allocator) void { + var it = self.faces.iterator(); + while (it.next()) |entry| { + for (entry.value.items) |*item| item.deinit(); + entry.value.deinit(alloc); + } +} + +/// Add a face to the list for the given style. This face will be added as +/// next in priority if others exist already, i.e. it'll be the _last_ to be +/// searched for a glyph in that list. +/// +/// The group takes ownership of the face. The face will be deallocated when +/// the group is deallocated. +pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void { + try self.faces.getPtr(style).append(alloc, face); +} + +/// This represents a specific font in the group. +pub const FontIndex = packed struct { + /// The number of bits we use for the index. + const idx_bits = 8 - StyleArray.len; + const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); + + style: Style, + idx: IndexInt, + + test { + // We never want to take up more than a byte since font indexes are + // everywhere so if we increase the size of this we'll dramatically + // increase our memory usage. + try std.testing.expectEqual(@sizeOf(u8), @sizeOf(FontIndex)); + } +}; + +/// Looks up the font that should be used for a specific codepoint. +/// The font index is valid as long as font faces aren't removed. This +/// isn't cached; it is expected that downstream users handle caching if +/// that is important. +pub fn indexForCodepoint(self: Group, style: Style, cp: u32) ?FontIndex { + // If we can find the exact value, then return that. + if (self.indexForCodepointExact(style, cp)) |value| return value; + + // If this is already regular, we're done falling back. + if (style == .regular) return null; + + // For non-regular fonts, we fall back to regular. + return self.indexForCodepointExact(.regular, cp); +} + +fn indexForCodepointExact(self: Group, style: Style, cp: u32) ?FontIndex { + for (self.faces.get(style).items) |face, i| { + if (face.glyphIndex(cp) != null) { + return FontIndex{ + .style = style, + .idx = @intCast(FontIndex.IndexInt, i), + }; + } + } + + // Not found + return null; +} + +/// Returns true if the glyph pointed to by the index requires color. +/// This is used to determine the proper atlas to pass in for rendering +/// the glyph. +pub fn indexRequiresColor(self: Group, index: FontIndex) bool { + return self.faces.get(index.style).items[@intCast(usize, index.idx)].hasColor(); +} + +/// Return the Face represented by a given FontIndex. +pub fn faceFromIndex(self: Group, index: FontIndex) Face { + return self.faces.get(index.style).items[@intCast(usize, index.idx)]; +} + +/// Render a glyph by glyph index into the given font atlas and return +/// metadata about it. +/// +/// This performs no caching, it is up to the caller to cache calls to this +/// if they want. This will also not resize the atlas if it is full. +/// +/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller +/// is expected to translate codepoints to glyph indexes in some way. The most +/// trivial way to do this is to get the Face and call glyphIndex. If you're +/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically +/// determine glyph indexes for a text run. +pub fn renderGlyph( + self: Group, + alloc: Allocator, + atlas: *Atlas, + index: FontIndex, + glyph_index: u32, +) !Glyph { + const face = self.faces.get(index.style).items[@intCast(usize, index.idx)]; + return try face.renderGlyph(alloc, atlas, glyph_index); +} + +test { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("test.zig").fontRegular; + const testEmoji = @import("test.zig").fontEmoji; + + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + var lib = try Library.init(); + defer lib.deinit(); + + var group = try init(alloc); + defer group.deinit(alloc); + + try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); + try group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 })); + + // Should find all visible ASCII + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = group.indexForCodepoint(.regular, i).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); + try testing.expect(!group.indexRequiresColor(idx)); + + // Render it + const face = group.faceFromIndex(idx); + const glyph_index = face.glyphIndex(i).?; + _ = try group.renderGlyph( + alloc, + &atlas_greyscale, + idx, + glyph_index, + ); + } + + // Try emoji + { + const idx = group.indexForCodepoint(.regular, '🥸').?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); + try testing.expect(group.indexRequiresColor(idx)); + } +} diff --git a/src/font/main.zig b/src/font/main.zig index 4560d0e0f..ef62f132f 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -2,16 +2,17 @@ const std = @import("std"); pub const Face = @import("Face.zig"); pub const Family = @import("Family.zig"); +pub const Group = @import("Group.zig"); pub const Glyph = @import("Glyph.zig"); pub const FallbackSet = @import("FallbackSet.zig"); pub const Library = @import("Library.zig"); /// The styles that a family can take. -pub const Style = enum { - regular, - bold, - italic, - bold_italic, +pub const Style = enum(u2) { + regular = 0, + bold = 1, + italic = 2, + bold_italic = 3, }; /// Returns the UTF-32 codepoint for the given value.