font/sprite: rework sprite font drawing

This is a fairly large rework of how we handle the sprite font drawing.
Drawing routines are now context-less, provided only a canvas and some
metrics. There is now a separate file per unicode block / PUA area.
Sprites are now drawn on canvases with an extra quarter-cell of padding
on each edge, and automatically cropped when sent to the atlas, this
allows sprites to extend past cell boundaries which makes it possible to
have, for example, diagonal box drawing characters that connect across
cell diagonals instead of being pinched in.

Most of the sprites the code is just directly ported from the old code,
but I've rewritten a handful. Moving forward, I'd like to rewrite more
of these since the way they're currently written isn't ideal.

This rework, in addition to improving the packing efficiency of sprites
on the atlas, and allowing for out-of-cell drawing, will make it a lot
easier to add new sprites in the future, since all it takes now is to
add a single function and an import (if it's a new file).

I reworked the regression/change testing to be more robust as well, it
now covers all sprite glyphs (except non-codepoint ones) and does so at
4 different sizes. Addition/removal of glyphs will no longer create diff
noise in the generated diff image, since the position in the image of
each glyph is now fixed.
pull/7732/head
Qwerasd 2025-06-29 15:33:58 -06:00
parent 66f73f7133
commit 1377e6d225
54 changed files with 5474 additions and 4653 deletions

View File

@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void {
_ = self.modified.fetchAdd(1, .monotonic);
}
/// Like `set` but allows specifying a width for the source data and an
/// offset x and y, so that a section of a larger buffer may be copied
/// in to the atlas.
pub fn setFromLarger(
self: *Atlas,
reg: Region,
src: []const u8,
src_width: u32,
src_x: u32,
src_y: u32,
) void {
assert(reg.x < (self.size - 1));
assert((reg.x + reg.width) <= (self.size - 1));
assert(reg.y < (self.size - 1));
assert((reg.y + reg.height) <= (self.size - 1));
const depth = self.format.depth();
var i: u32 = 0;
while (i < reg.height) : (i += 1) {
const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth;
const src_offset = (((src_y + i) * src_width) + src_x) * depth;
fastmem.copy(
u8,
self.data[tex_offset..],
src[src_offset .. src_offset + (reg.width * depth)],
);
}
_ = self.modified.fetchAdd(1, .monotonic);
}
// Grow the texture to the new size, preserving all previously written data.
pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void {
assert(size_new >= self.size);

View File

@ -33,12 +33,6 @@ pub const Sprite = enum(u32) {
cursor_hollow_rect,
cursor_bar,
// Note: we don't currently put the box drawing glyphs in here because
// there are a LOT and I'm lazy. What I want to do is spend more time
// studying the patterns to see if we can programmatically build our
// enum perhaps and comptime generate the drawing code at the same time.
// I'm not sure if that's advisable yet though.
test {
const testing = std.testing;
try testing.expectEqual(start, @intFromEnum(Sprite.underline));

File diff suppressed because it is too large Load Diff

View File

@ -16,25 +16,154 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const wuffs = @import("wuffs");
const z2d = @import("z2d");
const font = @import("../main.zig");
const Sprite = font.sprite.Sprite;
const Box = @import("Box.zig");
const Powerline = @import("Powerline.zig");
const underline = @import("underline.zig");
const cursor = @import("cursor.zig");
const special = @import("draw/special.zig");
const log = std.log.scoped(.font_sprite);
/// Grid metrics for rendering sprites.
metrics: font.Metrics,
pub const DrawFnError =
Allocator.Error ||
z2d.painter.FillError ||
z2d.painter.StrokeError ||
error{
/// Something went wrong while doing math.
MathError,
};
/// A function that draws a glyph on the provided canvas.
pub const DrawFn = fn (
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) DrawFnError!void;
const Range = struct {
min: u32,
max: u32,
draw: DrawFn,
};
/// Automatically collect ranges for functions with names
/// in the format `draw<CP>` or `draw<MIN>_<MAX>`.
const ranges = ranges: {
@setEvalBranchQuota(1_000_000);
// Structs containing drawing functions for codepoint ranges.
const structs = [_]type{
@import("draw/block.zig"),
@import("draw/box.zig"),
@import("draw/braille.zig"),
@import("draw/branch.zig"),
@import("draw/geometric_shapes.zig"),
@import("draw/powerline.zig"),
@import("draw/symbols_for_legacy_computing.zig"),
@import("draw/symbols_for_legacy_computing_supplement.zig"),
};
// Count how many draw fns we have
var range_count = 0;
for (structs) |s| {
for (@typeInfo(s).@"struct".decls) |decl| {
if (!@hasDecl(s, decl.name)) continue;
if (!std.mem.startsWith(u8, decl.name, "draw")) continue;
range_count += 1;
}
}
// Make an array and collect ranges for each function.
var r: [range_count]Range = undefined;
var names: [range_count][:0]const u8 = undefined;
var i = 0;
for (structs) |s| {
for (@typeInfo(s).@"struct".decls) |decl| {
if (!@hasDecl(s, decl.name)) continue;
if (!std.mem.startsWith(u8, decl.name, "draw")) continue;
const sep = std.mem.indexOfScalar(u8, decl.name, '_') orelse decl.name.len;
const min = std.fmt.parseInt(u21, decl.name[4..sep], 16) catch unreachable;
const max = if (sep == decl.name.len)
min
else
std.fmt.parseInt(u21, decl.name[sep + 1 ..], 16) catch unreachable;
r[i] = .{
.min = min,
.max = max,
.draw = @field(s, decl.name),
};
names[i] = decl.name;
i += 1;
}
}
// Sort ranges in ascending order
std.mem.sortUnstableContext(0, r.len, struct {
r: []Range,
names: [][:0]const u8,
pub fn lessThan(self: @This(), a: usize, b: usize) bool {
return self.r[a].min < self.r[b].min;
}
pub fn swap(self: @This(), a: usize, b: usize) void {
std.mem.swap(Range, &self.r[a], &self.r[b]);
std.mem.swap([:0]const u8, &self.names[a], &self.names[b]);
}
}{
.r = &r,
.names = &names,
});
// Ensure there's no overlapping ranges
i = 0;
for (r, 0..) |n, k| {
if (n.min <= i) {
@compileError(
std.fmt.comptimePrint(
"Codepoint range for {s}(...) overlaps range for {s}(...), {X} <= {X} <= {X}",
.{ names[k], names[k - 1], r[k - 1].min, n.min, r[k - 1].max },
),
);
}
i = n.max;
}
break :ranges r;
};
fn getDrawFn(cp: u32) ?*const DrawFn {
// For special sprites (cursors, underlines, etc.) all sprites are drawn
// by functions from `Special` that share the name of the enum field.
if (cp >= Sprite.start) switch (@as(Sprite, @enumFromInt(cp))) {
inline else => |sprite| {
return @field(special, @tagName(sprite));
},
};
// Pray that the compiler is smart enough to
// turn this in to a jump table or something...
inline for (ranges) |range| {
if (cp >= range.min and cp <= range.max) return range.draw;
}
return null;
}
/// Returns true if the codepoint exists in our sprite font.
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
// We ignore presentation. No matter what presentation is requested
// we always provide glyphs for our codepoints.
// We ignore presentation. No matter what presentation is
// requested we always provide glyphs for our codepoints.
_ = p;
_ = self;
return Kind.init(cp) != null;
return getDrawFn(cp) != null;
}
/// Render the glyph.
@ -52,18 +181,10 @@ pub fn renderGlyph(
}
}
const metrics = self.metrics;
// We adjust our sprite width based on the cell width.
const width = switch (opts.cell_width orelse 1) {
0, 1 => metrics.cell_width,
else => |width| metrics.cell_width * width,
};
// It should be impossible for this to be null and we assert that
// in runtime safety modes but in case it is its not worth memory
// corruption so we return a valid, blank glyph.
const kind = Kind.init(cp) orelse return .{
const draw = getDrawFn(cp) orelse return .{
.width = 0,
.height = 0,
.offset_x = 0,
@ -73,217 +194,349 @@ pub fn renderGlyph(
.advance_x = 0,
};
// Safe to ".?" because of the above assertion.
return switch (kind) {
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
const metrics = self.metrics;
.underline => try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
metrics.underline_position,
metrics.underline_thickness,
),
// We adjust our sprite width based on the cell width.
const width = switch (opts.cell_width orelse 1) {
0, 1 => metrics.cell_width,
else => |width| metrics.cell_width * width,
};
.strikethrough => try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
metrics.strikethrough_position,
metrics.strikethrough_thickness,
),
const height = metrics.cell_height;
.overline => overline: {
var g = try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
0,
metrics.overline_thickness,
);
const padding_x = width / 4;
const padding_y = height / 4;
// We have to manually subtract the overline position
// on the rendered glyph since it can be negative.
g.offset_y -= metrics.overline_position;
// Make a canvas of the desired size
var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y);
defer canvas.deinit();
break :overline g;
},
try draw(cp, &canvas, width, height, metrics);
.powerline => powerline: {
const f: Powerline = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.box_thickness,
};
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
break :powerline try f.renderGlyph(alloc, atlas, cp);
},
.cursor => cursor: {
var g = try cursor.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cursor_height,
metrics.cursor_thickness,
);
// Cursors are drawn at their specified height
// and are centered vertically within the cell.
const cursor_height: i32 = @intCast(metrics.cursor_height);
const cell_height: i32 = @intCast(metrics.cell_height);
g.offset_y += @divTrunc(cell_height - cursor_height, 2);
break :cursor g;
},
return font.Glyph{
.width = region.width,
.height = region.height,
.offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)),
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatFromInt(width),
};
}
/// Kind of sprites we have. Drawing is implemented separately for each kind.
const Kind = enum {
box,
underline,
overline,
strikethrough,
powerline,
cursor,
/// Used in `testDrawRanges`, checks for diff between the provided atlas
/// and the reference file for the range, returns true if there is a diff.
fn testDiffAtlas(
alloc: Allocator,
atlas: *z2d.Surface,
path: []const u8,
i: u32,
width: u32,
height: u32,
thickness: u32,
) !bool {
// Get the file contents, we compare the PNG data first in
// order to ensure that no one smuggles arbitrary binary
// data in to the reference PNGs.
const test_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
defer test_file.close();
const test_bytes = try test_file.readToEndAlloc(
alloc,
std.math.maxInt(usize),
);
defer alloc.free(test_bytes);
pub fn init(cp: u32) ?Kind {
return switch (cp) {
Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) {
.underline,
.underline_double,
.underline_dotted,
.underline_dashed,
.underline_curly,
=> .underline,
const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
defer alloc.free(cwd_absolute);
.overline,
=> .overline,
// Get the reference file contents to compare.
const ref_path = try std.fmt.allocPrint(
alloc,
"./src/font/sprite/testdata/U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ i, i + 0xFF, width, height, thickness },
);
defer alloc.free(ref_path);
const ref_file =
std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| {
log.err("Can't open reference file {s}: {}\n", .{
ref_path,
err,
});
.strikethrough,
=> .strikethrough,
// Copy the test PNG in to the CWD so it isn't
// cleaned up with the rest of the tmp dir files.
const test_path = try std.fmt.allocPrint(
alloc,
"{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ cwd_absolute, i, i + 0xFF, width, height, thickness },
);
defer alloc.free(test_path);
try std.fs.copyFileAbsolute(path, test_path, .{});
.cursor_rect,
.cursor_hollow_rect,
.cursor_bar,
=> .cursor,
},
// == Box fonts ==
// "Box Drawing" block
//
//
//
//
0x2500...0x257F,
// "Block Elements" block
//
0x2580...0x259F,
// "Geometric Shapes" block
0x25e2...0x25e5, //
0x25f8...0x25fa, //
0x25ff, //
// "Braille" block
0x2800...0x28FF,
// "Symbols for Legacy Computing" block
// (Block Mosaics / "Sextants")
// 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠
// 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻
// (Smooth Mosaics)
// 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆
// 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑
// 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜
// 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧
// 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯
// (Block Elements)
// 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻
// 🭼 🭽 🭾 🭿 🮀 🮁
// 🮂 🮃 🮄 🮅 🮆
// 🮇 🮈 🮉 🮊 🮋
// (Rectangular Shade Characters)
// 🮌 🮍 🮎 🮏 🮐 🮑 🮒
0x1FB00...0x1FB92,
// (Rectangular Shade Characters)
// 🮔
// (Fill Characters)
// 🮕 🮖 🮗
// (Diagonal Fill Characters)
// 🮘 🮙
// (Smooth Mosaics)
// 🮚 🮛
// (Triangular Shade Characters)
// 🮜 🮝 🮞 🮟
// (Character Cell Diagonals)
// 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮
// (Light Solid Line With Stroke)
// 🮯
0x1FB94...0x1FBAF,
// (Negative Terminal Characters)
// 🮽 🮾 🮿
0x1FBBD...0x1FBBF,
// (Block Elements)
// 🯎 🯏
// (Character Cell Diagonals)
// 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟
// (Geometric Shapes)
// 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯
0x1FBCE...0x1FBEF,
// (Octants)
0x1CD00...0x1CDE5,
=> .box,
// Branch drawing character set, used for drawing git-like
// graphs in the terminal. Originally implemented in Kitty.
// Ref:
// - https://github.com/kovidgoyal/kitty/pull/7681
// - https://github.com/kovidgoyal/kitty/pull/7805
// NOTE: Kitty is GPL licensed, and its code was not referenced
// for these characters, only the loose specification of
// the character set in the pull request descriptions.
//
//
//
//
//
0xF5D0...0xF60D => .box,
// Separated Block Quadrants from Symbols for Legacy Computing Supplement
// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯
0x1CC21...0x1CC2F => .box,
// Powerline fonts
0xE0B0,
0xE0B1,
0xE0B3,
0xE0B4,
0xE0B6,
0xE0B2,
0xE0B8,
0xE0BA,
0xE0BC,
0xE0BE,
0xE0D2,
0xE0D4,
=> .powerline,
else => null,
return true;
};
defer ref_file.close();
const ref_bytes = try ref_file.readToEndAlloc(
alloc,
std.math.maxInt(usize),
);
defer alloc.free(ref_bytes);
// Do our PNG bytes comparison, if it's the same then we can
// move on, otherwise we'll decode the reference file and do
// a pixel-for-pixel diff.
if (std.mem.eql(u8, test_bytes, ref_bytes)) return false;
// Copy the test PNG in to the CWD so it isn't
// cleaned up with the rest of the tmp dir files.
const test_path = try std.fmt.allocPrint(
alloc,
"{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ cwd_absolute, i, i + 0xFF, width, height, thickness },
);
defer alloc.free(test_path);
try std.fs.copyFileAbsolute(path, test_path, .{});
// Use wuffs to decode the reference PNG to raw pixels.
// These will be RGBA, so when diffing we can just compare
// every fourth byte.
const ref_rgba = try wuffs.png.decode(alloc, ref_bytes);
defer alloc.free(ref_rgba.data);
assert(ref_rgba.width == atlas.getWidth());
assert(ref_rgba.height == atlas.getHeight());
// We'll make a visual representation of the diff using
// red for removed pixels and green for added. We make
// a z2d surface for that here.
var diff = try z2d.Surface.init(
.image_surface_rgb,
alloc,
atlas.getWidth(),
atlas.getHeight(),
);
defer diff.deinit(alloc);
const diff_pix = diff.image_surface_rgb.buf;
const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
var differs: bool = false;
for (0..test_gray.len) |j| {
const t = test_gray[j];
const r = ref_rgba.data[j * 4];
if (t == r) {
// If the pixels match, write it as a faded gray.
diff_pix[j].r = t / 3;
diff_pix[j].g = t / 3;
diff_pix[j].b = t / 3;
} else {
differs = true;
// Otherwise put the reference value in the red
// channel and the new value in the green channel.
diff_pix[j].r = r;
diff_pix[j].g = t;
}
}
};
// If the PNG data differs but not the raw pixels, that's
// a big red flag, since it could mean someone is trying to
// smuggle binary data in to the test files.
if (!differs) {
log.err(
"!!! Test PNG data does not match reference, but pixels do match! " ++
"Either z2d's PNG exporter changed or someone is " ++
"trying to smuggle binary data in the test files!\n" ++
"test={s}, reference={s}",
.{ test_path, ref_path },
);
return true;
}
// Drop the diff image as a PNG in the cwd.
const diff_path = try std.fmt.allocPrint(
alloc,
"./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ i, i + 0xFF, width, height, thickness },
);
defer alloc.free(diff_path);
try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{});
log.err(
"One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++
"test={s}, reference={s}, diff={s}",
.{ i, i + 0xFF, test_path, ref_path, diff_path },
);
return true;
}
/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions
/// against reference files, logs errors and exposes a diff for any difference
/// between the reference and test atlas.
///
/// Returns true if there was a diff.
fn testDrawRanges(
width: u32,
ascent: u32,
descent: u32,
thickness: u32,
) !bool {
const testing = std.testing;
const alloc = testing.allocator;
const metrics: font.Metrics = .calc(.{
.cell_width = @floatFromInt(width),
.ascent = @floatFromInt(ascent),
.descent = -@as(f64, @floatFromInt(descent)),
.line_gap = 0.0,
.underline_thickness = @floatFromInt(thickness),
.strikethrough_thickness = @floatFromInt(thickness),
});
const height = ascent + descent;
const padding_x = width / 4;
const padding_y = height / 4;
// Canvas to draw glyphs on, we'll re-use this for all glyphs.
var canvas = try font.sprite.Canvas.init(
alloc,
width,
height,
padding_x,
padding_y,
);
defer canvas.deinit();
// We render glyphs in batches of 256, which we copy (including padding) to
// a 16 by 16 surface to be compared with the reference file for that range.
const stride_x = width + 2 * padding_x;
const stride_y = height + 2 * padding_y;
var atlas = try z2d.Surface.init(
.image_surface_alpha8,
alloc,
@intCast(stride_x * 16),
@intCast(stride_y * 16),
);
defer atlas.deinit(alloc);
var i: u32 = std.mem.alignBackward(u32, ranges[0].min, 0x100);
// Try to make the sprite_face_test folder if it doesn't already exist.
var dir = testing.tmpDir(.{});
defer dir.cleanup();
const tmp_dir = try dir.dir.realpathAlloc(alloc, ".");
defer alloc.free(tmp_dir);
// We set this to true if we have any fails so we can
// return an error after we're done comparing all glyphs.
var fail: bool = false;
inline for (ranges) |range| {
for (range.min..range.max + 1) |cp| {
// If we've moved to a new batch of 256, check the
// current one and clear the surface for the next one.
if (cp - i >= 0x100) {
// Export to our tmp dir.
const path = try std.fmt.allocPrint(
alloc,
"{s}/U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ tmp_dir, i, i + 0xFF, width, height, thickness },
);
defer alloc.free(path);
try z2d.png_exporter.writeToPNGFile(atlas, path, .{});
if (try testDiffAtlas(
alloc,
&atlas,
path,
i,
width,
height,
thickness,
)) fail = true;
i = std.mem.alignBackward(u32, @intCast(cp), 0x100);
@memset(std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf), 0);
}
try getDrawFn(@intCast(cp)).?(
@intCast(cp),
&canvas,
width,
height,
metrics,
);
canvas.clearClippingRegions();
atlas.composite(
&canvas.sfc,
.src,
@intCast(stride_x * ((cp - i) % 16)),
@intCast(stride_y * ((cp - i) / 16)),
.{},
);
@memset(std.mem.sliceAsBytes(canvas.sfc.image_surface_alpha8.buf), 0);
canvas.clip_top = 0;
canvas.clip_left = 0;
canvas.clip_right = 0;
canvas.clip_bottom = 0;
}
}
const path = try std.fmt.allocPrint(
alloc,
"{s}/U+{X}...U+{X}-{d}x{d}+{d}.png",
.{ tmp_dir, i, i + 0xFF, width, height, thickness },
);
defer alloc.free(path);
try z2d.png_exporter.writeToPNGFile(atlas, path, .{});
if (try testDiffAtlas(
alloc,
&atlas,
path,
i,
width,
height,
thickness,
)) fail = true;
return fail;
}
test "sprite face render all sprites" {
// Renders all sprites to an atlas and compares
// it to a ground truth for regression testing.
var diff: bool = false;
// testDrawRanges(width, ascent, descent, thickness):
//
// We compare 4 different sets of metrics;
// - even cell size / even thickness
// - even cell size / odd thickness
// - odd cell size / even thickness
// - odd cell size / odd thickness
// (Also a decreasing range of sizes.)
if (try testDrawRanges(18, 30, 6, 4)) diff = true;
if (try testDrawRanges(12, 20, 4, 3)) diff = true;
if (try testDrawRanges(11, 19, 2, 2)) diff = true;
if (try testDrawRanges(9, 15, 2, 1)) diff = true;
try std.testing.expect(!diff); // There should be no diffs from reference.
}
// test "sprite face print all sprites" {
// std.debug.print("\n\n", .{});
// inline for (ranges) |range| {
// for (range.min..range.max + 1) |cp| {
// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) });
// }
// }
// std.debug.print("\n\n", .{});
// }
test {
@import("std").testing.refAllDecls(@This());
std.testing.refAllDecls(@This());
}

View File

@ -1,564 +0,0 @@
//! This file contains functions for drawing certain characters from Powerline
//! Extra (https://github.com/ryanoasis/powerline-extra-symbols). These
//! characters are similarly to box-drawing characters (see Box.zig), so the
//! logic will be mainly the same, just with a much reduced character set.
//!
//! Note that this is not the complete list of Powerline glyphs that may be
//! needed, so this may grow to add other glyphs from the set.
const Powerline = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Quad = @import("canvas.zig").Quad;
const log = std.log.scoped(.powerline_font);
/// The cell width and height because the boxes are fit perfectly
/// into a cell so that they all properly connect with zero spacing.
width: u32,
height: u32,
/// Base thickness value for glyphs that are not completely solid (backslashes,
/// thin half-circles, etc). If you want to do any DPI scaling, it is expected
/// to be done earlier.
///
/// TODO: this and Thickness are currently unused but will be when the
/// aforementioned glyphs are added.
thickness: u32,
/// The thickness of a line.
const Thickness = enum {
super_light,
light,
heavy,
/// Calculate the real height of a line based on its thickness
/// and a base thickness value. The base thickness value is expected
/// to be in pixels.
fn height(self: Thickness, base: u32) u32 {
return switch (self) {
.super_light => @max(base / 2, 1),
.light => base,
.heavy => base * 2,
};
}
};
inline fn sq(x: anytype) @TypeOf(x) {
return x * x;
}
pub fn renderGlyph(
self: Powerline,
alloc: Allocator,
atlas: *font.Atlas,
cp: u32,
) !font.Glyph {
// Create the canvas we'll use to draw
var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height);
defer canvas.deinit();
// Perform the actual drawing
try self.draw(alloc, &canvas, cp);
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
// Our coordinates start at the BOTTOM for our renderers so we have to
// specify an offset of the full height because we rendered a full size
// cell.
const offset_y = @as(i32, @intCast(self.height));
return font.Glyph{
.width = self.width,
.height = self.height,
.offset_x = 0,
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatFromInt(self.width),
};
}
fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void {
switch (cp) {
// Hard dividers and triangles
0xE0B0,
0xE0B2,
0xE0B8,
0xE0BA,
0xE0BC,
0xE0BE,
=> try self.draw_wedge_triangle(canvas, cp),
// Soft Dividers
0xE0B1,
0xE0B3,
=> try self.draw_chevron(canvas, cp),
// Half-circles
0xE0B4,
0xE0B6,
=> try self.draw_half_circle(alloc, canvas, cp),
// Mirrored top-down trapezoids
0xE0D2,
0xE0D4,
=> try self.draw_trapezoid_top_bottom(canvas, cp),
else => return error.InvalidCodepoint,
}
}
fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
const width = self.width;
const height = self.height;
var p1_x: u32 = 0;
var p1_y: u32 = 0;
var p2_x: u32 = 0;
var p2_y: u32 = 0;
var p3_x: u32 = 0;
var p3_y: u32 = 0;
switch (cp) {
0xE0B1 => {
p1_x = 0;
p1_y = 0;
p2_x = width;
p2_y = height / 2;
p3_x = 0;
p3_y = height;
},
0xE0B3 => {
p1_x = width;
p1_y = 0;
p2_x = 0;
p2_y = height / 2;
p3_x = width;
p3_y = height;
},
else => unreachable,
}
try canvas.triangle_outline(.{
.p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) },
.p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) },
.p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) },
}, @floatFromInt(Thickness.light.height(self.thickness)), .on);
}
fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
const width = self.width;
const height = self.height;
var p1_x: u32 = 0;
var p2_x: u32 = 0;
var p3_x: u32 = 0;
var p1_y: u32 = 0;
var p2_y: u32 = 0;
var p3_y: u32 = 0;
switch (cp) {
0xE0B0 => {
p1_x = 0;
p1_y = 0;
p2_x = width;
p2_y = height / 2;
p3_x = 0;
p3_y = height;
},
0xE0B2 => {
p1_x = width;
p1_y = 0;
p2_x = 0;
p2_y = height / 2;
p3_x = width;
p3_y = height;
},
0xE0B8 => {
p1_x = 0;
p1_y = 0;
p2_x = width;
p2_y = height;
p3_x = 0;
p3_y = height;
},
0xE0BA => {
p1_x = width;
p1_y = 0;
p2_x = width;
p2_y = height;
p3_x = 0;
p3_y = height;
},
0xE0BC => {
p1_x = 0;
p1_y = 0;
p2_x = width;
p2_y = 0;
p3_x = 0;
p3_y = height;
},
0xE0BE => {
p1_x = 0;
p1_y = 0;
p2_x = width;
p2_y = 0;
p3_x = width;
p3_y = height;
},
else => unreachable,
}
try canvas.triangle(.{
.p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) },
.p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) },
.p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) },
}, .on);
}
fn draw_half_circle(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void {
const supersample = 4;
// We make a canvas big enough for the whole circle, with the supersample
// applied.
const width = self.width * 2 * supersample;
const height = self.height * supersample;
// We set a minimum super-sampled canvas to assert on. The minimum cell
// size is 1x3px, and this looked safe in empirical testing.
std.debug.assert(width >= 8); // 1 * 2 * 4
std.debug.assert(height >= 12); // 3 * 4
const center_x = width / 2 - 1;
const center_y = height / 2 - 1;
// Our radii. We're technically drawing an ellipse here to ensure that this
// works for fonts with different aspect ratios than a typical 2:1 H*W, e.g.
// Iosevka (which is around 2.6:1).
const radius_x = width / 2 - 1; // This gives us a small margin for smoothing
const radius_y = height / 2;
// Pre-allocate a matrix to plot the points on.
const cap = height * width;
var points = try alloc.alloc(u8, cap);
defer alloc.free(points);
@memset(points, 0);
{
// This is a midpoint ellipse algorithm, similar to a midpoint circle
// algorithm in that we only draw the octants we need and then reflect
// the result across the other axes. Since an ellipse has two radii, we
// need to calculate two octants instead of one. There are variations
// on the algorithm and you can find many examples online. This one
// does use some floating point math in calculating the decision
// parameter, but I've found it clear in its implementation and it does
// not require adjustment for integer error.
//
// This algorithm has undergone some iterations, so the following
// references might be helpful for understanding:
//
// * "Drawing a circle, point by point, without floating point
// support" (Dennis Yurichev,
// https://yurichev.com/news/20220322_circle/), which describes the
// midpoint circle algorithm and implementation we initially adapted
// here.
//
// * "Ellipse-Generating Algorithms" (RTU Latvia,
// https://daugavpils.rtu.lv/wp-content/uploads/sites/34/2020/11/LEC_3.pdf),
// which was used to further adapt the algorithm for ellipses.
//
// * "An Effective Approach to Minimize Error in Midpoint Ellipse
// Drawing Algorithm" (Dr. M. Javed Idrisi, Aayesha Ashraf,
// https://arxiv.org/abs/2103.04033), which includes a synopsis of
// the history of ellipse drawing algorithms, and further references.
// Declare some casted constants for use in various calculations below
const rx: i32 = @intCast(radius_x);
const ry: i32 = @intCast(radius_y);
const rxf: f64 = @floatFromInt(radius_x);
const ryf: f64 = @floatFromInt(radius_y);
const cx: i32 = @intCast(center_x);
const cy: i32 = @intCast(center_y);
// Our plotting x and y
var x: i32 = 0;
var y: i32 = @intCast(radius_y);
// Decision parameter, initialized for region 1
var dparam: f64 = sq(ryf) - sq(rxf) * ryf + sq(rxf) * 0.25;
// Region 1
while (2 * sq(ry) * x < 2 * sq(rx) * y) {
// Right side
const x1 = @max(0, cx + x);
const y1 = @max(0, cy + y);
const x2 = @max(0, cx + x);
const y2 = @max(0, cy - y);
// Left side
const x3 = @max(0, cx - x);
const y3 = @max(0, cy + y);
const x4 = @max(0, cx - x);
const y4 = @max(0, cy - y);
// Points
const p1 = y1 * width + x1;
const p2 = y2 * width + x2;
const p3 = y3 * width + x3;
const p4 = y4 * width + x4;
// Set the points in the matrix, ignore any out of bounds
if (p1 < cap) points[p1] = 0xFF;
if (p2 < cap) points[p2] = 0xFF;
if (p3 < cap) points[p3] = 0xFF;
if (p4 < cap) points[p4] = 0xFF;
// Calculate next pixels based on midpoint bounds
x += 1;
if (dparam < 0) {
const xf: f64 = @floatFromInt(x);
dparam += 2 * sq(ryf) * xf + sq(ryf);
} else {
y -= 1;
const xf: f64 = @floatFromInt(x);
const yf: f64 = @floatFromInt(y);
dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(ryf);
}
}
// Region 2
{
// Reset our decision parameter for region 2
const xf: f64 = @floatFromInt(x);
const yf: f64 = @floatFromInt(y);
dparam = sq(ryf) * sq(xf + 0.5) + sq(rxf) * sq(yf - 1) - sq(rxf) * sq(ryf);
}
while (y >= 0) {
// Right side
const x1 = @max(0, cx + x);
const y1 = @max(0, cy + y);
const x2 = @max(0, cx + x);
const y2 = @max(0, cy - y);
// Left side
const x3 = @max(0, cx - x);
const y3 = @max(0, cy + y);
const x4 = @max(0, cx - x);
const y4 = @max(0, cy - y);
// Points
const p1 = y1 * width + x1;
const p2 = y2 * width + x2;
const p3 = y3 * width + x3;
const p4 = y4 * width + x4;
// Set the points in the matrix, ignore any out of bounds
if (p1 < cap) points[p1] = 0xFF;
if (p2 < cap) points[p2] = 0xFF;
if (p3 < cap) points[p3] = 0xFF;
if (p4 < cap) points[p4] = 0xFF;
// Calculate next pixels based on midpoint bounds
y -= 1;
if (dparam > 0) {
const yf: f64 = @floatFromInt(y);
dparam -= 2 * sq(rxf) * yf + sq(rxf);
} else {
x += 1;
const xf: f64 = @floatFromInt(x);
const yf: f64 = @floatFromInt(y);
dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(rxf);
}
}
}
// Fill
{
const u_height: u32 = @intCast(height);
const u_width: u32 = @intCast(width);
for (0..u_height) |yf| {
for (0..u_width) |left| {
// Count forward from the left to the first filled pixel
if (points[yf * u_width + left] != 0) {
// Count back to our left point from the right to the first
// filled pixel on the other side.
var right: usize = u_width - 1;
while (right > left) : (right -= 1) {
if (points[yf * u_width + right] != 0) {
break;
}
}
// Start filling 1 index after the left and go until we hit
// the right; this will be a no-op if the line length is <
// 3 as both left and right will have already been filled.
const start = yf * u_width + left;
const end = yf * u_width + right;
if (end - start >= 3) {
for (start + 1..end) |idx| {
points[idx] = 0xFF;
}
}
}
}
}
}
// Now that we have our points, we need to "split" our matrix on the x
// axis for the downsample.
{
// The side of the circle we're drawing
const offset_j: u32 = if (cp == 0xE0B4) center_x + 1 else 0;
for (0..self.height) |r| {
for (0..self.width) |c| {
var total: u32 = 0;
for (0..supersample) |i| {
for (0..supersample) |j| {
const idx = (r * supersample + i) * width + (c * supersample + j + offset_j);
total += points[idx];
}
}
const average = @as(u8, @intCast(@min(total / (supersample * supersample), 0xFF)));
canvas.rect(
.{
.x = @intCast(c),
.y = @intCast(r),
.width = 1,
.height = 1,
},
@as(font.sprite.Color, @enumFromInt(average)),
);
}
}
}
}
fn draw_trapezoid_top_bottom(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
const t_top: Quad(f64) = if (cp == 0xE0D4)
.{
.p0 = .{
.x = 0,
.y = 0,
},
.p1 = .{
.x = @floatFromInt(self.width - self.width / 3),
.y = @floatFromInt(self.height / 2 - self.height / 20),
},
.p2 = .{
.x = @floatFromInt(self.width),
.y = @floatFromInt(self.height / 2 - self.height / 20),
},
.p3 = .{
.x = @floatFromInt(self.width),
.y = 0,
},
}
else
.{
.p0 = .{
.x = 0,
.y = 0,
},
.p1 = .{
.x = 0,
.y = @floatFromInt(self.height / 2 - self.height / 20),
},
.p2 = .{
.x = @floatFromInt(self.width / 3),
.y = @floatFromInt(self.height / 2 - self.height / 20),
},
.p3 = .{
.x = @floatFromInt(self.width),
.y = 0,
},
};
const t_bottom: Quad(f64) = if (cp == 0xE0D4)
.{
.p0 = .{
.x = @floatFromInt(self.width - self.width / 3),
.y = @floatFromInt(self.height / 2 + self.height / 20),
},
.p1 = .{
.x = 0,
.y = @floatFromInt(self.height),
},
.p2 = .{
.x = @floatFromInt(self.width),
.y = @floatFromInt(self.height),
},
.p3 = .{
.x = @floatFromInt(self.width),
.y = @floatFromInt(self.height / 2 + self.height / 20),
},
}
else
.{
.p0 = .{
.x = 0,
.y = @floatFromInt(self.height / 2 + self.height / 20),
},
.p1 = .{
.x = 0,
.y = @floatFromInt(self.height),
},
.p2 = .{
.x = @floatFromInt(self.width),
.y = @floatFromInt(self.height),
},
.p3 = .{
.x = @floatFromInt(self.width / 3),
.y = @floatFromInt(self.height / 2 + self.height / 20),
},
};
try canvas.quad(t_top, .on);
try canvas.quad(t_bottom, .on);
}
test "all" {
const testing = std.testing;
const alloc = testing.allocator;
const cps = [_]u32{
0xE0B0,
0xE0B2,
0xE0B8,
0xE0BA,
0xE0BC,
0xE0BE,
0xE0B4,
0xE0B6,
0xE0D2,
0xE0D4,
0xE0B1,
0xE0B3,
};
for (cps) |cp| {
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
const face: Powerline = .{ .width = 18, .height = 36, .thickness = 2 };
const glyph = try face.renderGlyph(
alloc,
&atlas_grayscale,
cp,
);
try testing.expectEqual(@as(u32, face.width), glyph.width);
try testing.expectEqual(@as(u32, face.height), glyph.height);
}
}

View File

@ -81,19 +81,39 @@ pub const Canvas = struct {
/// The underlying z2d surface.
sfc: z2d.Surface,
padding_x: u32,
padding_y: u32,
clip_top: u32 = 0,
clip_left: u32 = 0,
clip_right: u32 = 0,
clip_bottom: u32 = 0,
alloc: Allocator,
pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas {
pub fn init(
alloc: Allocator,
width: u32,
height: u32,
padding_x: u32,
padding_y: u32,
) !Canvas {
// Create the surface we'll be using.
// We add padding to both sides (hence `2 *`)
const sfc = try z2d.Surface.initPixel(
.{ .alpha8 = .{ .a = 0 } },
alloc,
@intCast(width),
@intCast(height),
@intCast(width + 2 * padding_x),
@intCast(height + 2 * padding_y),
);
errdefer sfc.deinit(alloc);
return .{ .sfc = sfc, .alloc = alloc };
return .{
.sfc = sfc,
.padding_x = padding_x,
.padding_y = padding_y,
.alloc = alloc,
};
}
pub fn deinit(self: *Canvas) void {
@ -109,30 +129,33 @@ pub const Canvas = struct {
) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region {
assert(atlas.format == .grayscale);
const width = @as(u32, @intCast(self.sfc.getWidth()));
const height = @as(u32, @intCast(self.sfc.getHeight()));
self.trim();
const sfc_width: u32 = @intCast(self.sfc.getWidth());
const sfc_height: u32 = @intCast(self.sfc.getHeight());
// Subtract our clip margins from the
// width and height to get region size.
const region_width = sfc_width -| self.clip_left -| self.clip_right;
const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
// Allocate our texture atlas region
const region = region: {
// We need to add a 1px padding to the font so that we don't
// get fuzzy issues when blending textures.
const padding = 1;
// Get the full padded region
// Reserve a region with a 1px margin on the bottom and right edges
// so that we can avoid interpolation between adjacent glyphs during
// texture sampling.
var region = try atlas.reserve(
alloc,
width + (padding * 2), // * 2 because left+right
height + (padding * 2), // * 2 because top+bottom
region_width + 1,
region_height + 1,
);
// Modify the region so that we remove the padding so that
// we write to the non-zero location. The data in an Altlas
// is always initialized to zero (Atlas.clear) so we don't
// need to worry about zero-ing that.
region.x += padding;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
// Modify the region to remove the margin so that we write to the
// non-zero location. The data in an Altlas is always initialized
// to zero (Atlas.clear) so we don't need to worry about zero-ing
// that.
region.width -= 1;
region.height -= 1;
break :region region;
};
@ -140,38 +163,138 @@ pub const Canvas = struct {
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
// Write the glyph information into the atlas
assert(region.width == width);
assert(region.height == height);
atlas.set(region, buffer);
assert(region.width == region_width);
assert(region.height == region_height);
atlas.setFromLarger(
region,
buffer,
sfc_width,
self.clip_left,
self.clip_top,
);
}
return region;
}
// Adjust clip boundaries to trim off any fully transparent rows or columns.
// This circumvents abstractions from z2d so that it can be performant.
fn trim(self: *Canvas) void {
const width: u32 = @intCast(self.sfc.getWidth());
const height: u32 = @intCast(self.sfc.getHeight());
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
top: while (self.clip_top < height - self.clip_bottom) {
const y = self.clip_top;
const x0 = self.clip_left;
const x1 = width - self.clip_right;
for (buf[y * width ..][x0..x1]) |v| {
if (v != 0) break :top;
}
self.clip_top += 1;
}
bottom: while (self.clip_bottom < height - self.clip_top) {
const y = height - self.clip_bottom -| 1;
const x0 = self.clip_left;
const x1 = width - self.clip_right;
for (buf[y * width ..][x0..x1]) |v| {
if (v != 0) break :bottom;
}
self.clip_bottom += 1;
}
left: while (self.clip_left < width - self.clip_right) {
const x = self.clip_left;
const y0 = self.clip_top;
const y1 = height - self.clip_bottom;
for (y0..y1) |y| {
if (buf[y * width + x] != 0) break :left;
}
self.clip_left += 1;
}
right: while (self.clip_right < width - self.clip_left) {
const x = width - self.clip_right -| 1;
const y0 = self.clip_top;
const y1 = height - self.clip_bottom;
for (y0..y1) |y| {
if (buf[y * width + x] != 0) break :right;
}
self.clip_right += 1;
}
}
/// Only really useful for test purposes, since the clipping region is
/// automatically excluded when writing to an atlas with `writeAtlas`.
pub fn clearClippingRegions(self: *Canvas) void {
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
const width: usize = @intCast(self.sfc.getWidth());
const height: usize = @intCast(self.sfc.getHeight());
for (0..height) |y| {
for (0..self.clip_left) |x| {
buf[y * width + x] = 0;
}
}
for (0..height) |y| {
for (width - self.clip_right..width) |x| {
buf[y * width + x] = 0;
}
}
for (0..self.clip_top) |y| {
for (0..width) |x| {
buf[y * width + x] = 0;
}
}
for (height - self.clip_bottom..height) |y| {
for (0..width) |x| {
buf[y * width + x] = 0;
}
}
}
/// Return a transformation representing the translation for our padding.
pub fn transformation(self: Canvas) z2d.Transformation {
return .{
.ax = 1,
.by = 0,
.cx = 0,
.dy = 1,
.tx = @as(f64, @floatFromInt(self.padding_x)),
.ty = @as(f64, @floatFromInt(self.padding_y)),
};
}
/// Acquires a z2d drawing context, caller MUST deinit context.
pub fn getContext(self: *Canvas) z2d.Context {
return .init(self.alloc, &self.sfc);
var ctx = z2d.Context.init(self.alloc, &self.sfc);
// Offset by our padding to keep
// coordinates relative to the cell.
ctx.setTransformation(self.transformation());
return ctx;
}
/// Draw and fill a single pixel
pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void {
pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void {
self.sfc.putPixel(
@intCast(x),
@intCast(y),
x + @as(i32, @intCast(self.padding_x)),
y + @as(i32, @intCast(self.padding_y)),
.{ .alpha8 = .{ .a = @intFromEnum(color) } },
);
}
/// Draw and fill a rectangle. This is the main primitive for drawing
/// lines as well (which are just generally skinny rectangles...)
pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void {
const x0 = v.x;
const x1 = v.x + v.width;
const y0 = v.y;
const y1 = v.y + v.height;
for (y0..y1) |y| {
for (x0..x1) |x| {
pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void {
var y = v.y;
while (y < v.y + v.height) : (y += 1) {
var x = v.x;
while (x < v.x + v.width) : (x += 1) {
self.pixel(
@intCast(x),
@intCast(y),
@ -181,96 +304,226 @@ pub const Canvas = struct {
}
}
/// Convenience wrapper for `Canvas.rect`
pub fn box(
self: *Canvas,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
color: Color,
) void {
self.rect((Box(i32){
.p0 = .{ .x = x0, .y = y0 },
.p1 = .{ .x = x1, .y = y1 },
}).rect(), color);
}
/// Draw and fill a quad.
pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void {
var path: z2d.StaticPath(6) = .{};
path.init(); // nodes.len = 0
var path = self.staticPath(6); // nodes.len = 0
path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1
path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2
path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3
path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4
path.close(); // +2, nodes.len = 6
try z2d.painter.fill(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.wrapped_path.nodes.items,
.{},
);
try self.fillPath(path.wrapped_path, .{}, color);
}
/// Draw and fill a triangle.
pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void {
var path: z2d.StaticPath(5) = .{};
path.init(); // nodes.len = 0
var path = self.staticPath(5); // nodes.len = 0
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
path.close(); // +2, nodes.len = 5
try self.fillPath(path.wrapped_path, .{}, color);
}
/// Stroke a line.
pub fn line(
self: *Canvas,
l: Line(f64),
thickness: f64,
color: Color,
) !void {
var path = self.staticPath(2); // nodes.len = 0
path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1
path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2
try self.strokePath(
path.wrapped_path,
.{
.line_cap_mode = .butt,
.line_width = thickness,
},
color,
);
}
/// Create a static path of the provided len and initialize it.
/// Use this function instead of making the path manually since
/// it ensures that the transform is applied.
pub inline fn staticPath(
self: *Canvas,
comptime len: usize,
) z2d.StaticPath(len) {
var path: z2d.StaticPath(len) = .{};
path.init();
path.wrapped_path.transformation = self.transformation();
return path;
}
/// Stroke a z2d path.
pub fn strokePath(
self: *Canvas,
path: z2d.Path,
opts: z2d.painter.StrokeOpts,
color: Color,
) z2d.painter.StrokeError!void {
try z2d.painter.stroke(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.nodes.items,
opts,
);
}
/// Do an inner stroke on a z2d path, right now this involves a pretty
/// heavy workaround that uses two extra surfaces; in the future, z2d
/// should add inner and outer strokes natively.
pub fn innerStrokePath(
self: *Canvas,
path: z2d.Path,
opts: z2d.painter.StrokeOpts,
color: Color,
) (z2d.painter.StrokeError || z2d.painter.FillError)!void {
// On one surface we fill the shape, this will be a mask we
// multiply with the double-width stroke so that only the
// part inside is used.
var fill_sfc: z2d.Surface = try .init(
.image_surface_alpha8,
self.alloc,
self.sfc.getWidth(),
self.sfc.getHeight(),
);
defer fill_sfc.deinit(self.alloc);
// On the other we'll do the double width stroke.
var stroke_sfc: z2d.Surface = try .init(
.image_surface_alpha8,
self.alloc,
self.sfc.getWidth(),
self.sfc.getHeight(),
);
defer stroke_sfc.deinit(self.alloc);
// Make a closed version of the path for our fill, so
// that we can support open paths for inner stroke.
var closed_path = path;
closed_path.nodes = try path.nodes.clone(self.alloc);
defer closed_path.deinit(self.alloc);
try closed_path.close(self.alloc);
// Fill the shape in white to the fill surface, we use
// white because this is a mask that we'll multiply with
// the stroke, we want everything inside to be the stroke
// color.
try z2d.painter.fill(
self.alloc,
&fill_sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = 255 } },
} },
closed_path.nodes.items,
.{},
);
// Stroke the shape with double the desired width.
var mut_opts = opts;
mut_opts.line_width *= 2;
try z2d.painter.stroke(
self.alloc,
&stroke_sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.nodes.items,
mut_opts,
);
// We multiply the stroke sfc on to the fill surface.
// The z2d composite operation doesn't seem to work for
// this with alpha8 surfaces, so we have to do it manually.
for (
std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf),
std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf),
) |*d, s| {
d.* = @intFromFloat(@round(
255.0 *
(@as(f64, @floatFromInt(s)) / 255.0) *
(@as(f64, @floatFromInt(d.*)) / 255.0),
));
}
// Then we composite the result on to the main surface.
self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{});
}
/// Fill a z2d path.
pub fn fillPath(
self: *Canvas,
path: z2d.Path,
opts: z2d.painter.FillOpts,
color: Color,
) z2d.painter.FillError!void {
try z2d.painter.fill(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.wrapped_path.nodes.items,
.{},
);
}
pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void {
var path: z2d.StaticPath(3) = .{};
path.init(); // nodes.len = 0
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
try z2d.painter.stroke(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.wrapped_path.nodes.items,
.{
.line_cap_mode = .round,
.line_width = thickness,
},
);
}
/// Stroke a line.
pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void {
var path: z2d.StaticPath(2) = .{};
path.init(); // nodes.len = 0
path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1
path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2
try z2d.painter.stroke(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.wrapped_path.nodes.items,
.{
.line_cap_mode = .round,
.line_width = thickness,
},
path.nodes.items,
opts,
);
}
/// Invert all pixels on the canvas.
pub fn invert(self: *Canvas) void {
for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| {
v.* = 255 - v.*;
}
}
/// Mirror the canvas horizontally.
pub fn flipHorizontal(self: *Canvas) Allocator.Error!void {
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
const clone = try self.alloc.dupe(u8, buf);
defer self.alloc.free(clone);
const width: usize = @intCast(self.sfc.getWidth());
const height: usize = @intCast(self.sfc.getHeight());
for (0..height) |y| {
for (0..width) |x| {
buf[y * width + x] = clone[y * width + width - x - 1];
}
}
std.mem.swap(u32, &self.clip_left, &self.clip_right);
}
/// Mirror the canvas vertically.
pub fn flipVertical(self: *Canvas) Allocator.Error!void {
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
const clone = try self.alloc.dupe(u8, buf);
defer self.alloc.free(clone);
const width: usize = @intCast(self.sfc.getWidth());
const height: usize = @intCast(self.sfc.getHeight());
for (0..height) |y| {
for (0..width) |x| {
buf[y * width + x] = clone[(height - y - 1) * width + x];
}
}
std.mem.swap(u32, &self.clip_top, &self.clip_bottom);
}
};

View File

@ -1,65 +0,0 @@
//! This file renders cursor sprites.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Sprite = font.sprite.Sprite;
/// Draw a cursor.
pub fn renderGlyph(
alloc: Allocator,
atlas: *font.Atlas,
sprite: Sprite,
width: u32,
height: u32,
thickness: u32,
) !font.Glyph {
// Make a canvas of the desired size
var canvas = try font.sprite.Canvas.init(alloc, width, height);
defer canvas.deinit();
// Draw the appropriate sprite
switch (sprite) {
Sprite.cursor_rect => canvas.rect(.{
.x = 0,
.y = 0,
.width = width,
.height = height,
}, .on),
Sprite.cursor_hollow_rect => {
// left
canvas.rect(.{ .x = 0, .y = 0, .width = thickness, .height = height }, .on);
// right
canvas.rect(.{ .x = width -| thickness, .y = 0, .width = thickness, .height = height }, .on);
// top
canvas.rect(.{ .x = 0, .y = 0, .width = width, .height = thickness }, .on);
// bottom
canvas.rect(.{ .x = 0, .y = height -| thickness, .width = width, .height = thickness }, .on);
},
Sprite.cursor_bar => canvas.rect(.{
.x = 0,
.y = 0,
.width = thickness,
.height = height,
}, .on),
else => unreachable,
}
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
return font.Glyph{
// HACK: Set the width for the bar cursor to just the thickness,
// this is just for the benefit of the custom shader cursor
// uniform code. -- In the future code will be introduced to
// auto-crop the canvas so that this isn't needed.
.width = if (sprite == .cursor_bar) thickness else width,
.height = height,
.offset_x = 0,
.offset_y = @intCast(height),
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatFromInt(width),
};
}

View File

@ -0,0 +1,50 @@
# This is a *special* directory.
The files in this directory are imported by `../Face.zig` and scanned for pub
functions with names matching a specific format, which are then used to handle
drawing specified codepoints.
## IMPORTANT
When you add a new file here, you need to add the corresponding import in
`../Face.zig` for its draw functions to be picked up. I tried dynamically
listing these files to do this automatically but it was more pain than it
was worth.
## `draw*` functions
Any function named `draw<CODEPOINT>` or `draw<MIN>_<MAX>` will be used to
draw the codepoint or range of codepoints specified in the name. These are
hex-encoded values with upper case letters.
`draw*` functions are provided with these arguments:
```zig
/// The codepoint being drawn. For single-codepoint draw functions this can
/// just be discarded, but it's needed for range draw functions to determine
/// which value in the range needs to be drawn.
cp: u32,
/// The canvas on which to draw the codepoint.
////
/// This canvas has been prepared with an extra quarter of the width/height on
/// each edge, and its transform has been set so that [0, 0] is still the upper
/// left of the cell and [width, height] is still the bottom right; in order to
/// draw above or to the left, use negative values, and to draw below or to the
/// right use values greater than the width or the height.
///
/// Because the canvas has been prepared this way, it's possible to draw glyphs
/// that exit the cell bounds by some amount- an example of when this is useful
/// is in drawing box-drawing diagonals, with enough overlap so that they can
/// seamlessly connect across corners of cells.
canvas: *font.sprite.Canvas,
/// The width of the cell to draw for.
width: u32,
/// The height of the cell to draw for.
height: u32,
/// The font grid metrics.
metrics: font.Metrics,
```
`draw*` functions may only return `DrawFnError!void` (defined in `../Face.zig`).
## `special.zig`
The functions in `special.zig` are not for drawing unicode codepoints,
rather their names match the enum tag names in the `Sprite` enum from
`src/font/sprite.zig`. They are called with the same arguments as the
other `draw*` functions.

View File

@ -0,0 +1,184 @@
//! Block Elements | U+2580...U+259F
//! https://en.wikipedia.org/wiki/Block_Elements
//!
//!
//!
//!
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const common = @import("common.zig");
const Shade = common.Shade;
const Quads = common.Quads;
const Alignment = common.Alignment;
const rect = common.rect;
const font = @import("../../main.zig");
const Sprite = @import("../../sprite.zig").Sprite;
// Utility names for common fractions
const one_eighth: f64 = 0.125;
const one_quarter: f64 = 0.25;
const one_third: f64 = (1.0 / 3.0);
const three_eighths: f64 = 0.375;
const half: f64 = 0.5;
const five_eighths: f64 = 0.625;
const two_thirds: f64 = (2.0 / 3.0);
const three_quarters: f64 = 0.75;
const seven_eighths: f64 = 0.875;
pub fn draw2580_259F(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
// '▀' UPPER HALF BLOCK
0x2580 => block(metrics, canvas, .upper, 1, half),
// '▁' LOWER ONE EIGHTH BLOCK
0x2581 => block(metrics, canvas, .lower, 1, one_eighth),
// '▂' LOWER ONE QUARTER BLOCK
0x2582 => block(metrics, canvas, .lower, 1, one_quarter),
// '▃' LOWER THREE EIGHTHS BLOCK
0x2583 => block(metrics, canvas, .lower, 1, three_eighths),
// '▄' LOWER HALF BLOCK
0x2584 => block(metrics, canvas, .lower, 1, half),
// '▅' LOWER FIVE EIGHTHS BLOCK
0x2585 => block(metrics, canvas, .lower, 1, five_eighths),
// '▆' LOWER THREE QUARTERS BLOCK
0x2586 => block(metrics, canvas, .lower, 1, three_quarters),
// '▇' LOWER SEVEN EIGHTHS BLOCK
0x2587 => block(metrics, canvas, .lower, 1, seven_eighths),
// '█' FULL BLOCK
0x2588 => fullBlockShade(metrics, canvas, .on),
// '▉' LEFT SEVEN EIGHTHS BLOCK
0x2589 => block(metrics, canvas, .left, seven_eighths, 1),
// '▊' LEFT THREE QUARTERS BLOCK
0x258a => block(metrics, canvas, .left, three_quarters, 1),
// '▋' LEFT FIVE EIGHTHS BLOCK
0x258b => block(metrics, canvas, .left, five_eighths, 1),
// '▌' LEFT HALF BLOCK
0x258c => block(metrics, canvas, .left, half, 1),
// '▍' LEFT THREE EIGHTHS BLOCK
0x258d => block(metrics, canvas, .left, three_eighths, 1),
// '▎' LEFT ONE QUARTER BLOCK
0x258e => block(metrics, canvas, .left, one_quarter, 1),
// '▏' LEFT ONE EIGHTH BLOCK
0x258f => block(metrics, canvas, .left, one_eighth, 1),
// '▐' RIGHT HALF BLOCK
0x2590 => block(metrics, canvas, .right, half, 1),
// '░'
0x2591 => fullBlockShade(metrics, canvas, .light),
// '▒'
0x2592 => fullBlockShade(metrics, canvas, .medium),
// '▓'
0x2593 => fullBlockShade(metrics, canvas, .dark),
// '▔' UPPER ONE EIGHTH BLOCK
0x2594 => block(metrics, canvas, .upper, 1, one_eighth),
// '▕' RIGHT ONE EIGHTH BLOCK
0x2595 => block(metrics, canvas, .right, one_eighth, 1),
// '▖'
0x2596 => quadrant(metrics, canvas, .{ .bl = true }),
// '▗'
0x2597 => quadrant(metrics, canvas, .{ .br = true }),
// '▘'
0x2598 => quadrant(metrics, canvas, .{ .tl = true }),
// '▙'
0x2599 => quadrant(metrics, canvas, .{ .tl = true, .bl = true, .br = true }),
// '▚'
0x259a => quadrant(metrics, canvas, .{ .tl = true, .br = true }),
// '▛'
0x259b => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }),
// '▜'
0x259c => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .br = true }),
// '▝'
0x259d => quadrant(metrics, canvas, .{ .tr = true }),
// '▞'
0x259e => quadrant(metrics, canvas, .{ .tr = true, .bl = true }),
// '▟'
0x259f => quadrant(metrics, canvas, .{ .tr = true, .bl = true, .br = true }),
else => unreachable,
}
}
pub fn block(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime alignment: Alignment,
comptime width: f64,
comptime height: f64,
) void {
blockShade(metrics, canvas, alignment, width, height, .on);
}
pub fn blockShade(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime alignment: Alignment,
comptime width: f64,
comptime height: f64,
comptime shade: Shade,
) void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const w: u32 = @intFromFloat(@round(float_width * width));
const h: u32 = @intFromFloat(@round(float_height * height));
const x = switch (alignment.horizontal) {
.left => 0,
.right => metrics.cell_width - w,
.center => (metrics.cell_width - w) / 2,
};
const y = switch (alignment.vertical) {
.top => 0,
.bottom => metrics.cell_height - h,
.middle => (metrics.cell_height - h) / 2,
};
canvas.rect(.{
.x = @intCast(x),
.y = @intCast(y),
.width = @intCast(w),
.height = @intCast(h),
}, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade))));
}
pub fn fullBlockShade(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
shade: Shade,
) void {
canvas.box(
0,
0,
@intCast(metrics.cell_width),
@intCast(metrics.cell_height),
@as(font.sprite.Color, @enumFromInt(@intFromEnum(shade))),
);
}
fn quadrant(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime quads: Quads,
) void {
const center_x = metrics.cell_width / 2 + metrics.cell_width % 2;
const center_y = metrics.cell_height / 2 + metrics.cell_height % 2;
if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y);
if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y);
if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height);
if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height);
}

View File

@ -0,0 +1,947 @@
//! Box Drawing | U+2500...U+257F
//! https://en.wikipedia.org/wiki/Box_Drawing
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const common = @import("common.zig");
const Thickness = common.Thickness;
const Shade = common.Shade;
const Quads = common.Quads;
const Corner = common.Corner;
const Edge = common.Edge;
const Alignment = common.Alignment;
const rect = common.rect;
const hline = common.hline;
const vline = common.vline;
const hlineMiddle = common.hlineMiddle;
const vlineMiddle = common.vlineMiddle;
const font = @import("../../main.zig");
const Sprite = @import("../../sprite.zig").Sprite;
/// Specification of a traditional intersection-style line/box-drawing char,
/// which can have a different style of line from each edge to the center.
pub const Lines = packed struct(u8) {
up: Style = .none,
right: Style = .none,
down: Style = .none,
left: Style = .none,
const Style = enum(u2) {
none,
light,
heavy,
double,
};
};
pub fn draw2500_257F(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
// '─'
0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }),
// '━'
0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }),
// '│'
0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }),
// '┃'
0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }),
// '┄'
0x2504 => dashHorizontal(
metrics,
canvas,
3,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┅'
0x2505 => dashHorizontal(
metrics,
canvas,
3,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┆'
0x2506 => dashVertical(
metrics,
canvas,
3,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┇'
0x2507 => dashVertical(
metrics,
canvas,
3,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┈'
0x2508 => dashHorizontal(
metrics,
canvas,
4,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┉'
0x2509 => dashHorizontal(
metrics,
canvas,
4,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┊'
0x250a => dashVertical(
metrics,
canvas,
4,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┋'
0x250b => dashVertical(
metrics,
canvas,
4,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┌'
0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }),
// '┍'
0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }),
// '┎'
0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }),
// '┏'
0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }),
// '┐'
0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }),
// '┑'
0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }),
// '┒'
0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }),
// '┓'
0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }),
// '└'
0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }),
// '┕'
0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }),
// '┖'
0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }),
// '┗'
0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }),
// '┘'
0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }),
// '┙'
0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }),
// '┚'
0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }),
// '┛'
0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }),
// '├'
0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }),
// '┝'
0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }),
// '┞'
0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }),
// '┟'
0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }),
// '┠'
0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }),
// '┡'
0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }),
// '┢'
0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }),
// '┣'
0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }),
// '┤'
0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }),
// '┥'
0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }),
// '┦'
0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }),
// '┧'
0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }),
// '┨'
0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }),
// '┩'
0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }),
// '┪'
0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }),
// '┫'
0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }),
// '┬'
0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }),
// '┭'
0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }),
// '┮'
0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }),
// '┯'
0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }),
// '┰'
0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }),
// '┱'
0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }),
// '┲'
0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }),
// '┳'
0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }),
// '┴'
0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }),
// '┵'
0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }),
// '┶'
0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }),
// '┷'
0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }),
// '┸'
0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }),
// '┹'
0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }),
// '┺'
0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }),
// '┻'
0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }),
// '┼'
0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }),
// '┽'
0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }),
// '┾'
0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }),
// '┿'
0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }),
// '╀'
0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }),
// '╁'
0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }),
// '╂'
0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }),
// '╃'
0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }),
// '╄'
0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }),
// '╅'
0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }),
// '╆'
0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }),
// '╇'
0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }),
// '╈'
0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }),
// '╉'
0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }),
// '╊'
0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }),
// '╋'
0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }),
// '╌'
0x254c => dashHorizontal(
metrics,
canvas,
2,
Thickness.light.height(metrics.box_thickness),
Thickness.light.height(metrics.box_thickness),
),
// '╍'
0x254d => dashHorizontal(
metrics,
canvas,
2,
Thickness.heavy.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '╎'
0x254e => dashVertical(
metrics,
canvas,
2,
Thickness.light.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '╏'
0x254f => dashVertical(
metrics,
canvas,
2,
Thickness.heavy.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '═'
0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }),
// '║'
0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }),
// '╒'
0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }),
// '╓'
0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }),
// '╔'
0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }),
// '╕'
0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }),
// '╖'
0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }),
// '╗'
0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }),
// '╘'
0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }),
// '╙'
0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }),
// '╚'
0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }),
// '╛'
0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }),
// '╜'
0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }),
// '╝'
0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }),
// '╞'
0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }),
// '╟'
0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }),
// '╠'
0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }),
// '╡'
0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }),
// '╢'
0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }),
// '╣'
0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }),
// '╤'
0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }),
// '╥'
0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }),
// '╦'
0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }),
// '╧'
0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }),
// '╨'
0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }),
// '╩'
0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }),
// '╪'
0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }),
// '╫'
0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }),
// '╬'
0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }),
// '╭'
0x256d => try arc(metrics, canvas, .br, .light),
// '╮'
0x256e => try arc(metrics, canvas, .bl, .light),
// '╯'
0x256f => try arc(metrics, canvas, .tl, .light),
// '╰'
0x2570 => try arc(metrics, canvas, .tr, .light),
// ''
0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas),
// '╲'
0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas),
// ''
0x2573 => lightDiagonalCross(metrics, canvas),
// '╴'
0x2574 => linesChar(metrics, canvas, .{ .left = .light }),
// '╵'
0x2575 => linesChar(metrics, canvas, .{ .up = .light }),
// '╶'
0x2576 => linesChar(metrics, canvas, .{ .right = .light }),
// '╷'
0x2577 => linesChar(metrics, canvas, .{ .down = .light }),
// '╸'
0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }),
// '╹'
0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }),
// '╺'
0x257a => linesChar(metrics, canvas, .{ .right = .heavy }),
// '╻'
0x257b => linesChar(metrics, canvas, .{ .down = .heavy }),
// '╼'
0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }),
// '╽'
0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }),
// '╾'
0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }),
// '╿'
0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }),
else => unreachable,
}
}
pub fn linesChar(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
lines: Lines,
) void {
const light_px = Thickness.light.height(metrics.box_thickness);
const heavy_px = Thickness.heavy.height(metrics.box_thickness);
// Top of light horizontal strokes
const h_light_top = (metrics.cell_height -| light_px) / 2;
// Bottom of light horizontal strokes
const h_light_bottom = h_light_top +| light_px;
// Top of heavy horizontal strokes
const h_heavy_top = (metrics.cell_height -| heavy_px) / 2;
// Bottom of heavy horizontal strokes
const h_heavy_bottom = h_heavy_top +| heavy_px;
// Top of the top doubled horizontal stroke (bottom is `h_light_top`)
const h_double_top = h_light_top -| light_px;
// Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`)
const h_double_bottom = h_light_bottom +| light_px;
// Left of light vertical strokes
const v_light_left = (metrics.cell_width -| light_px) / 2;
// Right of light vertical strokes
const v_light_right = v_light_left +| light_px;
// Left of heavy vertical strokes
const v_heavy_left = (metrics.cell_width -| heavy_px) / 2;
// Right of heavy vertical strokes
const v_heavy_right = v_heavy_left +| heavy_px;
// Left of the left doubled vertical stroke (right is `v_light_left`)
const v_double_left = v_light_left -| light_px;
// Right of the right doubled vertical stroke (left is `v_light_right`)
const v_double_right = v_light_right +| light_px;
// The bottom of the up line
const up_bottom = if (lines.left == .heavy or lines.right == .heavy)
h_heavy_bottom
else if (lines.left != lines.right or lines.down == lines.up)
if (lines.left == .double or lines.right == .double)
h_double_bottom
else
h_light_bottom
else if (lines.left == .none and lines.right == .none)
h_light_bottom
else
h_light_top;
// The top of the down line
const down_top = if (lines.left == .heavy or lines.right == .heavy)
h_heavy_top
else if (lines.left != lines.right or lines.up == lines.down)
if (lines.left == .double or lines.right == .double)
h_double_top
else
h_light_top
else if (lines.left == .none and lines.right == .none)
h_light_top
else
h_light_bottom;
// The right of the left line
const left_right = if (lines.up == .heavy or lines.down == .heavy)
v_heavy_right
else if (lines.up != lines.down or lines.left == lines.right)
if (lines.up == .double or lines.down == .double)
v_double_right
else
v_light_right
else if (lines.up == .none and lines.down == .none)
v_light_right
else
v_light_left;
// The left of the right line
const right_left = if (lines.up == .heavy or lines.down == .heavy)
v_heavy_left
else if (lines.up != lines.down or lines.right == lines.left)
if (lines.up == .double or lines.down == .double)
v_double_left
else
v_light_left
else if (lines.up == .none and lines.down == .none)
v_light_left
else
v_light_right;
switch (lines.up) {
.none => {},
.light => canvas.box(
@intCast(v_light_left),
0,
@intCast(v_light_right),
@intCast(up_bottom),
.on,
),
.heavy => canvas.box(
@intCast(v_heavy_left),
0,
@intCast(v_heavy_right),
@intCast(up_bottom),
.on,
),
.double => {
const left_bottom = if (lines.left == .double) h_light_top else up_bottom;
const right_bottom = if (lines.right == .double) h_light_top else up_bottom;
canvas.box(
@intCast(v_double_left),
0,
@intCast(v_light_left),
@intCast(left_bottom),
.on,
);
canvas.box(
@intCast(v_light_right),
0,
@intCast(v_double_right),
@intCast(right_bottom),
.on,
);
},
}
switch (lines.right) {
.none => {},
.light => canvas.box(
@intCast(right_left),
@intCast(h_light_top),
@intCast(metrics.cell_width),
@intCast(h_light_bottom),
.on,
),
.heavy => canvas.box(
@intCast(right_left),
@intCast(h_heavy_top),
@intCast(metrics.cell_width),
@intCast(h_heavy_bottom),
.on,
),
.double => {
const top_left = if (lines.up == .double) v_light_right else right_left;
const bottom_left = if (lines.down == .double) v_light_right else right_left;
canvas.box(
@intCast(top_left),
@intCast(h_double_top),
@intCast(metrics.cell_width),
@intCast(h_light_top),
.on,
);
canvas.box(
@intCast(bottom_left),
@intCast(h_light_bottom),
@intCast(metrics.cell_width),
@intCast(h_double_bottom),
.on,
);
},
}
switch (lines.down) {
.none => {},
.light => canvas.box(
@intCast(v_light_left),
@intCast(down_top),
@intCast(v_light_right),
@intCast(metrics.cell_height),
.on,
),
.heavy => canvas.box(
@intCast(v_heavy_left),
@intCast(down_top),
@intCast(v_heavy_right),
@intCast(metrics.cell_height),
.on,
),
.double => {
const left_top = if (lines.left == .double) h_light_bottom else down_top;
const right_top = if (lines.right == .double) h_light_bottom else down_top;
canvas.box(
@intCast(v_double_left),
@intCast(left_top),
@intCast(v_light_left),
@intCast(metrics.cell_height),
.on,
);
canvas.box(
@intCast(v_light_right),
@intCast(right_top),
@intCast(v_double_right),
@intCast(metrics.cell_height),
.on,
);
},
}
switch (lines.left) {
.none => {},
.light => canvas.box(
0,
@intCast(h_light_top),
@intCast(left_right),
@intCast(h_light_bottom),
.on,
),
.heavy => canvas.box(
0,
@intCast(h_heavy_top),
@intCast(left_right),
@intCast(h_heavy_bottom),
.on,
),
.double => {
const top_right = if (lines.up == .double) v_light_left else left_right;
const bottom_right = if (lines.down == .double) v_light_left else left_right;
canvas.box(
0,
@intCast(h_double_top),
@intCast(top_right),
@intCast(h_light_top),
.on,
);
canvas.box(
0,
@intCast(h_light_bottom),
@intCast(bottom_right),
@intCast(h_double_bottom),
.on,
);
},
}
}
pub fn lightDiagonalUpperRightToLowerLeft(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
const slope_x: f64 = @min(1.0, float_width / float_height);
const slope_y: f64 = @min(1.0, float_height / float_width);
canvas.line(.{
.p0 = .{
.x = float_width + 0.5 * slope_x,
.y = -0.5 * slope_y,
},
.p1 = .{
.x = -0.5 * slope_x,
.y = float_height + 0.5 * slope_y,
},
}, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}
pub fn lightDiagonalUpperLeftToLowerRight(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
const slope_x: f64 = @min(1.0, float_width / float_height);
const slope_y: f64 = @min(1.0, float_height / float_width);
canvas.line(.{
.p0 = .{
.x = -0.5 * slope_x,
.y = -0.5 * slope_y,
},
.p1 = .{
.x = float_width + 0.5 * slope_x,
.y = float_height + 0.5 * slope_y,
},
}, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}
pub fn lightDiagonalCross(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
lightDiagonalUpperRightToLowerLeft(metrics, canvas);
lightDiagonalUpperLeftToLowerRight(metrics, canvas);
}
fn quadrant(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime quads: Quads,
) void {
const center_x = metrics.cell_width / 2 + metrics.cell_width % 2;
const center_y = metrics.cell_height / 2 + metrics.cell_height % 2;
if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y);
if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y);
if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height);
if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height);
}
pub fn arc(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime corner: Corner,
comptime thickness: Thickness,
) !void {
const thick_px = thickness.height(metrics.box_thickness);
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2;
const r = @min(float_width, float_height) / 2;
// Fraction away from the center to place the middle control points,
const s: f64 = 0.25;
var path = canvas.staticPath(4);
switch (corner) {
.tl => {
path.moveTo(center_x, 0);
path.lineTo(center_x, center_y - r);
path.curveTo(
center_x,
center_y - s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.lineTo(0, center_y);
},
.tr => {
path.moveTo(center_x, 0);
path.lineTo(center_x, center_y - r);
path.curveTo(
center_x,
center_y - s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.lineTo(float_width, center_y);
},
.bl => {
path.moveTo(center_x, float_height);
path.lineTo(center_x, center_y + r);
path.curveTo(
center_x,
center_y + s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.lineTo(0, center_y);
},
.br => {
path.moveTo(center_x, float_height);
path.lineTo(center_x, center_y + r);
path.curveTo(
center_x,
center_y + s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.lineTo(float_width, center_y);
},
}
try canvas.strokePath(
path.wrapped_path,
.{
.line_cap_mode = .butt,
.line_width = float_thick,
},
.on,
);
}
fn dashHorizontal(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
count: u8,
thick_px: u32,
desired_gap: u32,
) void {
assert(count >= 2 and count <= 4);
// +------------+
// | |
// | |
// | |
// | |
// | -- -- -- |
// | |
// | |
// | |
// | |
// +------------+
// Our dashed line should be made such that when tiled horizontally
// it creates one consistent line with no uneven gap or segment sizes.
// In order to make sure this is the case, we should have half-sized
// gaps on the left and right so that it is centered properly.
// For N dashes, there are N - 1 gaps between them, but we also have
// half-sized gaps on either side, adding up to N total gaps.
const gap_count = count;
// We need at least 1 pixel for each gap and each dash, if we don't
// have that then we can't draw our dashed line correctly so we just
// draw a solid line and return.
if (metrics.cell_width < count + gap_count) {
hlineMiddle(metrics, canvas, .light);
return;
}
// We never want the gaps to take up more than 50% of the space,
// because if they do the dashes are too small and look wrong.
const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count)));
const total_gap_width: i32 = gap_count * gap_width;
const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width;
const dash_width: i32 = @divFloor(total_dash_width, count);
const remaining: i32 = @mod(total_dash_width, count);
assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width);
// Our dashes should be centered vertically.
const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2);
// We start at half a gap from the left edge, in order to center
// our dashes properly.
var x: i32 = @divFloor(gap_width, 2);
// We'll distribute the extra space in to dash widths, 1px at a
// time. We prefer this to making gaps larger since that is much
// more visually obvious.
var extra: i32 = remaining;
for (0..count) |_| {
var x1 = x + dash_width;
// We distribute left-over size in to dash widths,
// since it's less obvious there than in the gaps.
if (extra > 0) {
extra -= 1;
x1 += 1;
}
hline(canvas, x, x1, y, thick_px);
// Advance by the width of the dash we drew and the width
// of a gap to get the the start of the next dash.
x = x1 + gap_width;
}
}
fn dashVertical(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime count: u8,
thick_px: u32,
desired_gap: u32,
) void {
assert(count >= 2 and count <= 4);
// +-----------+
// | | |
// | | |
// | |
// | | |
// | | |
// | |
// | | |
// | | |
// | |
// +-----------+
// Our dashed line should be made such that when tiled vertically it
// it creates one consistent line with no uneven gap or segment sizes.
// In order to make sure this is the case, we should have an extra gap
// gap at the bottom.
//
// A single full-sized extra gap is preferred to two half-sized ones for
// vertical to allow better joining to solid characters without creating
// visible half-sized gaps. Unlike horizontal, centering is a lot less
// important, visually.
// Because of the extra gap at the bottom, there are as many gaps as
// there are dashes.
const gap_count = count;
// We need at least 1 pixel for each gap and each dash, if we don't
// have that then we can't draw our dashed line correctly so we just
// draw a solid line and return.
if (metrics.cell_height < count + gap_count) {
vlineMiddle(metrics, canvas, .light);
return;
}
// We never want the gaps to take up more than 50% of the space,
// because if they do the dashes are too small and look wrong.
const gap_height: i32 = @intCast(@min(desired_gap, metrics.cell_height / (2 * count)));
const total_gap_height: i32 = gap_count * gap_height;
const total_dash_height: i32 = @as(i32, @intCast(metrics.cell_height)) - total_gap_height;
const dash_height: i32 = @divFloor(total_dash_height, count);
const remaining: i32 = @mod(total_dash_height, count);
assert(dash_height * count + gap_height * gap_count + remaining == metrics.cell_height);
// Our dashes should be centered horizontally.
const x: i32 = @intCast((metrics.cell_width -| thick_px) / 2);
// We start at the top of the cell.
var y: i32 = 0;
// We'll distribute the extra space in to dash heights, 1px at a
// time. We prefer this to making gaps larger since that is much
// more visually obvious.
var extra: i32 = remaining;
inline for (0..count) |_| {
var y1 = y + dash_height;
// We distribute left-over size in to dash widths,
// since it's less obvious there than in the gaps.
if (extra > 0) {
extra -= 1;
y1 += 1;
}
vline(canvas, y, y1, x, thick_px);
// Advance by the height of the dash we drew and the height
// of a gap to get the the start of the next dash.
y = y1 + gap_height;
}
}

View File

@ -0,0 +1,148 @@
//! Braille Patterns | U+2800...U+28FF
//! https://en.wikipedia.org/wiki/Braille_Patterns
//!
//! (6 dot patterns)
//!
//!
//!
//!
//!
//! (8 dot patterns)
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
//!
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../../main.zig");
/// A braille pattern.
///
/// Mnemonic:
/// [t]op - . .
/// [u]pper - . .
/// [l]ower - . .
/// [b]ottom - . .
/// | |
/// [l]eft, [r]ight
///
/// Struct layout matches bit patterns of unicode codepoints.
const Pattern = packed struct(u8) {
tl: bool,
ul: bool,
ll: bool,
tr: bool,
ur: bool,
lr: bool,
bl: bool,
br: bool,
fn from(cp: u32) Pattern {
return @bitCast(@as(u8, @truncate(cp)));
}
};
pub fn draw2800_28FF(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = metrics;
var w: i32 = @intCast(@min(width / 4, height / 8));
var x_spacing: i32 = @intCast(width / 4);
var y_spacing: i32 = @intCast(height / 8);
var x_margin: i32 = @divFloor(x_spacing, 2);
var y_margin: i32 = @divFloor(y_spacing, 2);
var x_px_left: i32 =
@as(i32, @intCast(width)) - 2 * x_margin - x_spacing - 2 * w;
var y_px_left: i32 =
@as(i32, @intCast(height)) - 2 * y_margin - 3 * y_spacing - 4 * w;
// First, try hard to ensure the DOT width is non-zero
if (x_px_left >= 2 and y_px_left >= 4 and w == 0) {
w += 1;
x_px_left -= 2;
y_px_left -= 4;
}
// Second, prefer a non-zero margin
if (x_px_left >= 2 and x_margin == 0) {
x_margin = 1;
x_px_left -= 2;
}
if (y_px_left >= 2 and y_margin == 0) {
y_margin = 1;
y_px_left -= 2;
}
// Third, increase spacing
if (x_px_left >= 1) {
x_spacing += 1;
x_px_left -= 1;
}
if (y_px_left >= 3) {
y_spacing += 1;
y_px_left -= 3;
}
// Fourth, margins (spacing, but on the sides)
if (x_px_left >= 2) {
x_margin += 1;
x_px_left -= 2;
}
if (y_px_left >= 2) {
y_margin += 1;
y_px_left -= 2;
}
// Last - increase dot width
if (x_px_left >= 2 and y_px_left >= 4) {
w += 1;
x_px_left -= 2;
y_px_left -= 4;
}
assert(x_px_left <= 1 or y_px_left <= 1);
assert(2 * x_margin + 2 * w + x_spacing <= width);
assert(2 * y_margin + 4 * w + 3 * y_spacing <= height);
const x = [2]i32{ x_margin, x_margin + w + x_spacing };
const y = y: {
var y: [4]i32 = undefined;
y[0] = y_margin;
y[1] = y[0] + w + y_spacing;
y[2] = y[1] + w + y_spacing;
y[3] = y[2] + w + y_spacing;
break :y y;
};
assert(cp >= 0x2800);
assert(cp <= 0x28ff);
const p: Pattern = .from(cp);
if (p.tl) canvas.box(x[0], y[0], x[0] + w, y[0] + w, .on);
if (p.ul) canvas.box(x[0], y[1], x[0] + w, y[1] + w, .on);
if (p.ll) canvas.box(x[0], y[2], x[0] + w, y[2] + w, .on);
if (p.bl) canvas.box(x[0], y[3], x[0] + w, y[3] + w, .on);
if (p.tr) canvas.box(x[1], y[0], x[1] + w, y[0] + w, .on);
if (p.ur) canvas.box(x[1], y[1], x[1] + w, y[1] + w, .on);
if (p.lr) canvas.box(x[1], y[2], x[1] + w, y[2] + w, .on);
if (p.br) canvas.box(x[1], y[3], x[1] + w, y[3] + w, .on);
}

View File

@ -0,0 +1,505 @@
//! Branch Drawing Characters | U+F5D0...U+F60D
//!
//! Branch drawing character set, used for drawing git-like
//! graphs in the terminal. Originally implemented in Kitty.
//! Ref:
//! - https://github.com/kovidgoyal/kitty/pull/7681
//! - https://github.com/kovidgoyal/kitty/pull/7805
//! NOTE: Kitty is GPL licensed, and its code was not referenced
//! for these characters, only the loose specification of
//! the character set in the pull request descriptions.
//!
//!
//!
//!
//!
//!
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const common = @import("common.zig");
const Thickness = common.Thickness;
const Shade = common.Shade;
const Edge = common.Edge;
const hlineMiddle = common.hlineMiddle;
const vlineMiddle = common.vlineMiddle;
const arc = @import("box.zig").arc;
const font = @import("../../main.zig");
/// Specification of a branch drawing node, which consists of a
/// circle which is either empty or filled, and lines connecting
/// optionally between the circle and each of the 4 edges.
const BranchNode = packed struct(u5) {
up: bool = false,
right: bool = false,
down: bool = false,
left: bool = false,
filled: bool = false,
};
pub fn drawF5D0_F60D(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
// ''
0x0f5d0 => hlineMiddle(metrics, canvas, .light),
// ''
0x0f5d1 => vlineMiddle(metrics, canvas, .light),
// ''
0x0f5d2 => fadingLine(metrics, canvas, .right, .light),
// ''
0x0f5d3 => fadingLine(metrics, canvas, .left, .light),
// ''
0x0f5d4 => fadingLine(metrics, canvas, .bottom, .light),
// ''
0x0f5d5 => fadingLine(metrics, canvas, .top, .light),
// ''
0x0f5d6 => try arc(metrics, canvas, .br, .light),
// ''
0x0f5d7 => try arc(metrics, canvas, .bl, .light),
// ''
0x0f5d8 => try arc(metrics, canvas, .tr, .light),
// ''
0x0f5d9 => try arc(metrics, canvas, .tl, .light),
// ''
0x0f5da => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tr, .light);
},
// ''
0x0f5db => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5dc => {
try arc(metrics, canvas, .tr, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5dd => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tl, .light);
},
// ''
0x0f5de => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .bl, .light);
},
// ''
0x0f5df => {
try arc(metrics, canvas, .tl, .light);
try arc(metrics, canvas, .bl, .light);
},
// ''
0x0f5e0 => {
try arc(metrics, canvas, .bl, .light);
hlineMiddle(metrics, canvas, .light);
},
// ''
0x0f5e1 => {
try arc(metrics, canvas, .br, .light);
hlineMiddle(metrics, canvas, .light);
},
// ''
0x0f5e2 => {
try arc(metrics, canvas, .br, .light);
try arc(metrics, canvas, .bl, .light);
},
// ''
0x0f5e3 => {
try arc(metrics, canvas, .tl, .light);
hlineMiddle(metrics, canvas, .light);
},
// ''
0x0f5e4 => {
try arc(metrics, canvas, .tr, .light);
hlineMiddle(metrics, canvas, .light);
},
// ''
0x0f5e5 => {
try arc(metrics, canvas, .tr, .light);
try arc(metrics, canvas, .tl, .light);
},
// ''
0x0f5e6 => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tl, .light);
try arc(metrics, canvas, .tr, .light);
},
// ''
0x0f5e7 => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .bl, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5e8 => {
hlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .bl, .light);
try arc(metrics, canvas, .tl, .light);
},
// ''
0x0f5e9 => {
hlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tr, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5ea => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tl, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5eb => {
vlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tr, .light);
try arc(metrics, canvas, .bl, .light);
},
// ''
0x0f5ec => {
hlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tl, .light);
try arc(metrics, canvas, .br, .light);
},
// ''
0x0f5ed => {
hlineMiddle(metrics, canvas, .light);
try arc(metrics, canvas, .tr, .light);
try arc(metrics, canvas, .bl, .light);
},
// ''
0x0f5ee => branchNode(metrics, canvas, .{ .filled = true }, .light),
// ''
0x0f5ef => branchNode(metrics, canvas, .{}, .light),
// ''
0x0f5f0 => branchNode(metrics, canvas, .{
.right = true,
.filled = true,
}, .light),
// ''
0x0f5f1 => branchNode(metrics, canvas, .{
.right = true,
}, .light),
// ''
0x0f5f2 => branchNode(metrics, canvas, .{
.left = true,
.filled = true,
}, .light),
// ''
0x0f5f3 => branchNode(metrics, canvas, .{
.left = true,
}, .light),
// ''
0x0f5f4 => branchNode(metrics, canvas, .{
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f5f5 => branchNode(metrics, canvas, .{
.left = true,
.right = true,
}, .light),
// ''
0x0f5f6 => branchNode(metrics, canvas, .{
.down = true,
.filled = true,
}, .light),
// ''
0x0f5f7 => branchNode(metrics, canvas, .{
.down = true,
}, .light),
// ''
0x0f5f8 => branchNode(metrics, canvas, .{
.up = true,
.filled = true,
}, .light),
// ''
0x0f5f9 => branchNode(metrics, canvas, .{
.up = true,
}, .light),
// ''
0x0f5fa => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5fb => branchNode(metrics, canvas, .{
.up = true,
.down = true,
}, .light),
// ''
0x0f5fc => branchNode(metrics, canvas, .{
.right = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5fd => branchNode(metrics, canvas, .{
.right = true,
.down = true,
}, .light),
// ''
0x0f5fe => branchNode(metrics, canvas, .{
.left = true,
.down = true,
.filled = true,
}, .light),
// ''
0x0f5ff => branchNode(metrics, canvas, .{
.left = true,
.down = true,
}, .light),
// ''
0x0f600 => branchNode(metrics, canvas, .{
.up = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f601 => branchNode(metrics, canvas, .{
.up = true,
.right = true,
}, .light),
// ''
0x0f602 => branchNode(metrics, canvas, .{
.up = true,
.left = true,
.filled = true,
}, .light),
// ''
0x0f603 => branchNode(metrics, canvas, .{
.up = true,
.left = true,
}, .light),
// ''
0x0f604 => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f605 => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.right = true,
}, .light),
// ''
0x0f606 => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.left = true,
.filled = true,
}, .light),
// ''
0x0f607 => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.left = true,
}, .light),
// ''
0x0f608 => branchNode(metrics, canvas, .{
.down = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f609 => branchNode(metrics, canvas, .{
.down = true,
.left = true,
.right = true,
}, .light),
// ''
0x0f60a => branchNode(metrics, canvas, .{
.up = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f60b => branchNode(metrics, canvas, .{
.up = true,
.left = true,
.right = true,
}, .light),
// ''
0x0f60c => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.left = true,
.right = true,
.filled = true,
}, .light),
// ''
0x0f60d => branchNode(metrics, canvas, .{
.up = true,
.down = true,
.left = true,
.right = true,
}, .light),
else => unreachable,
}
}
fn branchNode(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
node: BranchNode,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(metrics.box_thickness);
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
// Top of horizontal strokes
const h_top = (metrics.cell_height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (metrics.cell_width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
// We calculate the center of the circle this way
// to ensure it aligns with box drawing characters
// since the lines are sometimes off center to
// make sure they aren't split between pixels.
const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2;
const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2;
// The radius needs to be the smallest distance from the center to an edge.
const r: f64 = @min(
@min(cx, cy),
@min(float_width - cx, float_height - cy),
);
var ctx = canvas.getContext();
defer ctx.deinit();
ctx.setSource(.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
} });
ctx.setLineWidth(float_thick);
// These @intFromFloat casts shouldn't ever fail since r can never
// be greater than cx or cy, so when subtracting it from them the
// result can never be negative.
if (node.up) canvas.box(
@intCast(v_left),
0,
@intCast(v_right),
@intFromFloat(@ceil(cy - r + float_thick / 2)),
.on,
);
if (node.right) canvas.box(
@intFromFloat(@floor(cx + r - float_thick / 2)),
@intCast(h_top),
@intCast(metrics.cell_width),
@intCast(h_bottom),
.on,
);
if (node.down) canvas.box(
@intCast(v_left),
@intFromFloat(@floor(cy + r - float_thick / 2)),
@intCast(v_right),
@intCast(metrics.cell_height),
.on,
);
if (node.left) canvas.box(
0,
@intCast(h_top),
@intFromFloat(@ceil(cx - r + float_thick / 2)),
@intCast(h_bottom),
.on,
);
if (node.filled) {
ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return;
ctx.closePath() catch return;
ctx.fill() catch return;
} else {
ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return;
ctx.closePath() catch return;
ctx.stroke() catch return;
}
}
fn fadingLine(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime to: Edge,
comptime thickness: Thickness,
) void {
const thick_px = thickness.height(metrics.box_thickness);
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
// Top of horizontal strokes
const h_top = (metrics.cell_height -| thick_px) / 2;
// Bottom of horizontal strokes
const h_bottom = h_top +| thick_px;
// Left of vertical strokes
const v_left = (metrics.cell_width -| thick_px) / 2;
// Right of vertical strokes
const v_right = v_left +| thick_px;
// If we're fading to the top or left, we start with 0.0
// and increment up as we progress, otherwise we start
// at 255.0 and increment down (negative).
var color: f64 = switch (to) {
.top, .left => 0.0,
.bottom, .right => 255.0,
};
const inc: f64 = 255.0 / switch (to) {
.top => float_height,
.bottom => -float_height,
.left => float_width,
.right => -float_width,
};
switch (to) {
.top, .bottom => {
for (0..metrics.cell_height) |y| {
for (v_left..v_right) |x| {
canvas.pixel(
@intCast(x),
@intCast(y),
@enumFromInt(@as(u8, @intFromFloat(@round(color)))),
);
}
color += inc;
}
},
.left, .right => {
for (0..metrics.cell_width) |x| {
for (h_top..h_bottom) |y| {
canvas.pixel(
@intCast(x),
@intCast(y),
@enumFromInt(@as(u8, @intFromFloat(@round(color)))),
);
}
color += inc;
}
},
}
}

View File

@ -0,0 +1,244 @@
//! This file contains a set of useful helper functions
//! and types for drawing our sprite font glyphs. These
//! are generally applicable to multiple sets of glyphs
//! rather than being single-use.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const font = @import("../../main.zig");
const Sprite = @import("../../sprite.zig").Sprite;
const log = std.log.scoped(.sprite_font);
// Utility names for common fractions
pub const one_eighth: f64 = 0.125;
pub const one_quarter: f64 = 0.25;
pub const one_third: f64 = (1.0 / 3.0);
pub const three_eighths: f64 = 0.375;
pub const half: f64 = 0.5;
pub const five_eighths: f64 = 0.625;
pub const two_thirds: f64 = (2.0 / 3.0);
pub const three_quarters: f64 = 0.75;
pub const seven_eighths: f64 = 0.875;
/// The thickness of a line.
pub const Thickness = enum {
super_light,
light,
heavy,
/// Calculate the real height of a line based on its
/// thickness and a base thickness value. The base
/// thickness value is expected to be in pixels.
pub fn height(self: Thickness, base: u32) u32 {
return switch (self) {
.super_light => @max(base / 2, 1),
.light => base,
.heavy => base * 2,
};
}
};
/// Shades.
pub const Shade = enum(u8) {
off = 0x00,
light = 0x40,
medium = 0x80,
dark = 0xc0,
on = 0xff,
_,
};
/// Applicable to any set of glyphs with features
/// that may be present or not in each quadrant.
pub const Quads = packed struct(u4) {
tl: bool = false,
tr: bool = false,
bl: bool = false,
br: bool = false,
};
/// A corner of a cell.
pub const Corner = enum(u2) {
tl,
tr,
bl,
br,
};
/// An edge of a cell.
pub const Edge = enum(u2) {
top,
left,
bottom,
right,
};
/// Alignment of a figure within a cell.
pub const Alignment = struct {
horizontal: enum {
left,
right,
center,
} = .center,
vertical: enum {
top,
bottom,
middle,
} = .middle,
pub const upper: Alignment = .{ .vertical = .top };
pub const lower: Alignment = .{ .vertical = .bottom };
pub const left: Alignment = .{ .horizontal = .left };
pub const right: Alignment = .{ .horizontal = .right };
pub const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left };
pub const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right };
pub const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left };
pub const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right };
pub const center: Alignment = .{};
pub const upper_center = upper;
pub const lower_center = lower;
pub const middle_left = left;
pub const middle_right = right;
pub const middle_center: Alignment = center;
pub const top = upper;
pub const bottom = lower;
pub const center_top = top;
pub const center_bottom = bottom;
pub const top_left = upper_left;
pub const top_right = upper_right;
pub const bottom_left = lower_left;
pub const bottom_right = lower_right;
};
/// Fill a rect, clamped to within the cell boundaries.
///
/// TODO: Eliminate usages of this, prefer `canvas.box`.
pub fn rect(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
x1: u32,
y1: u32,
x2: u32,
y2: u32,
) void {
canvas.box(
@intCast(@min(@max(x1, 0), metrics.cell_width)),
@intCast(@min(@max(y1, 0), metrics.cell_height)),
@intCast(@min(@max(x2, 0), metrics.cell_width)),
@intCast(@min(@max(y2, 0), metrics.cell_height)),
.on,
);
}
/// Centered vertical line of the provided thickness.
pub fn vlineMiddle(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
thickness: Thickness,
) void {
const thick_px = thickness.height(metrics.box_thickness);
vline(
canvas,
0,
@intCast(metrics.cell_height),
@intCast((metrics.cell_width -| thick_px) / 2),
thick_px,
);
}
/// Centered horizontal line of the provided thickness.
pub fn hlineMiddle(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
thickness: Thickness,
) void {
const thick_px = thickness.height(metrics.box_thickness);
hline(
canvas,
0,
@intCast(metrics.cell_width),
@intCast((metrics.cell_height -| thick_px) / 2),
thick_px,
);
}
/// Vertical line with the left edge at `x`, between `y1` and `y2`.
pub fn vline(
canvas: *font.sprite.Canvas,
y1: i32,
y2: i32,
x: i32,
thickness_px: u32,
) void {
canvas.box(x, y1, x + @as(i32, @intCast(thickness_px)), y2, .on);
}
/// Horizontal line with the top edge at `y`, between `x1` and `x2`.
pub fn hline(
canvas: *font.sprite.Canvas,
x1: i32,
x2: i32,
y: i32,
thickness_px: u32,
) void {
canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on);
}
/// xHalfs[0] should be used as the right edge of a left-aligned half.
/// xHalfs[1] should be used as the left edge of a right-aligned half.
pub fn xHalfs(metrics: font.Metrics) [2]u32 {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const half_width: u32 = @intFromFloat(@round(0.5 * float_width));
return .{ half_width, metrics.cell_width - half_width };
}
/// Use these values as such:
/// yThirds[0] bottom edge of the first third.
/// yThirds[1] top edge of the second third.
/// yThirds[2] bottom edge of the second third.
/// yThirds[3] top edge of the final third.
pub fn yThirds(metrics: font.Metrics) [4]u32 {
const float_height: f64 = @floatFromInt(metrics.cell_height);
const one_third_height: u32 = @intFromFloat(@round(one_third * float_height));
const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height));
return .{
one_third_height,
metrics.cell_height - two_thirds_height,
two_thirds_height,
metrics.cell_height - one_third_height,
};
}
/// Use these values as such:
/// yQuads[0] bottom edge of first quarter.
/// yQuads[1] top edge of second quarter.
/// yQuads[2] bottom edge of second quarter.
/// yQuads[3] top edge of third quarter.
/// yQuads[4] bottom edge of third quarter
/// yQuads[5] top edge of fourth quarter.
pub fn yQuads(metrics: font.Metrics) [6]u32 {
const float_height: f64 = @floatFromInt(metrics.cell_height);
const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height));
const half_height: u32 = @intFromFloat(@round(0.50 * float_height));
const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height));
return .{
quarter_height,
metrics.cell_height - three_quarters_height,
half_height,
metrics.cell_height - half_height,
three_quarters_height,
metrics.cell_height - quarter_height,
};
}

View File

@ -0,0 +1,200 @@
//! Geometric Shapes | U+25A0...U+25FF
//! https://en.wikipedia.org/wiki/Geometric_Shapes_(Unicode_block)
//!
//!
//!
//!
//!
//!
//!
//!
//! Only a subset of this block is viable for sprite drawing; filling
//! out this file to have full coverage of this block is not the goal.
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const common = @import("common.zig");
const Thickness = common.Thickness;
const Corner = common.Corner;
const Shade = common.Shade;
const font = @import("../../main.zig");
///
pub fn draw25E2_25E5(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
//
0x25e2 => try cornerTriangleShade(metrics, canvas, .br, .on),
//
0x25e3 => try cornerTriangleShade(metrics, canvas, .bl, .on),
//
0x25e4 => try cornerTriangleShade(metrics, canvas, .tl, .on),
//
0x25e5 => try cornerTriangleShade(metrics, canvas, .tr, .on),
else => unreachable,
}
}
///
pub fn draw25F8_25FA(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
//
0x25f8 => try cornerTriangleOutline(metrics, canvas, .tl),
//
0x25f9 => try cornerTriangleOutline(metrics, canvas, .tr),
//
0x25fa => try cornerTriangleOutline(metrics, canvas, .bl),
else => unreachable,
}
}
///
pub fn draw25FF(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
try cornerTriangleOutline(metrics, canvas, .br);
}
pub fn cornerTriangleShade(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime corner: Corner,
comptime shade: Shade,
) !void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const x0, const y0, const x1, const y1, const x2, const y2 =
switch (corner) {
.tl => .{
0,
0,
0,
float_height,
float_width,
0,
},
.tr => .{
0,
0,
float_width,
float_height,
float_width,
0,
},
.bl => .{
0,
0,
0,
float_height,
float_width,
float_height,
},
.br => .{
0,
float_height,
float_width,
float_height,
float_width,
0,
},
};
var path = canvas.staticPath(5); // nodes.len = 0
path.moveTo(x0, y0); // +1, nodes.len = 1
path.lineTo(x1, y1); // +1, nodes.len = 2
path.lineTo(x2, y2); // +1, nodes.len = 3
path.close(); // +2, nodes.len = 5
try canvas.fillPath(
path.wrapped_path,
.{},
@enumFromInt(@intFromEnum(shade)),
);
}
pub fn cornerTriangleOutline(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime corner: Corner,
) !void {
const float_thick: f64 = @floatFromInt(Thickness.light.height(metrics.box_thickness));
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const x0, const y0, const x1, const y1, const x2, const y2 =
switch (corner) {
.tl => .{
0,
0,
0,
float_height,
float_width,
0,
},
.tr => .{
0,
0,
float_width,
float_height,
float_width,
0,
},
.bl => .{
0,
0,
0,
float_height,
float_width,
float_height,
},
.br => .{
0,
float_height,
float_width,
float_height,
float_width,
0,
},
};
var path = canvas.staticPath(5); // nodes.len = 0
path.moveTo(x0, y0); // +1, nodes.len = 1
path.lineTo(x1, y1); // +1, nodes.len = 2
path.lineTo(x2, y2); // +1, nodes.len = 3
path.close(); // +2, nodes.len = 5
try canvas.innerStrokePath(path.wrapped_path, .{
.line_cap_mode = .butt,
.line_width = float_thick,
}, .on);
}

View File

@ -0,0 +1,396 @@
//! Powerline + Powerline Extra Symbols | U+E0B0...U+E0D4
//! https://github.com/ryanoasis/powerline-extra-symbols
//!
//!
//!
//!
//!
//! We implement the more geometric glyphs here, but not the stylized ones.
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const common = @import("common.zig");
const Thickness = common.Thickness;
const Shade = common.Shade;
const box = @import("box.zig");
const font = @import("../../main.zig");
const Quad = font.sprite.Canvas.Quad;
///
pub fn drawE0B0(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{ .x = float_width, .y = float_height / 2 },
.p2 = .{ .x = 0, .y = float_height },
}, .on);
}
///
pub fn drawE0B2(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = float_width, .y = 0 },
.p1 = .{ .x = 0, .y = float_height / 2 },
.p2 = .{ .x = float_width, .y = float_height },
}, .on);
}
///
pub fn drawE0B8(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{ .x = float_width, .y = float_height },
.p2 = .{ .x = 0, .y = float_height },
}, .on);
}
///
pub fn drawE0B9(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
box.lightDiagonalUpperLeftToLowerRight(metrics, canvas);
}
///
pub fn drawE0BA(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = float_width, .y = 0 },
.p1 = .{ .x = float_width, .y = float_height },
.p2 = .{ .x = 0, .y = float_height },
}, .on);
}
///
pub fn drawE0BB(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
box.lightDiagonalUpperRightToLowerLeft(metrics, canvas);
}
///
pub fn drawE0BC(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{ .x = float_width, .y = 0 },
.p2 = .{ .x = 0, .y = float_height },
}, .on);
}
///
pub fn drawE0BD(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
box.lightDiagonalUpperRightToLowerLeft(metrics, canvas);
}
///
pub fn drawE0BE(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
try canvas.triangle(.{
.p0 = .{ .x = 0, .y = 0 },
.p1 = .{ .x = float_width, .y = 0 },
.p2 = .{ .x = float_width, .y = float_height },
}, .on);
}
///
pub fn drawE0BF(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
box.lightDiagonalUpperLeftToLowerRight(metrics, canvas);
}
///
pub fn drawE0B1(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
var path = canvas.staticPath(3);
path.moveTo(0, 0);
path.lineTo(float_width, float_height / 2);
path.lineTo(0, float_height);
try canvas.strokePath(
path.wrapped_path,
.{
.line_cap_mode = .butt,
.line_width = @floatFromInt(
Thickness.light.height(metrics.box_thickness),
),
},
.on,
);
}
///
pub fn drawE0B3(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
try drawE0B1(cp, canvas, width, height, metrics);
try canvas.flipHorizontal();
}
///
pub fn drawE0B4(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
// Coefficient for approximating a circular arc.
const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0;
const radius: f64 = @min(float_width, float_height / 2);
var path = canvas.staticPath(6);
path.moveTo(0, 0);
path.curveTo(
radius * c,
0,
radius,
radius - radius * c,
radius,
radius,
);
path.lineTo(radius, float_height - radius);
path.curveTo(
radius,
float_height - radius + radius * c,
radius * c,
float_height,
0,
float_height,
);
path.close();
try canvas.fillPath(path.wrapped_path, .{}, .on);
}
///
pub fn drawE0B5(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
// Coefficient for approximating a circular arc.
const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0;
const radius: f64 = @min(float_width, float_height / 2);
var path = canvas.staticPath(4);
path.moveTo(0, 0);
path.curveTo(
radius * c,
0,
radius,
radius - radius * c,
radius,
radius,
);
path.lineTo(radius, float_height - radius);
path.curveTo(
radius,
float_height - radius + radius * c,
radius * c,
float_height,
0,
float_height,
);
try canvas.innerStrokePath(path.wrapped_path, .{
.line_width = @floatFromInt(metrics.box_thickness),
.line_cap_mode = .butt,
}, .on);
}
///
pub fn drawE0B6(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
try drawE0B4(cp, canvas, width, height, metrics);
try canvas.flipHorizontal();
}
///
pub fn drawE0B7(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
try drawE0B5(cp, canvas, width, height, metrics);
try canvas.flipHorizontal();
}
///
pub fn drawE0D2(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
const float_width: f64 = @floatFromInt(width);
const float_height: f64 = @floatFromInt(height);
const float_thick: f64 = @floatFromInt(metrics.box_thickness);
// Top piece
{
var path = canvas.staticPath(6);
path.moveTo(0, 0);
path.lineTo(float_width, 0);
path.lineTo(float_width / 2, float_height / 2 - float_thick / 2);
path.lineTo(0, float_height / 2 - float_thick / 2);
path.close();
try canvas.fillPath(path.wrapped_path, .{}, .on);
}
// Bottom piece
{
var path = canvas.staticPath(6);
path.moveTo(0, float_height);
path.lineTo(float_width, float_height);
path.lineTo(float_width / 2, float_height / 2 + float_thick / 2);
path.lineTo(0, float_height / 2 + float_thick / 2);
path.close();
try canvas.fillPath(path.wrapped_path, .{}, .on);
}
}
///
pub fn drawE0D4(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
try drawE0D2(cp, canvas, width, height, metrics);
try canvas.flipHorizontal();
}

View File

@ -0,0 +1,328 @@
//! This file contains glyph drawing functions for all of the
//! non-Unicode sprite glyphs, such as cursors and underlines.
//!
//! The naming convention in this file differs from the usual
//! because the draw functions for special sprites are found by
//! having names that exactly match the enum fields in Sprite.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../../main.zig");
const Sprite = font.sprite.Sprite;
pub fn underline(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
canvas.rect(.{
.x = 0,
.y = @intCast(metrics.underline_position),
.width = @intCast(width),
.height = @intCast(metrics.underline_thickness),
}, .on);
}
pub fn underline_double(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
// We place one underline above the underline position, and one below
// by one thickness, creating a "negative" underline where the single
// underline would be placed.
canvas.rect(.{
.x = 0,
.y = @intCast(metrics.underline_position -| metrics.underline_thickness),
.width = @intCast(width),
.height = @intCast(metrics.underline_thickness),
}, .on);
canvas.rect(.{
.x = 0,
.y = @intCast(metrics.underline_position +| metrics.underline_thickness),
.width = @intCast(width),
.height = @intCast(metrics.underline_thickness),
}, .on);
}
pub fn underline_dotted(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
// TODO: Rework this now that we can go out of bounds, just
// make sure that adjacent versions of this glyph align.
const dot_width = @max(metrics.underline_thickness, 3);
const dot_count = @max((width / dot_width) / 2, 1);
const gap_width = std.math.divCeil(
u32,
width -| (dot_count * dot_width),
dot_count,
) catch return error.MathError;
var i: u32 = 0;
while (i < dot_count) : (i += 1) {
// Ensure we never go out of bounds for the rect
const x = @min(i * (dot_width + gap_width), width - 1);
const rect_width = @min(width - x, dot_width);
canvas.rect(.{
.x = @intCast(x),
.y = @intCast(metrics.underline_position),
.width = @intCast(rect_width),
.height = @intCast(metrics.underline_thickness),
}, .on);
}
}
pub fn underline_dashed(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
const dash_width = width / 3 + 1;
const dash_count = (width / dash_width) + 1;
var i: u32 = 0;
while (i < dash_count) : (i += 2) {
// Ensure we never go out of bounds for the rect
const x = @min(i * dash_width, width - 1);
const rect_width = @min(width - x, dash_width);
canvas.rect(.{
.x = @intCast(x),
.y = @intCast(metrics.underline_position),
.width = @intCast(rect_width),
.height = @intCast(metrics.underline_thickness),
}, .on);
}
}
pub fn underline_curly(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
// TODO: Rework this using z2d, this is pretty cool code and all but
// it doesn't need to be highly optimized and z2d path drawing
// code would be clearer and nicer to have.
const float_width: f64 = @floatFromInt(width);
// Because of we way we draw the undercurl, we end up making it around 1px
// thicker than it should be, to fix this we just reduce the thickness by 1.
//
// We use a minimum thickness of 0.414 because this empirically produces
// the nicest undercurls at 1px underline thickness; thinner tends to look
// too thin compared to straight underlines and has artefacting.
const float_thick: f64 = @max(
0.414,
@as(f64, @floatFromInt(metrics.underline_thickness -| 1)),
);
// Calculate the wave period for a single character
// `2 * pi...` = 1 peak per character
// `4 * pi...` = 2 peaks per character
const wave_period = 2 * std.math.pi / float_width;
// The full amplitude of the wave can be from the bottom to the
// underline position. We also calculate our mid y point of the wave
const half_amplitude = 1.0 / wave_period;
const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1;
// Offset to move the undercurl up slightly.
const y_off: u32 = @intFromFloat(half_amplitude * 0.5);
// This is used in calculating the offset curve estimate below.
const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(
1.0,
half_amplitude * wave_period,
);
// follow Xiaolin Wu's antialias algorithm to draw the curve
var x: u32 = 0;
while (x < width) : (x += 1) {
// We sample the wave function at the *middle* of each
// pixel column, to ensure that it renders symmetrically.
const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period;
// Use the slope at this location to add thickness to
// the line on this column, counteracting the thinning
// caused by the slope.
//
// This is not the exact offset curve for a sine wave,
// but it's a decent enough approximation.
//
// How did I derive this? I stared at Desmos and fiddled
// with numbers for an hour until it was good enough.
const t_u: f64 = t + std.math.pi;
const slope_factor_u: f64 =
(@sin(t_u) * @sin(t_u) * offset_factor) /
((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period);
const slope_factor_l: f64 =
(@sin(t) * @sin(t) * offset_factor) /
((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period);
const cosx: f64 = @cos(t);
// This will be the center of our stroke.
const y: f64 = y_mid + half_amplitude * cosx;
// The upper pixel and lower pixel are
// calculated relative to the center.
const y_u: f64 = y - float_thick * 0.5 - slope_factor_u;
const y_l: f64 = y + float_thick * 0.5 + slope_factor_l;
const y_upper: u32 = @intFromFloat(@floor(y_u));
const y_lower: u32 = @intFromFloat(@ceil(y_l));
const alpha_u: u8 = @intFromFloat(
@round(255 * (1.0 - @abs(y_u - @floor(y_u)))),
);
const alpha_l: u8 = @intFromFloat(
@round(255 * (1.0 - @abs(y_l - @ceil(y_l)))),
);
// upper and lower bounds
canvas.pixel(
@intCast(x),
@intCast(metrics.underline_position +| y_upper -| y_off),
@enumFromInt(alpha_u),
);
canvas.pixel(
@intCast(x),
@intCast(metrics.underline_position +| y_lower -| y_off),
@enumFromInt(alpha_l),
);
// fill between upper and lower bound
var y_fill: u32 = y_upper + 1;
while (y_fill < y_lower) : (y_fill += 1) {
canvas.pixel(
@intCast(x),
@intCast(metrics.underline_position +| y_fill -| y_off),
.on,
);
}
}
}
pub fn strikethrough(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
canvas.rect(.{
.x = 0,
.y = @intCast(metrics.strikethrough_position),
.width = @intCast(width),
.height = @intCast(metrics.strikethrough_thickness),
}, .on);
}
pub fn overline(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = height;
canvas.rect(.{
.x = 0,
.y = @intCast(metrics.overline_position),
.width = @intCast(width),
.height = @intCast(metrics.overline_thickness),
}, .on);
}
pub fn cursor_rect(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = metrics;
canvas.rect(.{
.x = 0,
.y = 0,
.width = @intCast(width),
.height = @intCast(height),
}, .on);
}
pub fn cursor_hollow_rect(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
// We fill the entire rect and then hollow out the inside, this isn't very
// efficient but it doesn't need to be and it's the easiest way to write it.
canvas.rect(.{
.x = 0,
.y = 0,
.width = @intCast(width),
.height = @intCast(height),
}, .on);
canvas.rect(.{
.x = @intCast(metrics.cursor_thickness),
.y = @intCast(metrics.cursor_thickness),
.width = @intCast(width -| metrics.cursor_thickness * 2),
.height = @intCast(height -| metrics.cursor_thickness * 2),
}, .off);
}
pub fn cursor_bar(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
// We place the bar cursor half of its thickness over the left edge of the
// cell, so that it sits centered between characters, not biased to a side.
//
// We round up (add 1 before dividing by 2) because, empirically, having a
// 1px cursor shifted left a pixel looks better than having it not shifted.
canvas.rect(.{
.x = -@as(i32, @intCast((metrics.cursor_thickness + 1) / 2)),
.y = 0,
.width = @intCast(metrics.cursor_thickness),
.height = @intCast(height),
}, .on);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,193 @@
//! Symbols for Legacy Computing Supplement | U+1CC00...U+1CEBF
//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement
//!
//! 𜰀 𜰁 𜰂 𜰃 𜰄 𜰅 𜰆 𜰇 𜰈 𜰉 𜰊 𜰋 𜰌 𜰍 𜰎 𜰏
//! 𜰐 𜰑 𜰒 𜰓 𜰔 𜰕 𜰖 𜰗 𜰘 𜰙 𜰚 𜰛 𜰜 𜰝 𜰞 𜰟
//! 𜰠 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯
//! 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿
//! 𜱀 𜱁 𜱂 𜱃 𜱄 𜱅 𜱆 𜱇 𜱈 𜱉 𜱊 𜱋 𜱌 𜱍 𜱎 𜱏
//! 𜱐 𜱑 𜱒 𜱓 𜱔 𜱕 𜱖 𜱗 𜱘 𜱙 𜱚 𜱛 𜱜 𜱝 𜱞 𜱟
//! 𜱠 𜱡 𜱢 𜱣 𜱤 𜱥 𜱦 𜱧 𜱨 𜱩 𜱪 𜱫 𜱬 𜱭 𜱮 𜱯
//! 𜱰 𜱱 𜱲 𜱳 𜱴 𜱵 𜱶 𜱷 𜱸 𜱹 𜱺 𜱻 𜱼 𜱽 𜱾 𜱿
//! 𜲀 𜲁 𜲂 𜲃 𜲄 𜲅 𜲆 𜲇 𜲈 𜲉 𜲊 𜲋 𜲌 𜲍 𜲎 𜲏
//! 𜲐 𜲑 𜲒 𜲓 𜲔 𜲕 𜲖 𜲗 𜲘 𜲙 𜲚 𜲛 𜲜 𜲝 𜲞 𜲟
//! 𜲠 𜲡 𜲢 𜲣 𜲤 𜲥 𜲦 𜲧 𜲨 𜲩 𜲪 𜲫 𜲬 𜲭 𜲮 𜲯
//! 𜲰 𜲱 𜲲 𜲳 𜲴 𜲵 𜲶 𜲷 𜲸 𜲹 𜲺 𜲻 𜲼 𜲽 𜲾 𜲿
//! 𜳀 𜳁 𜳂 𜳃 𜳄 𜳅 𜳆 𜳇 𜳈 𜳉 𜳊 𜳋 𜳌 𜳍 𜳎 𜳏
//! 𜳐 𜳑 𜳒 𜳓 𜳔 𜳕 𜳖 𜳗 𜳘 𜳙 𜳚 𜳛 𜳜 𜳝 𜳞 𜳟
//! 𜳠 𜳡 𜳢 𜳣 𜳤 𜳥 𜳦 𜳧 𜳨 𜳩 𜳪 𜳫 𜳬 𜳭 𜳮 𜳯
//! 𜳰 𜳱 𜳲 𜳳 𜳴 𜳵 𜳶 𜳷 𜳸 𜳹
//! 𜴀 𜴁 𜴂 𜴃 𜴄 𜴅 𜴆 𜴇 𜴈 𜴉 𜴊 𜴋 𜴌 𜴍 𜴎 𜴏
//! 𜴐 𜴑 𜴒 𜴓 𜴔 𜴕 𜴖 𜴗 𜴘 𜴙 𜴚 𜴛 𜴜 𜴝 𜴞 𜴟
//! 𜴠 𜴡 𜴢 𜴣 𜴤 𜴥 𜴦 𜴧 𜴨 𜴩 𜴪 𜴫 𜴬 𜴭 𜴮 𜴯
//! 𜴰 𜴱 𜴲 𜴳 𜴴 𜴵 𜴶 𜴷 𜴸 𜴹 𜴺 𜴻 𜴼 𜴽 𜴾 𜴿
//! 𜵀 𜵁 𜵂 𜵃 𜵄 𜵅 𜵆 𜵇 𜵈 𜵉 𜵊 𜵋 𜵌 𜵍 𜵎 𜵏
//! 𜵐 𜵑 𜵒 𜵓 𜵔 𜵕 𜵖 𜵗 𜵘 𜵙 𜵚 𜵛 𜵜 𜵝 𜵞 𜵟
//! 𜵠 𜵡 𜵢 𜵣 𜵤 𜵥 𜵦 𜵧 𜵨 𜵩 𜵪 𜵫 𜵬 𜵭 𜵮 𜵯
//! 𜵰 𜵱 𜵲 𜵳 𜵴 𜵵 𜵶 𜵷 𜵸 𜵹 𜵺 𜵻 𜵼 𜵽 𜵾 𜵿
//! 𜶀 𜶁 𜶂 𜶃 𜶄 𜶅 𜶆 𜶇 𜶈 𜶉 𜶊 𜶋 𜶌 𜶍 𜶎 𜶏
//! 𜶐 𜶑 𜶒 𜶓 𜶔 𜶕 𜶖 𜶗 𜶘 𜶙 𜶚 𜶛 𜶜 𜶝 𜶞 𜶟
//! 𜶠 𜶡 𜶢 𜶣 𜶤 𜶥 𜶦 𜶧 𜶨 𜶩 𜶪 𜶫 𜶬 𜶭 𜶮 𜶯
//! 𜶰 𜶱 𜶲 𜶳 𜶴 𜶵 𜶶 𜶷 𜶸 𜶹 𜶺 𜶻 𜶼 𜶽 𜶾 𜶿
//! 𜷀 𜷁 𜷂 𜷃 𜷄 𜷅 𜷆 𜷇 𜷈 𜷉 𜷊 𜷋 𜷌 𜷍 𜷎 𜷏
//! 𜷐 𜷑 𜷒 𜷓 𜷔 𜷕 𜷖 𜷗 𜷘 𜷙 𜷚 𜷛 𜷜 𜷝 𜷞 𜷟
//! 𜷠 𜷡 𜷢 𜷣 𜷤 𜷥 𜷦 𜷧 𜷨 𜷩 𜷪 𜷫 𜷬 𜷭 𜷮 𜷯
//! 𜷰 𜷱 𜷲 𜷳 𜷴 𜷵 𜷶 𜷷 𜷸 𜷹 𜷺 𜷻 𜷼 𜷽 𜷾 𜷿
//! 𜸀 𜸁 𜸂 𜸃 𜸄 𜸅 𜸆 𜸇 𜸈 𜸉 𜸊 𜸋 𜸌 𜸍 𜸎 𜸏
//! 𜸐 𜸑 𜸒 𜸓 𜸔 𜸕 𜸖 𜸗 𜸘 𜸙 𜸚 𜸛 𜸜 𜸝 𜸞 𜸟
//! 𜸠 𜸡 𜸢 𜸣 𜸤 𜸥 𜸦 𜸧 𜸨 𜸩 𜸪 𜸫 𜸬 𜸭 𜸮 𜸯
//! 𜸰 𜸱 𜸲 𜸳 𜸴 𜸵 𜸶 𜸷 𜸸 𜸹 𜸺 𜸻 𜸼 𜸽 𜸾 𜸿
//! 𜹀 𜹁 𜹂 𜹃 𜹄 𜹅 𜹆 𜹇 𜹈 𜹉 𜹊 𜹋 𜹌 𜹍 𜹎 𜹏
//! 𜹐 𜹑 𜹒 𜹓 𜹔 𜹕 𜹖 𜹗 𜹘 𜹙 𜹚 𜹛 𜹜 𜹝 𜹞 𜹟
//! 𜹠 𜹡 𜹢 𜹣 𜹤 𜹥 𜹦 𜹧 𜹨 𜹩 𜹪 𜹫 𜹬 𜹭 𜹮 𜹯
//! 𜹰 𜹱 𜹲 𜹳 𜹴 𜹵 𜹶 𜹷 𜹸 𜹹 𜹺 𜹻 𜹼 𜹽 𜹾 𜹿
//! 𜺀 𜺁 𜺂 𜺃 𜺄 𜺅 𜺆 𜺇 𜺈 𜺉 𜺊 𜺋 𜺌 𜺍 𜺎 𜺏
//! 𜺐 𜺑 𜺒 𜺓 𜺔 𜺕 𜺖 𜺗 𜺘 𜺙 𜺚 𜺛 𜺜 𜺝 𜺞 𜺟
//! 𜺠 𜺡 𜺢 𜺣 𜺤 𜺥 𜺦 𜺧 𜺨 𜺩 𜺪 𜺫 𜺬 𜺭 𜺮 𜺯
//! 𜺰 𜺱 𜺲 𜺳
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const z2d = @import("z2d");
const common = @import("common.zig");
const Thickness = common.Thickness;
const Corner = common.Corner;
const Shade = common.Shade;
const xHalfs = common.xHalfs;
const yQuads = common.yQuads;
const rect = common.rect;
const font = @import("../../main.zig");
const octant_min = 0x1cd00;
const octant_max = 0x1cde5;
/// Octants
pub fn draw1CD00_1CDE5(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
// Octant representation. We use the funny numeric string keys
// so its easier to parse the actual name used in the Symbols for
// Legacy Computing spec.
const Octant = packed struct(u8) {
@"1": bool = false,
@"2": bool = false,
@"3": bool = false,
@"4": bool = false,
@"5": bool = false,
@"6": bool = false,
@"7": bool = false,
@"8": bool = false,
};
// Parse the octant data. This is all done at comptime so
// that this is static data that is embedded in the binary.
const octants_len = octant_max - octant_min + 1;
const octants: [octants_len]Octant = comptime octants: {
@setEvalBranchQuota(10_000);
var result: [octants_len]Octant = @splat(.{});
var i: usize = 0;
const data = @embedFile("octants.txt");
var it = std.mem.splitScalar(u8, data, '\n');
while (it.next()) |line| {
// Skip comments
if (line.len == 0 or line[0] == '#') continue;
const current = &result[i];
i += 1;
// Octants are in the format "BLOCK OCTANT-1235". The numbers
// at the end are keys into our packed struct. Since we're
// at comptime we can metaprogram it all.
const idx = std.mem.indexOfScalar(u8, line, '-').?;
for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true;
}
assert(i == octants_len);
break :octants result;
};
const x_halfs = xHalfs(metrics);
const y_quads = yQuads(metrics);
const oct = octants[cp - octant_min];
if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]);
if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]);
if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]);
if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]);
if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]);
if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]);
if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height);
if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height);
}
// Separated Block Quadrants
// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯
pub fn draw1CC21_1CC2F(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = metrics;
// Struct laid out to match the codepoint order so we can cast from it.
const Quads = packed struct(u4) {
tl: bool,
tr: bool,
bl: bool,
br: bool,
};
const quad: Quads = @bitCast(@as(u4, @truncate(cp - 0x1CC20)));
const gap: i32 = @intCast(@max(1, width / 12));
const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2));
const mid_gap_y: i32 = gap * 2 + @as(i32, @intCast(height % 2));
const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2);
const h: i32 = @divExact(@as(i32, @intCast(height)) - gap * 2 - mid_gap_y, 2);
if (quad.tl) canvas.box(
gap,
gap,
gap + w,
gap + h,
.on,
);
if (quad.tr) canvas.box(
gap + w + mid_gap_x,
gap,
gap + w + mid_gap_x + w,
gap + h,
.on,
);
if (quad.bl) canvas.box(
gap,
gap + h + mid_gap_y,
gap + w,
gap + h + mid_gap_y + h,
.on,
);
if (quad.br) canvas.box(
gap + w + mid_gap_x,
gap + h + mid_gap_y,
gap + w + mid_gap_x + w,
gap + h + mid_gap_y + h,
.on,
);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

View File

@ -1,312 +0,0 @@
//! This file renders underline sprites. To draw underlines, we render the
//! full cell-width as a sprite and then draw it as a separate pass to the
//! text.
//!
//! We used to render the underlines directly in the GPU shaders but its
//! annoying to support multiple types of underlines and its also annoying
//! to maintain and debug another set of shaders for each renderer instead of
//! just relying on the glyph system we already need to support for text
//! anyways.
//!
//! This also renders strikethrough, so its really more generally a
//! "horizontal line" renderer.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Sprite = font.sprite.Sprite;
/// Draw an underline.
pub fn renderGlyph(
alloc: Allocator,
atlas: *font.Atlas,
sprite: Sprite,
width: u32,
height: u32,
line_pos: u32,
line_thickness: u32,
) !font.Glyph {
// Draw the appropriate sprite
var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) {
.underline => try drawSingle(alloc, width, line_thickness),
.underline_double => try drawDouble(alloc, width, line_thickness),
.underline_dotted => try drawDotted(alloc, width, line_thickness),
.underline_dashed => try drawDashed(alloc, width, line_thickness),
.underline_curly => try drawCurly(alloc, width, line_thickness),
.overline => try drawSingle(alloc, width, line_thickness),
.strikethrough => try drawSingle(alloc, width, line_thickness),
else => unreachable,
};
defer canvas.deinit();
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
return font.Glyph{
.width = width,
.height = @intCast(region.height),
.offset_x = 0,
// Glyph.offset_y is the distance between the top of the glyph and the
// bottom of the cell. We want the top of the glyph to be at line_pos
// from the TOP of the cell, and then offset by the offset_y from the
// draw function.
.offset_y = @as(i32, @intCast(height -| line_pos)) - offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatFromInt(width),
};
}
/// A tuple with the canvas that the desired sprite was drawn on and
/// a recommended offset (+Y = down) to shift its Y position by, to
/// correct for underline styles with additional thickness.
const CanvasAndOffset = struct { font.sprite.Canvas, i32 };
/// Draw a single underline.
fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
const height: u32 = thickness;
var canvas = try font.sprite.Canvas.init(alloc, width, height);
canvas.rect(.{
.x = 0,
.y = 0,
.width = width,
.height = thickness,
}, .on);
const offset_y: i32 = 0;
return .{ canvas, offset_y };
}
/// Draw a double underline.
fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
// Our gap between lines will be at least 2px.
// (i.e. if our thickness is 1, we still have a gap of 2)
const gap = @max(2, thickness);
const height: u32 = thickness * 2 * gap;
var canvas = try font.sprite.Canvas.init(alloc, width, height);
canvas.rect(.{
.x = 0,
.y = 0,
.width = width,
.height = thickness,
}, .on);
canvas.rect(.{
.x = 0,
.y = thickness * 2,
.width = width,
.height = thickness,
}, .on);
const offset_y: i32 = -@as(i32, @intCast(thickness));
return .{ canvas, offset_y };
}
/// Draw a dotted underline.
fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
const height: u32 = thickness;
var canvas = try font.sprite.Canvas.init(alloc, width, height);
const dot_width = @max(thickness, 3);
const dot_count = @max((width / dot_width) / 2, 1);
const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count);
var i: u32 = 0;
while (i < dot_count) : (i += 1) {
// Ensure we never go out of bounds for the rect
const x = @min(i * (dot_width + gap_width), width - 1);
const rect_width = @min(width - x, dot_width);
canvas.rect(.{
.x = @intCast(x),
.y = 0,
.width = rect_width,
.height = thickness,
}, .on);
}
const offset_y: i32 = 0;
return .{ canvas, offset_y };
}
/// Draw a dashed underline.
fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
const height: u32 = thickness;
var canvas = try font.sprite.Canvas.init(alloc, width, height);
const dash_width = width / 3 + 1;
const dash_count = (width / dash_width) + 1;
var i: u32 = 0;
while (i < dash_count) : (i += 2) {
// Ensure we never go out of bounds for the rect
const x = @min(i * dash_width, width - 1);
const rect_width = @min(width - x, dash_width);
canvas.rect(.{
.x = @intCast(x),
.y = 0,
.width = rect_width,
.height = thickness,
}, .on);
}
const offset_y: i32 = 0;
return .{ canvas, offset_y };
}
/// Draw a curly underline. Thanks to Wez Furlong for providing
/// the basic math structure for this since I was lazy with the
/// geometry.
fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
const float_width: f64 = @floatFromInt(width);
// Because of we way we draw the undercurl, we end up making it around 1px
// thicker than it should be, to fix this we just reduce the thickness by 1.
//
// We use a minimum thickness of 0.414 because this empirically produces
// the nicest undercurls at 1px underline thickness; thinner tends to look
// too thin compared to straight underlines and has artefacting.
const float_thick: f64 = @max(0.414, @as(f64, @floatFromInt(thickness -| 1)));
// Calculate the wave period for a single character
// `2 * pi...` = 1 peak per character
// `4 * pi...` = 2 peaks per character
const wave_period = 2 * std.math.pi / float_width;
// The full amplitude of the wave can be from the bottom to the
// underline position. We also calculate our mid y point of the wave
const half_amplitude = 1.0 / wave_period;
const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1;
// This is used in calculating the offset curve estimate below.
const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(1.0, half_amplitude * wave_period);
const height: u32 = @intFromFloat(@ceil(half_amplitude + float_thick + 1) * 2);
var canvas = try font.sprite.Canvas.init(alloc, width, height);
// follow Xiaolin Wu's antialias algorithm to draw the curve
var x: u32 = 0;
while (x < width) : (x += 1) {
// We sample the wave function at the *middle* of each
// pixel column, to ensure that it renders symmetrically.
const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period;
// Use the slope at this location to add thickness to
// the line on this column, counteracting the thinning
// caused by the slope.
//
// This is not the exact offset curve for a sine wave,
// but it's a decent enough approximation.
//
// How did I derive this? I stared at Desmos and fiddled
// with numbers for an hour until it was good enough.
const t_u: f64 = t + std.math.pi;
const slope_factor_u: f64 = (@sin(t_u) * @sin(t_u) * offset_factor) / ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period);
const slope_factor_l: f64 = (@sin(t) * @sin(t) * offset_factor) / ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period);
const cosx: f64 = @cos(t);
// This will be the center of our stroke.
const y: f64 = y_mid + half_amplitude * cosx;
// The upper pixel and lower pixel are
// calculated relative to the center.
const y_u: f64 = y - float_thick * 0.5 - slope_factor_u;
const y_l: f64 = y + float_thick * 0.5 + slope_factor_l;
const y_upper: u32 = @intFromFloat(@floor(y_u));
const y_lower: u32 = @intFromFloat(@ceil(y_l));
const alpha_u: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_u - @floor(y_u)))));
const alpha_l: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_l - @ceil(y_l)))));
// upper and lower bounds
canvas.pixel(x, @min(y_upper, height - 1), @enumFromInt(alpha_u));
canvas.pixel(x, @min(y_lower, height - 1), @enumFromInt(alpha_l));
// fill between upper and lower bound
var y_fill: u32 = y_upper + 1;
while (y_fill < y_lower) : (y_fill += 1) {
canvas.pixel(x, @min(y_fill, height - 1), .on);
}
}
const offset_y: i32 = @intFromFloat(-@round(half_amplitude));
return .{ canvas, offset_y };
}
test "single" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
_ = try renderGlyph(
alloc,
&atlas_grayscale,
.underline,
36,
18,
9,
2,
);
}
test "strikethrough" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
_ = try renderGlyph(
alloc,
&atlas_grayscale,
.strikethrough,
36,
18,
9,
2,
);
}
test "single large thickness" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
// unrealistic thickness but used to cause a crash
// https://github.com/mitchellh/ghostty/pull/1548
_ = try renderGlyph(
alloc,
&atlas_grayscale,
.underline,
36,
18,
9,
200,
);
}
test "curly" {
const testing = std.testing;
const alloc = testing.allocator;
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
_ = try renderGlyph(
alloc,
&atlas_grayscale,
.underline_curly,
36,
18,
9,
2,
);
}

View File

@ -32,6 +32,8 @@ extend-ignore-re = [
# Ignore typos in test expectations
"testing\\.expect[^;]*;",
"kHOM\\d*",
# Ignore "typos" in sprite font draw fn names
"draw[0-9A-F]+(_[0-9A-F]+)?\\(",
]
[default.extend-words]