diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/sprite | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/sprite')
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 Binary files differnew file mode 100644 index 00000000..e04e7726 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..ce1b1c42 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..1c21f688 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..459822e6 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..8d7de36a --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..ab6bec96 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..43035aef --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..fc111e2d --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..b3cda82d --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..e076da7c --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..366be386 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..5cd7a4ef --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..023396fe --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..1a4dcb01 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..73ead485 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..b58ded46 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..9ae5b45f --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..89011df4 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..05ff2a5f --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..cf96b7d1 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..804e3017 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..aa3e2f99 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..49f431a4 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..4308c536 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..addd1f49 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..70ae8102 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..32a42097 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..46151697 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..10385379 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..b1bb803a --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..e7415b7a --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..730a4c42 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png 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 Binary files differnew file mode 100644 index 00000000..b216174e --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png 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 Binary files differnew file mode 100644 index 00000000..2243248b --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png 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 Binary files differnew file mode 100644 index 00000000..f3a887d2 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png 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 Binary files differnew file mode 100644 index 00000000..82ce8246 --- /dev/null +++ b/vendor/libghostty-vt/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png |
