Merge branch 'ghostty-org:main' into hu_HU_localization

This commit is contained in:
Balázs Szücs
2025-07-02 14:57:36 +02:00
committed by GitHub
70 changed files with 6169 additions and 4686 deletions

View File

@ -20,8 +20,8 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
.hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW",
.url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
.hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
.lazy = true,
},
.zig_objc = .{

6
build.zig.zon.json generated
View File

@ -124,10 +124,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": {
"z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
"hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="
},
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf",

6
build.zig.zon.nix generated
View File

@ -282,11 +282,11 @@ in
};
}
{
name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW";
name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz";
hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=";
url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz";
hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -31,4 +31,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025c
https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz
https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz

View File

@ -1,7 +1,11 @@
[Unit]
Description=@NAME@
After=graphical-session.target
[Service]
Type=dbus
BusName=@APPID@
ExecStart=@GHOSTTY@ --launched-from=systemd
[Install]
WantedBy=graphical-session.target

View File

@ -151,9 +151,9 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
"dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW",
"sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45"
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
"sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e"
},
{
"type": "archive",

View File

@ -112,6 +112,9 @@ class AppDelegate: NSObject,
/// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil
/// Signals
private var signals: [DispatchSourceSignal] = []
/// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage? = nil {
didSet {
@ -249,6 +252,9 @@ class AppDelegate: NSObject,
// Setup our menu
setupMenuImages()
// Setup signal handlers
setupSignals()
}
func applicationDidBecomeActive(_ notification: Notification) {
@ -406,6 +412,34 @@ class AppDelegate: NSObject,
return dockMenu
}
/// Setup signal handlers
private func setupSignals() {
// Register a signal handler for config reloading. It appears that all
// of this is required. I've commented each line because its a bit unclear.
// Warning: signal handlers don't work when run via Xcode. They have to be
// run on a real app bundle.
// We need to ignore signals we register with makeSignalSource or they
// don't seem to handle.
signal(SIGUSR2, SIG_IGN)
// Make the signal source and register our event handle. We keep a weak
// ref to ourself so we don't create a retain cycle.
let sigusr2 = DispatchSource.makeSignalSource(signal: SIGUSR2, queue: .main)
sigusr2.setEventHandler { [weak self] in
guard let self else { return }
Ghostty.logger.info("reloading configuration in response to SIGUSR2")
self.ghostty.reloadConfig()
}
// The signal source starts unactivated, so we have to resume it once
// we setup the event handler.
sigusr2.resume()
// We need to keep a strong reference to it so it isn't disabled.
signals.append(sigusr2)
}
/// Setup all the images for our menu items.
private func setupMenuImages() {
// Note: This COULD Be done all in the xib file, but I find it easier to

View File

@ -1002,10 +1002,11 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: {
// On macOS, our exit code detection doesn't work, possibly
// because of our `login` wrapper. More investigation required.
if (comptime builtin.target.os.tag.isDarwin()) break :runtime;
// If the exit code is 0 then we it was a good exit.
if (comptime !builtin.target.os.tag.isDarwin()) {
// If the exit code is 0 then it was a good exit.
if (info.exit_code == 0) break :runtime;
}
log.warn("abnormal process exit detected, showing error message", .{});
// Update our terminal to note the abnormal exit. In the future we

View File

@ -373,6 +373,13 @@ pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
.{},
);
// Setup a listener for SIGUSR2 to reload the configuration.
_ = glib.unixSignalAdd(
std.posix.SIG.USR2,
sigusr2,
self,
);
// We don't use g_application_run, we want to manually control the
// loop so we have to do the same things the run function does:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
@ -1508,6 +1515,22 @@ pub fn quitNow(self: *App) void {
self.running = false;
}
// SIGUSR2 signal handler via g_unix_signal_add
fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int {
const self: *App = @ptrCast(@alignCast(ud orelse
return @intFromBool(glib.SOURCE_CONTINUE)));
log.info("received SIGUSR2, reloading configuration", .{});
self.reloadConfig(.app, .{ .soft = false }) catch |err| {
log.err(
"error reloading configuration for SIGUSR2: {}",
.{err},
);
};
return @intFromBool(glib.SOURCE_CONTINUE);
}
/// This is called by the `activate` signal. This is sent on program startup and
/// also when a secondary instance launches and requests a new window.
fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {

View File

@ -405,12 +405,11 @@ pub fn add(
})) |dep| {
step.root_module.addImport("xev", dep.module("xev"));
}
if (b.lazyDependency("z2d", .{})) |dep| {
step.root_module.addImport("z2d", b.addModule("z2d", .{
.root_source_file = dep.path("src/z2d.zig"),
if (b.lazyDependency("z2d", .{
.target = target,
.optimize = optimize,
}));
})) |dep| {
step.root_module.addImport("z2d", dep.module("z2d"));
}
if (b.lazyDependency("ziglyph", .{
.target = target,

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);
@ -556,6 +587,35 @@ test "writing data" {
try testing.expectEqual(@as(u8, 4), atlas.data[66]);
}
test "writing data from a larger source" {
const alloc = testing.allocator;
var atlas = try init(alloc, 32, .grayscale);
defer atlas.deinit(alloc);
const reg = try atlas.reserve(alloc, 2, 2);
const old = atlas.modified.load(.monotonic);
// zig fmt: off
atlas.setFromLarger(reg, &[_]u8{
8, 8, 8, 8, 8,
8, 8, 1, 2, 8,
8, 8, 3, 4, 8,
8, 8, 8, 8, 8,
}, 5, 2, 1);
// zig fmt: on
const new = atlas.modified.load(.monotonic);
try testing.expect(new > old);
// 33 because of the 1px border and so on
try testing.expectEqual(@as(u8, 1), atlas.data[33]);
try testing.expectEqual(@as(u8, 2), atlas.data[34]);
try testing.expectEqual(@as(u8, 3), atlas.data[65]);
try testing.expectEqual(@as(u8, 4), atlas.data[66]);
// None of the `8`s from the source data outside of the
// specified region should have made it on to the atlas.
try testing.expectEqual(null, std.mem.indexOfScalar(u8, atlas.data, 8));
}
test "grow" {
const alloc = testing.allocator;
var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border

View File

@ -20,3 +20,6 @@ atlas_y: u32,
/// horizontal position to increase drawing position for strings
advance_x: f32,
/// Whether we drew this glyph ourselves with the sprite font.
sprite: bool = false,

View File

@ -32,12 +32,7 @@ pub const Sprite = enum(u32) {
cursor_rect,
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.
cursor_underline,
test {
const testing = std.testing;

File diff suppressed because it is too large Load Diff

View File

@ -16,25 +16,158 @@ 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: []const Range = 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;
}
// We need to copy in to a const rather than a var in order to take
// the reference at comptime so that we can break with a slice here.
const fixed = r;
break :ranges &fixed;
};
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 +185,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 +198,350 @@ 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,
),
.strikethrough => try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
metrics.strikethrough_position,
metrics.strikethrough_thickness,
),
.overline => overline: {
var g = try underline.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
0,
metrics.overline_thickness,
);
// We have to manually subtract the overline position
// on the rendered glyph since it can be negative.
g.offset_y -= metrics.overline_position;
break :overline g;
},
.powerline => powerline: {
const f: Powerline = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.box_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,
};
break :powerline try f.renderGlyph(alloc, atlas, cp);
},
const height = metrics.cell_height;
.cursor => cursor: {
var g = try cursor.renderGlyph(
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cursor_height,
metrics.cursor_thickness,
);
const padding_x = width / 4;
const padding_y = height / 4;
// 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);
// 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 :cursor g;
},
try draw(cp, &canvas, width, height, metrics);
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
return .{
.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),
.sprite = true,
};
}
/// 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,
},
return true;
};
defer ref_file.close();
const ref_bytes = try ref_file.readToEndAlloc(
alloc,
std.math.maxInt(usize),
);
defer alloc.free(ref_bytes);
// == Box fonts ==
// 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;
// "Box Drawing" block
//
//
//
//
0x2500...0x257F,
// 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, .{});
// "Block Elements" block
//
0x2580...0x259F,
// 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);
// "Geometric Shapes" block
0x25e2...0x25e5, //
0x25f8...0x25fa, //
0x25ff, //
assert(ref_rgba.width == atlas.getWidth());
assert(ref_rgba.height == atlas.getHeight());
// "Braille" block
0x2800...0x28FF,
// 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;
// "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,
const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
// 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.
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):
//
//
//
//
//
0xF5D0...0xF60D => .box,
// 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;
// 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,
};
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,55 @@
# 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,181 @@
//! 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 fill = common.fill;
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 {
if (quads.tl) fill(metrics, canvas, .zero, .half, .zero, .half);
if (quads.tr) fill(metrics, canvas, .half, .full, .zero, .half);
if (quads.bl) fill(metrics, canvas, .zero, .half, .half, .full);
if (quads.br) fill(metrics, canvas, .half, .full, .half, .full);
}

View File

@ -0,0 +1,932 @@
//! 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 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);
}
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,378 @@
//! 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;
};
/// A value that indicates some fraction across
/// the cell either horizontally or vertically.
///
/// This has some redundant names in it so that you can
/// use whichever one feels most semantically appropriate.
pub const Fraction = enum {
// Names for the min edge
start,
left,
top,
zero,
// Names based on eighths
eighth,
one_eighth,
two_eighths,
three_eighths,
four_eighths,
five_eighths,
six_eighths,
seven_eighths,
// Names based on quarters
quarter,
one_quarter,
two_quarters,
three_quarters,
// Names based on thirds
third,
one_third,
two_thirds,
// Names based on halves
half,
one_half,
// Alternative names for 1/2
center,
middle,
// Names for the max edge
end,
right,
bottom,
one,
full,
/// This can be indexed to get the fraction for `i/8`.
pub const eighths: [9]Fraction = .{
.zero,
.one_eighth,
.two_eighths,
.three_eighths,
.four_eighths,
.five_eighths,
.six_eighths,
.seven_eighths,
.one,
};
/// This can be indexed to get the fraction for `i/4`.
pub const quarters: [5]Fraction = .{
.zero,
.one_quarter,
.two_quarters,
.three_quarters,
.one,
};
/// This can be indexed to get the fraction for `i/3`.
pub const thirds: [4]Fraction = .{
.zero,
.one_third,
.two_thirds,
.one,
};
/// This can be indexed to get the fraction for `i/2`.
pub const halves: [3]Fraction = .{
.zero,
.one_half,
.one,
};
/// Get the x position for this fraction across a particular
/// size (width or height), assuming it will be used as the
/// min (left/top) coordinate for a block.
///
/// `size` can be any integer type, since it will be coerced
pub inline fn min(self: Fraction, size: anytype) i32 {
const s: f64 = @as(f64, @floatFromInt(size));
// For min coordinates, we want to align with the complementary
// fraction taken from the end, this ensures that rounding evens
// out, so that for example, if `size` is `7`, and we're looking
// at the `half` line, `size - round((1 - 0.5) * size)` => `3`;
// whereas the max coordinate directly rounds, which means that
// both `start` -> `half` and `half` -> `end` will be 4px, from
// `0` -> `4` and `3` -> `7`.
return @intFromFloat(s - @round((1.0 - self.fraction()) * s));
}
/// Get the x position for this fraction across a particular
/// size (width or height), assuming it will be used as the
/// max (right/bottom) coordinate for a block.
///
/// `size` can be any integer type, since it will be coerced
/// with `@floatFromInt`.
pub inline fn max(self: Fraction, size: anytype) i32 {
const s: f64 = @as(f64, @floatFromInt(size));
// See explanation of why these are different in `min`.
return @intFromFloat(@round(self.fraction() * s));
}
/// Get this fraction across a particular size (width/height).
/// If you need an integer, use `min` or `max` instead, since
/// they contain special logic for consistent alignment. This
/// is for when you're drawing with paths and don't care about
/// pixel alignment.
///
/// `size` can be any integer type, since it will be coerced
/// with `@floatFromInt`.
pub inline fn float(self: Fraction, size: anytype) f64 {
return self.fraction() * @as(f64, @floatFromInt(size));
}
/// Get a float for the fraction this represents.
pub inline fn fraction(self: Fraction) f64 {
return switch (self) {
.start,
.left,
.top,
.zero,
=> 0.0,
.eighth,
.one_eighth,
=> 0.125,
.quarter,
.one_quarter,
.two_eighths,
=> 0.25,
.third,
.one_third,
=> 1.0 / 3.0,
.three_eighths,
=> 0.375,
.half,
.one_half,
.two_quarters,
.four_eighths,
.center,
.middle,
=> 0.5,
.five_eighths,
=> 0.625,
.two_thirds,
=> 2.0 / 3.0,
.three_quarters,
.six_eighths,
=> 0.75,
.seven_eighths,
=> 0.875,
.end,
.right,
.bottom,
.one,
.full,
=> 1.0,
};
}
};
/// Fill a section of the cell, specified by a
/// horizontal and vertical pair of fraction lines.
pub fn fill(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
x0: Fraction,
x1: Fraction,
y0: Fraction,
y1: Fraction,
) void {
canvas.box(
x0.min(metrics.cell_width),
y0.min(metrics.cell_height),
x1.max(metrics.cell_width),
y1.max(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);
}

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,346 @@
//! 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);
}
pub fn cursor_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.cursor_thickness),
}, .on);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,628 @@
//! 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 Fraction = common.Fraction;
const Corner = common.Corner;
const Shade = common.Shade;
const fill = common.fill;
const box = @import("box.zig");
const sflc = @import("symbols_for_legacy_computing.zig");
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 oct = octants[cp - octant_min];
if (oct.@"1") fill(metrics, canvas, .zero, .half, .zero, .one_quarter);
if (oct.@"2") fill(metrics, canvas, .half, .full, .zero, .one_quarter);
if (oct.@"3") fill(metrics, canvas, .zero, .half, .one_quarter, .two_quarters);
if (oct.@"4") fill(metrics, canvas, .half, .full, .one_quarter, .two_quarters);
if (oct.@"5") fill(metrics, canvas, .zero, .half, .two_quarters, .three_quarters);
if (oct.@"6") fill(metrics, canvas, .half, .full, .two_quarters, .three_quarters);
if (oct.@"7") fill(metrics, canvas, .zero, .half, .three_quarters, .end);
if (oct.@"8") fill(metrics, canvas, .half, .full, .three_quarters, .end);
}
// 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,
);
}
/// Twelfth and Quarter circle pieces.
/// 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿
///
/// 𜰰𜰱𜰲𜰳
/// 𜰴𜰵𜰶𜰷
/// 𜰸𜰹𜰺𜰻
/// 𜰼𜰽𜰾𜰿
///
/// These are actually ellipses, sized to touch
/// the edge of their enclosing set of cells.
pub fn draw1CC30_1CC3F(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
switch (cp) {
// 𜰰 UPPER LEFT TWELFTH CIRCLE
0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2, .tl),
// 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE
0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2, .tl),
// 𜰲 UPPER CENTRE RIGHT TWELFTH CIRCLE
0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2, .tr),
// 𜰳 UPPER RIGHT TWELFTH CIRCLE
0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2, .tr),
// 𜰴 UPPER MIDDLE LEFT TWELFTH CIRCLE
0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2, .tl),
// 𜰵 UPPER LEFT QUARTER CIRCLE
0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1, .tl),
// 𜰶 UPPER RIGHT QUARTER CIRCLE
0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1, .tr),
// 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE
0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2, .tr),
// 𜰸 LOWER MIDDLE LEFT TWELFTH CIRCLE
0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2, .bl),
// 𜰹 LOWER LEFT QUARTER CIRCLE
0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1, .bl),
// 𜰺 LOWER RIGHT QUARTER CIRCLE
0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1, .br),
// 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE
0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2, .br),
// 𜰼 LOWER LEFT TWELFTH CIRCLE
0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2, .bl),
// 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE
0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2, .bl),
// 𜰾 LOWER CENTRE RIGHT TWELFTH CIRCLE
0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2, .br),
// 𜰿 LOWER RIGHT TWELFTH CIRCLE
0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2, .br),
else => unreachable,
}
}
/// TODO: These two characters should be easy, but it's not clear how they're
/// meant to align with adjacent cells, what characters they're meant to
/// be used with:
/// - 1CC1F 𜰟 BOX DRAWINGS DOUBLE DIAGONAL UPPER RIGHT TO LOWER LEFT
/// - 1CC20 𜰠 BOX DRAWINGS DOUBLE DIAGONAL UPPER LEFT TO LOWER RIGHT
pub fn draw1CC1B_1CC1E(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
const w: i32 = @intCast(width);
const h: i32 = @intCast(height);
const t: i32 = @intCast(metrics.box_thickness);
switch (cp) {
// 𜰛 BOX DRAWINGS LIGHT HORIZONTAL AND UPPER RIGHT
0x1CC1B => {
box.linesChar(metrics, canvas, .{ .left = .light, .right = .light });
canvas.box(w - t, 0, w, @divFloor(h, 2), .on);
},
// 𜰜 BOX DRAWINGS LIGHT HORIZONTAL AND LOWER RIGHT
0x1CC1C => {
box.linesChar(metrics, canvas, .{ .left = .light, .right = .light });
canvas.box(w - t, @divFloor(h, 2), w, h, .on);
},
// 𜰝 BOX DRAWINGS LIGHT TOP AND UPPER LEFT
0x1CC1D => {
canvas.box(0, 0, w, t, .on);
canvas.box(0, 0, t, @divFloor(h, 2), .on);
},
// 𜰞 BOX DRAWINGS LIGHT BOTTOM AND LOWER LEFT
0x1CC1E => {
canvas.box(0, h - t, w, h, .on);
canvas.box(0, @divFloor(h, 2), t, h, .on);
},
else => unreachable,
}
}
/// 𜸀 RIGHT HALF AND LEFT HALF WHITE CIRCLE
pub fn draw1CE00(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
sflc.circle(metrics, canvas, .left, false);
sflc.circle(metrics, canvas, .right, false);
}
/// 𜸁 LOWER HALF AND UPPER HALF WHITE CIRCLE
pub fn draw1CE01(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
_ = width;
_ = height;
sflc.circle(metrics, canvas, .top, false);
sflc.circle(metrics, canvas, .bottom, false);
}
/// 𜸋 LEFT HALF WHITE ELLIPSE
pub fn draw1CE0B(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .tl);
try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .bl);
}
/// 𜸌 RIGHT HALF WHITE ELLIPSE
pub fn draw1CE0C(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = cp;
try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .tr);
try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .br);
}
pub fn draw1CE16_1CE19(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
const w: i32 = @intCast(width);
const h: i32 = @intCast(height);
const t: i32 = @intCast(metrics.box_thickness);
switch (cp) {
// 𜸖 BOX DRAWINGS LIGHT VERTICAL AND TOP RIGHT
0x1CE16 => {
box.linesChar(metrics, canvas, .{ .up = .light, .down = .light });
canvas.box(@divFloor(w, 2), 0, w, t, .on);
},
// 𜸗 BOX DRAWINGS LIGHT VERTICAL AND BOTTOM RIGHT
0x1CE17 => {
box.linesChar(metrics, canvas, .{ .up = .light, .down = .light });
canvas.box(@divFloor(w, 2), h - t, w, h, .on);
},
// 𜸘 BOX DRAWINGS LIGHT VERTICAL AND TOP LEFT
0x1CE18 => {
box.linesChar(metrics, canvas, .{ .up = .light, .down = .light });
canvas.box(0, 0, @divFloor(w, 2), t, .on);
},
// 𜸙 BOX DRAWINGS LIGHT VERTICAL AND BOTTOM LEFT
0x1CE19 => {
box.linesChar(metrics, canvas, .{ .up = .light, .down = .light });
canvas.box(0, h - t, @divFloor(w, 2), h, .on);
},
else => unreachable,
}
}
/// Separated Block Sextants
pub fn draw1CE51_1CE8F(
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 Sextants = packed struct(u6) {
tl: bool,
tr: bool,
ml: bool,
mr: bool,
bl: bool,
br: bool,
};
const sex: Sextants = @bitCast(@as(u6, @truncate(cp - 0x1CE50)));
const gap: i32 = @intCast(@max(1, width / 12));
const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2));
const y_extra: i32 = @as(i32, @intCast(height % 3));
const mid_gap_y: i32 = gap * 2 + @divFloor(y_extra, 2);
const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2);
const h: i32 = @divFloor(
@as(i32, @intCast(height)) - gap * 2 - mid_gap_y * 2,
3,
);
// Distribute any leftover height in to the middle row of blocks.
const h_m: i32 = @as(i32, @intCast(height)) - gap * 2 - mid_gap_y * 2 - h * 2;
if (sex.tl) canvas.box(
gap,
gap,
gap + w,
gap + h,
.on,
);
if (sex.tr) canvas.box(
gap + w + mid_gap_x,
gap,
gap + w + mid_gap_x + w,
gap + h,
.on,
);
if (sex.ml) canvas.box(
gap,
gap + h + mid_gap_y,
gap + w,
gap + h + mid_gap_y + h_m,
.on,
);
if (sex.mr) canvas.box(
gap + w + mid_gap_x,
gap + h + mid_gap_y,
gap + w + mid_gap_x + w,
gap + h + mid_gap_y + h_m,
.on,
);
if (sex.bl) canvas.box(
gap,
gap + h + mid_gap_y + h_m + mid_gap_y,
gap + w,
gap + h + mid_gap_y + h_m + mid_gap_y + h,
.on,
);
if (sex.br) canvas.box(
gap + w + mid_gap_x,
gap + h + mid_gap_y + h_m + mid_gap_y,
gap + w + mid_gap_x + w,
gap + h + mid_gap_y + h_m + mid_gap_y + h,
.on,
);
}
/// Sixteenth Blocks
pub fn draw1CE90_1CEAF(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
const q = Fraction.quarters;
switch (cp) {
// 𜺐 UPPER LEFT ONE SIXTEENTH BLOCK
0x1CE90 => fill(metrics, canvas, q[0], q[1], q[0], q[1]),
// 𜺑 UPPER CENTRE LEFT ONE SIXTEENTH BLOCK
0x1CE91 => fill(metrics, canvas, q[1], q[2], q[0], q[1]),
// 𜺒 UPPER CENTRE RIGHT ONE SIXTEENTH BLOCK
0x1CE92 => fill(metrics, canvas, q[2], q[3], q[0], q[1]),
// 𜺓 UPPER RIGHT ONE SIXTEENTH BLOCK
0x1CE93 => fill(metrics, canvas, q[3], q[4], q[0], q[1]),
// 𜺔 UPPER MIDDLE LEFT ONE SIXTEENTH BLOCK
0x1CE94 => fill(metrics, canvas, q[0], q[1], q[1], q[2]),
// 𜺕 UPPER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK
0x1CE95 => fill(metrics, canvas, q[1], q[2], q[1], q[2]),
// 𜺖 UPPER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK
0x1CE96 => fill(metrics, canvas, q[2], q[3], q[1], q[2]),
// 𜺗 UPPER MIDDLE RIGHT ONE SIXTEENTH BLOCK
0x1CE97 => fill(metrics, canvas, q[3], q[4], q[1], q[2]),
// 𜺘 LOWER MIDDLE LEFT ONE SIXTEENTH BLOCK
0x1CE98 => fill(metrics, canvas, q[0], q[1], q[2], q[3]),
// 𜺙 LOWER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK
0x1CE99 => fill(metrics, canvas, q[1], q[2], q[2], q[3]),
// 𜺚 LOWER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK
0x1CE9A => fill(metrics, canvas, q[2], q[3], q[2], q[3]),
// 𜺛 LOWER MIDDLE RIGHT ONE SIXTEENTH BLOCK
0x1CE9B => fill(metrics, canvas, q[3], q[4], q[2], q[3]),
// 𜺜 LOWER LEFT ONE SIXTEENTH BLOCK
0x1CE9C => fill(metrics, canvas, q[0], q[1], q[3], q[4]),
// 𜺝 LOWER CENTRE LEFT ONE SIXTEENTH BLOCK
0x1CE9D => fill(metrics, canvas, q[1], q[2], q[3], q[4]),
// 𜺞 LOWER CENTRE RIGHT ONE SIXTEENTH BLOCK
0x1CE9E => fill(metrics, canvas, q[2], q[3], q[3], q[4]),
// 𜺟 LOWER RIGHT ONE SIXTEENTH BLOCK
0x1CE9F => fill(metrics, canvas, q[3], q[4], q[3], q[4]),
// 𜺠 RIGHT HALF LOWER ONE QUARTER BLOCK
0x1CEA0 => fill(metrics, canvas, q[2], q[4], q[3], q[4]),
// 𜺡 RIGHT THREE QUARTERS LOWER ONE QUARTER BLOCK
0x1CEA1 => fill(metrics, canvas, q[1], q[4], q[3], q[4]),
// 𜺢 LEFT THREE QUARTERS LOWER ONE QUARTER BLOCK
0x1CEA2 => fill(metrics, canvas, q[0], q[3], q[3], q[4]),
// 𜺣 LEFT HALF LOWER ONE QUARTER BLOCK
0x1CEA3 => fill(metrics, canvas, q[0], q[2], q[3], q[4]),
// 𜺤 LOWER HALF LEFT ONE QUARTER BLOCK
0x1CEA4 => fill(metrics, canvas, q[0], q[1], q[2], q[4]),
// 𜺥 LOWER THREE QUARTERS LEFT ONE QUARTER BLOCK
0x1CEA5 => fill(metrics, canvas, q[0], q[1], q[1], q[4]),
// 𜺦 UPPER THREE QUARTERS LEFT ONE QUARTER BLOCK
0x1CEA6 => fill(metrics, canvas, q[0], q[1], q[0], q[3]),
// 𜺧 UPPER HALF LEFT ONE QUARTER BLOCK
0x1CEA7 => fill(metrics, canvas, q[0], q[1], q[0], q[2]),
// 𜺨 LEFT HALF UPPER ONE QUARTER BLOCK
0x1CEA8 => fill(metrics, canvas, q[0], q[2], q[0], q[1]),
// 𜺩 LEFT THREE QUARTERS UPPER ONE QUARTER BLOCK
0x1CEA9 => fill(metrics, canvas, q[0], q[3], q[0], q[1]),
// 𜺪 RIGHT THREE QUARTERS UPPER ONE QUARTER BLOCK
0x1CEAA => fill(metrics, canvas, q[1], q[4], q[0], q[1]),
// 𜺫 RIGHT HALF UPPER ONE QUARTER BLOCK
0x1CEAB => fill(metrics, canvas, q[2], q[4], q[0], q[1]),
// 𜺬 UPPER HALF RIGHT ONE QUARTER BLOCK
0x1CEAC => fill(metrics, canvas, q[3], q[4], q[0], q[2]),
// 𜺭 UPPER THREE QUARTERS RIGHT ONE QUARTER BLOCK
0x1CEAD => fill(metrics, canvas, q[3], q[4], q[0], q[3]),
// 𜺮 LOWER THREE QUARTERS RIGHT ONE QUARTER BLOCK
0x1CEAE => fill(metrics, canvas, q[3], q[4], q[1], q[4]),
// 𜺯 LOWER HALF RIGHT ONE QUARTER BLOCK
0x1CEAF => fill(metrics, canvas, q[3], q[4], q[2], q[4]),
else => unreachable,
}
}
fn circlePiece(
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
x: f64,
y: f64,
w: f64,
h: f64,
corner: Corner,
) !void {
// Radius in pixels of the arc we need to draw.
const wdth: f64 = @as(f64, @floatFromInt(width)) * w;
const hght: f64 = @as(f64, @floatFromInt(height)) * h;
// Position in pixels (rather than cells) for x/y
const xp: f64 = @as(f64, @floatFromInt(width)) * x;
const yp: f64 = @as(f64, @floatFromInt(height)) * y;
// Set the clip so we don't include anything outside of the cell.
canvas.clip_left = canvas.padding_x;
canvas.clip_right = canvas.padding_x;
canvas.clip_top = canvas.padding_y;
canvas.clip_bottom = canvas.padding_y;
// Coefficient for approximating a circular arc.
const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0;
const cw = c * wdth;
const ch = c * hght;
const thick: f64 = @floatFromInt(metrics.box_thickness);
const ht = thick * 0.5;
var path = canvas.staticPath(2);
switch (corner) {
.tl => {
path.moveTo(wdth - xp, ht - yp);
path.curveTo(
wdth - cw - xp,
ht - yp,
ht - xp,
hght - ch - yp,
ht - xp,
hght - yp,
);
},
.tr => {
path.moveTo(wdth - xp, ht - yp);
path.curveTo(
wdth + cw - xp,
ht - yp,
wdth * 2 - ht - xp,
hght - ch - yp,
wdth * 2 - ht - xp,
hght - yp,
);
},
.bl => {
path.moveTo(ht - xp, hght - yp);
path.curveTo(
ht - xp,
hght + ch - yp,
wdth - cw - xp,
hght * 2 - ht - yp,
wdth - xp,
hght * 2 - ht - yp,
);
},
.br => {
path.moveTo(wdth * 2 - ht - xp, hght - yp);
path.curveTo(
wdth * 2 - ht - xp,
hght + ch - yp,
wdth + cw - xp,
hght * 2 - ht - yp,
wdth - xp,
hght * 2 - ht - yp,
);
},
}
try canvas.strokePath(path.wrapped_path, .{
.line_cap_mode = .butt,
.line_width = @floatFromInt(metrics.box_thickness),
}, .on);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 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: 1006 B

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: 751 B

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.7 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.5 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: 894 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: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 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

@ -3039,7 +3039,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return;
}
const mode: shaderpkg.CellText.Mode = switch (fgMode(
// We always use fg mode for sprite glyphs, since we know we never
// need to constrain them, and we don't have any color sprites.
//
// Otherwise we defer to `fgMode`.
const mode: shaderpkg.CellText.Mode =
if (render.glyph.sprite)
.fg
else switch (fgMode(
render.presentation,
cell_pin,
)) {
@ -3098,7 +3105,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.block => .cursor_rect,
.block_hollow => .cursor_hollow_rect,
.bar => .cursor_bar,
.underline => .underline,
.underline => .cursor_underline,
.lock => unreachable,
};

View File

@ -32,13 +32,13 @@ 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]
Pn = "Pn"
thr = "thr"
# Should be "halves", but for now skip it as it would make diff huge
halfs = "halfs"
# Swift oddities
Requestor = "Requestor"
iterm = "iterm"