Merge branch 'ghostty-org:main' into hu_HU_localization
@ -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
@ -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
@ -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
@ -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
|
||||
|
4
dist/linux/systemd.service.in
vendored
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
55
src/font/sprite/draw/README.md
Normal 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.
|
181
src/font/sprite/draw/block.zig
Normal 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);
|
||||
}
|
932
src/font/sprite/draw/box.zig
Normal 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;
|
||||
}
|
||||
}
|
148
src/font/sprite/draw/braille.zig
Normal 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);
|
||||
}
|
505
src/font/sprite/draw/branch.zig
Normal 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;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
378
src/font/sprite/draw/common.zig
Normal 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);
|
||||
}
|
200
src/font/sprite/draw/geometric_shapes.zig
Normal 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);
|
||||
}
|
396
src/font/sprite/draw/powerline.zig
Normal 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();
|
||||
}
|
346
src/font/sprite/draw/special.zig
Normal 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);
|
||||
}
|
1415
src/font/sprite/draw/symbols_for_legacy_computing.zig
Normal file
628
src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig
Normal 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);
|
||||
}
|
BIN
src/font/sprite/testdata/Box.ppm
vendored
BIN
src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 794 B |
BIN
src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1006 B |
BIN
src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 751 B |
BIN
src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1022 B |
BIN
src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 917 B |
BIN
src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 894 B |
BIN
src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 872 B |
BIN
src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png
vendored
Normal file
After Width: | Height: | Size: 495 B |
BIN
src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png
vendored
Normal file
After Width: | Height: | Size: 637 B |
BIN
src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png
vendored
Normal file
After Width: | Height: | Size: 393 B |
@ -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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|