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:
Mitchell Hashimoto
2022-08-20 17:33:59 -07:00
committed by GitHub
22 changed files with 903 additions and 169 deletions

View File

@ -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
View File

@ -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

View File

@ -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,11 +152,46 @@ 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
// 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 = .{
@ -155,7 +201,6 @@ fn addDeps(
});
// Freetype
step.addPackage(freetype.pkg);
_ = try freetype.link(b, step, .{
.libpng = freetype.Options.Libpng{
.enabled = true,
@ -170,23 +215,9 @@ fn addDeps(
},
});
// 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, .{});
}
}

View File

@ -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

View File

@ -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
View 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
View File

@ -0,0 +1,3 @@
pub usingnamespace @cImport({
@cInclude("utf8proc.h");
});

8
pkg/utf8proc/main.zig Normal file
View 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)));
}

View File

@ -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;

View File

@ -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

View File

@ -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);
// 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(
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,6 +280,7 @@ 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);
@ -240,6 +296,28 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
.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,20 +643,39 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
/// Updates the font texture atlas if it is dirty.
fn flushAtlas(self: *Grid) !void {
{
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, self.font_atlas.atlas.size),
@intCast(c_int, self.font_atlas.atlas.size),
@intCast(c_int, atlas.size),
@intCast(c_int, atlas.size),
.Red,
.UnsignedByte,
self.font_atlas.atlas.data.ptr,
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
/// the cells.
pub fn render(self: *Grid) !void {
@ -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");

View File

@ -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 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
@ -74,21 +97,35 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
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
View 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);
}

View File

@ -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;

View File

@ -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

Binary file not shown.

View File

@ -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");

View File

@ -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.
_,

View File

@ -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 });
}
// 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;

View File

@ -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,22 +343,44 @@ 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;
// 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;
// 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);
// Move to the next line
self.index();
self.screen.cursor.x = 0;
// If we're soft-wrapping, then handle that first.
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1)
_ = self.printWrap();
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();
}
// 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);
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,
}
// 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.

View File

@ -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

@ -0,0 +1 @@
Subproject commit 63f31c908ef7656415f73d6c178f08181239f74c