ghostty/src/TextRenderer.zig
Mitchell Hashimoto 544286509f grid render a few cells
2022-04-14 17:14:49 -07:00

221 lines
6.5 KiB
Zig

const TextRenderer = @This();
const std = @import("std");
const assert = std.debug.assert;
const ftc = @import("freetype/c.zig");
const gl = @import("opengl.zig");
const gb = @import("gb_math.zig");
const Atlas = @import("Atlas.zig");
const FontAtlas = @import("FontAtlas.zig");
const log = std.log.scoped(.text_renderer);
alloc: std.mem.Allocator,
projection: gb.gbMat4 = undefined,
font: FontAtlas,
atlas: Atlas,
program: gl.Program,
tex: gl.Texture,
const CharList = std.ArrayListUnmanaged(Char);
const Char = struct {
tex: gl.Texture,
size: @Vector(2, f32),
bearing: @Vector(2, f32),
advance: c_uint,
};
pub fn init(alloc: std.mem.Allocator) !TextRenderer {
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, 30);
// We'll calculate the cell width as the widest glyph advance
// in the set of visible ASCII characters.
var cell_width: f32 = 0;
// Load all visible ASCII characters.
var i: u8 = 32;
while (i <= 126) : (i += 1) {
const glyph = try font.addGlyph(alloc, i);
if (glyph.advance_x > cell_width) {
cell_width = @ceil(glyph.advance_x);
}
}
// The cell height is the vertical height required to render underscore
// '_' which should live at the bottom of a cell.
const cell_height: f32 = cell_height: {
// TODO(render): kitty does a calculation based on other font
// metrics that we probably want to research more. For now, this is
// fine.
assert(font.ft_face != null);
const glyph = font.getGlyph('_').?;
var res: i32 = font.ft_face.*.ascender >> 6;
res -= glyph.offset_y;
res += @intCast(i32, glyph.height);
break :cell_height @intToFloat(f32, res);
};
log.debug("cell size w={d} h={d}", .{ cell_width, cell_height });
// Build our texture
const tex = try gl.Texture.create();
errdefer tex.destroy();
const binding = try tex.bind(.@"2D");
try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.MinFilter, gl.c.GL_LINEAR);
try binding.parameter(.MagFilter, gl.c.GL_LINEAR);
try binding.image2D(
0,
.Red,
@intCast(c_int, atlas.size),
@intCast(c_int, atlas.size),
0,
.Red,
.UnsignedByte,
atlas.data.ptr,
);
// Create our shader
const program = try gl.Program.createVF(
@embedFile("../shaders/text-atlas.v.glsl"),
@embedFile("../shaders/text-atlas.f.glsl"),
);
var res = TextRenderer{
.alloc = alloc,
.font = font,
.atlas = atlas,
.program = program,
.tex = tex,
};
// Update the initialize size so we have some projection. We
// expect this will get updated almost immediately.
try res.setScreenSize(3000, 1666);
//try res.setScreenSize(1432, 874);
return res;
}
pub fn deinit(self: *TextRenderer, alloc: std.mem.Allocator) void {
self.font.deinit(alloc);
self.atlas.deinit(alloc);
self.* = undefined;
}
pub fn setScreenSize(self: *TextRenderer, w: i32, h: i32) !void {
gb.gb_mat4_ortho2d(
&self.projection,
0,
@intToFloat(f32, w),
0,
@intToFloat(f32, h),
);
const bind = try self.program.use();
defer bind.unbind();
try self.program.setUniform("projection", self.projection);
}
pub fn render(
self: TextRenderer,
text: []const u8,
x: f32,
y: f32,
color: @Vector(3, f32),
) !void {
const r = color[0];
const g = color[1];
const b = color[2];
const a: f32 = 1.0;
var vertices: std.ArrayListUnmanaged([4][9]f32) = .{};
try vertices.ensureUnusedCapacity(self.alloc, text.len);
defer vertices.deinit(self.alloc);
var indices: std.ArrayListUnmanaged([6]u32) = .{};
try indices.ensureUnusedCapacity(self.alloc, text.len);
defer indices.deinit(self.alloc);
var curx: f32 = x;
for (text) |c, i| {
if (self.font.getGlyph(c)) |glyph_ptr| {
const glyph = glyph_ptr.*;
const kerning = 0; // for now
curx += kerning;
const x0 = curx + @intToFloat(f32, glyph.offset_x);
const y0 = y + @intToFloat(f32, glyph.offset_y);
const x1 = x0 + @intToFloat(f32, glyph.width);
const y1 = y0 - @intToFloat(f32, glyph.height);
const s0 = glyph.s0;
const t0 = glyph.t0;
const s1 = glyph.s1;
const t1 = glyph.t1;
//std.log.info("CHAR ch={} x0={} y0={} x1={} y1={}", .{ c, x0, y0, x1, y1 });
const vert = [4][9]f32{
.{ x0, y0, 0, s0, t0, r, g, b, a },
.{ x0, y1, 0, s0, t1, r, g, b, a },
.{ x1, y1, 0, s1, t1, r, g, b, a },
.{ x1, y0, 0, s1, t0, r, g, b, a },
};
vertices.appendAssumeCapacity(vert);
const idx = @intCast(u32, 4 * i);
indices.appendAssumeCapacity([6]u32{
idx, idx + 1, idx + 2, // 0, 1, 2
idx, idx + 2, idx + 3, // 0, 2, 3
});
curx += glyph.advance_x;
}
}
const pbind = try self.program.use();
defer pbind.unbind();
// Bind our texture and set our data
try gl.Texture.active(gl.c.GL_TEXTURE0);
var texbind = try self.tex.bind(.@"2D");
defer texbind.unbind();
// Configure VAO/VBO for glyph rendering
const vao = try gl.VertexArray.create();
defer vao.destroy();
try vao.bind();
// Array buffer
const vbo = try gl.Buffer.create();
defer vbo.destroy();
var binding = try vbo.bind(.ArrayBuffer);
defer binding.unbind();
try binding.setData(vertices.items, .DynamicDraw);
try binding.attribute(0, 3, [9]f32, 0);
try binding.attribute(1, 2, [9]f32, 3);
try binding.attribute(2, 4, [9]f32, 5);
// Element buffer
const ebo = try gl.Buffer.create();
defer ebo.destroy();
var ebobinding = try ebo.bind(.ElementArrayBuffer);
defer ebobinding.unbind();
try ebobinding.setData(indices.items, .DynamicDraw);
//try gl.drawArrays(gl.c.GL_TRIANGLES, 0, @intCast(c_int, vertices.items.len * 6));
try gl.drawElements(gl.c.GL_TRIANGLES, @intCast(c_int, indices.items.len * 6), gl.c.GL_UNSIGNED_INT, 0);
try gl.VertexArray.unbind();
}
const face_ttf = @embedFile("../fonts/FiraCode-Regular.ttf");
//const face_ttf = @embedFile("../fonts/Inconsolata-Regular.ttf");