mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
starting FontAtlas
This commit is contained in:
@ -47,5 +47,6 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
// Tests
|
// Tests
|
||||||
const test_step = b.step("test", "Run all tests");
|
const test_step = b.step("test", "Run all tests");
|
||||||
const lib_tests = b.addTest("src/main.zig");
|
const lib_tests = b.addTest("src/main.zig");
|
||||||
|
ftlib.link(lib_tests);
|
||||||
test_step.dependOn(&lib_tests.step);
|
test_step.dependOn(&lib_tests.step);
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,10 @@ data: []u8,
|
|||||||
|
|
||||||
/// Width and height of the atlas texture. The current implementation is
|
/// Width and height of the atlas texture. The current implementation is
|
||||||
/// always square so this is both the width and the height.
|
/// 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 {
|
const Node = struct {
|
||||||
x: u32,
|
x: u32,
|
||||||
@ -41,6 +42,8 @@ pub const Error = error{
|
|||||||
AtlasFull,
|
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 {
|
pub const Region = struct {
|
||||||
x: u32,
|
x: u32,
|
||||||
y: 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)) {
|
if (node.x < (prev.x + prev.width)) {
|
||||||
const shrink = prev.x + prev.width - node.x;
|
const shrink = prev.x + prev.width - node.x;
|
||||||
node.x += shrink;
|
node.x += shrink;
|
||||||
node.width -= shrink;
|
node.width -|= shrink;
|
||||||
if (node.width <= 0) {
|
if (node.width <= 0) {
|
||||||
_ = self.nodes.orderedRemove(i);
|
_ = self.nodes.orderedRemove(i);
|
||||||
i -= 1;
|
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 {
|
test {
|
||||||
_ = @import("Atlas.zig");
|
_ = @import("Atlas.zig");
|
||||||
|
_ = @import("FontAtlas.zig");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user