mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
starting FontAtlas
This commit is contained in:
@ -47,5 +47,6 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// Tests
|
||||
const test_step = b.step("test", "Run all tests");
|
||||
const lib_tests = b.addTest("src/main.zig");
|
||||
ftlib.link(lib_tests);
|
||||
test_step.dependOn(&lib_tests.step);
|
||||
}
|
||||
|
@ -26,9 +26,10 @@ data: []u8,
|
||||
|
||||
/// Width and height of the atlas texture. The current implementation is
|
||||
/// always square so this is both the width and the height.
|
||||
size: u32,
|
||||
size: u32 = 0,
|
||||
|
||||
nodes: std.ArrayListUnmanaged(Node),
|
||||
/// The nodes (rectangles) of available space.
|
||||
nodes: std.ArrayListUnmanaged(Node) = .{},
|
||||
|
||||
const Node = struct {
|
||||
x: u32,
|
||||
@ -41,6 +42,8 @@ pub const Error = error{
|
||||
AtlasFull,
|
||||
};
|
||||
|
||||
/// A region within the texture atlas. These can be acquired using the
|
||||
/// "reserve" function. A region reservation is required to write data.
|
||||
pub const Region = struct {
|
||||
x: u32,
|
||||
y: u32,
|
||||
@ -121,7 +124,7 @@ pub fn reserve(self: *Atlas, alloc: Allocator, width: u32, height: u32) !Region
|
||||
if (node.x < (prev.x + prev.width)) {
|
||||
const shrink = prev.x + prev.width - node.x;
|
||||
node.x += shrink;
|
||||
node.width -= shrink;
|
||||
node.width -|= shrink;
|
||||
if (node.width <= 0) {
|
||||
_ = self.nodes.orderedRemove(i);
|
||||
i -= 1;
|
||||
|
185
src/FontAtlas.zig
Normal file
185
src/FontAtlas.zig
Normal file
@ -0,0 +1,185 @@
|
||||
//! 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,
|
||||
|
||||
/// normalized x, y (s, t) coordinates
|
||||
s0: f32,
|
||||
t0: f32,
|
||||
s1: f32,
|
||||
t1: f32,
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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, codepoint: anytype) !void {
|
||||
assert(self.ft_face != null);
|
||||
|
||||
// We need a UTF32 codepoint for freetype
|
||||
const utf32 = switch (@TypeOf(codepoint)) {
|
||||
comptime_int, u8 => @intCast(u32, codepoint),
|
||||
[]const u8 => @intCast(u32, try std.unicode.utfDecode(codepoint)),
|
||||
else => @compileError("invalid codepoint type"),
|
||||
};
|
||||
|
||||
const glyph_index = ftc.FT_Get_Char_Index(self.ft_face, utf32);
|
||||
|
||||
// TODO: probably not an error because we want to add a box.
|
||||
if (glyph_index == 0) return error.CodepointNotFound;
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
const gop = try self.glyphs.getOrPut(alloc, utf32);
|
||||
gop.value_ptr.* = .{
|
||||
.width = tgt_w,
|
||||
.height = tgt_h,
|
||||
.offset_x = glyph.*.bitmap_left,
|
||||
.offset_y = glyph.*.bitmap_top,
|
||||
.s0 = @intToFloat(f32, region.x) / @intToFloat(f32, self.atlas.size),
|
||||
.t0 = @intToFloat(f32, region.y) / @intToFloat(f32, self.atlas.size),
|
||||
.s1 = @intToFloat(f32, region.x + tgt_w) / @intToFloat(f32, self.atlas.size),
|
||||
.t1 = @intToFloat(f32, region.y + tgt_h) / @intToFloat(f32, self.atlas.size),
|
||||
.advance_x = @intToFloat(f32, glyph.*.advance.x),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const testFont = @embedFile("../fonts/Inconsolata-Regular.ttf");
|
@ -20,4 +20,5 @@ pub fn main() !void {
|
||||
|
||||
test {
|
||||
_ = @import("Atlas.zig");
|
||||
_ = @import("FontAtlas.zig");
|
||||
}
|
||||
|
Reference in New Issue
Block a user