aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/sprite
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/sprite
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/sprite')
-rw-r--r--vendor/libghostty-vt/src/font/sprite/Face.zig550
-rw-r--r--vendor/libghostty-vt/src/font/sprite/canvas.zig512
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/README.md55
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/block.zig177
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/box.zig929
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/braille.zig148
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/branch.zig504
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/common.zig374
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/geometric_shapes.zig198
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/octants.txt234
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/powerline.zig394
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/special.zig372
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing.zig1413
-rw-r--r--vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig633
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.pngbin0 -> 1032 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.pngbin0 -> 1295 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.pngbin0 -> 2193 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.pngbin0 -> 794 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.pngbin0 -> 1280 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.pngbin0 -> 1870 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.pngbin0 -> 3411 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.pngbin0 -> 1103 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.pngbin0 -> 1006 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.pngbin0 -> 1255 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.pngbin0 -> 2247 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.pngbin0 -> 751 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.pngbin0 -> 5427 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.pngbin0 -> 5724 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.pngbin0 -> 9975 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.pngbin0 -> 4341 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.pngbin0 -> 2225 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.pngbin0 -> 2635 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.pngbin0 -> 4570 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.pngbin0 -> 1853 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.pngbin0 -> 1022 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.pngbin0 -> 1541 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.pngbin0 -> 2501 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.pngbin0 -> 917 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.pngbin0 -> 1102 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.pngbin0 -> 1255 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.pngbin0 -> 2217 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.pngbin0 -> 894 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.pngbin0 -> 1114 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.pngbin0 -> 1421 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.pngbin0 -> 2473 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.pngbin0 -> 872 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.pngbin0 -> 495 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.pngbin0 -> 637 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.pngbin0 -> 1210 bytes
-rw-r--r--vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.pngbin0 -> 393 bytes
50 files changed, 6493 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/sprite/Face.zig b/vendor/libghostty-vt/src/font/sprite/Face.zig
new file mode 100644
index 00000000..596a9204
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/Face.zig
@@ -0,0 +1,550 @@
+//! This implements the built-in "sprite face". This font renders
+//! the built-in glyphs for the terminal, such as box drawing fonts, as well
+//! as specific sprites that are part of our rendering model such as
+//! text decorations (underlines).
+//!
+//! This isn't really a "font face" so much as it is quacks like a font
+//! face with regards to how it works with font.Group. We don't use any
+//! dynamic dispatch so it isn't truly an interface but the functions
+//! and behaviors are close enough to a system face that it makes it easy
+//! to integrate with font.Group. This is desirable so that higher level
+//! processes such as GroupCache, Shaper, etc. don't need to be aware of
+//! special sprite handling and just treat it like a normal font face.
+const Face = @This();
+
+const std = @import("std");
+const assert = @import("../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const wuffs = @import("wuffs");
+const z2d = @import("z2d");
+const font = @import("../main.zig");
+const Sprite = font.sprite.Sprite;
+
+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.Path.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.
+ _ = p;
+ _ = self;
+ return getDrawFn(cp) != null;
+}
+
+/// Render the glyph.
+pub fn renderGlyph(
+ self: Face,
+ alloc: Allocator,
+ atlas: *font.Atlas,
+ cp: u32,
+ opts: font.face.RenderOptions,
+) !font.Glyph {
+ if (std.debug.runtime_safety) {
+ if (!self.hasCodepoint(cp, null)) {
+ log.err("invalid codepoint cp={x}", .{cp});
+ unreachable; // crash
+ }
+ }
+
+ // 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 draw = getDrawFn(cp) orelse return .{
+ .width = 0,
+ .height = 0,
+ .offset_x = 0,
+ .offset_y = 0,
+ .atlas_x = 0,
+ .atlas_y = 0,
+ };
+
+ 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,
+ };
+
+ const height = metrics.cell_height;
+
+ const padding_x = width / 4;
+ const padding_y = height / 4;
+
+ // Make a canvas of the desired size
+ var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y);
+ defer canvas.deinit();
+
+ 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,
+ };
+}
+
+/// 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);
+
+ const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
+ defer alloc.free(cwd_absolute);
+
+ // 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,
+ });
+
+ // 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, .{});
+
+ return true;
+ };
+ defer ref_file.close();
+ const ref_bytes = try ref_file.readToEndAlloc(
+ alloc,
+ std.math.maxInt(usize),
+ );
+ defer alloc.free(ref_bytes);
+
+ // Do our PNG bytes comparison, if it's the same then we can
+ // move on, otherwise we'll decode the reference file and do
+ // a pixel-for-pixel diff.
+ if (std.mem.eql(u8, test_bytes, ref_bytes)) return false;
+
+ // Copy the test PNG in to the CWD so it isn't
+ // cleaned up with the rest of the tmp dir files.
+ const test_path = try std.fmt.allocPrint(
+ alloc,
+ "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png",
+ .{ cwd_absolute, i, i + 0xFF, width, height, thickness },
+ );
+ defer alloc.free(test_path);
+ try std.fs.copyFileAbsolute(path, test_path, .{});
+
+ // Use wuffs to decode the reference PNG to raw pixels.
+ // These will be RGBA, so when diffing we can just compare
+ // every fourth byte.
+ const ref_rgba = try wuffs.png.decode(alloc, ref_bytes);
+ defer alloc.free(ref_rgba.data);
+
+ assert(ref_rgba.width == atlas.getWidth());
+ assert(ref_rgba.height == atlas.getHeight());
+
+ // We'll make a visual representation of the diff using
+ // red for removed pixels and green for added. We make
+ // a z2d surface for that here.
+ var diff = try z2d.Surface.init(
+ .image_surface_rgb,
+ alloc,
+ atlas.getWidth(),
+ atlas.getHeight(),
+ );
+ defer diff.deinit(alloc);
+ const diff_pix = diff.image_surface_rgb.buf;
+
+ const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
+
+ var differs: bool = false;
+ for (0..test_gray.len) |j| {
+ const t = test_gray[j];
+ const r = ref_rgba.data[j * 4];
+ if (t == r) {
+ // If the pixels match, write it as a faded gray.
+ diff_pix[j].r = t / 3;
+ diff_pix[j].g = t / 3;
+ diff_pix[j].b = t / 3;
+ } else {
+ differs = true;
+ // Otherwise put the reference value in the red
+ // channel and the new value in the green channel.
+ diff_pix[j].r = r;
+ diff_pix[j].g = t;
+ }
+ }
+
+ // If the PNG data differs but not the raw pixels, that's
+ // a big red flag, since it could mean someone is trying to
+ // smuggle binary data in to the test files.
+ if (!differs) {
+ log.err(
+ "!!! Test PNG data does not match reference, but pixels do match! " ++
+ "Either z2d's PNG exporter changed or someone is " ++
+ "trying to smuggle binary data in the test files!\n" ++
+ "test={s}, reference={s}",
+ .{ test_path, ref_path },
+ );
+ return true;
+ }
+
+ // Drop the diff image as a PNG in the cwd.
+ const diff_path = try std.fmt.allocPrint(
+ alloc,
+ "./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png",
+ .{ i, i + 0xFF, width, height, thickness },
+ );
+ defer alloc.free(diff_path);
+ try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{});
+ log.err(
+ "One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++
+ "test={s}, reference={s}, diff={s}",
+ .{ i, i + 0xFF, test_path, ref_path, diff_path },
+ );
+
+ return true;
+}
+
+/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions
+/// against reference files, logs errors and exposes a diff for any difference
+/// between the reference and test atlas.
+///
+/// Returns true if there was a diff.
+fn testDrawRanges(
+ width: u32,
+ ascent: u32,
+ descent: u32,
+ thickness: u32,
+) !bool {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const metrics: font.Metrics = .calc(.{
+ // Fudged number, not used in anything we care about here.
+ .px_per_em = 16,
+
+ .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 reuse 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" {
+ // This test is way too slow to run under Valgrind, unfortunately.
+ if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
+
+ // Renders all sprites to an atlas and compares
+ // it to a ground truth for regression testing.
+
+ var diff: bool = false;
+
+ // testDrawRanges(width, ascent, descent, thickness):
+ //
+ // We compare 4 different sets of metrics;
+ // - even cell size / even thickness
+ // - even cell size / odd thickness
+ // - odd cell size / even thickness
+ // - odd cell size / odd thickness
+ // (Also a decreasing range of sizes.)
+ if (try testDrawRanges(18, 30, 6, 4)) diff = true;
+ if (try testDrawRanges(12, 20, 4, 3)) diff = true;
+ if (try testDrawRanges(11, 19, 2, 2)) diff = true;
+ if (try testDrawRanges(9, 15, 2, 1)) diff = true;
+
+ try std.testing.expect(!diff); // There should be no diffs from reference.
+}
+
+// test "sprite face print all sprites" {
+// std.debug.print("\n\n", .{});
+// inline for (ranges) |range| {
+// for (range.min..range.max + 1) |cp| {
+// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) });
+// }
+// }
+// std.debug.print("\n\n", .{});
+// }
+
+test {
+ std.testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/canvas.zig b/vendor/libghostty-vt/src/font/sprite/canvas.zig
new file mode 100644
index 00000000..7904f20a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/canvas.zig
@@ -0,0 +1,512 @@
+//! This exposes primitives to draw 2D graphics and export the graphic to
+//! a font atlas.
+const std = @import("std");
+const assert = @import("../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const z2d = @import("z2d");
+const font = @import("../main.zig");
+
+pub fn Point(comptime T: type) type {
+ return struct {
+ x: T,
+ y: T,
+ };
+}
+
+pub fn Line(comptime T: type) type {
+ return struct {
+ p0: Point(T),
+ p1: Point(T),
+ };
+}
+
+pub fn Box(comptime T: type) type {
+ return struct {
+ p0: Point(T),
+ p1: Point(T),
+
+ pub fn rect(self: Box(T)) Rect(T) {
+ const tl_x = @min(self.p0.x, self.p1.x);
+ const tl_y = @min(self.p0.y, self.p1.y);
+ const br_x = @max(self.p0.x, self.p1.x);
+ const br_y = @max(self.p0.y, self.p1.y);
+
+ return .{
+ .x = tl_x,
+ .y = tl_y,
+ .width = br_x - tl_x,
+ .height = br_y - tl_y,
+ };
+ }
+ };
+}
+
+pub fn Rect(comptime T: type) type {
+ return struct {
+ x: T,
+ y: T,
+ width: T,
+ height: T,
+ };
+}
+
+pub fn Triangle(comptime T: type) type {
+ return struct {
+ p0: Point(T),
+ p1: Point(T),
+ p2: Point(T),
+ };
+}
+
+pub fn Quad(comptime T: type) type {
+ return struct {
+ p0: Point(T),
+ p1: Point(T),
+ p2: Point(T),
+ p3: Point(T),
+ };
+}
+
+/// We only use alpha-channel so a pixel can only be "on" or "off".
+pub const Color = enum(u8) {
+ on = 255,
+ off = 0,
+ _,
+};
+
+/// This is a managed struct, it keeps a reference to the allocator that is
+/// used to initialize it, and the same allocator is used for any further
+/// necessary allocations when drawing.
+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,
+ 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 + 2 * padding_x),
+ @intCast(height + 2 * padding_y),
+ );
+ errdefer sfc.deinit(alloc);
+
+ return .{
+ .sfc = sfc,
+ .padding_x = padding_x,
+ .padding_y = padding_y,
+ .alloc = alloc,
+ };
+ }
+
+ pub fn deinit(self: *Canvas) void {
+ self.sfc.deinit(self.alloc);
+ self.* = undefined;
+ }
+
+ /// Write the data in this drawing to the atlas.
+ pub fn writeAtlas(
+ self: *Canvas,
+ alloc: Allocator,
+ atlas: *font.Atlas,
+ ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region {
+ assert(atlas.format == .grayscale);
+
+ 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 = try atlas.reserve(alloc, region_width, region_height);
+
+ if (region.width > 0 and region.height > 0) {
+ const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
+
+ // Write the glyph information into the atlas
+ 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 {
+ 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: i32, y: i32, color: Color) void {
+ self.sfc.putPixel(
+ 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(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),
+ color,
+ );
+ }
+ }
+ }
+
+ /// 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 = 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 self.fillPath(path.wrapped_path, .{}, color);
+ }
+
+ /// Draw and fill a triangle.
+ pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void {
+ 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.StrokeOptions,
+ 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.StrokeOptions,
+ 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.FillOptions,
+ color: Color,
+ ) z2d.painter.FillError!void {
+ try z2d.painter.fill(
+ self.alloc,
+ &self.sfc,
+ &.{ .opaque_pattern = .{
+ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
+ } },
+ 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);
+ }
+};
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/README.md b/vendor/libghostty-vt/src/font/sprite/draw/README.md
new file mode 100644
index 00000000..d1603599
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/README.md
@@ -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.
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/block.zig b/vendor/libghostty-vt/src/font/sprite/draw/block.zig
new file mode 100644
index 00000000..1731d2f5
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/block.zig
@@ -0,0 +1,177 @@
+//! Block Elements | U+2580...U+259F
+//! https://en.wikipedia.org/wiki/Block_Elements
+//!
+//! ▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏
+//! ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟
+//!
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+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");
+
+// 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);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/box.zig b/vendor/libghostty-vt/src/font/sprite/draw/box.zig
new file mode 100644
index 00000000..da3edb51
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/box.zig
@@ -0,0 +1,929 @@
+//! Box Drawing | U+2500...U+257F
+//! https://en.wikipedia.org/wiki/Box_Drawing
+//!
+//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏
+//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟
+//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯
+//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿
+//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏
+//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟
+//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯
+//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿
+//!
+
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+
+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");
+
+/// 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 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 start of the next dash.
+ y = y1 + gap_height;
+ }
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/braille.zig b/vendor/libghostty-vt/src/font/sprite/draw/braille.zig
new file mode 100644
index 00000000..fb2d5474
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/braille.zig
@@ -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 = @import("../../../quirks.zig").inlineAssert;
+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);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/branch.zig b/vendor/libghostty-vt/src/font/sprite/draw/branch.zig
new file mode 100644
index 00000000..034f1e39
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/branch.zig
@@ -0,0 +1,504 @@
+//! 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 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;
+ }
+ },
+ }
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/common.zig b/vendor/libghostty-vt/src/font/sprite/draw/common.zig
new file mode 100644
index 00000000..290c4496
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/common.zig
@@ -0,0 +1,374 @@
+//! 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 Allocator = std.mem.Allocator;
+
+const font = @import("../../main.zig");
+
+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);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/geometric_shapes.zig b/vendor/libghostty-vt/src/font/sprite/draw/geometric_shapes.zig
new file mode 100644
index 00000000..f6402cf0
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/geometric_shapes.zig
@@ -0,0 +1,198 @@
+//! 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 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);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/octants.txt b/vendor/libghostty-vt/src/font/sprite/draw/octants.txt
new file mode 100644
index 00000000..db79aa2c
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/octants.txt
@@ -0,0 +1,234 @@
+# This is the list of all the octants for the Symbols for Legacy
+# Computing block. It is used at comptime to generate the lookup
+# table for drawing them since we weren't able to discern a
+# mathematical pattern for them.
+BLOCK OCTANT-3
+BLOCK OCTANT-23
+BLOCK OCTANT-123
+BLOCK OCTANT-4
+BLOCK OCTANT-14
+BLOCK OCTANT-124
+BLOCK OCTANT-34
+BLOCK OCTANT-134
+BLOCK OCTANT-234
+BLOCK OCTANT-5
+BLOCK OCTANT-15
+BLOCK OCTANT-25
+BLOCK OCTANT-125
+BLOCK OCTANT-135
+BLOCK OCTANT-235
+BLOCK OCTANT-1235
+BLOCK OCTANT-45
+BLOCK OCTANT-145
+BLOCK OCTANT-245
+BLOCK OCTANT-1245
+BLOCK OCTANT-345
+BLOCK OCTANT-1345
+BLOCK OCTANT-2345
+BLOCK OCTANT-12345
+BLOCK OCTANT-6
+BLOCK OCTANT-16
+BLOCK OCTANT-26
+BLOCK OCTANT-126
+BLOCK OCTANT-36
+BLOCK OCTANT-136
+BLOCK OCTANT-236
+BLOCK OCTANT-1236
+BLOCK OCTANT-146
+BLOCK OCTANT-246
+BLOCK OCTANT-1246
+BLOCK OCTANT-346
+BLOCK OCTANT-1346
+BLOCK OCTANT-2346
+BLOCK OCTANT-12346
+BLOCK OCTANT-56
+BLOCK OCTANT-156
+BLOCK OCTANT-256
+BLOCK OCTANT-1256
+BLOCK OCTANT-356
+BLOCK OCTANT-1356
+BLOCK OCTANT-2356
+BLOCK OCTANT-12356
+BLOCK OCTANT-456
+BLOCK OCTANT-1456
+BLOCK OCTANT-2456
+BLOCK OCTANT-12456
+BLOCK OCTANT-3456
+BLOCK OCTANT-13456
+BLOCK OCTANT-23456
+BLOCK OCTANT-17
+BLOCK OCTANT-27
+BLOCK OCTANT-127
+BLOCK OCTANT-37
+BLOCK OCTANT-137
+BLOCK OCTANT-237
+BLOCK OCTANT-1237
+BLOCK OCTANT-47
+BLOCK OCTANT-147
+BLOCK OCTANT-247
+BLOCK OCTANT-1247
+BLOCK OCTANT-347
+BLOCK OCTANT-1347
+BLOCK OCTANT-2347
+BLOCK OCTANT-12347
+BLOCK OCTANT-157
+BLOCK OCTANT-257
+BLOCK OCTANT-1257
+BLOCK OCTANT-357
+BLOCK OCTANT-2357
+BLOCK OCTANT-12357
+BLOCK OCTANT-457
+BLOCK OCTANT-1457
+BLOCK OCTANT-12457
+BLOCK OCTANT-3457
+BLOCK OCTANT-13457
+BLOCK OCTANT-23457
+BLOCK OCTANT-67
+BLOCK OCTANT-167
+BLOCK OCTANT-267
+BLOCK OCTANT-1267
+BLOCK OCTANT-367
+BLOCK OCTANT-1367
+BLOCK OCTANT-2367
+BLOCK OCTANT-12367
+BLOCK OCTANT-467
+BLOCK OCTANT-1467
+BLOCK OCTANT-2467
+BLOCK OCTANT-12467
+BLOCK OCTANT-3467
+BLOCK OCTANT-13467
+BLOCK OCTANT-23467
+BLOCK OCTANT-123467
+BLOCK OCTANT-567
+BLOCK OCTANT-1567
+BLOCK OCTANT-2567
+BLOCK OCTANT-12567
+BLOCK OCTANT-3567
+BLOCK OCTANT-13567
+BLOCK OCTANT-23567
+BLOCK OCTANT-123567
+BLOCK OCTANT-4567
+BLOCK OCTANT-14567
+BLOCK OCTANT-24567
+BLOCK OCTANT-124567
+BLOCK OCTANT-34567
+BLOCK OCTANT-134567
+BLOCK OCTANT-234567
+BLOCK OCTANT-1234567
+BLOCK OCTANT-18
+BLOCK OCTANT-28
+BLOCK OCTANT-128
+BLOCK OCTANT-38
+BLOCK OCTANT-138
+BLOCK OCTANT-238
+BLOCK OCTANT-1238
+BLOCK OCTANT-48
+BLOCK OCTANT-148
+BLOCK OCTANT-248
+BLOCK OCTANT-1248
+BLOCK OCTANT-348
+BLOCK OCTANT-1348
+BLOCK OCTANT-2348
+BLOCK OCTANT-12348
+BLOCK OCTANT-58
+BLOCK OCTANT-158
+BLOCK OCTANT-258
+BLOCK OCTANT-1258
+BLOCK OCTANT-358
+BLOCK OCTANT-1358
+BLOCK OCTANT-2358
+BLOCK OCTANT-12358
+BLOCK OCTANT-458
+BLOCK OCTANT-1458
+BLOCK OCTANT-2458
+BLOCK OCTANT-12458
+BLOCK OCTANT-3458
+BLOCK OCTANT-13458
+BLOCK OCTANT-23458
+BLOCK OCTANT-123458
+BLOCK OCTANT-168
+BLOCK OCTANT-268
+BLOCK OCTANT-1268
+BLOCK OCTANT-368
+BLOCK OCTANT-2368
+BLOCK OCTANT-12368
+BLOCK OCTANT-468
+BLOCK OCTANT-1468
+BLOCK OCTANT-12468
+BLOCK OCTANT-3468
+BLOCK OCTANT-13468
+BLOCK OCTANT-23468
+BLOCK OCTANT-568
+BLOCK OCTANT-1568
+BLOCK OCTANT-2568
+BLOCK OCTANT-12568
+BLOCK OCTANT-3568
+BLOCK OCTANT-13568
+BLOCK OCTANT-23568
+BLOCK OCTANT-123568
+BLOCK OCTANT-4568
+BLOCK OCTANT-14568
+BLOCK OCTANT-24568
+BLOCK OCTANT-124568
+BLOCK OCTANT-34568
+BLOCK OCTANT-134568
+BLOCK OCTANT-234568
+BLOCK OCTANT-1234568
+BLOCK OCTANT-178
+BLOCK OCTANT-278
+BLOCK OCTANT-1278
+BLOCK OCTANT-378
+BLOCK OCTANT-1378
+BLOCK OCTANT-2378
+BLOCK OCTANT-12378
+BLOCK OCTANT-478
+BLOCK OCTANT-1478
+BLOCK OCTANT-2478
+BLOCK OCTANT-12478
+BLOCK OCTANT-3478
+BLOCK OCTANT-13478
+BLOCK OCTANT-23478
+BLOCK OCTANT-123478
+BLOCK OCTANT-578
+BLOCK OCTANT-1578
+BLOCK OCTANT-2578
+BLOCK OCTANT-12578
+BLOCK OCTANT-3578
+BLOCK OCTANT-13578
+BLOCK OCTANT-23578
+BLOCK OCTANT-123578
+BLOCK OCTANT-4578
+BLOCK OCTANT-14578
+BLOCK OCTANT-24578
+BLOCK OCTANT-124578
+BLOCK OCTANT-34578
+BLOCK OCTANT-134578
+BLOCK OCTANT-234578
+BLOCK OCTANT-1234578
+BLOCK OCTANT-678
+BLOCK OCTANT-1678
+BLOCK OCTANT-2678
+BLOCK OCTANT-12678
+BLOCK OCTANT-3678
+BLOCK OCTANT-13678
+BLOCK OCTANT-23678
+BLOCK OCTANT-123678
+BLOCK OCTANT-4678
+BLOCK OCTANT-14678
+BLOCK OCTANT-24678
+BLOCK OCTANT-124678
+BLOCK OCTANT-34678
+BLOCK OCTANT-134678
+BLOCK OCTANT-234678
+BLOCK OCTANT-1234678
+BLOCK OCTANT-15678
+BLOCK OCTANT-25678
+BLOCK OCTANT-125678
+BLOCK OCTANT-35678
+BLOCK OCTANT-235678
+BLOCK OCTANT-1235678
+BLOCK OCTANT-45678
+BLOCK OCTANT-145678
+BLOCK OCTANT-1245678
+BLOCK OCTANT-1345678
+BLOCK OCTANT-2345678
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/powerline.zig b/vendor/libghostty-vt/src/font/sprite/draw/powerline.zig
new file mode 100644
index 00000000..8658d855
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/powerline.zig
@@ -0,0 +1,394 @@
+//! 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 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();
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/special.zig b/vendor/libghostty-vt/src/font/sprite/draw/special.zig
new file mode 100644
index 00000000..8cad9ceb
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/special.zig
@@ -0,0 +1,372 @@
+//! 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 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;
+
+ // We can go beyond the height of the cell a bit, but
+ // we want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell below the cell
+ // height.
+ const y = @min(
+ metrics.underline_position,
+ height +| canvas.padding_y -| metrics.underline_thickness,
+ );
+
+ canvas.rect(.{
+ .x = 0,
+ .y = @intCast(y),
+ .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;
+
+ // We can go beyond the height of the cell a bit, but
+ // we want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell below the cell
+ // height.
+ const y = @min(
+ metrics.underline_position,
+ height +| canvas.padding_y -| 2 * metrics.underline_thickness,
+ );
+
+ // 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(y -| metrics.underline_thickness),
+ .width = @intCast(width),
+ .height = @intCast(metrics.underline_thickness),
+ }, .on);
+ canvas.rect(.{
+ .x = 0,
+ .y = @intCast(y +| 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;
+
+ var ctx = canvas.getContext();
+ defer ctx.deinit();
+
+ const float_width: f64 = @floatFromInt(width);
+ const float_height: f64 = @floatFromInt(height);
+ const float_pos: f64 = @floatFromInt(metrics.underline_position);
+ const float_thick: f64 = @floatFromInt(metrics.underline_thickness);
+
+ // The diameter will be sqrt2 * the usual underline thickness
+ // since otherwise dotted underlines look somewhat anemic.
+ const radius = std.math.sqrt1_2 * float_thick;
+
+ // We can go beyond the height of the cell a bit, but
+ // we want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell below the cell
+ // height.
+ const padding: f64 = @floatFromInt(canvas.padding_y);
+ const y = @min(
+ // The center of the underline stem.
+ float_pos + 0.5 * float_thick,
+ // The lowest we can go on the canvas and not get clipped.
+ float_height + padding - @ceil(radius),
+ );
+
+ const dot_count: f64 = @max(
+ @min(
+ // We should try to have enough dots that the
+ // space between them matches their diameter.
+ @ceil(float_width / (4 * radius)),
+ // And not enough that the space between
+ // each dot is less than their radius.
+ @floor(float_width / (3 * radius)),
+ // And definitely not enough that the space
+ // between them is less than a single pixel.
+ @floor(float_width / (2 * radius + 1)),
+ ),
+ // And we must have at least one dot per cell.
+ 1.0,
+ );
+
+ // What we essentially do is divide the cell in to
+ // dot_count areas with a dot centered in each one.
+ var x: f64 = (float_width / dot_count) / 2;
+ for (0..@as(usize, @intFromFloat(dot_count))) |_| {
+ try ctx.arc(x, y, radius, 0.0, std.math.tau);
+ try ctx.closePath();
+ x += float_width / dot_count;
+ }
+
+ try ctx.fill();
+}
+
+pub fn underline_dashed(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+
+ // We can go beyond the height of the cell a bit, but
+ // we want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell below the cell
+ // height.
+ const y = @min(
+ metrics.underline_position,
+ height +| canvas.padding_y -| metrics.underline_thickness,
+ );
+
+ const dash_width = width / 3 + 1;
+ const dash_count = (width / dash_width) + 1;
+ var i: u32 = 0;
+ while (i < dash_count) : (i += 2) {
+ const x = i * dash_width;
+ canvas.rect(.{
+ .x = @intCast(x),
+ .y = @intCast(y),
+ .width = @intCast(dash_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;
+
+ var ctx = canvas.getContext();
+ defer ctx.deinit();
+
+ const float_width: f64 = @floatFromInt(width);
+ const float_height: f64 = @floatFromInt(height);
+ const float_pos: f64 = @floatFromInt(metrics.underline_position);
+
+ // 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.
+ ctx.line_width = @floatFromInt(metrics.underline_thickness);
+
+ // Rounded caps, adjacent underlines will have these overlap and so not be
+ // visible, but it makes the ends look cleaner.
+ ctx.line_cap_mode = .round;
+
+ // Empirically this looks good.
+ const amplitude = float_width / std.math.pi;
+
+ // Make sure we don't exceed the drawable area. This can still be outside
+ // of the cell by some amount (one quarter of the height), but we don't
+ // want underlines to disappear for fonts with bad metadata or when users
+ // set their underline position way too low.
+ const padding: f64 = @floatFromInt(canvas.padding_y);
+ const top: f64 = @min(
+ float_pos,
+ // The lowest we can draw this and not get clipped.
+ float_height + padding - amplitude - ctx.line_width,
+ );
+ const bottom = top + amplitude;
+
+ // Curvature multiplier.
+ // To my eye, 0.4 creates a nice smooth wiggle.
+ const r = 0.4;
+
+ const center = 0.5 * float_width;
+
+ // We create a single cycle of a wave that peaks at the center of the cell.
+ try ctx.moveTo(0, bottom);
+ try ctx.curveTo(
+ center * r,
+ bottom,
+ center - center * r,
+ top,
+ center,
+ top,
+ );
+ try ctx.curveTo(
+ center + center * r,
+ top,
+ float_width - center * r,
+ bottom,
+ float_width,
+ bottom,
+ );
+ try ctx.stroke();
+}
+
+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;
+
+ // We can go beyond the top of the cell a bit, but we
+ // want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell above the top
+ // of the cell.
+ const y = @max(
+ metrics.overline_position,
+ -@as(i32, @intCast(canvas.padding_y)),
+ );
+
+ canvas.rect(.{
+ .x = 0,
+ .y = y,
+ .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;
+
+ // We can go beyond the height of the cell a bit, but
+ // we want to be sure never to exceed the height of the
+ // canvas, which extends a quarter cell below the cell
+ // height.
+ const y = @min(
+ metrics.underline_position,
+ height +| canvas.padding_y -| metrics.underline_thickness,
+ );
+
+ canvas.rect(.{
+ .x = 0,
+ .y = @intCast(y),
+ .width = @intCast(width),
+ .height = @intCast(metrics.cursor_thickness),
+ }, .on);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing.zig b/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing.zig
new file mode 100644
index 00000000..d99fc870
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing.zig
@@ -0,0 +1,1413 @@
+//! Symbols for Legacy Computing | U+1FB00...U+1FBFF
+//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing
+//!
+//! 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏
+//! 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟
+//! 🬠 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯
+//! 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 🬼 🬽 🬾 🬿
+//! 🭀 🭁 🭂 🭃 🭄 🭅 🭆 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏
+//! 🭐 🭑 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 🭝 🭞 🭟
+//! 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯
+//! 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 🭼 🭽 🭾 🭿
+//! 🮀 🮁 🮂 🮃 🮄 🮅 🮆 🮇 🮈 🮉 🮊 🮋 🮌 🮍 🮎 🮏
+//! 🮐 🮑 🮒 🮔 🮕 🮖 🮗 🮘 🮙 🮚 🮛 🮜 🮝 🮞 🮟
+//! 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 🮯
+//! 🮰 🮱 🮲 🮳 🮴 🮵 🮶 🮷 🮸 🮹 🮺 🮻 🮼 🮽 🮾 🮿
+//! 🯀 🯁 🯂 🯃 🯄 🯅 🯆 🯇 🯈 🯉 🯊 🯋 🯌 🯍 🯎 🯏
+//! 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟
+//! 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯
+//! 🯰 🯱 🯲 🯳 🯴 🯵 🯶 🯷 🯸 🯹
+//!
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const common = @import("common.zig");
+const Thickness = common.Thickness;
+const Alignment = common.Alignment;
+const Fraction = common.Fraction;
+const Corner = common.Corner;
+const Quads = common.Quads;
+const Edge = common.Edge;
+const Shade = common.Shade;
+const fill = common.fill;
+
+const box = @import("box.zig");
+const block = @import("block.zig");
+const geo = @import("geometric_shapes.zig");
+
+const font = @import("../../main.zig");
+
+// 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;
+
+const SmoothMosaic = packed struct(u10) {
+ tl: bool,
+ ul: bool,
+ ll: bool,
+ bl: bool,
+ bc: bool,
+ br: bool,
+ lr: bool,
+ ur: bool,
+ tr: bool,
+ tc: bool,
+
+ fn from(comptime pattern: *const [15:0]u8) SmoothMosaic {
+ return .{
+ .tl = pattern[0] == '#',
+
+ .ul = pattern[4] == '#' and
+ (pattern[0] != '#' or pattern[8] != '#'),
+
+ .ll = pattern[8] == '#' and
+ (pattern[4] != '#' or pattern[12] != '#'),
+
+ .bl = pattern[12] == '#',
+
+ .bc = pattern[13] == '#' and
+ (pattern[12] != '#' or pattern[14] != '#'),
+
+ .br = pattern[14] == '#',
+
+ .lr = pattern[10] == '#' and
+ (pattern[14] != '#' or pattern[6] != '#'),
+
+ .ur = pattern[6] == '#' and
+ (pattern[10] != '#' or pattern[2] != '#'),
+
+ .tr = pattern[2] == '#',
+
+ .tc = pattern[1] == '#' and
+ (pattern[2] != '#' or pattern[0] != '#'),
+ };
+ }
+};
+
+/// Sextants
+pub fn draw1FB00_1FB3B(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ const Sextants = packed struct(u6) {
+ tl: bool,
+ tr: bool,
+ ml: bool,
+ mr: bool,
+ bl: bool,
+ br: bool,
+ };
+
+ assert(cp >= 0x1fb00 and cp <= 0x1fb3b);
+ const idx = cp - 0x1fb00;
+ const sex: Sextants = @bitCast(@as(u6, @intCast(
+ idx + (idx / 0x14) + 1,
+ )));
+ if (sex.tl) fill(metrics, canvas, .zero, .half, .zero, .one_third);
+ if (sex.tr) fill(metrics, canvas, .half, .full, .zero, .one_third);
+ if (sex.ml) fill(metrics, canvas, .zero, .half, .one_third, .two_thirds);
+ if (sex.mr) fill(metrics, canvas, .half, .full, .one_third, .two_thirds);
+ if (sex.bl) fill(metrics, canvas, .zero, .half, .two_thirds, .end);
+ if (sex.br) fill(metrics, canvas, .half, .full, .two_thirds, .end);
+}
+
+/// Smooth Mosaics
+pub fn draw1FB3C_1FB67(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ // Hand written lookup table for these shapes since I couldn't
+ // determine any sort of mathematical pattern in the codepoints.
+ const mosaic: SmoothMosaic = switch (cp) {
+ // '🬼'
+ 0x1fb3c => SmoothMosaic.from(
+ \\...
+ \\...
+ \\#..
+ \\##.
+ ),
+ // '🬽'
+ 0x1fb3d => SmoothMosaic.from(
+ \\...
+ \\...
+ \\#\.
+ \\###
+ ),
+ // '🬾'
+ 0x1fb3e => SmoothMosaic.from(
+ \\...
+ \\#..
+ \\#\.
+ \\##.
+ ),
+ // '🬿'
+ 0x1fb3f => SmoothMosaic.from(
+ \\...
+ \\#..
+ \\##.
+ \\###
+ ),
+ // '🭀'
+ 0x1fb40 => SmoothMosaic.from(
+ \\#..
+ \\#..
+ \\##.
+ \\##.
+ ),
+
+ // '🭁'
+ 0x1fb41 => SmoothMosaic.from(
+ \\/##
+ \\###
+ \\###
+ \\###
+ ),
+ // '🭂'
+ 0x1fb42 => SmoothMosaic.from(
+ \\./#
+ \\###
+ \\###
+ \\###
+ ),
+ // '🭃'
+ 0x1fb43 => SmoothMosaic.from(
+ \\.##
+ \\.##
+ \\###
+ \\###
+ ),
+ // '🭄'
+ 0x1fb44 => SmoothMosaic.from(
+ \\..#
+ \\.##
+ \\###
+ \\###
+ ),
+ // '🭅'
+ 0x1fb45 => SmoothMosaic.from(
+ \\.##
+ \\.##
+ \\.##
+ \\###
+ ),
+ // '🭆'
+ 0x1fb46 => SmoothMosaic.from(
+ \\...
+ \\./#
+ \\###
+ \\###
+ ),
+
+ // '🭇'
+ 0x1fb47 => SmoothMosaic.from(
+ \\...
+ \\...
+ \\..#
+ \\.##
+ ),
+ // '🭈'
+ 0x1fb48 => SmoothMosaic.from(
+ \\...
+ \\...
+ \\./#
+ \\###
+ ),
+ // '🭉'
+ 0x1fb49 => SmoothMosaic.from(
+ \\...
+ \\..#
+ \\./#
+ \\.##
+ ),
+ // '🭊'
+ 0x1fb4a => SmoothMosaic.from(
+ \\...
+ \\..#
+ \\.##
+ \\###
+ ),
+ // '🭋'
+ 0x1fb4b => SmoothMosaic.from(
+ \\..#
+ \\..#
+ \\.##
+ \\.##
+ ),
+
+ // '🭌'
+ 0x1fb4c => SmoothMosaic.from(
+ \\##\
+ \\###
+ \\###
+ \\###
+ ),
+ // '🭍'
+ 0x1fb4d => SmoothMosaic.from(
+ \\#\.
+ \\###
+ \\###
+ \\###
+ ),
+ // '🭎'
+ 0x1fb4e => SmoothMosaic.from(
+ \\##.
+ \\##.
+ \\###
+ \\###
+ ),
+ // '🭏'
+ 0x1fb4f => SmoothMosaic.from(
+ \\#..
+ \\##.
+ \\###
+ \\###
+ ),
+ // '🭐'
+ 0x1fb50 => SmoothMosaic.from(
+ \\##.
+ \\##.
+ \\##.
+ \\###
+ ),
+ // '🭑'
+ 0x1fb51 => SmoothMosaic.from(
+ \\...
+ \\#\.
+ \\###
+ \\###
+ ),
+
+ // '🭒'
+ 0x1fb52 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\###
+ \\\##
+ ),
+ // '🭓'
+ 0x1fb53 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\###
+ \\.\#
+ ),
+ // '🭔'
+ 0x1fb54 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\.##
+ \\.##
+ ),
+ // '🭕'
+ 0x1fb55 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\.##
+ \\..#
+ ),
+ // '🭖'
+ 0x1fb56 => SmoothMosaic.from(
+ \\###
+ \\.##
+ \\.##
+ \\.##
+ ),
+
+ // '🭗'
+ 0x1fb57 => SmoothMosaic.from(
+ \\##.
+ \\#..
+ \\...
+ \\...
+ ),
+ // '🭘'
+ 0x1fb58 => SmoothMosaic.from(
+ \\###
+ \\#/.
+ \\...
+ \\...
+ ),
+ // '🭙'
+ 0x1fb59 => SmoothMosaic.from(
+ \\##.
+ \\#/.
+ \\#..
+ \\...
+ ),
+ // '🭚'
+ 0x1fb5a => SmoothMosaic.from(
+ \\###
+ \\##.
+ \\#..
+ \\...
+ ),
+ // '🭛'
+ 0x1fb5b => SmoothMosaic.from(
+ \\##.
+ \\##.
+ \\#..
+ \\#..
+ ),
+
+ // '🭜'
+ 0x1fb5c => SmoothMosaic.from(
+ \\###
+ \\###
+ \\#/.
+ \\...
+ ),
+ // '🭝'
+ 0x1fb5d => SmoothMosaic.from(
+ \\###
+ \\###
+ \\###
+ \\##/
+ ),
+ // '🭞'
+ 0x1fb5e => SmoothMosaic.from(
+ \\###
+ \\###
+ \\###
+ \\#/.
+ ),
+ // '🭟'
+ 0x1fb5f => SmoothMosaic.from(
+ \\###
+ \\###
+ \\##.
+ \\##.
+ ),
+ // '🭠'
+ 0x1fb60 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\##.
+ \\#..
+ ),
+ // '🭡'
+ 0x1fb61 => SmoothMosaic.from(
+ \\###
+ \\##.
+ \\##.
+ \\##.
+ ),
+
+ // '🭢'
+ 0x1fb62 => SmoothMosaic.from(
+ \\.##
+ \\..#
+ \\...
+ \\...
+ ),
+ // '🭣'
+ 0x1fb63 => SmoothMosaic.from(
+ \\###
+ \\.\#
+ \\...
+ \\...
+ ),
+ // '🭤'
+ 0x1fb64 => SmoothMosaic.from(
+ \\.##
+ \\.\#
+ \\..#
+ \\...
+ ),
+ // '🭥'
+ 0x1fb65 => SmoothMosaic.from(
+ \\###
+ \\.##
+ \\..#
+ \\...
+ ),
+ // '🭦'
+ 0x1fb66 => SmoothMosaic.from(
+ \\.##
+ \\.##
+ \\..#
+ \\..#
+ ),
+ // '🭧'
+ 0x1fb67 => SmoothMosaic.from(
+ \\###
+ \\###
+ \\.\#
+ \\...
+ ),
+ else => unreachable,
+ };
+
+ const top: f64 = 0.0;
+ const upper: f64 = Fraction.one_third.float(metrics.cell_height);
+ const lower: f64 = Fraction.two_thirds.float(metrics.cell_height);
+ const bottom: f64 = @floatFromInt(metrics.cell_height);
+ const left: f64 = 0.0;
+ const center: f64 = Fraction.half.float(metrics.cell_width);
+ const right: f64 = @floatFromInt(metrics.cell_width);
+
+ var path = canvas.staticPath(12); // nodes.len = 0
+ if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1
+ if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2
+ if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3
+ if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4
+ if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5
+ if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6
+ if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7
+ if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8
+ if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9
+ if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10
+ path.close(); // +2, nodes.len = 12
+
+ try canvas.fillPath(path.wrapped_path, .{}, .on);
+}
+
+pub fn draw1FB68_1FB6F(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ switch (cp) {
+ // '🭨'
+ 0x1fb68 => {
+ try edgeTriangle(metrics, canvas, .left);
+ canvas.invert();
+ // 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;
+ },
+ // '🭩'
+ 0x1fb69 => {
+ try edgeTriangle(metrics, canvas, .top);
+ canvas.invert();
+ // 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;
+ },
+ // '🭪'
+ 0x1fb6a => {
+ try edgeTriangle(metrics, canvas, .right);
+ canvas.invert();
+ // 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;
+ },
+ // '🭫'
+ 0x1fb6b => {
+ try edgeTriangle(metrics, canvas, .bottom);
+ canvas.invert();
+ // 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;
+ },
+ // '🭬'
+ 0x1fb6c => try edgeTriangle(metrics, canvas, .left),
+ // '🭭'
+ 0x1fb6d => try edgeTriangle(metrics, canvas, .top),
+ // '🭮'
+ 0x1fb6e => try edgeTriangle(metrics, canvas, .right),
+ // '🭯'
+ 0x1fb6f => try edgeTriangle(metrics, canvas, .bottom),
+
+ else => unreachable,
+ }
+}
+
+/// Vertical one eighth blocks
+pub fn draw1FB70_1FB75(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ const n = cp + 1 - 0x1fb70;
+
+ fill(
+ metrics,
+ canvas,
+ Fraction.eighths[n],
+ Fraction.eighths[n + 1],
+ .top,
+ .bottom,
+ );
+}
+
+/// Horizontal one eighth blocks
+pub fn draw1FB76_1FB7B(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ const n = cp + 1 - 0x1fb76;
+
+ fill(
+ metrics,
+ canvas,
+ .left,
+ .right,
+ Fraction.eighths[n],
+ Fraction.eighths[n + 1],
+ );
+}
+
+pub fn draw1FB7C_1FB97(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ switch (cp) {
+
+ // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK
+ 0x1fb7c => {
+ block.block(metrics, canvas, .left, one_eighth, 1);
+ block.block(metrics, canvas, .lower, 1, one_eighth);
+ },
+ // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK
+ 0x1fb7d => {
+ block.block(metrics, canvas, .left, one_eighth, 1);
+ block.block(metrics, canvas, .upper, 1, one_eighth);
+ },
+ // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK
+ 0x1fb7e => {
+ block.block(metrics, canvas, .right, one_eighth, 1);
+ block.block(metrics, canvas, .upper, 1, one_eighth);
+ },
+ // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK
+ 0x1fb7f => {
+ block.block(metrics, canvas, .right, one_eighth, 1);
+ block.block(metrics, canvas, .lower, 1, one_eighth);
+ },
+ // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK
+ 0x1fb80 => {
+ block.block(metrics, canvas, .upper, 1, one_eighth);
+ block.block(metrics, canvas, .lower, 1, one_eighth);
+ },
+ // '🮁' Horizontal One Eighth Block 1358
+ 0x1fb81 => {
+ // We just call the draw function for each of the relevant codepoints.
+ // The first codepoint is actually a lie, it's before the range, but
+ // we need it to get the first (0th) block position. This might be a
+ // bit brittle, oh well, if it breaks we can fix it.
+ try draw1FB76_1FB7B(0x1fb74 + 1, canvas, width, height, metrics);
+ try draw1FB76_1FB7B(0x1fb74 + 3, canvas, width, height, metrics);
+ try draw1FB76_1FB7B(0x1fb74 + 5, canvas, width, height, metrics);
+ try draw1FB76_1FB7B(0x1fb74 + 8, canvas, width, height, metrics);
+ },
+
+ // '🮂' UPPER ONE QUARTER BLOCK
+ 0x1fb82 => block.block(metrics, canvas, .upper, 1, one_quarter),
+ // '🮃' UPPER THREE EIGHTHS BLOCK
+ 0x1fb83 => block.block(metrics, canvas, .upper, 1, three_eighths),
+ // '🮄' UPPER FIVE EIGHTHS BLOCK
+ 0x1fb84 => block.block(metrics, canvas, .upper, 1, five_eighths),
+ // '🮅' UPPER THREE QUARTERS BLOCK
+ 0x1fb85 => block.block(metrics, canvas, .upper, 1, three_quarters),
+ // '🮆' UPPER SEVEN EIGHTHS BLOCK
+ 0x1fb86 => block.block(metrics, canvas, .upper, 1, seven_eighths),
+
+ // '🮇' RIGHT ONE QUARTER BLOCK
+ 0x1fb87 => block.block(metrics, canvas, .right, one_quarter, 1),
+ // '🮈' RIGHT THREE EIGHTHS BLOCK
+ 0x1fb88 => block.block(metrics, canvas, .right, three_eighths, 1),
+ // '🮉' RIGHT FIVE EIGHTHS BLOCK
+ 0x1fb89 => block.block(metrics, canvas, .right, five_eighths, 1),
+ // '🮊' RIGHT THREE QUARTERS BLOCK
+ 0x1fb8a => block.block(metrics, canvas, .right, three_quarters, 1),
+ // '🮋' RIGHT SEVEN EIGHTHS BLOCK/
+ 0x1fb8b => block.block(metrics, canvas, .right, seven_eighths, 1),
+
+ // '🮌'
+ 0x1fb8c => block.blockShade(metrics, canvas, .left, half, 1, .medium),
+ // '🮍'
+ 0x1fb8d => block.blockShade(metrics, canvas, .right, half, 1, .medium),
+ // '🮎'
+ 0x1fb8e => block.blockShade(metrics, canvas, .upper, 1, half, .medium),
+ // '🮏'
+ 0x1fb8f => block.blockShade(metrics, canvas, .lower, 1, half, .medium),
+
+ // '🮐'
+ 0x1fb90 => block.fullBlockShade(metrics, canvas, .medium),
+ // '🮑'
+ 0x1fb91 => {
+ block.fullBlockShade(metrics, canvas, .medium);
+ block.block(metrics, canvas, .upper, 1, half);
+ },
+ // '🮒'
+ 0x1fb92 => {
+ block.fullBlockShade(metrics, canvas, .medium);
+ block.block(metrics, canvas, .lower, 1, half);
+ },
+ 0x1fb93 => {
+ // NOTE: This codepoint is currently un-allocated, it's a hole
+ // in the unicode block, so it's safe to just render it
+ // as an empty glyph, probably.
+ },
+ // '🮔'
+ 0x1fb94 => {
+ block.fullBlockShade(metrics, canvas, .medium);
+ block.block(metrics, canvas, .right, half, 1);
+ },
+ // '🮕'
+ 0x1fb95 => checkerboardFill(metrics, canvas, 0),
+ // '🮖'
+ 0x1fb96 => checkerboardFill(metrics, canvas, 1),
+ // '🮗'
+ 0x1fb97 => {
+ canvas.box(
+ 0,
+ @intCast(height / 4),
+ @intCast(width),
+ @intCast(2 * height / 4),
+ .on,
+ );
+ canvas.box(
+ 0,
+ @intCast(3 * height / 4),
+ @intCast(width),
+ @intCast(height),
+ .on,
+ );
+ },
+
+ else => unreachable,
+ }
+}
+
+/// Upper Left to Lower Right Fill
+/// 🮘
+pub fn draw1FB98(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ // 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;
+
+ // TODO: This doesn't align properly for most cell sizes, fix that.
+
+ const thick_px = Thickness.light.height(metrics.box_thickness);
+ const line_count = metrics.cell_width / (2 * thick_px);
+
+ const float_width: f64 = @floatFromInt(metrics.cell_width);
+ const float_height: f64 = @floatFromInt(metrics.cell_height);
+ const float_thick: f64 = @floatFromInt(thick_px);
+ const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
+
+ for (0..line_count * 2 + 1) |_i| {
+ const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count));
+ const top_x = @as(f64, @floatFromInt(i)) * stride;
+ const bottom_x = float_width + top_x;
+ canvas.line(.{
+ .p0 = .{ .x = top_x, .y = 0 },
+ .p1 = .{ .x = bottom_x, .y = float_height },
+ }, float_thick, .on) catch {};
+ }
+}
+
+/// Upper Right to Lower Left Fill
+/// 🮙
+pub fn draw1FB99(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ // 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;
+
+ // TODO: This doesn't align properly for most cell sizes, fix that.
+
+ const thick_px = Thickness.light.height(metrics.box_thickness);
+ const line_count = metrics.cell_width / (2 * thick_px);
+
+ const float_width: f64 = @floatFromInt(metrics.cell_width);
+ const float_height: f64 = @floatFromInt(metrics.cell_height);
+ const float_thick: f64 = @floatFromInt(thick_px);
+ const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
+
+ for (0..line_count * 2 + 1) |_i| {
+ const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count));
+ const bottom_x = @as(f64, @floatFromInt(i)) * stride;
+ const top_x = float_width + bottom_x;
+ canvas.line(.{
+ .p0 = .{ .x = top_x, .y = 0 },
+ .p1 = .{ .x = bottom_x, .y = float_height },
+ }, float_thick, .on) catch {};
+ }
+}
+
+pub fn draw1FB9A_1FB9F(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ switch (cp) {
+ // '🮚'
+ 0x1fb9a => {
+ try edgeTriangle(metrics, canvas, .top);
+ try edgeTriangle(metrics, canvas, .bottom);
+ },
+ // '🮛'
+ 0x1fb9b => {
+ try edgeTriangle(metrics, canvas, .left);
+ try edgeTriangle(metrics, canvas, .right);
+ },
+ // '🮜'
+ 0x1fb9c => try geo.cornerTriangleShade(metrics, canvas, .tl, .medium),
+ // '🮝'
+ 0x1fb9d => try geo.cornerTriangleShade(metrics, canvas, .tr, .medium),
+ // '🮞'
+ 0x1fb9e => try geo.cornerTriangleShade(metrics, canvas, .br, .medium),
+ // '🮟'
+ 0x1fb9f => try geo.cornerTriangleShade(metrics, canvas, .bl, .medium),
+
+ else => unreachable,
+ }
+}
+
+pub fn draw1FBA0_1FBAE(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ switch (cp) {
+ // '🮠'
+ 0x1fba0 => cornerDiagonalLines(metrics, canvas, .{ .tl = true }),
+ // '🮡'
+ 0x1fba1 => cornerDiagonalLines(metrics, canvas, .{ .tr = true }),
+ // '🮢'
+ 0x1fba2 => cornerDiagonalLines(metrics, canvas, .{ .bl = true }),
+ // '🮣'
+ 0x1fba3 => cornerDiagonalLines(metrics, canvas, .{ .br = true }),
+ // '🮤'
+ 0x1fba4 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true }),
+ // '🮥'
+ 0x1fba5 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .br = true }),
+ // '🮦'
+ 0x1fba6 => cornerDiagonalLines(metrics, canvas, .{ .bl = true, .br = true }),
+ // '🮧'
+ 0x1fba7 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true }),
+ // '🮨'
+ 0x1fba8 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .br = true }),
+ // '🮩'
+ 0x1fba9 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true }),
+ // '🮪'
+ 0x1fbaa => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true, .br = true }),
+ // '🮫'
+ 0x1fbab => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true, .br = true }),
+ // '🮬'
+ 0x1fbac => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .br = true }),
+ // '🮭'
+ 0x1fbad => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }),
+ // '🮮'
+ 0x1fbae => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }),
+
+ else => unreachable,
+ }
+}
+
+/// 🮯
+pub fn draw1FBAF(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ box.linesChar(metrics, canvas, .{
+ .up = .heavy,
+ .down = .heavy,
+ .left = .light,
+ .right = .light,
+ });
+}
+
+/// 🮽
+pub fn draw1FBBD(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ box.lightDiagonalCross(metrics, canvas);
+ canvas.invert();
+ // 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;
+}
+
+/// 🮾
+pub fn draw1FBBE(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ cornerDiagonalLines(metrics, canvas, .{ .br = true });
+ canvas.invert();
+ // 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;
+}
+
+/// 🮿
+pub fn draw1FBBF(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ cornerDiagonalLines(metrics, canvas, .{
+ .tl = true,
+ .tr = true,
+ .bl = true,
+ .br = true,
+ });
+ canvas.invert();
+ // 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;
+}
+
+/// 🯎
+pub fn draw1FBCE(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ block.block(metrics, canvas, .left, two_thirds, 1);
+}
+
+// 🯏
+pub fn draw1FBCF(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = cp;
+ _ = width;
+ _ = height;
+
+ block.block(metrics, canvas, .left, one_third, 1);
+}
+
+/// Cell diagonals.
+pub fn draw1FBD0_1FBDF(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ switch (cp) {
+ // '🯐'
+ 0x1fbd0 => cellDiagonal(
+ metrics,
+ canvas,
+ .middle_right,
+ .lower_left,
+ ),
+ // '🯑'
+ 0x1fbd1 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_right,
+ .middle_left,
+ ),
+ // '🯒'
+ 0x1fbd2 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .middle_right,
+ ),
+ // '🯓'
+ 0x1fbd3 => cellDiagonal(
+ metrics,
+ canvas,
+ .middle_left,
+ .lower_right,
+ ),
+ // '🯔'
+ 0x1fbd4 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .lower_center,
+ ),
+ // '🯕'
+ 0x1fbd5 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_center,
+ .lower_right,
+ ),
+ // '🯖'
+ 0x1fbd6 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_right,
+ .lower_center,
+ ),
+ // '🯗'
+ 0x1fbd7 => cellDiagonal(
+ metrics,
+ canvas,
+ .upper_center,
+ .lower_left,
+ ),
+ // '🯘'
+ 0x1fbd8 => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .middle_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_center,
+ .upper_right,
+ );
+ },
+ // '🯙'
+ 0x1fbd9 => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_right,
+ .middle_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_center,
+ .lower_right,
+ );
+ },
+ // '🯚'
+ 0x1fbda => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .lower_left,
+ .middle_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_center,
+ .lower_right,
+ );
+ },
+ // '🯛'
+ 0x1fbdb => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .middle_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_center,
+ .lower_left,
+ );
+ },
+ // '🯜'
+ 0x1fbdc => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .lower_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .lower_center,
+ .upper_right,
+ );
+ },
+ // '🯝'
+ 0x1fbdd => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_right,
+ .middle_left,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_left,
+ .lower_right,
+ );
+ },
+ // '🯞'
+ 0x1fbde => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .lower_left,
+ .upper_center,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_center,
+ .lower_right,
+ );
+ },
+ // '🯟'
+ 0x1fbdf => {
+ cellDiagonal(
+ metrics,
+ canvas,
+ .upper_left,
+ .middle_right,
+ );
+ cellDiagonal(
+ metrics,
+ canvas,
+ .middle_right,
+ .lower_left,
+ );
+ },
+
+ else => unreachable,
+ }
+}
+
+pub fn draw1FBE0_1FBEF(
+ cp: u32,
+ canvas: *font.sprite.Canvas,
+ width: u32,
+ height: u32,
+ metrics: font.Metrics,
+) !void {
+ _ = width;
+ _ = height;
+
+ switch (cp) {
+ // '🯠'
+ 0x1fbe0 => circle(metrics, canvas, .top, false),
+ // '🯡'
+ 0x1fbe1 => circle(metrics, canvas, .right, false),
+ // '🯢'
+ 0x1fbe2 => circle(metrics, canvas, .bottom, false),
+ // '🯣'
+ 0x1fbe3 => circle(metrics, canvas, .left, false),
+ // '🯤'
+ 0x1fbe4 => block.block(metrics, canvas, .upper_center, 0.5, 0.5),
+ // '🯥'
+ 0x1fbe5 => block.block(metrics, canvas, .lower_center, 0.5, 0.5),
+ // '🯦'
+ 0x1fbe6 => block.block(metrics, canvas, .middle_left, 0.5, 0.5),
+ // '🯧'
+ 0x1fbe7 => block.block(metrics, canvas, .middle_right, 0.5, 0.5),
+ // '🯨'
+ 0x1fbe8 => circle(metrics, canvas, .top, true),
+ // '🯩'
+ 0x1fbe9 => circle(metrics, canvas, .right, true),
+ // '🯪'
+ 0x1fbea => circle(metrics, canvas, .bottom, true),
+ // '🯫'
+ 0x1fbeb => circle(metrics, canvas, .left, true),
+ // '🯬'
+ 0x1fbec => circle(metrics, canvas, .top_right, true),
+ // '🯭'
+ 0x1fbed => circle(metrics, canvas, .bottom_left, true),
+ // '🯮'
+ 0x1fbee => circle(metrics, canvas, .bottom_right, true),
+ // '🯯'
+ 0x1fbef => circle(metrics, canvas, .top_left, true),
+
+ else => unreachable,
+ }
+}
+
+fn edgeTriangle(
+ metrics: font.Metrics,
+ canvas: *font.sprite.Canvas,
+ comptime edge: Edge,
+) !void {
+ const upper: f64 = 0.0;
+ const middle: f64 = @round(@as(f64, @floatFromInt(metrics.cell_height)) / 2);
+ const lower: f64 = @floatFromInt(metrics.cell_height);
+ const left: f64 = 0.0;
+ const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2);
+ const right: f64 = @floatFromInt(metrics.cell_width);
+
+ const x0, const y0, const x1, const y1 = switch (edge) {
+ .top => .{ right, upper, left, upper },
+ .left => .{ left, upper, left, lower },
+ .bottom => .{ left, lower, right, lower },
+ .right => .{ right, lower, right, upper },
+ };
+
+ var path = canvas.staticPath(5); // nodes.len = 0
+ path.moveTo(center, middle); // +1, nodes.len = 1
+ path.lineTo(x0, y0); // +1, nodes.len = 2
+ path.lineTo(x1, y1); // +1, nodes.len = 3
+ path.close(); // +2, nodes.len = 5
+
+ try canvas.fillPath(path.wrapped_path, .{}, .on);
+}
+
+fn cornerDiagonalLines(
+ metrics: font.Metrics,
+ canvas: *font.sprite.Canvas,
+ comptime corners: Quads,
+) void {
+ const thick_px = Thickness.light.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 = @floatFromInt(metrics.cell_width / 2 + metrics.cell_width % 2);
+ const center_y: f64 = @floatFromInt(metrics.cell_height / 2 + metrics.cell_height % 2);
+
+ if (corners.tl) canvas.line(.{
+ .p0 = .{ .x = center_x, .y = 0 },
+ .p1 = .{ .x = 0, .y = center_y },
+ }, float_thick, .on) catch {};
+
+ if (corners.tr) canvas.line(.{
+ .p0 = .{ .x = center_x, .y = 0 },
+ .p1 = .{ .x = float_width, .y = center_y },
+ }, float_thick, .on) catch {};
+
+ if (corners.bl) canvas.line(.{
+ .p0 = .{ .x = center_x, .y = float_height },
+ .p1 = .{ .x = 0, .y = center_y },
+ }, float_thick, .on) catch {};
+
+ if (corners.br) canvas.line(.{
+ .p0 = .{ .x = center_x, .y = float_height },
+ .p1 = .{ .x = float_width, .y = center_y },
+ }, float_thick, .on) catch {};
+}
+
+fn cellDiagonal(
+ metrics: font.Metrics,
+ canvas: *font.sprite.Canvas,
+ comptime from: Alignment,
+ comptime to: Alignment,
+) void {
+ const float_width: f64 = @floatFromInt(metrics.cell_width);
+ const float_height: f64 = @floatFromInt(metrics.cell_height);
+
+ const x0: f64 = switch (from.horizontal) {
+ .left => 0,
+ .right => float_width,
+ .center => float_width / 2,
+ };
+ const y0: f64 = switch (from.vertical) {
+ .top => 0,
+ .bottom => float_height,
+ .middle => float_height / 2,
+ };
+ const x1: f64 = switch (to.horizontal) {
+ .left => 0,
+ .right => float_width,
+ .center => float_width / 2,
+ };
+ const y1: f64 = switch (to.vertical) {
+ .top => 0,
+ .bottom => float_height,
+ .middle => float_height / 2,
+ };
+
+ canvas.line(
+ .{
+ .p0 = .{ .x = x0, .y = y0 },
+ .p1 = .{ .x = x1, .y = y1 },
+ },
+ @floatFromInt(Thickness.light.height(metrics.box_thickness)),
+ .on,
+ ) catch {};
+}
+
+fn checkerboardFill(
+ metrics: font.Metrics,
+ canvas: *font.sprite.Canvas,
+ parity: u1,
+) void {
+ const float_width: f64 = @floatFromInt(metrics.cell_width);
+ const float_height: f64 = @floatFromInt(metrics.cell_height);
+ const x_size: usize = 4;
+ const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width)));
+ for (0..x_size) |x| {
+ const x0 = (metrics.cell_width * x) / x_size;
+ const x1 = (metrics.cell_width * (x + 1)) / x_size;
+ for (0..y_size) |y| {
+ const y0 = (metrics.cell_height * y) / y_size;
+ const y1 = (metrics.cell_height * (y + 1)) / y_size;
+ if ((x + y) % 2 == parity) {
+ canvas.rect(.{
+ .x = @intCast(x0),
+ .y = @intCast(y0),
+ .width = @intCast(x1 -| x0),
+ .height = @intCast(y1 -| y0),
+ }, .on);
+ }
+ }
+ }
+}
+
+pub fn circle(
+ metrics: font.Metrics,
+ canvas: *font.sprite.Canvas,
+ comptime position: Alignment,
+ comptime filled: bool,
+) void {
+ // 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;
+
+ const float_width: f64 = @floatFromInt(metrics.cell_width);
+ const float_height: f64 = @floatFromInt(metrics.cell_height);
+
+ const x: f64 = switch (position.horizontal) {
+ .left => 0,
+ .right => float_width,
+ .center => float_width / 2,
+ };
+ const y: f64 = switch (position.vertical) {
+ .top => 0,
+ .bottom => float_height,
+ .middle => float_height / 2,
+ };
+ const r: f64 = 0.5 * @min(float_width, float_height);
+
+ var ctx = canvas.getContext();
+ defer ctx.deinit();
+ ctx.setSource(.{ .opaque_pattern = .{
+ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
+ } });
+ ctx.setLineWidth(
+ @floatFromInt(Thickness.light.height(metrics.box_thickness)),
+ );
+
+ if (filled) {
+ ctx.arc(x, y, r, 0, std.math.pi * 2) catch return;
+ ctx.closePath() catch return;
+ ctx.fill() catch return;
+ } else {
+ ctx.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2) catch return;
+ ctx.closePath() catch return;
+ ctx.stroke() catch return;
+ }
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig
new file mode 100644
index 00000000..46c7165a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig
@@ -0,0 +1,633 @@
+//! 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 = @import("../../../quirks.zig").inlineAssert;
+
+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()) |raw_line| {
+ // Trim \r so this works with both LF and CRLF line endings,
+ // since git may convert octants.txt to CRLF on Windows checkouts.
+ const line = if (raw_line.len > 0 and raw_line[raw_line.len - 1] == '\r')
+ raw_line[0 .. raw_line.len - 1]
+ else
+ raw_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);
+}
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png
new file mode 100644
index 00000000..e04e7726
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png
new file mode 100644
index 00000000..ce1b1c42
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png
new file mode 100644
index 00000000..1c21f688
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png
new file mode 100644
index 00000000..459822e6
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png
new file mode 100644
index 00000000..8d7de36a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png
new file mode 100644
index 00000000..ab6bec96
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png
new file mode 100644
index 00000000..43035aef
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png
new file mode 100644
index 00000000..fc111e2d
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png
new file mode 100644
index 00000000..b3cda82d
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png
new file mode 100644
index 00000000..e076da7c
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png
new file mode 100644
index 00000000..366be386
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png
new file mode 100644
index 00000000..5cd7a4ef
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png
new file mode 100644
index 00000000..023396fe
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png
new file mode 100644
index 00000000..1a4dcb01
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png
new file mode 100644
index 00000000..73ead485
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png
new file mode 100644
index 00000000..b58ded46
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png
new file mode 100644
index 00000000..9ae5b45f
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png
new file mode 100644
index 00000000..89011df4
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png
new file mode 100644
index 00000000..05ff2a5f
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png
new file mode 100644
index 00000000..cf96b7d1
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png
new file mode 100644
index 00000000..804e3017
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png
new file mode 100644
index 00000000..aa3e2f99
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png
new file mode 100644
index 00000000..49f431a4
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png
new file mode 100644
index 00000000..4308c536
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png
new file mode 100644
index 00000000..addd1f49
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png
new file mode 100644
index 00000000..70ae8102
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png
new file mode 100644
index 00000000..32a42097
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png
new file mode 100644
index 00000000..46151697
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png
new file mode 100644
index 00000000..10385379
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png
new file mode 100644
index 00000000..b1bb803a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png
new file mode 100644
index 00000000..e7415b7a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png
new file mode 100644
index 00000000..730a4c42
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png
new file mode 100644
index 00000000..b216174e
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png
new file mode 100644
index 00000000..2243248b
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png
new file mode 100644
index 00000000..f3a887d2
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png
Binary files differ
diff --git a/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png
new file mode 100644
index 00000000..82ce8246
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png
Binary files differ