diff --git a/build.zig b/build.zig index 6d59b7aca..c46ed74cf 100644 --- a/build.zig +++ b/build.zig @@ -15,10 +15,15 @@ pub fn build(b: *std.build.Builder) !void { exe.addPackagePath("glfw", "vendor/mach/glfw/src/main.zig"); glfw.link(b, exe, .{}); + exe.linkSystemLibrary("epoxy"); + const ftlib = try ft.create(b, target, mode, .{}); ftlib.link(exe); - - exe.linkSystemLibrary("epoxy"); + // to link to system: + // exe.linkSystemLibrary("freetype2"); + // exe.linkSystemLibrary("libpng"); + // exe.linkSystemLibrary("bzip2"); + // ftlib.addIncludeDirs(exe); // stb if we need it // exe.addIncludeDir("vendor/stb"); diff --git a/fonts/Inconsolata-Regular.ttf b/fonts/Inconsolata-Regular.ttf new file mode 100755 index 000000000..3b74e08a1 Binary files /dev/null and b/fonts/Inconsolata-Regular.ttf differ diff --git a/fonts/Inconsolata.ttf b/fonts/Inconsolata.ttf new file mode 100755 index 000000000..20251d93e Binary files /dev/null and b/fonts/Inconsolata.ttf differ diff --git a/nix/devshell.nix b/nix/devshell.nix index 3dd0a9828..ecda1371b 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -8,8 +8,11 @@ , vttest , zig +, bzip2 , fontconfig +, freetype , libepoxy +, libpng , libGL , libX11 , libXcursor @@ -33,10 +36,14 @@ buildInputs = [ # TODO: non-linux ] ++ lib.optionals stdenv.isLinux [ - fontconfig libepoxy libGL + fontconfig + freetype + libpng + bzip2 + libX11 libXcursor libXext diff --git a/shaders/text.f.glsl b/shaders/text.f.glsl new file mode 100644 index 000000000..03858922a --- /dev/null +++ b/shaders/text.f.glsl @@ -0,0 +1,9 @@ +#version 120 + +varying vec2 texcoord; +uniform sampler2D tex; +uniform vec4 color; + +void main(void) { + gl_FragColor = vec4(1, 1, 1, texture2D(tex, texcoord).r) * color; +} diff --git a/shaders/text.v.glsl b/shaders/text.v.glsl new file mode 100644 index 000000000..63be28019 --- /dev/null +++ b/shaders/text.v.glsl @@ -0,0 +1,9 @@ +#version 120 + +attribute vec4 coord; +varying vec2 texcoord; + +void main(void) { + gl_Position = vec4(coord.xy, 0, 1); + texcoord = coord.zw; +} diff --git a/src/App.zig b/src/App.zig index 152e67094..09c6f8808 100644 --- a/src/App.zig +++ b/src/App.zig @@ -4,8 +4,9 @@ const App = @This(); const std = @import("std"); -const gl = @import("opengl.zig"); const glfw = @import("glfw"); +const gl = @import("opengl.zig"); +const TextRenderer = @import("TextRenderer.zig"); const log = std.log; @@ -17,7 +18,11 @@ vao: gl.VertexArray, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. -pub fn init() !App { +pub fn init(alloc: std.mem.Allocator) !App { + // Setup our text renderer + var texter = try TextRenderer.init(alloc); + defer texter.deinit(); + // Create our window const window = try glfw.Window.create(640, 480, "ghostty", null, null, .{ .context_version_major = 3, @@ -39,6 +44,10 @@ pub fn init() !App { } }).callback); + // Blending for text + gl.c.glEnable(gl.c.GL_BLEND); + gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA); + // Compile our shaders const vs = try gl.Shader.create(gl.c.GL_VERTEX_SHADER); try vs.setSourceAndCompile(vs_source); diff --git a/src/TextRenderer.zig b/src/TextRenderer.zig new file mode 100644 index 000000000..2a78ea598 --- /dev/null +++ b/src/TextRenderer.zig @@ -0,0 +1,116 @@ +const TextRenderer = @This(); + +const std = @import("std"); +const ftc = @import("freetype/c.zig"); +const gl = @import("opengl.zig"); + +alloc: std.mem.Allocator, +ft: ftc.FT_Library, +face: ftc.FT_Face, +chars: CharList, + +const CharList = std.ArrayListUnmanaged(Char); +const Char = struct { + tex: gl.Texture, + size: @Vector(2, c_uint), + bearing: @Vector(2, c_int), + advance: c_uint, +}; + +pub fn init(alloc: std.mem.Allocator) !TextRenderer { + var ft: ftc.FT_Library = undefined; + if (ftc.FT_Init_FreeType(&ft) != 0) { + return error.FreetypeInitFailed; + } + + var face: ftc.FT_Face = undefined; + if (ftc.FT_New_Memory_Face( + ft, + face_ttf, + face_ttf.len, + 0, + &face, + ) != 0) { + return error.FreetypeFaceFailed; + } + + _ = ftc.FT_Set_Pixel_Sizes(face, 0, 48); + + // disable byte-alignment restriction + gl.c.glPixelStorei(gl.c.GL_UNPACK_ALIGNMENT, 1); + + // Pre-render all the ASCII characters + var chars = try CharList.initCapacity(alloc, 128); + var i: usize = 0; + while (i < chars.capacity) : (i += 1) { + // Load the character + if (ftc.FT_Load_Char(face, i, ftc.FT_LOAD_RENDER) != 0) { + return error.GlyphLoadFailed; + } + + if (face.*.glyph.*.bitmap.buffer == null) { + // Unrenderable characters + chars.appendAssumeCapacity(.{ + .tex = undefined, + .size = undefined, + .bearing = undefined, + .advance = undefined, + }); + continue; + } + + // Generate the texture + const tex = try gl.Texture.create(); + var binding = try tex.bind(gl.c.GL_TEXTURE_2D); + defer binding.unbind(); + try binding.image2D( + 0, + gl.c.GL_RED, + @intCast(c_int, face.*.glyph.*.bitmap.width), + @intCast(c_int, face.*.glyph.*.bitmap.rows), + 0, + gl.c.GL_RED, + gl.c.GL_UNSIGNED_BYTE, + face.*.glyph.*.bitmap.buffer, + ); + try binding.parameter(gl.c.GL_TEXTURE_WRAP_S, gl.c.GL_CLAMP_TO_EDGE); + try binding.parameter(gl.c.GL_TEXTURE_WRAP_T, gl.c.GL_CLAMP_TO_EDGE); + try binding.parameter(gl.c.GL_TEXTURE_MIN_FILTER, gl.c.GL_LINEAR); + try binding.parameter(gl.c.GL_TEXTURE_MAG_FILTER, gl.c.GL_LINEAR); + + // Store the character + chars.appendAssumeCapacity(.{ + .tex = tex, + .size = .{ + face.*.glyph.*.bitmap.width, + face.*.glyph.*.bitmap.rows, + }, + .bearing = .{ + face.*.glyph.*.bitmap_left, + face.*.glyph.*.bitmap_top, + }, + .advance = @intCast(c_uint, face.*.glyph.*.advance.x), + }); + } + + return TextRenderer{ + .alloc = alloc, + .ft = ft, + .face = face, + .chars = chars, + }; +} + +pub fn deinit(self: *TextRenderer) void { + // TODO: delete textures + self.chars.deinit(self.alloc); + + if (ftc.FT_Done_Face(self.face) != 0) + std.log.err("freetype face deinitialization failed", .{}); + if (ftc.FT_Done_FreeType(self.ft) != 0) + std.log.err("freetype library deinitialization failed", .{}); + + self.* = undefined; +} + +const face_ttf = @embedFile("../fonts/Inconsolata-Regular.ttf"); diff --git a/src/freetype/build.zig b/src/freetype/build.zig index d46d4042a..da3ac95f7 100644 --- a/src/freetype/build.zig +++ b/src/freetype/build.zig @@ -7,6 +7,7 @@ pub const Library = struct { /// statically link this library into the given step pub fn link(self: Library, other: *std.build.LibExeObjStep) void { self.addIncludeDirs(other); + other.addIncludeDir(include_dir); other.linkLibrary(self.step); } @@ -15,7 +16,6 @@ pub const Library = struct { /// library. pub fn addIncludeDirs(self: Library, other: *std.build.LibExeObjStep) void { _ = self; - other.addIncludeDir(include_dir); // We need to add this directory to the include path for the final // app so that we can access "freetype-zig.h". diff --git a/src/main.zig b/src/main.zig index 81ab80c47..2e3a041bd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,18 +1,19 @@ const std = @import("std"); const glfw = @import("glfw"); -const gl = @import("opengl.zig"); -const stb = @import("stb.zig"); -const fonts = @import("fonts.zig"); const App = @import("App.zig"); pub fn main() !void { + var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; + const gpa = general_purpose_allocator.allocator(); + defer _ = general_purpose_allocator.deinit(); + // List our fonts try glfw.init(.{}); defer glfw.terminate(); // Run our app - var app = try App.init(); + var app = try App.init(gpa); defer app.deinit(); try app.run(); }