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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
# Run our go tests within the context of the dev shell from the flake. This
|
# Cross-compile the binary. We always use static building for this
|
||||||
# will ensure we have all our dependencies.
|
# because its the only way to access the headers.
|
||||||
- name: Test Build
|
- name: Test Build
|
||||||
run: nix develop -c zig build -Dtarget=${{ matrix.target }}
|
run: nix develop -c zig build -Dstatic=true -Dtarget=${{ matrix.target }}
|
||||||
|
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
@ -57,7 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
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
|
- name: test
|
||||||
run: nix develop -c zig build 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"]
|
[submodule "vendor/libpng"]
|
||||||
path = vendor/libpng
|
path = vendor/libpng
|
||||||
url = https://github.com/glennrp/libpng.git
|
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 freetype = @import("pkg/freetype/build.zig");
|
||||||
const libuv = @import("pkg/libuv/build.zig");
|
const libuv = @import("pkg/libuv/build.zig");
|
||||||
const libpng = @import("pkg/libpng/build.zig");
|
const libpng = @import("pkg/libpng/build.zig");
|
||||||
|
const utf8proc = @import("pkg/utf8proc/build.zig");
|
||||||
const zlib = @import("pkg/zlib/build.zig");
|
const zlib = @import("pkg/zlib/build.zig");
|
||||||
const tracylib = @import("pkg/tracy/build.zig");
|
const tracylib = @import("pkg/tracy/build.zig");
|
||||||
const system_sdk = @import("vendor/mach/glfw/system_sdk.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;
|
var tracy: bool = false;
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) !void {
|
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)",
|
"Enable Tracy integration (default true in Debug on Linux)",
|
||||||
) orelse (mode == .Debug and target.isLinux());
|
) 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 conformance = b.option(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
"conformance",
|
"conformance",
|
||||||
@ -51,7 +58,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
exe.install();
|
exe.install();
|
||||||
|
|
||||||
// Add the shared dependencies
|
// Add the shared dependencies
|
||||||
try addDeps(b, exe);
|
try addDeps(b, exe, static);
|
||||||
}
|
}
|
||||||
|
|
||||||
// term.wasm
|
// term.wasm
|
||||||
@ -64,7 +71,11 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
||||||
wasm.setBuildMode(mode);
|
wasm.setBuildMode(mode);
|
||||||
wasm.setOutputDir("zig-out");
|
wasm.setOutputDir("zig-out");
|
||||||
|
|
||||||
|
// Wasm-specific deps
|
||||||
wasm.addPackage(tracylib.pkg);
|
wasm.addPackage(tracylib.pkg);
|
||||||
|
wasm.addPackage(utf8proc.pkg);
|
||||||
|
_ = try utf8proc.link(b, wasm);
|
||||||
|
|
||||||
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
||||||
step.dependOn(&wasm.step);
|
step.dependOn(&wasm.step);
|
||||||
@ -104,7 +115,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main_test.setTarget(target);
|
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 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", .{});
|
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);
|
var test_ = b.addTestSource(pkg.source);
|
||||||
|
|
||||||
test_.setTarget(target);
|
test_.setTarget(target);
|
||||||
try addDeps(b, test_);
|
try addDeps(b, test_, true);
|
||||||
if (pkg.dependencies) |children| {
|
if (pkg.dependencies) |children| {
|
||||||
test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
||||||
try test_.packages.appendSlice(children);
|
try test_.packages.appendSlice(children);
|
||||||
@ -141,53 +152,73 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
fn addDeps(
|
fn addDeps(
|
||||||
b: *std.build.Builder,
|
b: *std.build.Builder,
|
||||||
step: *std.build.LibExeObjStep,
|
step: *std.build.LibExeObjStep,
|
||||||
|
static: bool,
|
||||||
) !void {
|
) !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.addIncludeDir("vendor/glad/include/");
|
||||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
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
|
// Tracy
|
||||||
step.addPackage(tracylib.pkg);
|
step.addPackage(tracylib.pkg);
|
||||||
if (tracy) {
|
if (tracy) {
|
||||||
var tracy_step = try tracylib.link(b, step);
|
var tracy_step = try tracylib.link(b, step);
|
||||||
system_sdk.include(b, tracy_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(
|
fn conformanceSteps(
|
||||||
|
@ -19,18 +19,26 @@
|
|||||||
, freetype
|
, freetype
|
||||||
, libpng
|
, libpng
|
||||||
, libGL
|
, libGL
|
||||||
|
, libuv
|
||||||
, libX11
|
, libX11
|
||||||
, libXcursor
|
, libXcursor
|
||||||
, libXext
|
, libXext
|
||||||
, libXi
|
, libXi
|
||||||
, libXinerama
|
, libXinerama
|
||||||
, libXrandr
|
, libXrandr
|
||||||
|
, zlib
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# See package.nix. Keep in sync.
|
# See package.nix. Keep in sync.
|
||||||
rpathLibs = [
|
rpathLibs = [
|
||||||
libGL
|
libGL
|
||||||
] ++ lib.optionals stdenv.isLinux [
|
] ++ lib.optionals stdenv.isLinux [
|
||||||
|
bzip2
|
||||||
|
freetype
|
||||||
|
libpng
|
||||||
|
libuv
|
||||||
|
zlib
|
||||||
|
|
||||||
libX11
|
libX11
|
||||||
libXcursor
|
libXcursor
|
||||||
libXi
|
libXi
|
||||||
@ -61,6 +69,12 @@ in mkShell rec {
|
|||||||
buildInputs = [
|
buildInputs = [
|
||||||
# TODO: non-linux
|
# TODO: non-linux
|
||||||
] ++ lib.optionals stdenv.isLinux [
|
] ++ lib.optionals stdenv.isLinux [
|
||||||
|
bzip2
|
||||||
|
freetype
|
||||||
|
libpng
|
||||||
|
libuv
|
||||||
|
zlib
|
||||||
|
|
||||||
libX11
|
libX11
|
||||||
libXcursor
|
libXcursor
|
||||||
libXext
|
libXext
|
||||||
|
@ -3,7 +3,7 @@ const std = @import("std");
|
|||||||
/// Directories with our includes.
|
/// Directories with our includes.
|
||||||
const root = thisDir() ++ "../../../vendor/freetype/";
|
const root = thisDir() ++ "../../../vendor/freetype/";
|
||||||
const include_path = root ++ "include";
|
const include_path = root ++ "include";
|
||||||
const include_path_self = thisDir();
|
pub const include_path_self = thisDir();
|
||||||
|
|
||||||
pub const pkg = std.build.Pkg{
|
pub const pkg = std.build.Pkg{
|
||||||
.name = "freetype",
|
.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
|
// 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,30 @@ 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;
|
||||||
|
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
||||||
|
|
||||||
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,10 +6,12 @@
|
|||||||
// 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;
|
||||||
const uint MODE_UNDERLINE = 6u;
|
const uint MODE_UNDERLINE = 6u;
|
||||||
|
const uint MODE_WIDE_MASK = 128u; // 0b1000_0000
|
||||||
|
|
||||||
// The grid coordinates (x, y) where x < columns and y < rows
|
// The grid coordinates (x, y) where x < columns and y < rows
|
||||||
layout (location = 0) in vec2 grid_coord;
|
layout (location = 0) in vec2 grid_coord;
|
||||||
@ -50,6 +52,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;
|
||||||
@ -75,8 +78,12 @@ uniform float glyph_baseline;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// We always forward our mode
|
// Remove any masks from our mode
|
||||||
mode = mode_in;
|
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
|
// Top-left cell coordinates converted to world space
|
||||||
// Example: (1,0) with a 30 wide cell is converted to (30,0)
|
// 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.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
|
||||||
position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
|
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:
|
case MODE_BG:
|
||||||
// Calculate the final position of our cell in world space.
|
// Calculate the final position of our cell in world space.
|
||||||
// We have to add our cell size since our vertices are offset
|
// 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)
|
// 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);
|
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||||
color = bg_color_in / 255.0;
|
color = bg_color_in / 255.0;
|
||||||
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_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
|
// 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_scaled.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) {
|
||||||
|
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;
|
||||||
@ -137,7 +171,7 @@ void main() {
|
|||||||
|
|
||||||
case MODE_CURSOR_RECT:
|
case MODE_CURSOR_RECT:
|
||||||
// Same as background since we're taking up the whole cell.
|
// 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);
|
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||||
color = bg_color_in / 255.0;
|
color = bg_color_in / 255.0;
|
||||||
@ -148,7 +182,7 @@ void main() {
|
|||||||
screen_cell_pos = cell_pos;
|
screen_cell_pos = cell_pos;
|
||||||
|
|
||||||
// Same as background since we're taking up the whole cell.
|
// 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);
|
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||||
color = bg_color_in / 255.0;
|
color = bg_color_in / 255.0;
|
||||||
@ -168,11 +202,11 @@ void main() {
|
|||||||
case MODE_UNDERLINE:
|
case MODE_UNDERLINE:
|
||||||
// Make the underline a smaller version of our cell
|
// Make the underline a smaller version of our cell
|
||||||
// TODO: use real font underline thickness
|
// 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
|
// Position our underline so that it is midway between the glyph
|
||||||
// baseline and the bottom of the cell.
|
// 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
|
// Go to the bottom of the cell, take away the size of the
|
||||||
// underline, and that is our position. We also float it slightly
|
// 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,
|
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_set: font.FallbackSet,
|
||||||
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
|
||||||
@ -108,7 +109,30 @@ const GPUCell = struct {
|
|||||||
bg_a: u8,
|
bg_a: u8,
|
||||||
|
|
||||||
/// uint mode
|
/// 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 {
|
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.
|
// font atlas with all the visible ASCII characters since they are common.
|
||||||
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
||||||
errdefer atlas.deinit(alloc);
|
errdefer atlas.deinit(alloc);
|
||||||
var fam = try font.Family.init(atlas);
|
|
||||||
errdefer fam.deinit(alloc);
|
// Load our emoji font
|
||||||
try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size");
|
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||||
try fam.loadFaceFromMemory(.bold, face_bold_ttf, config.@"font-size");
|
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
|
// Load all visible ASCII characters and build our cell width based on
|
||||||
// the widest character that we see.
|
// 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 cell_width: f32 = 0;
|
||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i <= 126) : (i += 1) {
|
while (i <= 126) : (i += 1) {
|
||||||
const glyph = try fam.addGlyph(alloc, i, .regular);
|
const goa = try font_set.getOrAddGlyph(alloc, i, .regular);
|
||||||
if (glyph.advance_x > cell_width) {
|
if (goa.glyph.advance_x > cell_width) {
|
||||||
cell_width = @ceil(glyph.advance_x);
|
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
|
// The cell height is the vertical height required to render underscore
|
||||||
// '_' which should live at the bottom of a cell.
|
// '_' which should live at the bottom of a cell.
|
||||||
const cell_height: f32 = cell_height: {
|
const cell_height: f32 = cell_height: {
|
||||||
|
const fam = &font_set.families.items[0];
|
||||||
|
|
||||||
// This is the height reported by the font face
|
// This is the height reported by the font face
|
||||||
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
|
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
|
||||||
|
|
||||||
// Determine the height of the underscore char
|
// 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);
|
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
|
||||||
res -= glyph.offset_y;
|
res -= glyph.offset_y;
|
||||||
res += @intCast(i32, glyph.height);
|
res += @intCast(i32, glyph.height);
|
||||||
@ -154,10 +202,13 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
|
|
||||||
break :cell_height @intToFloat(f32, res);
|
break :cell_height @intToFloat(f32, res);
|
||||||
};
|
};
|
||||||
const cell_baseline = cell_height - @intToFloat(
|
const cell_baseline = cell_baseline: {
|
||||||
f32,
|
const fam = &font_set.families.items[0];
|
||||||
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
|
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 });
|
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
||||||
|
|
||||||
// Create our shader
|
// 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("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_color", 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 +280,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 +329,8 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
|||||||
.ebo = ebo,
|
.ebo = ebo,
|
||||||
.vbo = vbo,
|
.vbo = vbo,
|
||||||
.texture = tex,
|
.texture = tex,
|
||||||
.font_atlas = fam,
|
.texture_color = tex_color,
|
||||||
|
.font_set = font_set,
|
||||||
.atlas_dirty = false,
|
.atlas_dirty = false,
|
||||||
.cursor_visible = true,
|
.cursor_visible = true,
|
||||||
.cursor_style = .box,
|
.cursor_style = .box,
|
||||||
@ -261,9 +340,14 @@ 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);
|
for (self.font_set.families.items) |*family| {
|
||||||
self.font_atlas.deinit(self.alloc);
|
family.atlas.deinit(self.alloc);
|
||||||
|
family.deinit(self.alloc);
|
||||||
|
}
|
||||||
|
self.font_set.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();
|
||||||
@ -338,8 +422,19 @@ pub fn finalizeCells(self: *Grid, term: Terminal) !void {
|
|||||||
fn addCursor(self: *Grid, term: Terminal) void {
|
fn addCursor(self: *Grid, term: Terminal) void {
|
||||||
// Add the cursor
|
// Add the cursor
|
||||||
if (self.cursor_visible and term.screen.viewportIsBottom()) {
|
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(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = @enumToInt(self.cursor_style),
|
.mode = mode,
|
||||||
.grid_col = @intCast(u16, term.screen.cursor.x),
|
.grid_col = @intCast(u16, term.screen.cursor.x),
|
||||||
.grid_row = @intCast(u16, term.screen.cursor.y),
|
.grid_row = @intCast(u16, term.screen.cursor.y),
|
||||||
.fg_r = 0,
|
.fg_r = 0,
|
||||||
@ -415,6 +510,9 @@ pub fn updateCell(
|
|||||||
break :colors res;
|
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.
|
// Calculate the amount of space we need in the cells list.
|
||||||
const needed = needed: {
|
const needed = needed: {
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
@ -427,8 +525,11 @@ pub fn updateCell(
|
|||||||
|
|
||||||
// If the cell has a background, we always draw it.
|
// If the cell has a background, we always draw it.
|
||||||
if (colors.bg) |rgb| {
|
if (colors.bg) |rgb| {
|
||||||
|
var mode: GPUCellMode = .bg;
|
||||||
|
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = 1,
|
.mode = mode,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
.grid_row = @intCast(u16, y),
|
.grid_row = @intCast(u16, y),
|
||||||
.glyph_x = 0,
|
.glyph_x = 0,
|
||||||
@ -456,17 +557,19 @@ pub fn updateCell(
|
|||||||
else
|
else
|
||||||
.regular;
|
.regular;
|
||||||
|
|
||||||
// Get our glyph
|
var mode: GPUCellMode = .fg;
|
||||||
// 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|
|
// Get our glyph. Try our normal font atlas first.
|
||||||
glyph
|
const goa = try self.font_set.getOrAddGlyph(self.alloc, cell.char, style);
|
||||||
else glyph: {
|
if (!goa.found_existing) self.atlas_dirty = true;
|
||||||
self.atlas_dirty = true;
|
if (goa.family == 1) mode = .fg_color;
|
||||||
break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char, style);
|
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(.{
|
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,
|
||||||
@ -487,8 +590,11 @@ pub fn updateCell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cell.attrs.underline == 1) {
|
if (cell.attrs.underline == 1) {
|
||||||
|
var mode: GPUCellMode = .underline;
|
||||||
|
if (cell.attrs.wide == 1) mode = mode.mask(.wide_mask);
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = 6, // underline
|
.mode = mode,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
.grid_row = @intCast(u16, y),
|
.grid_row = @intCast(u16, y),
|
||||||
.glyph_x = 0,
|
.glyph_x = 0,
|
||||||
@ -537,18 +643,37 @@ 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();
|
const atlas = &self.font_set.families.items[0].atlas;
|
||||||
try texbind.subImage2D(
|
var texbind = try self.texture.bind(.@"2D");
|
||||||
0,
|
defer texbind.unbind();
|
||||||
0,
|
try texbind.subImage2D(
|
||||||
0,
|
0,
|
||||||
@intCast(c_int, self.font_atlas.atlas.size),
|
0,
|
||||||
@intCast(c_int, self.font_atlas.atlas.size),
|
0,
|
||||||
.Red,
|
@intCast(c_int, atlas.size),
|
||||||
.UnsignedByte,
|
@intCast(c_int, atlas.size),
|
||||||
self.font_atlas.atlas.data.ptr,
|
.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
|
/// 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);
|
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 +815,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");
|
||||||
|
@ -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)
|
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
|
||||||
return error.FaceLoadFailed;
|
return error.FaceLoadFailed;
|
||||||
|
|
||||||
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
|
// If we have fixed sizes, we just have to try to pick the one closest
|
||||||
return error.FaceLoadFailed;
|
// 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
|
/// 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
|
// We need a UTF32 codepoint for freetype
|
||||||
const glyph_index = glyph_index: {
|
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);
|
const idx = ftc.FT_Get_Char_Index(self.ft_face, cp);
|
||||||
if (idx > 0) break :glyph_index idx;
|
if (idx > 0) break :glyph_index idx;
|
||||||
|
|
||||||
// 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});
|
||||||
|
|
||||||
|
// 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(
|
if (ftc.FT_Load_Glyph(
|
||||||
self.ft_face,
|
self.ft_face,
|
||||||
glyph_index,
|
glyph_index,
|
||||||
ftc.FT_LOAD_RENDER,
|
load_flags,
|
||||||
) != ftok) return error.LoadGlyphFailed;
|
) != ftok) return error.LoadGlyphFailed;
|
||||||
|
|
||||||
const glyph = self.ft_face.*.glyph;
|
const glyph = self.ft_face.*.glyph;
|
||||||
const bitmap = glyph.*.bitmap;
|
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_w = bitmap.width;
|
||||||
const src_h = bitmap.rows;
|
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 we have data, copy it into the atlas
|
||||||
if (region.width > 0 and region.height > 0) {
|
if (region.width > 0 and region.height > 0) {
|
||||||
|
const depth = @enumToInt(format);
|
||||||
|
|
||||||
// We can avoid a buffer copy if our atlas width and bitmap
|
// We can avoid a buffer copy if our atlas width and bitmap
|
||||||
// width match and the bitmap pitch is just the width (meaning
|
// width match and the bitmap pitch is just the width (meaning
|
||||||
// the data is tightly packed).
|
// 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.
|
// If we need to copy the data, we copy it into a temporary buffer.
|
||||||
const buffer = if (needs_copy) 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 dst_ptr = temp;
|
||||||
var src_ptr = bitmap.buffer;
|
var src_ptr = bitmap.buffer;
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < src_h) : (i += 1) {
|
while (i < src_h) : (i += 1) {
|
||||||
std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]);
|
std.mem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]);
|
||||||
dst_ptr = dst_ptr[tgt_w..];
|
dst_ptr = dst_ptr[tgt_w * depth ..];
|
||||||
src_ptr += @intCast(usize, bitmap.pitch);
|
src_ptr += @intCast(usize, bitmap.pitch);
|
||||||
}
|
}
|
||||||
break :buffer temp;
|
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);
|
defer if (buffer.ptr != bitmap.buffer) alloc.free(buffer);
|
||||||
|
|
||||||
// Write the glyph information into the atlas
|
// Write the glyph information into the atlas
|
||||||
@ -149,6 +188,8 @@ fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
const testFont = @import("test.zig").fontRegular;
|
||||||
|
|
||||||
var ft_lib: ftc.FT_Library = undefined;
|
var ft_lib: ftc.FT_Library = undefined;
|
||||||
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
|
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
|
||||||
return error.FreeTypeInitFailed;
|
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 Glyph = @import("main.zig").Glyph;
|
||||||
const Style = @import("main.zig").Style;
|
const Style = @import("main.zig").Style;
|
||||||
const testFont = @import("test.zig").fontRegular;
|
const testFont = @import("test.zig").fontRegular;
|
||||||
|
const codepoint = @import("main.zig").codepoint;
|
||||||
|
|
||||||
const log = std.log.scoped(.font_family);
|
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);
|
errdefer _ = self.glyphs.remove(glyphKey);
|
||||||
|
|
||||||
// Get the glyph and add it to the atlas.
|
// Get the glyph and add it to the atlas.
|
||||||
// TODO: handle glyph not found
|
|
||||||
gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32);
|
gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32);
|
||||||
return gop.value_ptr;
|
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 {
|
test {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
pub const Face = @import("Face.zig");
|
pub const Face = @import("Face.zig");
|
||||||
pub const Family = @import("Family.zig");
|
pub const Family = @import("Family.zig");
|
||||||
pub const Glyph = @import("Glyph.zig");
|
pub const Glyph = @import("Glyph.zig");
|
||||||
|
pub const FallbackSet = @import("FallbackSet.zig");
|
||||||
|
|
||||||
/// Embedded fonts (for now)
|
/// Embedded fonts (for now)
|
||||||
pub const fontRegular = @import("test.zig").fontRegular;
|
pub const fontRegular = @import("test.zig").fontRegular;
|
||||||
@ -14,8 +17,20 @@ pub const Style = enum {
|
|||||||
bold_italic,
|
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 {
|
test {
|
||||||
_ = Face;
|
_ = Face;
|
||||||
_ = Family;
|
_ = Family;
|
||||||
_ = Glyph;
|
_ = 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 fontRegular = @embedFile("res/Inconsolata-Regular.ttf");
|
||||||
pub const fontBold = @embedFile("res/Inconsolata-Bold.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.
|
/// 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.
|
||||||
_,
|
_,
|
||||||
|
@ -25,8 +25,10 @@ const Screen = @This();
|
|||||||
// one day.
|
// one day.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const utf8proc = @import("utf8proc");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const Selection = @import("Selection.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
|
/// should have this set. The first cell of the next row is actually
|
||||||
/// part of this row in raw input.
|
/// part of this row in raw input.
|
||||||
wrap: u1 = 0,
|
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
|
/// 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;
|
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 ' ';
|
const char = if (cell.char > 0) cell.char else ' ';
|
||||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
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 ' ';
|
const char = if (cell.char > 0) cell.char else ' ';
|
||||||
i += try std.unicode.utf8Encode(@intCast(u21, char), buf[i..]);
|
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
|
/// 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
|
/// 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.
|
/// 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
|
// 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.
|
// seeing if the cell index plus the offset cleanly divides by screen cols.
|
||||||
top_offset: usize,
|
top_offset: usize,
|
||||||
@ -979,10 +998,35 @@ fn selectionSlices(self: Screen, sel: Selection) struct {
|
|||||||
} {
|
} {
|
||||||
// Note: this function is tested via selectionString
|
// Note: this function is tested via selectionString
|
||||||
|
|
||||||
assert(sel.start.y < self.totalRows());
|
assert(sel_raw.start.y < self.totalRows());
|
||||||
assert(sel.end.y < self.totalRows());
|
assert(sel_raw.end.y < self.totalRows());
|
||||||
assert(sel.start.x < self.cols);
|
assert(sel_raw.start.x < self.cols);
|
||||||
assert(sel.end.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"
|
// Get the true "top" and "bottom"
|
||||||
const sel_top = sel.topLeft();
|
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 {
|
fn testWriteString(self: *Screen, text: []const u8) void {
|
||||||
var y: usize = 0;
|
var y: usize = 0;
|
||||||
var x: 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
|
// Explicit newline forces a new row
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
y += 1;
|
y += 1;
|
||||||
@ -1073,7 +1120,39 @@ fn testWriteString(self: *Screen, text: []const u8) void {
|
|||||||
row = self.getRow(.{ .active = y });
|
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;
|
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" {
|
test "Screen: resize more rows no scrollback" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -6,9 +6,11 @@ const Terminal = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const utf8proc = @import("utf8proc");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
const sgr = @import("sgr.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 we're not on the main display, do nothing for now
|
||||||
if (self.status_display != .main) return;
|
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 we're soft-wrapping, then handle that first.
|
||||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1)
|
||||||
// Mark that the cell is wrapped, which guarantees that there is
|
_ = self.printWrap();
|
||||||
// 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
|
switch (width) {
|
||||||
self.index();
|
// Single cell is very easy: just write in the cell
|
||||||
self.screen.cursor.x = 0;
|
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
|
// Move the cursor
|
||||||
self.screen.cursor.x += 1;
|
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'
|
/// Resets all margins and fills the whole screen with the character 'E'
|
||||||
///
|
///
|
||||||
/// Sets the cursor to the top left corner.
|
/// Sets the cursor to the top left corner.
|
||||||
|
@ -49,11 +49,11 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
//log.debug("char: {x}", .{c});
|
//log.debug("char: {x}", .{c});
|
||||||
const actions = self.parser.next(c);
|
const actions = self.parser.next(c);
|
||||||
for (actions) |action_opt| {
|
for (actions) |action_opt| {
|
||||||
if (action_opt) |action| {
|
// if (action_opt) |action| {
|
||||||
if (action != .print) {
|
// if (action != .print) {
|
||||||
log.info("action: {}", .{action});
|
// log.info("action: {}", .{action});
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
switch (action_opt orelse continue) {
|
switch (action_opt orelse continue) {
|
||||||
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
.print => |p| if (@hasDecl(T, "print")) try self.handler.print(p),
|
||||||
.execute => |code| try self.execute(code),
|
.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