mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
really hacked in emoji support, time to clean it up
This commit is contained in:
@ -18,6 +18,7 @@ layout(location = 0) out vec4 out_FragColor;
|
|||||||
|
|
||||||
// Font texture
|
// Font texture
|
||||||
uniform sampler2D text;
|
uniform sampler2D text;
|
||||||
|
uniform sampler2D text_color;
|
||||||
|
|
||||||
// Dimensions of the cell
|
// Dimensions of the cell
|
||||||
uniform vec2 cell_size;
|
uniform vec2 cell_size;
|
||||||
@ -25,22 +26,29 @@ uniform vec2 cell_size;
|
|||||||
// See vertex shader
|
// See vertex shader
|
||||||
const uint MODE_BG = 1u;
|
const uint MODE_BG = 1u;
|
||||||
const uint MODE_FG = 2u;
|
const uint MODE_FG = 2u;
|
||||||
|
const uint MODE_FG_COLOR = 7u;
|
||||||
const uint MODE_CURSOR_RECT = 3u;
|
const uint MODE_CURSOR_RECT = 3u;
|
||||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||||
const uint MODE_CURSOR_BAR = 5u;
|
const uint MODE_CURSOR_BAR = 5u;
|
||||||
const uint MODE_UNDERLINE = 6u;
|
const uint MODE_UNDERLINE = 6u;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
float a;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case MODE_BG:
|
case MODE_BG:
|
||||||
out_FragColor = color;
|
out_FragColor = color;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_FG:
|
case MODE_FG:
|
||||||
float a = texture(text, glyph_tex_coords).r;
|
a = texture(text, glyph_tex_coords).r;
|
||||||
out_FragColor = vec4(color.rgb, color.a*a);
|
out_FragColor = vec4(color.rgb, color.a*a);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MODE_FG_COLOR:
|
||||||
|
out_FragColor = texture(text_color, glyph_tex_coords);
|
||||||
|
break;
|
||||||
|
|
||||||
case MODE_CURSOR_RECT:
|
case MODE_CURSOR_RECT:
|
||||||
out_FragColor = color;
|
out_FragColor = color;
|
||||||
break;
|
break;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// NOTE: this must be kept in sync with the fragment shader
|
// NOTE: this must be kept in sync with the fragment shader
|
||||||
const uint MODE_BG = 1u;
|
const uint MODE_BG = 1u;
|
||||||
const uint MODE_FG = 2u;
|
const uint MODE_FG = 2u;
|
||||||
|
const uint MODE_FG_COLOR = 7u;
|
||||||
const uint MODE_CURSOR_RECT = 3u;
|
const uint MODE_CURSOR_RECT = 3u;
|
||||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||||
const uint MODE_CURSOR_BAR = 5u;
|
const uint MODE_CURSOR_BAR = 5u;
|
||||||
@ -50,6 +51,7 @@ flat out vec2 screen_cell_pos;
|
|||||||
flat out uint mode;
|
flat out uint mode;
|
||||||
|
|
||||||
uniform sampler2D text;
|
uniform sampler2D text;
|
||||||
|
uniform sampler2D text_color;
|
||||||
uniform vec2 cell_size;
|
uniform vec2 cell_size;
|
||||||
uniform mat4 projection;
|
uniform mat4 projection;
|
||||||
uniform float glyph_baseline;
|
uniform float glyph_baseline;
|
||||||
@ -113,20 +115,41 @@ void main() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_FG:
|
case MODE_FG:
|
||||||
|
case MODE_FG_COLOR:
|
||||||
|
vec2 glyph_offset_calc = glyph_offset;
|
||||||
|
|
||||||
|
// If the glyph is larger than our cell, we need to downsample it
|
||||||
|
// TODO: for now, we assume this means it is a full width character
|
||||||
|
// TODO: in the future, use unicode libs to verify this.
|
||||||
|
vec2 glyph_size_downsampled = glyph_size;
|
||||||
|
if (glyph_size.x > cell_size.x) {
|
||||||
|
glyph_size_downsampled.x = cell_size.x * 2;
|
||||||
|
glyph_size_downsampled.y = glyph_size.y * (glyph_size_downsampled.x / glyph_size.x);
|
||||||
|
glyph_offset_calc.y = glyph_offset.y * (glyph_size_downsampled.x / glyph_size.x);
|
||||||
|
}
|
||||||
|
|
||||||
// The glyph_offset.y is the y bearing, a y value that when added
|
// The glyph_offset.y is the y bearing, a y value that when added
|
||||||
// to the baseline is the offset (+y is up). Our grid goes down.
|
// to the baseline is the offset (+y is up). Our grid goes down.
|
||||||
// So we flip it with `cell_size.y - glyph_offset.y`. The glyph_baseline
|
// So we flip it with `cell_size.y - glyph_offset.y`. The glyph_baseline
|
||||||
// uniform sets our line baseline where characters "sit".
|
// uniform sets our line baseline where characters "sit".
|
||||||
vec2 glyph_offset_calc = glyph_offset;
|
glyph_offset_calc.y = cell_size.y - glyph_offset_calc.y - glyph_baseline;
|
||||||
glyph_offset_calc.y = cell_size.y - glyph_offset.y - glyph_baseline;
|
|
||||||
|
|
||||||
// Calculate the final position of the cell.
|
// Calculate the final position of the cell.
|
||||||
cell_pos = cell_pos + glyph_size * position + glyph_offset_calc;
|
cell_pos = cell_pos + glyph_size_downsampled * position + glyph_offset_calc;
|
||||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||||
|
|
||||||
// We need to convert our texture position and size to normalized
|
// We need to convert our texture position and size to normalized
|
||||||
// device coordinates (0 to 1.0) by dividing by the size of the texture.
|
// device coordinates (0 to 1.0) by dividing by the size of the texture.
|
||||||
ivec2 text_size = textureSize(text, 0);
|
ivec2 text_size;
|
||||||
|
switch(mode_in) {
|
||||||
|
case MODE_FG:
|
||||||
|
text_size = textureSize(text, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MODE_FG_COLOR:
|
||||||
|
text_size = textureSize(text_color, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
vec2 glyph_tex_pos = glyph_pos / text_size;
|
vec2 glyph_tex_pos = glyph_pos / text_size;
|
||||||
vec2 glyph_tex_size = glyph_size / text_size;
|
vec2 glyph_tex_size = glyph_size / text_size;
|
||||||
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
|
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
|
||||||
|
142
src/Grid.zig
142
src/Grid.zig
@ -42,9 +42,11 @@ vao: gl.VertexArray,
|
|||||||
ebo: gl.Buffer,
|
ebo: gl.Buffer,
|
||||||
vbo: gl.Buffer,
|
vbo: gl.Buffer,
|
||||||
texture: gl.Texture,
|
texture: gl.Texture,
|
||||||
|
texture_color: gl.Texture,
|
||||||
|
|
||||||
/// The font atlas.
|
/// The font atlas.
|
||||||
font_atlas: font.Family,
|
font_atlas: font.Family,
|
||||||
|
font_emoji: font.Family,
|
||||||
atlas_dirty: bool,
|
atlas_dirty: bool,
|
||||||
|
|
||||||
/// Whether the cursor is visible or not. This is used to control cursor
|
/// Whether the cursor is visible or not. This is used to control cursor
|
||||||
@ -160,6 +162,13 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
);
|
);
|
||||||
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
||||||
|
|
||||||
|
// Load our emoji font
|
||||||
|
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||||
|
errdefer atlas_color.deinit(alloc);
|
||||||
|
var fam_emoji = try font.Family.init(atlas_color);
|
||||||
|
errdefer fam_emoji.deinit(alloc);
|
||||||
|
try fam_emoji.loadFaceFromMemory(.regular, face_emoji_ttf, config.@"font-size");
|
||||||
|
|
||||||
// Create our shader
|
// Create our shader
|
||||||
const program = try gl.Program.createVF(
|
const program = try gl.Program.createVF(
|
||||||
@embedFile("../shaders/cell.v.glsl"),
|
@embedFile("../shaders/cell.v.glsl"),
|
||||||
@ -172,6 +181,10 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
try program.setUniform("cell_size", @Vector(2, f32){ cell_width, cell_height });
|
try program.setUniform("cell_size", @Vector(2, f32){ cell_width, cell_height });
|
||||||
try program.setUniform("glyph_baseline", cell_baseline);
|
try program.setUniform("glyph_baseline", cell_baseline);
|
||||||
|
|
||||||
|
// Set all of our texture indexes
|
||||||
|
try program.setUniform("text", 0);
|
||||||
|
try program.setUniform("text_emoji", 1);
|
||||||
|
|
||||||
// Setup our VAO
|
// Setup our VAO
|
||||||
const vao = try gl.VertexArray.create();
|
const vao = try gl.VertexArray.create();
|
||||||
errdefer vao.destroy();
|
errdefer vao.destroy();
|
||||||
@ -225,21 +238,44 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
// Build our texture
|
// Build our texture
|
||||||
const tex = try gl.Texture.create();
|
const tex = try gl.Texture.create();
|
||||||
errdefer tex.destroy();
|
errdefer tex.destroy();
|
||||||
const texbind = try tex.bind(.@"2D");
|
{
|
||||||
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
const texbind = try tex.bind(.@"2D");
|
||||||
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||||
try texbind.image2D(
|
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||||
0,
|
try texbind.image2D(
|
||||||
.Red,
|
0,
|
||||||
@intCast(c_int, atlas.size),
|
.Red,
|
||||||
@intCast(c_int, atlas.size),
|
@intCast(c_int, atlas.size),
|
||||||
0,
|
@intCast(c_int, atlas.size),
|
||||||
.Red,
|
0,
|
||||||
.UnsignedByte,
|
.Red,
|
||||||
atlas.data.ptr,
|
.UnsignedByte,
|
||||||
);
|
atlas.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build our color texture
|
||||||
|
const tex_color = try gl.Texture.create();
|
||||||
|
errdefer tex_color.destroy();
|
||||||
|
{
|
||||||
|
const texbind = try tex_color.bind(.@"2D");
|
||||||
|
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.image2D(
|
||||||
|
0,
|
||||||
|
.RGBA,
|
||||||
|
@intCast(c_int, atlas_color.size),
|
||||||
|
@intCast(c_int, atlas_color.size),
|
||||||
|
0,
|
||||||
|
.BGRA,
|
||||||
|
.UnsignedByte,
|
||||||
|
atlas_color.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Grid{
|
return Grid{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
@ -251,7 +287,9 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
.ebo = ebo,
|
.ebo = ebo,
|
||||||
.vbo = vbo,
|
.vbo = vbo,
|
||||||
.texture = tex,
|
.texture = tex,
|
||||||
|
.texture_color = tex_color,
|
||||||
.font_atlas = fam,
|
.font_atlas = fam,
|
||||||
|
.font_emoji = fam_emoji,
|
||||||
.atlas_dirty = false,
|
.atlas_dirty = false,
|
||||||
.cursor_visible = true,
|
.cursor_visible = true,
|
||||||
.cursor_style = .box,
|
.cursor_style = .box,
|
||||||
@ -263,7 +301,10 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
pub fn deinit(self: *Grid) void {
|
pub fn deinit(self: *Grid) void {
|
||||||
self.font_atlas.atlas.deinit(self.alloc);
|
self.font_atlas.atlas.deinit(self.alloc);
|
||||||
self.font_atlas.deinit(self.alloc);
|
self.font_atlas.deinit(self.alloc);
|
||||||
|
self.font_emoji.atlas.deinit(self.alloc);
|
||||||
|
self.font_emoji.deinit(self.alloc);
|
||||||
self.texture.destroy();
|
self.texture.destroy();
|
||||||
|
self.texture_color.destroy();
|
||||||
self.vbo.destroy();
|
self.vbo.destroy();
|
||||||
self.ebo.destroy();
|
self.ebo.destroy();
|
||||||
self.vao.destroy();
|
self.vao.destroy();
|
||||||
@ -456,17 +497,32 @@ pub fn updateCell(
|
|||||||
else
|
else
|
||||||
.regular;
|
.regular;
|
||||||
|
|
||||||
// Get our glyph
|
var mode: u8 = 2; // MODE_FG
|
||||||
// TODO: if we add a glyph, I think we need to rerender the texture.
|
|
||||||
|
// Get our glyph. Try our normal font atlas first.
|
||||||
const glyph = if (self.font_atlas.getGlyph(cell.char, style)) |glyph|
|
const glyph = if (self.font_atlas.getGlyph(cell.char, style)) |glyph|
|
||||||
glyph
|
glyph
|
||||||
else glyph: {
|
else glyph: {
|
||||||
self.atlas_dirty = true;
|
self.atlas_dirty = true;
|
||||||
break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char, style);
|
break :glyph self.font_atlas.addGlyph(
|
||||||
|
self.alloc,
|
||||||
|
cell.char,
|
||||||
|
style,
|
||||||
|
) catch |err| switch (err) {
|
||||||
|
error.GlyphNotFound => not_found: {
|
||||||
|
mode = 7; // MODE_FG_COLOR
|
||||||
|
break :not_found try self.font_emoji.addGlyph(
|
||||||
|
self.alloc,
|
||||||
|
cell.char,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = 2,
|
.mode = mode,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
.grid_row = @intCast(u16, y),
|
.grid_row = @intCast(u16, y),
|
||||||
.glyph_x = glyph.atlas_x,
|
.glyph_x = glyph.atlas_x,
|
||||||
@ -537,18 +593,35 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
|
|||||||
|
|
||||||
/// Updates the font texture atlas if it is dirty.
|
/// Updates the font texture atlas if it is dirty.
|
||||||
fn flushAtlas(self: *Grid) !void {
|
fn flushAtlas(self: *Grid) !void {
|
||||||
var texbind = try self.texture.bind(.@"2D");
|
{
|
||||||
defer texbind.unbind();
|
var texbind = try self.texture.bind(.@"2D");
|
||||||
try texbind.subImage2D(
|
defer texbind.unbind();
|
||||||
0,
|
try texbind.subImage2D(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@intCast(c_int, self.font_atlas.atlas.size),
|
0,
|
||||||
@intCast(c_int, self.font_atlas.atlas.size),
|
@intCast(c_int, self.font_atlas.atlas.size),
|
||||||
.Red,
|
@intCast(c_int, self.font_atlas.atlas.size),
|
||||||
.UnsignedByte,
|
.Red,
|
||||||
self.font_atlas.atlas.data.ptr,
|
.UnsignedByte,
|
||||||
);
|
self.font_atlas.atlas.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var texbind = try self.texture_color.bind(.@"2D");
|
||||||
|
defer texbind.unbind();
|
||||||
|
try texbind.subImage2D(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
@intCast(c_int, self.font_emoji.atlas.size),
|
||||||
|
@intCast(c_int, self.font_emoji.atlas.size),
|
||||||
|
.BGRA,
|
||||||
|
.UnsignedByte,
|
||||||
|
self.font_emoji.atlas.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render renders the current cell state. This will not modify any of
|
/// Render renders the current cell state. This will not modify any of
|
||||||
@ -603,11 +676,15 @@ pub fn render(self: *Grid) !void {
|
|||||||
assert(self.gl_cells_written <= self.cells.items.len);
|
assert(self.gl_cells_written <= self.cells.items.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind our texture
|
// Bind our textures
|
||||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||||
var texbind = try self.texture.bind(.@"2D");
|
var texbind = try self.texture.bind(.@"2D");
|
||||||
defer texbind.unbind();
|
defer texbind.unbind();
|
||||||
|
|
||||||
|
try gl.Texture.active(gl.c.GL_TEXTURE1);
|
||||||
|
var texbind1 = try self.texture_color.bind(.@"2D");
|
||||||
|
defer texbind1.unbind();
|
||||||
|
|
||||||
try gl.drawElementsInstanced(
|
try gl.drawElementsInstanced(
|
||||||
gl.c.GL_TRIANGLES,
|
gl.c.GL_TRIANGLES,
|
||||||
6,
|
6,
|
||||||
@ -686,3 +763,4 @@ test "GridSize update rounding" {
|
|||||||
|
|
||||||
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||||
|
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||||
|
@ -98,9 +98,7 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
|||||||
|
|
||||||
// Unknown glyph.
|
// Unknown glyph.
|
||||||
log.warn("glyph not found: {x}", .{cp});
|
log.warn("glyph not found: {x}", .{cp});
|
||||||
|
return error.GlyphNotFound;
|
||||||
// TODO: render something more identifiable than a space
|
|
||||||
break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' ');
|
|
||||||
};
|
};
|
||||||
//log.warn("glyph index: {}", .{glyph_index});
|
//log.warn("glyph index: {}", .{glyph_index});
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ pub const Parameter = enum(c_uint) {
|
|||||||
/// Internal format enum for texture images.
|
/// Internal format enum for texture images.
|
||||||
pub const InternalFormat = enum(c_int) {
|
pub const InternalFormat = enum(c_int) {
|
||||||
Red = c.GL_RED,
|
Red = c.GL_RED,
|
||||||
|
RGBA = c.GL_RGBA,
|
||||||
|
|
||||||
// There are so many more that I haven't filled in.
|
// There are so many more that I haven't filled in.
|
||||||
_,
|
_,
|
||||||
@ -55,6 +56,7 @@ pub const InternalFormat = enum(c_int) {
|
|||||||
/// Format for texture images
|
/// Format for texture images
|
||||||
pub const Format = enum(c_uint) {
|
pub const Format = enum(c_uint) {
|
||||||
Red = c.GL_RED,
|
Red = c.GL_RED,
|
||||||
|
BGRA = c.GL_BGRA,
|
||||||
|
|
||||||
// There are so many more that I haven't filled in.
|
// There are so many more that I haven't filled in.
|
||||||
_,
|
_,
|
||||||
|
Reference in New Issue
Block a user