diff --git a/src/FontAtlas.zig b/src/FontAtlas.zig deleted file mode 100644 index d4dd4d691..000000000 --- a/src/FontAtlas.zig +++ /dev/null @@ -1,231 +0,0 @@ -//! Implements font loading and rendering into a texture atlas, using -//! Atlas as the backing implementation. The FontAtlas represents a single -//! face with a single size. -const FontAtlas = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const testing = std.testing; -const Allocator = std.mem.Allocator; -const ftc = @import("freetype/c.zig"); -const Atlas = @import("Atlas.zig"); - -const ftok = ftc.FT_Err_Ok; -const log = std.log.scoped(.font_atlas); - -/// The texture atlas where all the font glyphs are rendered. -/// This is NOT owned by the FontAtlas, deinitialization must -/// be manually done. -atlas: Atlas, - -/// The glyphs that are loaded into the atlas, keyed by codepoint. -glyphs: std.AutoHashMapUnmanaged(u32, Glyph), - -/// The FreeType library -ft_library: ftc.FT_Library, - -/// Our font face. -ft_face: ftc.FT_Face = null, - -/// Information about a single glyph. -pub const Glyph = struct { - /// width of glyph in pixels - width: u32, - - /// height of glyph in pixels - height: u32, - - /// left bearing - offset_x: i32, - - /// top bearing - offset_y: i32, - - /// coordinates in the atlas of the top-left corner. These have to - /// be normalized to be between 0 and 1 prior to use in shaders. - atlas_x: u32, - atlas_y: u32, - - /// horizontal position to increase drawing position for strings - advance_x: f32, -}; - -pub fn init(atlas: Atlas) !FontAtlas { - var res = FontAtlas{ - .atlas = atlas, - .ft_library = undefined, - .glyphs = .{}, - }; - - if (ftc.FT_Init_FreeType(&res.ft_library) != ftok) - return error.FreeTypeInitFailed; - - return res; -} - -pub fn deinit(self: *FontAtlas, alloc: Allocator) void { - self.glyphs.deinit(alloc); - - if (self.ft_face != null) { - if (ftc.FT_Done_Face(self.ft_face) != ftok) - log.err("failed to clean up font face", .{}); - } - - if (ftc.FT_Done_FreeType(self.ft_library) != ftok) - log.err("failed to clean up FreeType", .{}); - - self.* = undefined; -} - -/// Loads a font to use for the atlas. -/// -/// This can only be called if a font is not already loaded. -pub fn loadFaceFromMemory(self: *FontAtlas, source: [:0]const u8, size: u32) !void { - assert(self.ft_face == null); - - if (ftc.FT_New_Memory_Face( - self.ft_library, - source.ptr, - @intCast(c_long, source.len), - 0, - &self.ft_face, - ) != ftok) return error.FaceLoadFailed; - errdefer { - _ = ftc.FT_Done_Face(self.ft_face); - self.ft_face = null; - } - - if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok) - return error.FaceLoadFailed; - - if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok) - return error.FaceLoadFailed; -} - -/// Get the glyph for the given codepoint. -pub fn getGlyph(self: FontAtlas, v: anytype) ?*Glyph { - const utf32 = codepoint(v); - const entry = self.glyphs.getEntry(utf32) orelse return null; - return entry.value_ptr; -} - -/// Add a glyph to the font atlas. 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 addGlyph(self: *FontAtlas, alloc: Allocator, v: anytype) !*Glyph { - assert(self.ft_face != null); - - // We need a UTF32 codepoint for freetype - const utf32 = codepoint(v); - - // If we have this glyph loaded already then we're done. - const gop = try self.glyphs.getOrPut(alloc, utf32); - if (gop.found_existing) return gop.value_ptr; - errdefer _ = self.glyphs.remove(utf32); - - const glyph_index = glyph_index: { - // log.warn("glyph load: {x}", .{utf32}); - const idx = ftc.FT_Get_Char_Index(self.ft_face, utf32); - if (idx > 0) break :glyph_index idx; - - // Unknown glyph. - log.warn("glyph not found: {x}", .{utf32}); - - // TODO: render something more identifiable than a space - break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' '); - }; - - if (ftc.FT_Load_Glyph( - self.ft_face, - glyph_index, - ftc.FT_LOAD_RENDER, - ) != ftok) return error.LoadGlyphFailed; - - const glyph = self.ft_face.*.glyph; - const bitmap = glyph.*.bitmap; - - const src_w = bitmap.width; - const src_h = bitmap.rows; - const tgt_w = src_w; - const tgt_h = src_h; - - const region = try self.atlas.reserve(alloc, tgt_w, tgt_h); - - // Build our buffer - // - // TODO(perf): we can avoid a buffer copy here in some cases where - // tgt_w == bitmap.width and bitmap.width == bitmap.pitch - const buffer = try alloc.alloc(u8, tgt_w * tgt_h); - defer alloc.free(buffer); - var dst_ptr = buffer; - var src_ptr = bitmap.buffer; - var i: usize = 0; - while (i < src_h) : (i += 1) { - std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]); - dst_ptr = dst_ptr[tgt_w..]; - src_ptr += @intCast(usize, bitmap.pitch); - } - - // Write the glyph information into the atlas - assert(region.width == tgt_w); - assert(region.height == tgt_h); - self.atlas.set(region, buffer); - - // Store glyph metadata - gop.value_ptr.* = .{ - .width = tgt_w, - .height = tgt_h, - .offset_x = glyph.*.bitmap_left, - .offset_y = glyph.*.bitmap_top, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = f26dot6ToFloat(glyph.*.advance.x), - }; - - //log.debug("loaded glyph codepoint=U+{x} glyph={}", .{ utf32, gop.value_ptr.* }); - - return gop.value_ptr; -} - -/// Convert 16.6 pixel format to pixels based on the scale factor of the -/// current font size. -pub fn unitsToPxY(self: FontAtlas, units: i32) i32 { - return @intCast(i32, ftc.FT_MulFix(units, self.ft_face.*.size.*.metrics.y_scale) >> 6); -} - -/// Convert 26.6 pixel format to f32 -fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 { - return @intToFloat(f32, v >> 6); -} - -/// Returns the UTF-32 codepoint for the given value. -fn codepoint(v: anytype) u32 { - // We need a UTF32 codepoint for freetype - return switch (@TypeOf(v)) { - u32 => v, - comptime_int, u8 => @intCast(u32, v), - []const u8 => @intCast(u32, try std.unicode.utfDecode(v)), - else => @compileError("invalid codepoint type"), - }; -} - -test { - const alloc = testing.allocator; - var font = try init(try Atlas.init(alloc, 512)); - defer font.deinit(alloc); - defer font.atlas.deinit(alloc); - - try font.loadFaceFromMemory(testFont, 48); - - // Generate all visible ASCII - var i: u8 = 32; - while (i < 127) : (i += 1) { - _ = try font.addGlyph(alloc, i); - } - - i = 32; - while (i < 127) : (i += 1) { - try testing.expect(font.getGlyph(i) != null); - } -} - -const testFont = @embedFile("../fonts/Inconsolata-Regular.ttf"); diff --git a/src/Grid.zig b/src/Grid.zig index 7aa84620f..a53d245c6 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -6,7 +6,7 @@ const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; const Atlas = @import("Atlas.zig"); -const FontAtlas = @import("FontAtlas.zig"); +const font = @import("font/font.zig"); const terminal = @import("terminal/main.zig"); const Terminal = terminal.Terminal; const gl = @import("opengl.zig"); @@ -34,7 +34,7 @@ vbo: gl.Buffer, texture: gl.Texture, /// The font atlas. -font_atlas: FontAtlas, +font_atlas: font.Family, atlas_dirty: bool, /// Whether the cursor is visible or not. This is used to control cursor @@ -103,9 +103,9 @@ pub fn init(alloc: Allocator) !Grid { // font atlas with all the visible ASCII characters since they are common. var atlas = try Atlas.init(alloc, 512); errdefer atlas.deinit(alloc); - var font = try FontAtlas.init(atlas); - errdefer font.deinit(alloc); - try font.loadFaceFromMemory(face_ttf, 32); + var fam = try font.Family.init(atlas); + errdefer fam.deinit(alloc); + try fam.loadFaceFromMemory(.regular, face_ttf, 32); // Load all visible ASCII characters and build our cell width based on // the widest character that we see. @@ -113,7 +113,7 @@ pub fn init(alloc: Allocator) !Grid { var cell_width: f32 = 0; var i: u8 = 32; while (i <= 126) : (i += 1) { - const glyph = try font.addGlyph(alloc, i); + const glyph = try fam.addGlyph(alloc, i, .regular); if (glyph.advance_x > cell_width) { cell_width = @ceil(glyph.advance_x); } @@ -126,12 +126,11 @@ pub fn init(alloc: Allocator) !Grid { // '_' which should live at the bottom of a cell. const cell_height: f32 = cell_height: { // This is the height reported by the font face - const face_height: i32 = font.unitsToPxY(font.ft_face.*.height); + const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height); // Determine the height of the underscore char - assert(font.ft_face != null); - const glyph = font.getGlyph('_').?; - var res: i32 = font.unitsToPxY(font.ft_face.*.ascender); + const glyph = fam.getGlyph('_', .regular).?; + var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender); res -= glyph.offset_y; res += @intCast(i32, glyph.height); @@ -141,7 +140,10 @@ pub fn init(alloc: Allocator) !Grid { break :cell_height @intToFloat(f32, res); }; - const cell_baseline = cell_height - @intToFloat(f32, font.unitsToPxY(font.ft_face.*.ascender)); + const cell_baseline = cell_height - @intToFloat( + f32, + fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender), + ); log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline }); // Create our shader @@ -235,7 +237,7 @@ pub fn init(alloc: Allocator) !Grid { .ebo = ebo, .vbo = vbo, .texture = tex, - .font_atlas = font, + .font_atlas = fam, .atlas_dirty = false, .cursor_visible = true, .cursor_style = .box, @@ -310,11 +312,11 @@ pub fn updateCells(self: *Grid, term: Terminal) !void { // Get our glyph // TODO: if we add a glyph, I think we need to rerender the texture. - const glyph = if (self.font_atlas.getGlyph(cell.char)) |glyph| + const glyph = if (self.font_atlas.getGlyph(cell.char, .regular)) |glyph| glyph else glyph: { self.atlas_dirty = true; - break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char); + break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char, .regular); }; const fg = cell.fg orelse self.foreground; diff --git a/src/font/Face.zig b/src/font/Face.zig new file mode 100644 index 000000000..d8e3d1b0e --- /dev/null +++ b/src/font/Face.zig @@ -0,0 +1,164 @@ +//! Face represents a single font face. A single font face has a single set +//! of properties associated with it such as style, weight, etc. +//! +//! A Face isn't typically meant to be used directly. It is usually used +//! via a Family in order to store it in an Atlas. +const Face = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const ftc = @import("../freetype/c.zig"); +const Atlas = @import("../Atlas.zig"); +const Glyph = @import("font.zig").Glyph; + +const ftok = ftc.FT_Err_Ok; +const log = std.log.scoped(.font_face); + +/// The FreeType library +ft_library: ftc.FT_Library, + +/// Our font face. +ft_face: ftc.FT_Face = null, + +pub fn init(lib: ftc.FT_Library) !Face { + return Face{ + .ft_library = lib, + }; +} + +pub fn deinit(self: *Face) void { + if (self.ft_face != null) { + if (ftc.FT_Done_Face(self.ft_face) != ftok) + log.err("failed to clean up font face", .{}); + } + + self.* = undefined; +} + +/// Loads a font to use. +/// +/// This can only be called if a font is not already loaded. +pub fn loadFaceFromMemory(self: *Face, source: [:0]const u8, size: u32) !void { + assert(self.ft_face == null); + + if (ftc.FT_New_Memory_Face( + self.ft_library, + source.ptr, + @intCast(c_long, source.len), + 0, + &self.ft_face, + ) != ftok) return error.FaceLoadFailed; + errdefer { + _ = ftc.FT_Done_Face(self.ft_face); + self.ft_face = null; + } + + if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok) + return error.FaceLoadFailed; + + if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok) + return error.FaceLoadFailed; +} + +/// 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 { + assert(self.ft_face != null); + + // We need a UTF32 codepoint for freetype + const glyph_index = glyph_index: { + // log.warn("glyph load: {x}", .{cp}); + const idx = ftc.FT_Get_Char_Index(self.ft_face, cp); + if (idx > 0) break :glyph_index idx; + + // Unknown glyph. + log.warn("glyph not found: {x}", .{cp}); + + // TODO: render something more identifiable than a space + break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' '); + }; + + if (ftc.FT_Load_Glyph( + self.ft_face, + glyph_index, + ftc.FT_LOAD_RENDER, + ) != ftok) return error.LoadGlyphFailed; + + const glyph = self.ft_face.*.glyph; + const bitmap = glyph.*.bitmap; + + const src_w = bitmap.width; + const src_h = bitmap.rows; + const tgt_w = src_w; + const tgt_h = src_h; + + const region = try atlas.reserve(alloc, tgt_w, tgt_h); + + // Build our buffer + // + // TODO(perf): we can avoid a buffer copy here in some cases where + // tgt_w == bitmap.width and bitmap.width == bitmap.pitch + const buffer = try alloc.alloc(u8, tgt_w * tgt_h); + defer alloc.free(buffer); + var dst_ptr = buffer; + var src_ptr = bitmap.buffer; + var i: usize = 0; + while (i < src_h) : (i += 1) { + std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]); + dst_ptr = dst_ptr[tgt_w..]; + src_ptr += @intCast(usize, bitmap.pitch); + } + + // Write the glyph information into the atlas + assert(region.width == tgt_w); + assert(region.height == tgt_h); + atlas.set(region, buffer); + + // Store glyph metadata + return Glyph{ + .width = tgt_w, + .height = tgt_h, + .offset_x = glyph.*.bitmap_left, + .offset_y = glyph.*.bitmap_top, + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = f26dot6ToFloat(glyph.*.advance.x), + }; +} + +/// Convert 16.6 pixel format to pixels based on the scale factor of the +/// current font size. +pub fn unitsToPxY(self: Face, units: i32) i32 { + return @intCast(i32, ftc.FT_MulFix(units, self.ft_face.*.size.*.metrics.y_scale) >> 6); +} + +/// Convert 26.6 pixel format to f32 +fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 { + return @intToFloat(f32, v >> 6); +} + +test { + var ft_lib: ftc.FT_Library = undefined; + if (ftc.FT_Init_FreeType(&ft_lib) != ftok) + return error.FreeTypeInitFailed; + defer _ = ftc.FT_Done_FreeType(ft_lib); + + const alloc = testing.allocator; + var atlas = try Atlas.init(alloc, 512); + defer atlas.deinit(alloc); + + var font = try init(ft_lib); + defer font.deinit(); + + try font.loadFaceFromMemory(testFont, 48); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + _ = try font.loadGlyph(alloc, &atlas, i); + } +} + +const testFont = @embedFile("../../fonts/Inconsolata-Regular.ttf"); diff --git a/src/font/Family.zig b/src/font/Family.zig new file mode 100644 index 000000000..6a419026f --- /dev/null +++ b/src/font/Family.zig @@ -0,0 +1,172 @@ +//! Family represents a multiple styles of a single font: regular, bold, +//! italic, etc. It is able to cache the glyphs into a single atlas. +const Family = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Atlas = @import("../Atlas.zig"); +const ftc = @import("../freetype/c.zig"); +const ftok = ftc.FT_Err_Ok; +const Face = @import("font.zig").Face; +const Glyph = @import("font.zig").Glyph; +const Style = @import("font.zig").Style; +const testFont = @import("test.zig").fontRegular; + +const log = std.log.scoped(.font_family); + +// NOTE(mitchellh): I think eventually atlas and the freetype lib fields +// move even higher up into another struct that manages sets of font families +// in order to support fallback and so on. + +/// The texture atlas where all the font glyphs are rendered. +/// This is NOT owned by the Family, deinitialization must +/// be manually done. +atlas: Atlas, + +/// The FreeType library, initialized by this init func. +ft_library: ftc.FT_Library, + +/// The glyphs that are loaded into the atlas, keyed by codepoint. +glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{}, + +/// The font faces representing all the styles in this family. +/// These should be set directly or via various loader functions. +regular: ?Face = null, + +/// This struct is used for the hash key for glyphs. +const GlyphKey = struct { + style: Style, + codepoint: u32, +}; + +pub fn init(atlas: Atlas) !Family { + var res = Family{ + .atlas = atlas, + .ft_library = undefined, + }; + + if (ftc.FT_Init_FreeType(&res.ft_library) != ftok) + return error.FreeTypeInitFailed; + + return res; +} + +pub fn deinit(self: *Family, alloc: Allocator) void { + self.glyphs.deinit(alloc); + + if (self.regular) |*face| face.deinit(); + + if (ftc.FT_Done_FreeType(self.ft_library) != ftok) + log.err("failed to clean up FreeType", .{}); + + self.* = undefined; +} + +/// Loads a font to use from memory. +/// +/// This can only be called if a font is not already loaded for the given style. +pub fn loadFaceFromMemory( + self: *Family, + comptime style: Style, + source: [:0]const u8, + size: u32, +) !void { + var face = try Face.init(self.ft_library); + errdefer face.deinit(); + try face.loadFaceFromMemory(source, size); + + @field(self, switch (style) { + .regular => "regular", + .bold => unreachable, + .italic => unreachable, + .bold_italic => unreachable, + }) = face; +} + +/// Get the glyph for the given codepoint and style. If the glyph hasn't +/// been loaded yet this will return null. +pub fn getGlyph(self: Family, cp: anytype, style: Style) ?*Glyph { + const utf32 = codepoint(cp); + const entry = self.glyphs.getEntry(.{ + .style = style, + .codepoint = utf32, + }) orelse return null; + return entry.value_ptr; +} + +/// Add a glyph. If the glyph has already been loaded this will return +/// the existing loaded glyph. If a glyph style can't be found, this will +/// fall back to the "regular" style. If a glyph can't be found in the +/// "regular" style, this will fall back to the unknown glyph character. +/// +/// 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 addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Glyph { + const face = face: { + // Real is the face we SHOULD use for this style. + var real = switch (style) { + .regular => self.regular, + .bold => unreachable, + .italic => unreachable, + .bold_italic => unreachable, + }; + + // Fall back to regular if it is null + if (real == null) real = self.regular; + + // Return our face if we have it. + if (real) |ptr| break :face ptr; + + // If we reached this point, we have no font in the style we + // want OR the fallback. + return error.NoFontFallback; + }; + + // We need a UTF32 codepoint + const utf32 = codepoint(v); + + // If we have this glyph loaded already then we're done. + const glyphKey = .{ + .style = style, + .codepoint = utf32, + }; + const gop = try self.glyphs.getOrPut(alloc, glyphKey); + if (gop.found_existing) return gop.value_ptr; + errdefer _ = self.glyphs.remove(glyphKey); + + // Get the glyph and add it to the atlas. + // TODO: handle glyph not found + gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32); + return gop.value_ptr; +} + +/// Returns the UTF-32 codepoint for the given value. +fn codepoint(v: anytype) u32 { + // We need a UTF32 codepoint for freetype + return switch (@TypeOf(v)) { + u32 => v, + comptime_int, u8 => @intCast(u32, v), + []const u8 => @intCast(u32, try std.unicode.utfDecode(v)), + else => @compileError("invalid codepoint type"), + }; +} + +test { + const testing = std.testing; + const alloc = testing.allocator; + var fam = try init(try Atlas.init(alloc, 512)); + defer fam.deinit(alloc); + defer fam.atlas.deinit(alloc); + try fam.loadFaceFromMemory(.regular, testFont, 48); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + _ = try fam.addGlyph(alloc, i, .regular); + } + + i = 32; + while (i < 127) : (i += 1) { + try testing.expect(fam.getGlyph(i, .regular) != null); + } +} diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig new file mode 100644 index 000000000..5449e2440 --- /dev/null +++ b/src/font/Glyph.zig @@ -0,0 +1,22 @@ +//! Glyph is a single loaded glyph for a face. +const Glyph = @This(); + +/// width of glyph in pixels +width: u32, + +/// height of glyph in pixels +height: u32, + +/// left bearing +offset_x: i32, + +/// top bearing +offset_y: i32, + +/// coordinates in the atlas of the top-left corner. These have to +/// be normalized to be between 0 and 1 prior to use in shaders. +atlas_x: u32, +atlas_y: u32, + +/// horizontal position to increase drawing position for strings +advance_x: f32, diff --git a/src/font/font.zig b/src/font/font.zig new file mode 100644 index 000000000..ba56873b1 --- /dev/null +++ b/src/font/font.zig @@ -0,0 +1,20 @@ +pub const Face = @import("Face.zig"); +pub const Family = @import("Family.zig"); +pub const Glyph = @import("Glyph.zig"); + +/// Embedded fonts (for now) +pub const fontRegular = @import("test.zig").fontRegular; + +/// The styles that a family can take. +pub const Style = enum { + regular, + bold, + italic, + bold_italic, +}; + +test { + _ = Face; + _ = Family; + _ = Glyph; +} diff --git a/src/font/test.zig b/src/font/test.zig new file mode 100644 index 000000000..e87af119a --- /dev/null +++ b/src/font/test.zig @@ -0,0 +1 @@ +pub const fontRegular = @embedFile("../../fonts/Inconsolata-Regular.ttf"); diff --git a/src/main.zig b/src/main.zig index cdc3c3fb9..36f7fefb2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -59,11 +59,11 @@ pub fn tracy_enabled() bool { test { _ = @import("Atlas.zig"); - _ = @import("FontAtlas.zig"); _ = @import("Grid.zig"); _ = @import("Pty.zig"); _ = @import("Command.zig"); _ = @import("TempDir.zig"); + _ = @import("font/font.zig"); _ = @import("terminal/Terminal.zig"); // Libraries