mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Emoji and Wide Unicode Char Support (i.e. Asian characters)
Many changes to support emoji and wide chars: - Freetype built with libpng support (some emoji font use pngs) - Introduced `font.FallbackSet` structure for looking for glyphs in multiple fonts so we can find emojis - Texture atlas supports 3 and 4 channel colors (RGB/RGBA, although emojis are in BGRA) - `font.Face` supports loading colored fonts and glyphs - `font.Face` supports fonts that do not support variable pixel sizes - Shader supports two textures (greyscale and colored) - Shader will downsample glyphs that don't fit into the cell size because some fixed size emoji fonts are large - Shader supports wide (2x) cells across all types: cursor, fg, bg, underline - Terminal state supports detecting and managing wide (2x) chars by looking them up in the Unicode EastAsianWidth database and populating "spacer" cells. - Selection (and clipboard ops) support wide chars
This commit is contained in:
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@ -35,10 +35,10 @@ jobs:
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
# Run our go tests within the context of the dev shell from the flake. This
|
||||
# will ensure we have all our dependencies.
|
||||
# Cross-compile the binary. We always use static building for this
|
||||
# because its the only way to access the headers.
|
||||
- name: Test Build
|
||||
run: nix develop -c zig build -Dtarget=${{ matrix.target }}
|
||||
run: nix develop -c zig build -Dstatic=true -Dtarget=${{ matrix.target }}
|
||||
|
||||
test:
|
||||
strategy:
|
||||
@ -57,7 +57,9 @@ jobs:
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
# Run our go tests within the context of the dev shell from the flake. This
|
||||
# will ensure we have all our dependencies.
|
||||
- name: test
|
||||
run: nix develop -c zig build test
|
||||
|
||||
- name: Test Dynamic Build
|
||||
run: nix develop -c zig build -Dstatic=false
|
||||
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -19,3 +19,6 @@
|
||||
[submodule "vendor/libpng"]
|
||||
path = vendor/libpng
|
||||
url = https://github.com/glennrp/libpng.git
|
||||
[submodule "vendor/utf8proc"]
|
||||
path = vendor/utf8proc
|
||||
url = https://github.com/JuliaStrings/utf8proc.git
|
||||
|
113
build.zig
113
build.zig
@ -6,11 +6,12 @@ const glfw = @import("vendor/mach/glfw/build.zig");
|
||||
const freetype = @import("pkg/freetype/build.zig");
|
||||
const libuv = @import("pkg/libuv/build.zig");
|
||||
const libpng = @import("pkg/libpng/build.zig");
|
||||
const utf8proc = @import("pkg/utf8proc/build.zig");
|
||||
const zlib = @import("pkg/zlib/build.zig");
|
||||
const tracylib = @import("pkg/tracy/build.zig");
|
||||
const system_sdk = @import("vendor/mach/glfw/system_sdk.zig");
|
||||
|
||||
/// A build that is set to true if Tracy integration should be built.
|
||||
// Build options, see the build options help for more info.
|
||||
var tracy: bool = false;
|
||||
|
||||
pub fn build(b: *std.build.Builder) !void {
|
||||
@ -32,6 +33,12 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
"Enable Tracy integration (default true in Debug on Linux)",
|
||||
) orelse (mode == .Debug and target.isLinux());
|
||||
|
||||
const static = b.option(
|
||||
bool,
|
||||
"static",
|
||||
"Statically build as much as possible for the exe",
|
||||
) orelse true;
|
||||
|
||||
const conformance = b.option(
|
||||
[]const u8,
|
||||
"conformance",
|
||||
@ -51,7 +58,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
exe.install();
|
||||
|
||||
// Add the shared dependencies
|
||||
try addDeps(b, exe);
|
||||
try addDeps(b, exe, static);
|
||||
}
|
||||
|
||||
// term.wasm
|
||||
@ -64,7 +71,11 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
||||
wasm.setBuildMode(mode);
|
||||
wasm.setOutputDir("zig-out");
|
||||
|
||||
// Wasm-specific deps
|
||||
wasm.addPackage(tracylib.pkg);
|
||||
wasm.addPackage(utf8proc.pkg);
|
||||
_ = try utf8proc.link(b, wasm);
|
||||
|
||||
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
||||
step.dependOn(&wasm.step);
|
||||
@ -104,7 +115,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
}
|
||||
|
||||
main_test.setTarget(target);
|
||||
try addDeps(b, main_test);
|
||||
try addDeps(b, main_test, true);
|
||||
|
||||
var before = b.addLog("\x1b[" ++ color_map.get("cyan").? ++ "\x1b[" ++ color_map.get("b").? ++ "[{s} tests]" ++ "\x1b[" ++ color_map.get("d").? ++ " ----" ++ "\x1b[0m", .{"ghostty"});
|
||||
var after = b.addLog("\x1b[" ++ color_map.get("d").? ++ "–––---\n\n" ++ "\x1b[0m", .{});
|
||||
@ -122,7 +133,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
var test_ = b.addTestSource(pkg.source);
|
||||
|
||||
test_.setTarget(target);
|
||||
try addDeps(b, test_);
|
||||
try addDeps(b, test_, true);
|
||||
if (pkg.dependencies) |children| {
|
||||
test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
||||
try test_.packages.appendSlice(children);
|
||||
@ -141,53 +152,73 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
fn addDeps(
|
||||
b: *std.build.Builder,
|
||||
step: *std.build.LibExeObjStep,
|
||||
static: bool,
|
||||
) !void {
|
||||
// We always need the Zig packages
|
||||
step.addPackage(freetype.pkg);
|
||||
step.addPackage(glfw.pkg);
|
||||
step.addPackage(libuv.pkg);
|
||||
step.addPackage(utf8proc.pkg);
|
||||
|
||||
// We always statically compile glad
|
||||
step.addIncludeDir("vendor/glad/include/");
|
||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
||||
|
||||
// Dependencies of other dependencies
|
||||
const zlib_step = try zlib.link(b, step);
|
||||
const libpng_step = try libpng.link(b, step, .{
|
||||
.zlib = .{
|
||||
.step = zlib_step,
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
|
||||
// Freetype
|
||||
step.addPackage(freetype.pkg);
|
||||
_ = try freetype.link(b, step, .{
|
||||
.libpng = freetype.Options.Libpng{
|
||||
.enabled = true,
|
||||
.step = libpng_step,
|
||||
.include = &libpng.include_paths,
|
||||
},
|
||||
|
||||
.zlib = .{
|
||||
.enabled = true,
|
||||
.step = zlib_step,
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
|
||||
// Glfw
|
||||
step.addPackage(glfw.pkg);
|
||||
glfw.link(b, step, .{
|
||||
.metal = false,
|
||||
.opengl = false, // Found at runtime
|
||||
});
|
||||
|
||||
// Libuv
|
||||
step.addPackage(libuv.pkg);
|
||||
var libuv_step = try libuv.link(b, step);
|
||||
system_sdk.include(b, libuv_step, .{});
|
||||
|
||||
// Tracy
|
||||
step.addPackage(tracylib.pkg);
|
||||
if (tracy) {
|
||||
var tracy_step = try tracylib.link(b, step);
|
||||
system_sdk.include(b, tracy_step, .{});
|
||||
}
|
||||
|
||||
// utf8proc
|
||||
_ = try utf8proc.link(b, step);
|
||||
|
||||
// Glfw
|
||||
glfw.link(b, step, .{
|
||||
.metal = false,
|
||||
.opengl = false, // Found at runtime
|
||||
});
|
||||
|
||||
// Dynamic link
|
||||
if (!static) {
|
||||
step.addIncludePath(freetype.include_path_self);
|
||||
step.linkSystemLibrary("bzip2");
|
||||
step.linkSystemLibrary("freetype2");
|
||||
step.linkSystemLibrary("libpng");
|
||||
step.linkSystemLibrary("libuv");
|
||||
step.linkSystemLibrary("zlib");
|
||||
}
|
||||
|
||||
// Other dependencies, we may dynamically link
|
||||
if (static) {
|
||||
const zlib_step = try zlib.link(b, step);
|
||||
const libpng_step = try libpng.link(b, step, .{
|
||||
.zlib = .{
|
||||
.step = zlib_step,
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
|
||||
// Freetype
|
||||
_ = try freetype.link(b, step, .{
|
||||
.libpng = freetype.Options.Libpng{
|
||||
.enabled = true,
|
||||
.step = libpng_step,
|
||||
.include = &libpng.include_paths,
|
||||
},
|
||||
|
||||
.zlib = .{
|
||||
.enabled = true,
|
||||
.step = zlib_step,
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
|
||||
// Libuv
|
||||
var libuv_step = try libuv.link(b, step);
|
||||
system_sdk.include(b, libuv_step, .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn conformanceSteps(
|
||||
|
@ -19,18 +19,26 @@
|
||||
, freetype
|
||||
, libpng
|
||||
, libGL
|
||||
, libuv
|
||||
, libX11
|
||||
, libXcursor
|
||||
, libXext
|
||||
, libXi
|
||||
, libXinerama
|
||||
, libXrandr
|
||||
, zlib
|
||||
}:
|
||||
let
|
||||
# See package.nix. Keep in sync.
|
||||
rpathLibs = [
|
||||
libGL
|
||||
] ++ lib.optionals stdenv.isLinux [
|
||||
bzip2
|
||||
freetype
|
||||
libpng
|
||||
libuv
|
||||
zlib
|
||||
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
@ -61,6 +69,12 @@ in mkShell rec {
|
||||
buildInputs = [
|
||||
# TODO: non-linux
|
||||
] ++ lib.optionals stdenv.isLinux [
|
||||
bzip2
|
||||
freetype
|
||||
libpng
|
||||
libuv
|
||||
zlib
|
||||
|
||||
libX11
|
||||
libXcursor
|
||||
libXext
|
||||
|
@ -3,7 +3,7 @@ const std = @import("std");
|
||||
/// Directories with our includes.
|
||||
const root = thisDir() ++ "../../../vendor/freetype/";
|
||||
const include_path = root ++ "include";
|
||||
const include_path_self = thisDir();
|
||||
pub const include_path_self = thisDir();
|
||||
|
||||
pub const pkg = std.build.Pkg{
|
||||
.name = "freetype",
|
||||
|
51
pkg/utf8proc/build.zig
Normal file
51
pkg/utf8proc/build.zig
Normal file
@ -0,0 +1,51 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Directories with our includes.
|
||||
const root = thisDir() ++ "../../../vendor/utf8proc/";
|
||||
const include_path = root;
|
||||
|
||||
pub const include_paths = .{include_path};
|
||||
|
||||
pub const pkg = std.build.Pkg{
|
||||
.name = "utf8proc",
|
||||
.source = .{ .path = thisDir() ++ "/main.zig" },
|
||||
};
|
||||
|
||||
fn thisDir() []const u8 {
|
||||
return std.fs.path.dirname(@src().file) orelse ".";
|
||||
}
|
||||
|
||||
pub fn link(b: *std.build.Builder, step: *std.build.LibExeObjStep) !*std.build.LibExeObjStep {
|
||||
const lib = try buildLib(b, step);
|
||||
step.linkLibrary(lib);
|
||||
step.addIncludePath(include_path);
|
||||
return lib;
|
||||
}
|
||||
|
||||
pub fn buildLib(
|
||||
b: *std.build.Builder,
|
||||
step: *std.build.LibExeObjStep,
|
||||
) !*std.build.LibExeObjStep {
|
||||
const lib = b.addStaticLibrary("utf8proc", null);
|
||||
lib.setTarget(step.target);
|
||||
lib.setBuildMode(step.build_mode);
|
||||
|
||||
// Include
|
||||
lib.addIncludePath(include_path);
|
||||
|
||||
// Link
|
||||
lib.linkLibC();
|
||||
|
||||
// Compile
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
|
||||
// C files
|
||||
lib.addCSourceFiles(srcs, flags.items);
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
||||
const srcs = &.{
|
||||
root ++ "utf8proc.c",
|
||||
};
|
3
pkg/utf8proc/c.zig
Normal file
3
pkg/utf8proc/c.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub usingnamespace @cImport({
|
||||
@cInclude("utf8proc.h");
|
||||
});
|
8
pkg/utf8proc/main.zig
Normal file
8
pkg/utf8proc/main.zig
Normal file
@ -0,0 +1,8 @@
|
||||
pub const c = @import("c.zig");
|
||||
|
||||
/// Given a codepoint, return a character width analogous to `wcwidth(codepoint)`,
|
||||
/// except that a width of 0 is returned for non-printable codepoints
|
||||
/// instead of -1 as in `wcwidth`.
|
||||
pub fn charwidth(codepoint: u21) u8 {
|
||||
return @intCast(u8, c.utf8proc_charwidth(@intCast(i32, codepoint)));
|
||||
}
|
@ -18,6 +18,7 @@ layout(location = 0) out vec4 out_FragColor;
|
||||
|
||||
// Font texture
|
||||
uniform sampler2D text;
|
||||
uniform sampler2D text_color;
|
||||
|
||||
// Dimensions of the cell
|
||||
uniform vec2 cell_size;
|
||||
@ -25,22 +26,30 @@ uniform vec2 cell_size;
|
||||
// See vertex shader
|
||||
const uint MODE_BG = 1u;
|
||||
const uint MODE_FG = 2u;
|
||||
const uint MODE_FG_COLOR = 7u;
|
||||
const uint MODE_CURSOR_RECT = 3u;
|
||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||
const uint MODE_CURSOR_BAR = 5u;
|
||||
const uint MODE_UNDERLINE = 6u;
|
||||
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
||||
|
||||
void main() {
|
||||
float a;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_BG:
|
||||
out_FragColor = color;
|
||||
break;
|
||||
|
||||
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);
|
||||
break;
|
||||
|
||||
case MODE_FG_COLOR:
|
||||
out_FragColor = texture(text_color, glyph_tex_coords);
|
||||
break;
|
||||
|
||||
case MODE_CURSOR_RECT:
|
||||
out_FragColor = color;
|
||||
break;
|
||||
|
@ -6,10 +6,12 @@
|
||||
// NOTE: this must be kept in sync with the fragment shader
|
||||
const uint MODE_BG = 1u;
|
||||
const uint MODE_FG = 2u;
|
||||
const uint MODE_FG_COLOR = 7u;
|
||||
const uint MODE_CURSOR_RECT = 3u;
|
||||
const uint MODE_CURSOR_RECT_HOLLOW = 4u;
|
||||
const uint MODE_CURSOR_BAR = 5u;
|
||||
const uint MODE_UNDERLINE = 6u;
|
||||
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
||||
|
||||
// The grid coordinates (x, y) where x < columns and y < rows
|
||||
layout (location = 0) in vec2 grid_coord;
|
||||
@ -50,6 +52,7 @@ flat out vec2 screen_cell_pos;
|
||||
flat out uint mode;
|
||||
|
||||
uniform sampler2D text;
|
||||
uniform sampler2D text_color;
|
||||
uniform vec2 cell_size;
|
||||
uniform mat4 projection;
|
||||
uniform float glyph_baseline;
|
||||
@ -75,8 +78,12 @@ uniform float glyph_baseline;
|
||||
*/
|
||||
|
||||
void main() {
|
||||
// We always forward our mode
|
||||
mode = mode_in;
|
||||
// Remove any masks from our mode
|
||||
uint mode_unmasked = mode_in & ~MODE_WIDE_MASK;
|
||||
|
||||
// We always forward our mode unmasked because the fragment
|
||||
// shader doesn't use any of the masks.
|
||||
mode = mode_unmasked;
|
||||
|
||||
// Top-left cell coordinates converted to world space
|
||||
// Example: (1,0) with a 30 wide cell is converted to (30,0)
|
||||
@ -101,32 +108,59 @@ void main() {
|
||||
position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
|
||||
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
|
||||
|
||||
switch (mode_in) {
|
||||
// Scaled for wide chars
|
||||
vec2 cell_size_scaled = cell_size;
|
||||
if ((mode_in & MODE_WIDE_MASK) == MODE_WIDE_MASK) {
|
||||
cell_size_scaled.x = cell_size_scaled.x * 2;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case MODE_BG:
|
||||
// Calculate the final position of our cell in world space.
|
||||
// We have to add our cell size since our vertices are offset
|
||||
// one cell up and to the left. (Do the math to verify yourself)
|
||||
cell_pos = cell_pos + cell_size * position;
|
||||
cell_pos = cell_pos + cell_size_scaled * position;
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = bg_color_in / 255.0;
|
||||
break;
|
||||
|
||||
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_scaled.x;
|
||||
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
|
||||
// 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
|
||||
// uniform sets our line baseline where characters "sit".
|
||||
vec2 glyph_offset_calc = glyph_offset;
|
||||
glyph_offset_calc.y = cell_size.y - glyph_offset.y - glyph_baseline;
|
||||
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y - glyph_baseline;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
ivec2 text_size = textureSize(text, 0);
|
||||
ivec2 text_size;
|
||||
switch(mode) {
|
||||
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_size = glyph_size / text_size;
|
||||
glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
|
||||
@ -137,7 +171,7 @@ void main() {
|
||||
|
||||
case MODE_CURSOR_RECT:
|
||||
// Same as background since we're taking up the whole cell.
|
||||
cell_pos = cell_pos + cell_size * position;
|
||||
cell_pos = cell_pos + cell_size_scaled * position;
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = bg_color_in / 255.0;
|
||||
@ -148,7 +182,7 @@ void main() {
|
||||
screen_cell_pos = cell_pos;
|
||||
|
||||
// Same as background since we're taking up the whole cell.
|
||||
cell_pos = cell_pos + cell_size * position;
|
||||
cell_pos = cell_pos + cell_size_scaled * position;
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = bg_color_in / 255.0;
|
||||
@ -168,11 +202,11 @@ void main() {
|
||||
case MODE_UNDERLINE:
|
||||
// Make the underline a smaller version of our cell
|
||||
// TODO: use real font underline thickness
|
||||
vec2 underline_size = vec2(cell_size.x, cell_size.y*0.05);
|
||||
vec2 underline_size = vec2(cell_size_scaled.x, cell_size_scaled.y*0.05);
|
||||
|
||||
// Position our underline so that it is midway between the glyph
|
||||
// baseline and the bottom of the cell.
|
||||
vec2 underline_offset = vec2(cell_size.x, cell_size.y - (glyph_baseline / 2));
|
||||
vec2 underline_offset = vec2(cell_size_scaled.x, cell_size_scaled.y - (glyph_baseline / 2));
|
||||
|
||||
// Go to the bottom of the cell, take away the size of the
|
||||
// underline, and that is our position. We also float it slightly
|
||||
|
244
src/Grid.zig
244
src/Grid.zig
@ -42,9 +42,10 @@ vao: gl.VertexArray,
|
||||
ebo: gl.Buffer,
|
||||
vbo: gl.Buffer,
|
||||
texture: gl.Texture,
|
||||
texture_color: gl.Texture,
|
||||
|
||||
/// The font atlas.
|
||||
font_atlas: font.Family,
|
||||
font_set: font.FallbackSet,
|
||||
atlas_dirty: bool,
|
||||
|
||||
/// Whether the cursor is visible or not. This is used to control cursor
|
||||
@ -108,7 +109,30 @@ const GPUCell = struct {
|
||||
bg_a: u8,
|
||||
|
||||
/// uint mode
|
||||
mode: u8,
|
||||
mode: GPUCellMode,
|
||||
};
|
||||
|
||||
const GPUCellMode = enum(u8) {
|
||||
bg = 1,
|
||||
fg = 2,
|
||||
fg_color = 7,
|
||||
cursor_rect = 3,
|
||||
cursor_rect_hollow = 4,
|
||||
cursor_bar = 5,
|
||||
underline = 6,
|
||||
|
||||
wide_mask = 0b1000_0000,
|
||||
|
||||
// Non-exhaustive because masks change it
|
||||
_,
|
||||
|
||||
/// Apply a mask to the mode.
|
||||
pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode {
|
||||
return @intToEnum(
|
||||
GPUCellMode,
|
||||
@enumToInt(self) | @enumToInt(m),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
@ -116,10 +140,32 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
// font atlas with all the visible ASCII characters since they are common.
|
||||
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
||||
errdefer atlas.deinit(alloc);
|
||||
var fam = try font.Family.init(atlas);
|
||||
errdefer fam.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size");
|
||||
try fam.loadFaceFromMemory(.bold, face_bold_ttf, config.@"font-size");
|
||||
|
||||
// Load our emoji font
|
||||
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||
errdefer atlas_color.deinit(alloc);
|
||||
|
||||
// Build our fallback set so we can look up all codepoints
|
||||
var font_set: font.FallbackSet = .{};
|
||||
try font_set.families.ensureTotalCapacity(alloc, 2);
|
||||
errdefer font_set.deinit(alloc);
|
||||
|
||||
// Regular text
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
var fam = try font.Family.init(atlas);
|
||||
errdefer fam.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size");
|
||||
try fam.loadFaceFromMemory(.bold, face_bold_ttf, config.@"font-size");
|
||||
break :fam fam;
|
||||
});
|
||||
|
||||
// Emoji
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
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");
|
||||
break :fam fam_emoji;
|
||||
});
|
||||
|
||||
// Load all visible ASCII characters and build our cell width based on
|
||||
// the widest character that we see.
|
||||
@ -127,9 +173,9 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
var cell_width: f32 = 0;
|
||||
var i: u8 = 32;
|
||||
while (i <= 126) : (i += 1) {
|
||||
const glyph = try fam.addGlyph(alloc, i, .regular);
|
||||
if (glyph.advance_x > cell_width) {
|
||||
cell_width = @ceil(glyph.advance_x);
|
||||
const goa = try font_set.getOrAddGlyph(alloc, i, .regular);
|
||||
if (goa.glyph.advance_x > cell_width) {
|
||||
cell_width = @ceil(goa.glyph.advance_x);
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,11 +185,13 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
// 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: {
|
||||
const fam = &font_set.families.items[0];
|
||||
|
||||
// This is the height reported by the font face
|
||||
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
|
||||
|
||||
// Determine the height of the underscore char
|
||||
const glyph = fam.getGlyph('_', .regular).?;
|
||||
const glyph = font_set.families.items[0].getGlyph('_', .regular).?;
|
||||
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
|
||||
res -= glyph.offset_y;
|
||||
res += @intCast(i32, glyph.height);
|
||||
@ -154,10 +202,13 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
|
||||
break :cell_height @intToFloat(f32, res);
|
||||
};
|
||||
const cell_baseline = cell_height - @intToFloat(
|
||||
f32,
|
||||
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
|
||||
);
|
||||
const cell_baseline = cell_baseline: {
|
||||
const fam = &font_set.families.items[0];
|
||||
break :cell_baseline cell_height - @intToFloat(
|
||||
f32,
|
||||
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
|
||||
);
|
||||
};
|
||||
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
||||
|
||||
// Create our shader
|
||||
@ -172,6 +223,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("glyph_baseline", cell_baseline);
|
||||
|
||||
// Set all of our texture indexes
|
||||
try program.setUniform("text", 0);
|
||||
try program.setUniform("text_color", 1);
|
||||
|
||||
// Setup our VAO
|
||||
const vao = try gl.VertexArray.create();
|
||||
errdefer vao.destroy();
|
||||
@ -225,21 +280,44 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
// Build our texture
|
||||
const tex = try gl.Texture.create();
|
||||
errdefer tex.destroy();
|
||||
const texbind = try tex.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,
|
||||
.Red,
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
0,
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
atlas.data.ptr,
|
||||
);
|
||||
{
|
||||
const texbind = try tex.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,
|
||||
.Red,
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
0,
|
||||
.Red,
|
||||
.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{
|
||||
.alloc = alloc,
|
||||
@ -251,7 +329,8 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
.ebo = ebo,
|
||||
.vbo = vbo,
|
||||
.texture = tex,
|
||||
.font_atlas = fam,
|
||||
.texture_color = tex_color,
|
||||
.font_set = font_set,
|
||||
.atlas_dirty = false,
|
||||
.cursor_visible = true,
|
||||
.cursor_style = .box,
|
||||
@ -261,9 +340,14 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Grid) void {
|
||||
self.font_atlas.atlas.deinit(self.alloc);
|
||||
self.font_atlas.deinit(self.alloc);
|
||||
for (self.font_set.families.items) |*family| {
|
||||
family.atlas.deinit(self.alloc);
|
||||
family.deinit(self.alloc);
|
||||
}
|
||||
self.font_set.deinit(self.alloc);
|
||||
|
||||
self.texture.destroy();
|
||||
self.texture_color.destroy();
|
||||
self.vbo.destroy();
|
||||
self.ebo.destroy();
|
||||
self.vao.destroy();
|
||||
@ -338,8 +422,19 @@ pub fn finalizeCells(self: *Grid, term: Terminal) !void {
|
||||
fn addCursor(self: *Grid, term: Terminal) void {
|
||||
// Add the cursor
|
||||
if (self.cursor_visible and term.screen.viewportIsBottom()) {
|
||||
const cell = term.screen.getCell(
|
||||
term.screen.cursor.y,
|
||||
term.screen.cursor.x,
|
||||
);
|
||||
|
||||
var mode: GPUCellMode = @intToEnum(
|
||||
GPUCellMode,
|
||||
@enumToInt(self.cursor_style),
|
||||
);
|
||||
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = @enumToInt(self.cursor_style),
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, term.screen.cursor.x),
|
||||
.grid_row = @intCast(u16, term.screen.cursor.y),
|
||||
.fg_r = 0,
|
||||
@ -415,6 +510,9 @@ pub fn updateCell(
|
||||
break :colors res;
|
||||
};
|
||||
|
||||
// If we are a trailing spacer, we never render anything.
|
||||
if (cell.attrs.wide_spacer_tail == 1) return true;
|
||||
|
||||
// Calculate the amount of space we need in the cells list.
|
||||
const needed = needed: {
|
||||
var i: usize = 0;
|
||||
@ -427,8 +525,11 @@ pub fn updateCell(
|
||||
|
||||
// If the cell has a background, we always draw it.
|
||||
if (colors.bg) |rgb| {
|
||||
var mode: GPUCellMode = .bg;
|
||||
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = 1,
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.glyph_x = 0,
|
||||
@ -456,17 +557,19 @@ pub fn updateCell(
|
||||
else
|
||||
.regular;
|
||||
|
||||
// Get our glyph
|
||||
// TODO: if we add a glyph, I think we need to rerender the texture.
|
||||
const glyph = if (self.font_atlas.getGlyph(cell.char, style)) |glyph|
|
||||
glyph
|
||||
else glyph: {
|
||||
self.atlas_dirty = true;
|
||||
break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char, style);
|
||||
};
|
||||
var mode: GPUCellMode = .fg;
|
||||
|
||||
// Get our glyph. Try our normal font atlas first.
|
||||
const goa = try self.font_set.getOrAddGlyph(self.alloc, cell.char, style);
|
||||
if (!goa.found_existing) self.atlas_dirty = true;
|
||||
if (goa.family == 1) mode = .fg_color;
|
||||
const glyph = goa.glyph;
|
||||
|
||||
// If the cell is wide, we need to note that in the mode
|
||||
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = 2,
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.glyph_x = glyph.atlas_x,
|
||||
@ -487,8 +590,11 @@ pub fn updateCell(
|
||||
}
|
||||
|
||||
if (cell.attrs.underline == 1) {
|
||||
var mode: GPUCellMode = .underline;
|
||||
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = 6, // underline
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(u16, x),
|
||||
.grid_row = @intCast(u16, y),
|
||||
.glyph_x = 0,
|
||||
@ -537,18 +643,37 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
|
||||
|
||||
/// Updates the font texture atlas if it is dirty.
|
||||
fn flushAtlas(self: *Grid) !void {
|
||||
var texbind = try self.texture.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
try texbind.subImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intCast(c_int, self.font_atlas.atlas.size),
|
||||
@intCast(c_int, self.font_atlas.atlas.size),
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
self.font_atlas.atlas.data.ptr,
|
||||
);
|
||||
{
|
||||
const atlas = &self.font_set.families.items[0].atlas;
|
||||
var texbind = try self.texture.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
try texbind.subImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
atlas.data.ptr,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const atlas = &self.font_set.families.items[1].atlas;
|
||||
var texbind = try self.texture_color.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
try texbind.subImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
.BGRA,
|
||||
.UnsignedByte,
|
||||
atlas.data.ptr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render renders the current cell state. This will not modify any of
|
||||
@ -603,11 +728,15 @@ pub fn render(self: *Grid) !void {
|
||||
assert(self.gl_cells_written <= self.cells.items.len);
|
||||
}
|
||||
|
||||
// Bind our texture
|
||||
// Bind our textures
|
||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||
var texbind = try self.texture.bind(.@"2D");
|
||||
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(
|
||||
gl.c.GL_TRIANGLES,
|
||||
6,
|
||||
@ -686,3 +815,4 @@ test "GridSize update rounding" {
|
||||
|
||||
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
|
||||
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
|
||||
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
|
||||
|
@ -58,8 +58,31 @@ pub fn loadFaceFromMemory(self: *Face, source: [:0]const u8, size: u32) !void {
|
||||
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;
|
||||
// If we have fixed sizes, we just have to try to pick the one closest
|
||||
// to what the user requested. Otherwise, we can choose an arbitrary
|
||||
// pixel size.
|
||||
if (!ftc.FT_HAS_FIXED_SIZES(self.ft_face)) {
|
||||
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
} else try self.selectSizeNearest(size);
|
||||
}
|
||||
|
||||
/// Selects the fixed size in the loaded face that is closest to the
|
||||
/// requested pixel size.
|
||||
fn selectSizeNearest(self: *Face, size: u32) !void {
|
||||
var i: usize = 0;
|
||||
var best_i: usize = 0;
|
||||
var best_diff: i32 = 0;
|
||||
while (i < self.ft_face.*.num_fixed_sizes) : (i += 1) {
|
||||
const diff = @intCast(i32, size) - @intCast(i32, self.ft_face.*.available_sizes[i].width);
|
||||
if (i == 0 or diff < best_diff) {
|
||||
best_diff = diff;
|
||||
best_i = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (ftc.FT_Select_Size(self.ft_face, @intCast(c_int, best_i)) != ftok)
|
||||
return error.FaceSelectSizeFailed;
|
||||
}
|
||||
|
||||
/// Load a glyph for this face. The codepoint can be either a u8 or
|
||||
@ -69,26 +92,40 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
||||
|
||||
// We need a UTF32 codepoint for freetype
|
||||
const glyph_index = glyph_index: {
|
||||
// log.warn("glyph load: {x}", .{cp});
|
||||
//log.warn("glyph load: {x}", .{cp});
|
||||
const idx = ftc.FT_Get_Char_Index(self.ft_face, cp);
|
||||
if (idx > 0) break :glyph_index idx;
|
||||
|
||||
// Unknown glyph.
|
||||
log.warn("glyph not found: {x}", .{cp});
|
||||
|
||||
// TODO: render something more identifiable than a space
|
||||
break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' ');
|
||||
//log.warn("glyph not found: {x}", .{cp});
|
||||
return error.GlyphNotFound;
|
||||
};
|
||||
//log.warn("glyph index: {}", .{glyph_index});
|
||||
|
||||
// If our glyph has color, we want to render the color
|
||||
var load_flags: c_int = ftc.FT_LOAD_RENDER;
|
||||
if (ftc.FT_HAS_COLOR(self.ft_face)) load_flags |= @intCast(c_int, ftc.FT_LOAD_COLOR);
|
||||
|
||||
if (ftc.FT_Load_Glyph(
|
||||
self.ft_face,
|
||||
glyph_index,
|
||||
ftc.FT_LOAD_RENDER,
|
||||
load_flags,
|
||||
) != ftok) return error.LoadGlyphFailed;
|
||||
|
||||
const glyph = self.ft_face.*.glyph;
|
||||
const bitmap = glyph.*.bitmap;
|
||||
assert(bitmap.pixel_mode == ftc.FT_PIXEL_MODE_GRAY);
|
||||
|
||||
// Ensure we know how to work with the font format. And assure that
|
||||
// or color depth is as expected on the texture atlas.
|
||||
const format: Atlas.Format = switch (bitmap.pixel_mode) {
|
||||
ftc.FT_PIXEL_MODE_GRAY => .greyscale,
|
||||
ftc.FT_PIXEL_MODE_BGRA => .rgba,
|
||||
else => {
|
||||
log.warn("pixel mode={}", .{bitmap.pixel_mode});
|
||||
@panic("unsupported pixel mode");
|
||||
},
|
||||
};
|
||||
assert(atlas.format == format);
|
||||
|
||||
const src_w = bitmap.width;
|
||||
const src_h = bitmap.rows;
|
||||
@ -99,24 +136,26 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
||||
|
||||
// If we have data, copy it into the atlas
|
||||
if (region.width > 0 and region.height > 0) {
|
||||
const depth = @enumToInt(format);
|
||||
|
||||
// We can avoid a buffer copy if our atlas width and bitmap
|
||||
// width match and the bitmap pitch is just the width (meaning
|
||||
// the data is tightly packed).
|
||||
const needs_copy = !(tgt_w == bitmap.width and bitmap.width == bitmap.pitch);
|
||||
const needs_copy = !(tgt_w == bitmap.width and (bitmap.width * depth) == bitmap.pitch);
|
||||
|
||||
// If we need to copy the data, we copy it into a temporary buffer.
|
||||
const buffer = if (needs_copy) buffer: {
|
||||
var temp = try alloc.alloc(u8, tgt_w * tgt_h);
|
||||
var temp = try alloc.alloc(u8, tgt_w * tgt_h * depth);
|
||||
var dst_ptr = temp;
|
||||
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..];
|
||||
std.mem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]);
|
||||
dst_ptr = dst_ptr[tgt_w * depth ..];
|
||||
src_ptr += @intCast(usize, bitmap.pitch);
|
||||
}
|
||||
break :buffer temp;
|
||||
} else bitmap.buffer[0..(tgt_w * tgt_h)];
|
||||
} else bitmap.buffer[0..(tgt_w * tgt_h * depth)];
|
||||
defer if (buffer.ptr != bitmap.buffer) alloc.free(buffer);
|
||||
|
||||
// Write the glyph information into the atlas
|
||||
@ -149,6 +188,8 @@ fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
|
||||
}
|
||||
|
||||
test {
|
||||
const testFont = @import("test.zig").fontRegular;
|
||||
|
||||
var ft_lib: ftc.FT_Library = undefined;
|
||||
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
|
||||
return error.FreeTypeInitFailed;
|
||||
@ -170,4 +211,21 @@ test {
|
||||
}
|
||||
}
|
||||
|
||||
const testFont = @embedFile("res/Inconsolata-Regular.ttf");
|
||||
test "color emoji" {
|
||||
const testFont = @import("test.zig").fontEmoji;
|
||||
|
||||
var ft_lib: ftc.FT_Library = undefined;
|
||||
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
|
||||
return error.FreeTypeInitFailed;
|
||||
defer _ = ftc.FT_Done_FreeType(ft_lib);
|
||||
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try Atlas.init(alloc, 512, .rgba);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var font = try init(ft_lib);
|
||||
defer font.deinit();
|
||||
|
||||
try font.loadFaceFromMemory(testFont, 48);
|
||||
_ = try font.loadGlyph(alloc, &atlas, '🥸');
|
||||
}
|
||||
|
157
src/font/FallbackSet.zig
Normal file
157
src/font/FallbackSet.zig
Normal file
@ -0,0 +1,157 @@
|
||||
//! FallbackSet represents a set of families in priority order to load a glyph.
|
||||
//! This can be used to merge multiple font families together to find a glyph
|
||||
//! for a codepoint.
|
||||
const FallbackSet = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ftc = @import("freetype").c;
|
||||
const Atlas = @import("../Atlas.zig");
|
||||
const Family = @import("main.zig").Family;
|
||||
const Glyph = @import("main.zig").Glyph;
|
||||
const Style = @import("main.zig").Style;
|
||||
const codepoint = @import("main.zig").codepoint;
|
||||
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const log = std.log.scoped(.font_fallback);
|
||||
|
||||
/// The families to look for in order. This should be managed directly
|
||||
/// by the caller of the set. Deinit will deallocate this.
|
||||
families: std.ArrayListUnmanaged(Family) = .{},
|
||||
|
||||
/// A quick lookup that points directly to the family that loaded a glyph.
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, usize) = .{},
|
||||
|
||||
const GlyphKey = struct {
|
||||
style: Style,
|
||||
codepoint: u32,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *FallbackSet, alloc: Allocator) void {
|
||||
self.families.deinit(alloc);
|
||||
self.glyphs.deinit(alloc);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub const GetOrAdd = struct {
|
||||
/// Index of the family where the glyph was loaded from
|
||||
family: usize,
|
||||
|
||||
/// True if the glyph was found or whether it was newly loaded
|
||||
found_existing: bool,
|
||||
|
||||
/// The glyph
|
||||
glyph: *Glyph,
|
||||
};
|
||||
|
||||
pub fn getOrAddGlyph(
|
||||
self: *FallbackSet,
|
||||
alloc: Allocator,
|
||||
v: anytype,
|
||||
style: Style,
|
||||
) !GetOrAdd {
|
||||
assert(self.families.items.len > 0);
|
||||
|
||||
// We need a UTF32 codepoint
|
||||
const utf32 = codepoint(v);
|
||||
|
||||
// If we have this already, load it directly
|
||||
const glyphKey: GlyphKey = .{ .style = style, .codepoint = utf32 };
|
||||
const gop = try self.glyphs.getOrPut(alloc, glyphKey);
|
||||
if (gop.found_existing) {
|
||||
const i = gop.value_ptr.*;
|
||||
assert(i < self.families.items.len);
|
||||
return GetOrAdd{
|
||||
.family = i,
|
||||
.found_existing = true,
|
||||
.glyph = self.families.items[i].getGlyph(v, style) orelse unreachable,
|
||||
};
|
||||
}
|
||||
errdefer _ = self.glyphs.remove(glyphKey);
|
||||
|
||||
// Go through each familiy and look for a matching glyph
|
||||
var fam_i: ?usize = 0;
|
||||
const glyph = glyph: {
|
||||
for (self.families.items) |*family, i| {
|
||||
fam_i = i;
|
||||
|
||||
// If this family already has it loaded, return it.
|
||||
if (family.getGlyph(v, style)) |glyph| break :glyph glyph;
|
||||
|
||||
// Try to load it.
|
||||
if (family.addGlyph(alloc, v, style)) |glyph|
|
||||
break :glyph glyph
|
||||
else |err| switch (err) {
|
||||
// TODO: this probably doesn't belong here and should
|
||||
// be higher level... but how?
|
||||
error.AtlasFull => {
|
||||
try family.atlas.grow(alloc, family.atlas.size * 2);
|
||||
break :glyph try family.addGlyph(alloc, v, style);
|
||||
},
|
||||
|
||||
error.GlyphNotFound => {},
|
||||
else => return err,
|
||||
}
|
||||
}
|
||||
|
||||
// If we are regular, we use a fallback character
|
||||
log.warn("glyph not found, using fallback. codepoint={x}", .{utf32});
|
||||
fam_i = null;
|
||||
break :glyph try self.families.items[0].addGlyph(alloc, ' ', style);
|
||||
};
|
||||
|
||||
// If we found a real value, then cache it.
|
||||
// TODO: support caching fallbacks too
|
||||
if (fam_i) |i|
|
||||
gop.value_ptr.* = i
|
||||
else
|
||||
_ = self.glyphs.remove(glyphKey);
|
||||
|
||||
return GetOrAdd{
|
||||
.family = fam_i orelse 0,
|
||||
.glyph = glyph,
|
||||
|
||||
// Technically possible that we found this in a cache...
|
||||
.found_existing = false,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
const fontRegular = @import("test.zig").fontRegular;
|
||||
const fontEmoji = @import("test.zig").fontEmoji;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: FallbackSet = .{};
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .greyscale));
|
||||
try fam.loadFaceFromMemory(.regular, fontRegular, 48);
|
||||
break :fam fam;
|
||||
});
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .rgba));
|
||||
try fam.loadFaceFromMemory(.regular, fontEmoji, 48);
|
||||
break :fam fam;
|
||||
});
|
||||
|
||||
defer {
|
||||
for (set.families.items) |*family| {
|
||||
family.atlas.deinit(alloc);
|
||||
family.deinit(alloc);
|
||||
}
|
||||
set.deinit(alloc);
|
||||
}
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
_ = try set.getOrAddGlyph(alloc, i, .regular);
|
||||
}
|
||||
|
||||
// Emoji should work
|
||||
_ = try set.getOrAddGlyph(alloc, '🥸', .regular);
|
||||
_ = try set.getOrAddGlyph(alloc, '🥸', .bold);
|
||||
}
|
@ -11,6 +11,7 @@ const Face = @import("main.zig").Face;
|
||||
const Glyph = @import("main.zig").Glyph;
|
||||
const Style = @import("main.zig").Style;
|
||||
const testFont = @import("test.zig").fontRegular;
|
||||
const codepoint = @import("main.zig").codepoint;
|
||||
|
||||
const log = std.log.scoped(.font_family);
|
||||
|
||||
@ -137,22 +138,10 @@ pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Gly
|
||||
errdefer _ = self.glyphs.remove(glyphKey);
|
||||
|
||||
// Get the glyph and add it to the atlas.
|
||||
// TODO: handle glyph not found
|
||||
gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32);
|
||||
return gop.value_ptr;
|
||||
}
|
||||
|
||||
/// Returns the UTF-32 codepoint for the given value.
|
||||
fn codepoint(v: anytype) u32 {
|
||||
// We need a UTF32 codepoint for freetype
|
||||
return switch (@TypeOf(v)) {
|
||||
u32 => v,
|
||||
comptime_int, u8 => @intCast(u32, v),
|
||||
[]const u8 => @intCast(u32, try std.unicode.utfDecode(v)),
|
||||
else => @compileError("invalid codepoint type"),
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -1,6 +1,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Face = @import("Face.zig");
|
||||
pub const Family = @import("Family.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const FallbackSet = @import("FallbackSet.zig");
|
||||
|
||||
/// Embedded fonts (for now)
|
||||
pub const fontRegular = @import("test.zig").fontRegular;
|
||||
@ -14,8 +17,20 @@ pub const Style = enum {
|
||||
bold_italic,
|
||||
};
|
||||
|
||||
/// Returns the UTF-32 codepoint for the given value.
|
||||
pub fn codepoint(v: anytype) u32 {
|
||||
// We need a UTF32 codepoint for freetype
|
||||
return switch (@TypeOf(v)) {
|
||||
u32 => v,
|
||||
comptime_int, u8 => @intCast(u32, v),
|
||||
[]const u8 => @intCast(u32, try std.unicode.utfDecode(v)),
|
||||
else => @compileError("invalid codepoint type"),
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Face;
|
||||
_ = Family;
|
||||
_ = Glyph;
|
||||
_ = FallbackSet;
|
||||
}
|
||||
|
BIN
src/font/res/NotoColorEmoji.ttf
Executable file
BIN
src/font/res/NotoColorEmoji.ttf
Executable file
Binary file not shown.
@ -1,2 +1,3 @@
|
||||
pub const fontRegular = @embedFile("res/Inconsolata-Regular.ttf");
|
||||
pub const fontBold = @embedFile("res/Inconsolata-Bold.ttf");
|
||||
pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
|
||||
|
@ -47,6 +47,7 @@ pub const Parameter = enum(c_uint) {
|
||||
/// Internal format enum for texture images.
|
||||
pub const InternalFormat = enum(c_int) {
|
||||
Red = c.GL_RED,
|
||||
RGBA = c.GL_RGBA,
|
||||
|
||||
// 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
|
||||
pub const Format = enum(c_uint) {
|
||||
Red = c.GL_RED,
|
||||
BGRA = c.GL_BGRA,
|
||||
|
||||
// There are so many more that I haven't filled in.
|
||||
_,
|
||||
|
@ -25,8 +25,10 @@ const Screen = @This();
|
||||
// one day.
|
||||
|
||||
const std = @import("std");
|
||||
const utf8proc = @import("utf8proc");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const color = @import("color.zig");
|
||||
const point = @import("point.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
@ -71,6 +73,15 @@ pub const Cell = struct {
|
||||
/// should have this set. The first cell of the next row is actually
|
||||
/// part of this row in raw input.
|
||||
wrap: u1 = 0,
|
||||
|
||||
/// True if this is a wide character. This char takes up
|
||||
/// two cells. The following cell ALWAYS is a space.
|
||||
wide: u1 = 0,
|
||||
|
||||
/// Notes that this only exists to be blank for a preceeding
|
||||
/// wide character (tail) or following (head).
|
||||
wide_spacer_tail: u1 = 0,
|
||||
wide_spacer_head: u1 = 0,
|
||||
} = .{},
|
||||
|
||||
/// True if the cell should be skipped for drawing
|
||||
@ -932,6 +943,10 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Skip spacers
|
||||
if (cell.attrs.wide_spacer_head == 1 or
|
||||
cell.attrs.wide_spacer_tail == 1) continue;
|
||||
|
||||
const char = if (cell.char > 0) cell.char else ' ';
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||
}
|
||||
@ -955,6 +970,10 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
}
|
||||
}
|
||||
|
||||
// Skip spacers
|
||||
if (cell.attrs.wide_spacer_head == 1 or
|
||||
cell.attrs.wide_spacer_tail == 1) continue;
|
||||
|
||||
const char = if (cell.char > 0) cell.char else ' ';
|
||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
||||
}
|
||||
@ -970,7 +989,7 @@ pub fn selectionString(self: Screen, alloc: Allocator, sel: Selection) ![:0]cons
|
||||
/// Returns the slices that make up the selection, in order. There are at most
|
||||
/// two parts to handle the ring buffer. If the selection fits in one contiguous
|
||||
/// slice, then the second slice will have a length of zero.
|
||||
fn selectionSlices(self: Screen, sel: Selection) struct {
|
||||
fn selectionSlices(self: Screen, sel_raw: Selection) struct {
|
||||
// Top offset can be used to determine if a newline is required by
|
||||
// seeing if the cell index plus the offset cleanly divides by screen cols.
|
||||
top_offset: usize,
|
||||
@ -979,10 +998,35 @@ fn selectionSlices(self: Screen, sel: Selection) struct {
|
||||
} {
|
||||
// Note: this function is tested via selectionString
|
||||
|
||||
assert(sel.start.y < self.totalRows());
|
||||
assert(sel.end.y < self.totalRows());
|
||||
assert(sel.start.x < self.cols);
|
||||
assert(sel.end.x < self.cols);
|
||||
assert(sel_raw.start.y < self.totalRows());
|
||||
assert(sel_raw.end.y < self.totalRows());
|
||||
assert(sel_raw.start.x < self.cols);
|
||||
assert(sel_raw.end.x < self.cols);
|
||||
|
||||
const sel = sel: {
|
||||
var sel = sel_raw;
|
||||
|
||||
// If the end of our selection is a wide char leader, include the
|
||||
// first part of the next line.
|
||||
if (sel.end.x == self.cols - 1) {
|
||||
const row = self.getRow(.{ .screen = sel.end.y });
|
||||
if (row[sel.end.x].attrs.wide_spacer_head == 1) {
|
||||
sel.end.y += 1;
|
||||
sel.end.x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the start of our selection is a wide char spacer, include the
|
||||
// wide char.
|
||||
if (sel.start.x > 0) {
|
||||
const row = self.getRow(.{ .screen = sel.start.y });
|
||||
if (row[sel.start.x].attrs.wide_spacer_tail == 1) {
|
||||
sel.end.x -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
break :sel sel;
|
||||
};
|
||||
|
||||
// Get the true "top" and "bottom"
|
||||
const sel_top = sel.topLeft();
|
||||
@ -1044,7 +1088,10 @@ pub fn testString(self: Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8
|
||||
fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
var y: usize = 0;
|
||||
var x: usize = 0;
|
||||
for (text) |c| {
|
||||
|
||||
const view = std.unicode.Utf8View.init(text) catch unreachable;
|
||||
var iter = view.iterator();
|
||||
while (iter.nextCodepoint()) |c| {
|
||||
// Explicit newline forces a new row
|
||||
if (c == '\n') {
|
||||
y += 1;
|
||||
@ -1073,7 +1120,39 @@ fn testWriteString(self: *Screen, text: []const u8) void {
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
row[x].char = @intCast(u32, c);
|
||||
// If our character is double-width, handle it.
|
||||
const width = utf8proc.charwidth(c);
|
||||
assert(width == 1 or width == 2);
|
||||
switch (width) {
|
||||
1 => row[x].char = @intCast(u32, c),
|
||||
|
||||
2 => {
|
||||
if (x == self.cols - 1) {
|
||||
row[x].char = ' ';
|
||||
row[x].attrs.wide_spacer_head = 1;
|
||||
|
||||
// wrap
|
||||
row[x].attrs.wrap = 1;
|
||||
y += 1;
|
||||
x = 0;
|
||||
if (y >= self.rows) {
|
||||
y -= 1;
|
||||
self.scroll(.{ .delta = 1 });
|
||||
}
|
||||
row = self.getRow(.{ .active = y });
|
||||
}
|
||||
|
||||
row[x].char = @intCast(u32, c);
|
||||
row[x].attrs.wide = 1;
|
||||
|
||||
x += 1;
|
||||
row[x].char = ' ';
|
||||
row[x].attrs.wide_spacer_tail = 1;
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
@ -1459,6 +1538,66 @@ test "Screen: selectionString wrap around" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectionString wide char" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit(alloc);
|
||||
const str = "1A⚡";
|
||||
s.testWriteString(str);
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 3, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 2, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 3, .y = 0 },
|
||||
.end = .{ .x = 3, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = "⚡";
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectionString wide char with header" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 3, 5, 0);
|
||||
defer s.deinit(alloc);
|
||||
const str = "1ABC⚡";
|
||||
s.testWriteString(str);
|
||||
|
||||
{
|
||||
var contents = try s.selectionString(alloc, .{
|
||||
.start = .{ .x = 0, .y = 0 },
|
||||
.end = .{ .x = 4, .y = 0 },
|
||||
});
|
||||
defer alloc.free(contents);
|
||||
const expected = str;
|
||||
try testing.expectEqualStrings(expected, contents);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: resize more rows no scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -6,9 +6,11 @@ const Terminal = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const utf8proc = @import("utf8proc");
|
||||
const testing = std.testing;
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ansi = @import("ansi.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
@ -341,23 +343,45 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// If we're not on the main display, do nothing for now
|
||||
if (self.status_display != .main) return;
|
||||
|
||||
// Determine the width of this character so we can handle
|
||||
// non-single-width characters properly.
|
||||
const width = utf8proc.charwidth(c);
|
||||
assert(width == 1 or width == 2);
|
||||
|
||||
// If we're soft-wrapping, then handle that first.
|
||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
||||
// Mark that the cell is wrapped, which guarantees that there is
|
||||
// at least one cell after it in the next row.
|
||||
const cell = self.screen.getCell(self.screen.cursor.y, self.screen.cursor.x);
|
||||
cell.attrs.wrap = 1;
|
||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1)
|
||||
_ = self.printWrap();
|
||||
|
||||
// Move to the next line
|
||||
self.index();
|
||||
self.screen.cursor.x = 0;
|
||||
switch (width) {
|
||||
// Single cell is very easy: just write in the cell
|
||||
1 => _ = self.printCell(c),
|
||||
|
||||
// Wide character requires a spacer. We print this by
|
||||
// using two cells: the first is flagged "wide" and has the
|
||||
// wide char. The second is guaranteed to be a spacer if
|
||||
// we're not at the end of the line.
|
||||
2 => {
|
||||
// If we don't have space for the wide char, we need
|
||||
// to insert spacers and wrap. Then we just print the wide
|
||||
// char as normal.
|
||||
if (self.screen.cursor.x == self.cols - 1) {
|
||||
const spacer_head = self.printCell(' ');
|
||||
spacer_head.attrs.wide_spacer_head = 1;
|
||||
_ = self.printWrap();
|
||||
}
|
||||
|
||||
const wide_cell = self.printCell(c);
|
||||
wide_cell.attrs.wide = 1;
|
||||
|
||||
// Write our spacer
|
||||
self.screen.cursor.x += 1;
|
||||
const spacer = self.printCell(' ');
|
||||
spacer.attrs.wide_spacer_tail = 1;
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
// Build our cell
|
||||
const cell = self.screen.getCell(self.screen.cursor.y, self.screen.cursor.x);
|
||||
cell.* = self.screen.cursor.pen;
|
||||
cell.char = @intCast(u32, c);
|
||||
|
||||
// Move the cursor
|
||||
self.screen.cursor.x += 1;
|
||||
|
||||
@ -370,6 +394,69 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn printCell(self: *Terminal, c: u21) *Screen.Cell {
|
||||
const cell = self.screen.getCell(
|
||||
self.screen.cursor.y,
|
||||
self.screen.cursor.x,
|
||||
);
|
||||
|
||||
// If this cell is wide char then we need to clear it.
|
||||
// We ignore wide spacer HEADS because we can just write
|
||||
// single-width characters into that.
|
||||
if (cell.attrs.wide == 1) {
|
||||
const x = self.screen.cursor.x + 1;
|
||||
assert(x < self.cols);
|
||||
|
||||
const spacer_cell = self.screen.getCell(self.screen.cursor.y, x);
|
||||
spacer_cell.attrs.wide_spacer_tail = 0;
|
||||
|
||||
if (self.screen.cursor.x <= 1) {
|
||||
self.clearWideSpacerHead();
|
||||
}
|
||||
} else if (cell.attrs.wide_spacer_tail == 1) {
|
||||
assert(self.screen.cursor.x > 0);
|
||||
const x = self.screen.cursor.x - 1;
|
||||
|
||||
const wide_cell = self.screen.getCell(self.screen.cursor.y, x);
|
||||
wide_cell.attrs.wide = 0;
|
||||
|
||||
if (self.screen.cursor.x <= 1) {
|
||||
self.clearWideSpacerHead();
|
||||
}
|
||||
}
|
||||
|
||||
// Write
|
||||
cell.* = self.screen.cursor.pen;
|
||||
cell.char = @intCast(u32, c);
|
||||
return cell;
|
||||
}
|
||||
|
||||
fn printWrap(self: *Terminal) *Screen.Cell {
|
||||
// Mark that the cell is wrapped, which guarantees that there is
|
||||
// at least one cell after it in the next row.
|
||||
const cell = self.screen.getCell(
|
||||
self.screen.cursor.y,
|
||||
self.screen.cursor.x,
|
||||
);
|
||||
cell.attrs.wrap = 1;
|
||||
|
||||
// Move to the next line
|
||||
self.index();
|
||||
self.screen.cursor.x = 0;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
fn clearWideSpacerHead(self: *Terminal) void {
|
||||
// TODO: handle deleting wide char on row 0 of active
|
||||
assert(self.screen.cursor.y >= 1);
|
||||
const cell = self.screen.getCell(
|
||||
self.screen.cursor.y - 1,
|
||||
self.cols - 1,
|
||||
);
|
||||
cell.attrs.wide_spacer_head = 0;
|
||||
}
|
||||
|
||||
/// Resets all margins and fills the whole screen with the character 'E'
|
||||
///
|
||||
/// Sets the cursor to the top left corner.
|
||||
|
@ -49,11 +49,11 @@ pub fn Stream(comptime Handler: type) type {
|
||||
//log.debug("char: {x}", .{c});
|
||||
const actions = self.parser.next(c);
|
||||
for (actions) |action_opt| {
|
||||
if (action_opt) |action| {
|
||||
if (action != .print) {
|
||||
log.info("action: {}", .{action});
|
||||
}
|
||||
}
|
||||
// if (action_opt) |action| {
|
||||
// if (action != .print) {
|
||||
// log.info("action: {}", .{action});
|
||||
// }
|
||||
// }
|
||||
switch (action_opt orelse continue) {
|
||||
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
||||
.execute => |code| try self.execute(code),
|
||||
|
1
vendor/utf8proc
vendored
Submodule
1
vendor/utf8proc
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 63f31c908ef7656415f73d6c178f08181239f74c
|
Reference in New Issue
Block a user