From 1377e6d22595e78762b7a9887f5d04cba69cfdc9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 15:33:58 -0600 Subject: [PATCH 01/23] font/sprite: rework sprite font drawing This is a fairly large rework of how we handle the sprite font drawing. Drawing routines are now context-less, provided only a canvas and some metrics. There is now a separate file per unicode block / PUA area. Sprites are now drawn on canvases with an extra quarter-cell of padding on each edge, and automatically cropped when sent to the atlas, this allows sprites to extend past cell boundaries which makes it possible to have, for example, diagonal box drawing characters that connect across cell diagonals instead of being pinched in. Most of the sprites the code is just directly ported from the old code, but I've rewritten a handful. Moving forward, I'd like to rewrite more of these since the way they're currently written isn't ideal. This rework, in addition to improving the packing efficiency of sprites on the atlas, and allowing for out-of-cell drawing, will make it a lot easier to add new sprites in the future, since all it takes now is to add a single function and an import (if it's a new file). I reworked the regression/change testing to be more robust as well, it now covers all sprite glyphs (except non-codepoint ones) and does so at 4 different sizes. Addition/removal of glyphs will no longer create diff noise in the generated diff image, since the position in the image of each glyph is now fixed. --- src/font/Atlas.zig | 31 + src/font/sprite.zig | 6 - src/font/sprite/Box.zig | 3397 ----------------- src/font/sprite/Face.zig | 675 +++- src/font/sprite/Powerline.zig | 564 --- src/font/sprite/canvas.zig | 449 ++- src/font/sprite/cursor.zig | 65 - src/font/sprite/draw/README.md | 50 + src/font/sprite/draw/block.zig | 184 + src/font/sprite/draw/box.zig | 947 +++++ src/font/sprite/draw/braille.zig | 148 + src/font/sprite/draw/branch.zig | 505 +++ src/font/sprite/draw/common.zig | 244 ++ src/font/sprite/draw/geometric_shapes.zig | 200 + src/font/sprite/{ => draw}/octants.txt | 0 src/font/sprite/draw/powerline.zig | 396 ++ src/font/sprite/draw/special.zig | 328 ++ .../draw/symbols_for_legacy_computing.zig | 1431 +++++++ ...ymbols_for_legacy_computing_supplement.zig | 193 + src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 0 bytes .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 0 -> 403 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 0 -> 534 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 0 -> 1022 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 0 -> 316 bytes .../testdata/U+1CD00...U+1CDFF-11x21+2.png | Bin 0 -> 1275 bytes .../testdata/U+1CD00...U+1CDFF-12x24+3.png | Bin 0 -> 1870 bytes .../testdata/U+1CD00...U+1CDFF-18x36+4.png | Bin 0 -> 3404 bytes .../testdata/U+1CD00...U+1CDFF-9x17+1.png | Bin 0 -> 1101 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 0 -> 5450 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 0 -> 5724 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 0 -> 9997 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 0 -> 4298 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 0 -> 2223 bytes .../testdata/U+2500...U+25FF-12x24+3.png | Bin 0 -> 2638 bytes .../testdata/U+2500...U+25FF-18x36+4.png | Bin 0 -> 4541 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 0 -> 1848 bytes .../testdata/U+2800...U+28FF-11x21+2.png | Bin 0 -> 1022 bytes .../testdata/U+2800...U+28FF-12x24+3.png | Bin 0 -> 1547 bytes .../testdata/U+2800...U+28FF-18x36+4.png | Bin 0 -> 2490 bytes .../testdata/U+2800...U+28FF-9x17+1.png | Bin 0 -> 917 bytes .../testdata/U+E000...U+E0FF-11x21+2.png | Bin 0 -> 1104 bytes .../testdata/U+E000...U+E0FF-12x24+3.png | Bin 0 -> 1251 bytes .../testdata/U+E000...U+E0FF-18x36+4.png | Bin 0 -> 2228 bytes .../testdata/U+E000...U+E0FF-9x17+1.png | Bin 0 -> 895 bytes .../testdata/U+F500...U+F5FF-11x21+2.png | Bin 0 -> 1114 bytes .../testdata/U+F500...U+F5FF-12x24+3.png | Bin 0 -> 1423 bytes .../testdata/U+F500...U+F5FF-18x36+4.png | Bin 0 -> 2470 bytes .../testdata/U+F500...U+F5FF-9x17+1.png | Bin 0 -> 871 bytes .../testdata/U+F600...U+F6FF-11x21+2.png | Bin 0 -> 493 bytes .../testdata/U+F600...U+F6FF-12x24+3.png | Bin 0 -> 636 bytes .../testdata/U+F600...U+F6FF-18x36+4.png | Bin 0 -> 1218 bytes .../testdata/U+F600...U+F6FF-9x17+1.png | Bin 0 -> 394 bytes src/font/sprite/underline.zig | 312 -- typos.toml | 2 + 54 files changed, 5474 insertions(+), 4653 deletions(-) delete mode 100644 src/font/sprite/Box.zig delete mode 100644 src/font/sprite/Powerline.zig delete mode 100644 src/font/sprite/cursor.zig create mode 100644 src/font/sprite/draw/README.md create mode 100644 src/font/sprite/draw/block.zig create mode 100644 src/font/sprite/draw/box.zig create mode 100644 src/font/sprite/draw/braille.zig create mode 100644 src/font/sprite/draw/branch.zig create mode 100644 src/font/sprite/draw/common.zig create mode 100644 src/font/sprite/draw/geometric_shapes.zig rename src/font/sprite/{ => draw}/octants.txt (100%) create mode 100644 src/font/sprite/draw/powerline.zig create mode 100644 src/font/sprite/draw/special.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig delete mode 100644 src/font/sprite/testdata/Box.ppm create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png delete mode 100644 src/font/sprite/underline.zig diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 969318943..aac2e7e8d 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void { _ = self.modified.fetchAdd(1, .monotonic); } +/// Like `set` but allows specifying a width for the source data and an +/// offset x and y, so that a section of a larger buffer may be copied +/// in to the atlas. +pub fn setFromLarger( + self: *Atlas, + reg: Region, + src: []const u8, + src_width: u32, + src_x: u32, + src_y: u32, +) void { + assert(reg.x < (self.size - 1)); + assert((reg.x + reg.width) <= (self.size - 1)); + assert(reg.y < (self.size - 1)); + assert((reg.y + reg.height) <= (self.size - 1)); + + const depth = self.format.depth(); + var i: u32 = 0; + while (i < reg.height) : (i += 1) { + const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth; + const src_offset = (((src_y + i) * src_width) + src_x) * depth; + fastmem.copy( + u8, + self.data[tex_offset..], + src[src_offset .. src_offset + (reg.width * depth)], + ); + } + + _ = self.modified.fetchAdd(1, .monotonic); +} + // Grow the texture to the new size, preserving all previously written data. pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void { assert(size_new >= self.size); diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 6485d6008..4be06a918 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -33,12 +33,6 @@ pub const Sprite = enum(u32) { cursor_hollow_rect, cursor_bar, - // Note: we don't currently put the box drawing glyphs in here because - // there are a LOT and I'm lazy. What I want to do is spend more time - // studying the patterns to see if we can programmatically build our - // enum perhaps and comptime generate the drawing code at the same time. - // I'm not sure if that's advisable yet though. - test { const testing = std.testing; try testing.expectEqual(start, @intFromEnum(Sprite.underline)); diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig deleted file mode 100644 index f5140091d..000000000 --- a/src/font/sprite/Box.zig +++ /dev/null @@ -1,3397 +0,0 @@ -//! This file contains functions for drawing the box drawing characters -//! (https://en.wikipedia.org/wiki/Box-drawing_character) and related -//! characters that are provided by the terminal. -//! -//! The box drawing logic is based off similar logic in Kitty and Foot. -//! The primary drawing code was originally ported directly and slightly -//! modified from Foot (https://codeberg.org/dnkl/foot/). Foot is licensed -//! under the MIT license and is copyright 2019 Daniel Eklöf. -//! -//! The modifications made were primarily around spacing, DPI calculations, -//! and adapting the code to our atlas model. Further, more extensive changes -//! were made, refactoring the line characters to all share a single unified -//! function (draw_lines), as well as many of the fractional block characters -//! which now use draw_block instead of dedicated separate functions. -//! -//! Additional characters from Unicode 16.0 and beyond are original work. -const Box = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const z2d = @import("z2d"); - -const font = @import("../main.zig"); -const Sprite = @import("../sprite.zig").Sprite; - -const log = std.log.scoped(.box_font); - -/// Grid metrics for the rendering. -metrics: font.Metrics, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -/// Specification of a traditional intersection-style line/box-drawing char, -/// which can have a different style of line from each edge to the center. -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, - }; -}; - -/// Specification of a quadrants char, which has each of the -/// 4 quadrants of the character cell either filled or empty. -const Quads = packed struct(u4) { - tl: bool = false, - tr: bool = false, - bl: bool = false, - br: bool = false, -}; - -/// 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, -}; - -/// Alignment of a figure within a cell -const Alignment = struct { - horizontal: enum { - left, - right, - center, - } = .center, - - vertical: enum { - top, - bottom, - middle, - } = .middle, - - const upper: Alignment = .{ .vertical = .top }; - const lower: Alignment = .{ .vertical = .bottom }; - const left: Alignment = .{ .horizontal = .left }; - const right: Alignment = .{ .horizontal = .right }; - - const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; - const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; - const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; - const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; - - const center: Alignment = .{}; - - const upper_center = upper; - const lower_center = lower; - const middle_left = left; - const middle_right = right; - const middle_center: Alignment = center; - - const top = upper; - const bottom = lower; - const center_top = top; - const center_bottom = bottom; - - const top_left = upper_left; - const top_right = upper_right; - const bottom_left = lower_left; - const bottom_right = lower_right; -}; - -const Corner = enum(u2) { - tl, - tr, - bl, - br, -}; - -const Edge = enum(u2) { - top, - left, - bottom, - right, -}; - -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] != '#'), - }; - } -}; - -// Octant range, inclusive -const octant_min = 0x1cd00; -const octant_max = 0x1cde5; - -// 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; - -/// Shades -const Shade = enum(u8) { - off = 0x00, - light = 0x40, - medium = 0x80, - dark = 0xc0, - on = 0xff, - - _, -}; - -pub fn renderGlyph( - self: Box, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - const metrics = self.metrics; - - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init( - alloc, - metrics.cell_width, - metrics.cell_height, - ); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(metrics.cell_height)); - - return font.Glyph{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(metrics.cell_width), - }; -} - -fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - _ = alloc; - switch (cp) { - // '─' - 0x2500 => self.draw_lines(canvas, .{ .left = .light, .right = .light }), - // '━' - 0x2501 => self.draw_lines(canvas, .{ .left = .heavy, .right = .heavy }), - // '│' - 0x2502 => self.draw_lines(canvas, .{ .up = .light, .down = .light }), - // '┃' - 0x2503 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy }), - // '┄' - 0x2504 => self.draw_light_triple_dash_horizontal(canvas), - // '┅' - 0x2505 => self.draw_heavy_triple_dash_horizontal(canvas), - // '┆' - 0x2506 => self.draw_light_triple_dash_vertical(canvas), - // '┇' - 0x2507 => self.draw_heavy_triple_dash_vertical(canvas), - // '┈' - 0x2508 => self.draw_light_quadruple_dash_horizontal(canvas), - // '┉' - 0x2509 => self.draw_heavy_quadruple_dash_horizontal(canvas), - // '┊' - 0x250a => self.draw_light_quadruple_dash_vertical(canvas), - // '┋' - 0x250b => self.draw_heavy_quadruple_dash_vertical(canvas), - // '┌' - 0x250c => self.draw_lines(canvas, .{ .down = .light, .right = .light }), - // '┍' - 0x250d => self.draw_lines(canvas, .{ .down = .light, .right = .heavy }), - // '┎' - 0x250e => self.draw_lines(canvas, .{ .down = .heavy, .right = .light }), - // '┏' - 0x250f => self.draw_lines(canvas, .{ .down = .heavy, .right = .heavy }), - - // '┐' - 0x2510 => self.draw_lines(canvas, .{ .down = .light, .left = .light }), - // '┑' - 0x2511 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy }), - // '┒' - 0x2512 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light }), - // '┓' - 0x2513 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy }), - // '└' - 0x2514 => self.draw_lines(canvas, .{ .up = .light, .right = .light }), - // '┕' - 0x2515 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy }), - // '┖' - 0x2516 => self.draw_lines(canvas, .{ .up = .heavy, .right = .light }), - // '┗' - 0x2517 => self.draw_lines(canvas, .{ .up = .heavy, .right = .heavy }), - // '┘' - 0x2518 => self.draw_lines(canvas, .{ .up = .light, .left = .light }), - // '┙' - 0x2519 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy }), - // '┚' - 0x251a => self.draw_lines(canvas, .{ .up = .heavy, .left = .light }), - // '┛' - 0x251b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy }), - // '├' - 0x251c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .light }), - // '┝' - 0x251d => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .heavy }), - // '┞' - 0x251e => self.draw_lines(canvas, .{ .up = .heavy, .right = .light, .down = .light }), - // '┟' - 0x251f => self.draw_lines(canvas, .{ .down = .heavy, .right = .light, .up = .light }), - - // '┠' - 0x2520 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), - // '┡' - 0x2521 => self.draw_lines(canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), - // '┢' - 0x2522 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), - // '┣' - 0x2523 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), - // '┤' - 0x2524 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light }), - // '┥' - 0x2525 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy }), - // '┦' - 0x2526 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .down = .light }), - // '┧' - 0x2527 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .up = .light }), - // '┨' - 0x2528 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), - // '┩' - 0x2529 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), - // '┪' - 0x252a => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), - // '┫' - 0x252b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), - // '┬' - 0x252c => self.draw_lines(canvas, .{ .down = .light, .left = .light, .right = .light }), - // '┭' - 0x252d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .down = .light }), - // '┮' - 0x252e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .down = .light }), - // '┯' - 0x252f => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), - - // '┰' - 0x2530 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .right = .light }), - // '┱' - 0x2531 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), - // '┲' - 0x2532 => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), - // '┳' - 0x2533 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), - // '┴' - 0x2534 => self.draw_lines(canvas, .{ .up = .light, .left = .light, .right = .light }), - // '┵' - 0x2535 => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light }), - // '┶' - 0x2536 => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light }), - // '┷' - 0x2537 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), - // '┸' - 0x2538 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .right = .light }), - // '┹' - 0x2539 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), - // '┺' - 0x253a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), - // '┻' - 0x253b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), - // '┼' - 0x253c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), - // '┽' - 0x253d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), - // '┾' - 0x253e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), - // '┿' - 0x253f => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), - - // '╀' - 0x2540 => self.draw_lines(canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), - // '╁' - 0x2541 => self.draw_lines(canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), - // '╂' - 0x2542 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - // '╃' - 0x2543 => self.draw_lines(canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), - // '╄' - 0x2544 => self.draw_lines(canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), - // '╅' - 0x2545 => self.draw_lines(canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), - // '╆' - 0x2546 => self.draw_lines(canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), - // '╇' - 0x2547 => self.draw_lines(canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), - // '╈' - 0x2548 => self.draw_lines(canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╉' - 0x2549 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), - // '╊' - 0x254a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), - // '╋' - 0x254b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╌' - 0x254c => self.draw_light_double_dash_horizontal(canvas), - // '╍' - 0x254d => self.draw_heavy_double_dash_horizontal(canvas), - // '╎' - 0x254e => self.draw_light_double_dash_vertical(canvas), - // '╏' - 0x254f => self.draw_heavy_double_dash_vertical(canvas), - - // '═' - 0x2550 => self.draw_lines(canvas, .{ .left = .double, .right = .double }), - // '║' - 0x2551 => self.draw_lines(canvas, .{ .up = .double, .down = .double }), - // '╒' - 0x2552 => self.draw_lines(canvas, .{ .down = .light, .right = .double }), - // '╓' - 0x2553 => self.draw_lines(canvas, .{ .down = .double, .right = .light }), - // '╔' - 0x2554 => self.draw_lines(canvas, .{ .down = .double, .right = .double }), - // '╕' - 0x2555 => self.draw_lines(canvas, .{ .down = .light, .left = .double }), - // '╖' - 0x2556 => self.draw_lines(canvas, .{ .down = .double, .left = .light }), - // '╗' - 0x2557 => self.draw_lines(canvas, .{ .down = .double, .left = .double }), - // '╘' - 0x2558 => self.draw_lines(canvas, .{ .up = .light, .right = .double }), - // '╙' - 0x2559 => self.draw_lines(canvas, .{ .up = .double, .right = .light }), - // '╚' - 0x255a => self.draw_lines(canvas, .{ .up = .double, .right = .double }), - // '╛' - 0x255b => self.draw_lines(canvas, .{ .up = .light, .left = .double }), - // '╜' - 0x255c => self.draw_lines(canvas, .{ .up = .double, .left = .light }), - // '╝' - 0x255d => self.draw_lines(canvas, .{ .up = .double, .left = .double }), - // '╞' - 0x255e => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .double }), - // '╟' - 0x255f => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .light }), - - // '╠' - 0x2560 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .double }), - // '╡' - 0x2561 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double }), - // '╢' - 0x2562 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light }), - // '╣' - 0x2563 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double }), - // '╤' - 0x2564 => self.draw_lines(canvas, .{ .down = .light, .left = .double, .right = .double }), - // '╥' - 0x2565 => self.draw_lines(canvas, .{ .down = .double, .left = .light, .right = .light }), - // '╦' - 0x2566 => self.draw_lines(canvas, .{ .down = .double, .left = .double, .right = .double }), - // '╧' - 0x2567 => self.draw_lines(canvas, .{ .up = .light, .left = .double, .right = .double }), - // '╨' - 0x2568 => self.draw_lines(canvas, .{ .up = .double, .left = .light, .right = .light }), - // '╩' - 0x2569 => self.draw_lines(canvas, .{ .up = .double, .left = .double, .right = .double }), - // '╪' - 0x256a => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), - // '╫' - 0x256b => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), - // '╬' - 0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), - // '╭' - 0x256d => try self.draw_arc(canvas, .br, .light), - // '╮' - 0x256e => try self.draw_arc(canvas, .bl, .light), - // '╯' - 0x256f => try self.draw_arc(canvas, .tl, .light), - - // '╰' - 0x2570 => try self.draw_arc(canvas, .tr, .light), - // '╱' - 0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas), - // '╲' - 0x2572 => self.draw_light_diagonal_upper_left_to_lower_right(canvas), - // '╳' - 0x2573 => self.draw_light_diagonal_cross(canvas), - // '╴' - 0x2574 => self.draw_lines(canvas, .{ .left = .light }), - // '╵' - 0x2575 => self.draw_lines(canvas, .{ .up = .light }), - // '╶' - 0x2576 => self.draw_lines(canvas, .{ .right = .light }), - // '╷' - 0x2577 => self.draw_lines(canvas, .{ .down = .light }), - // '╸' - 0x2578 => self.draw_lines(canvas, .{ .left = .heavy }), - // '╹' - 0x2579 => self.draw_lines(canvas, .{ .up = .heavy }), - // '╺' - 0x257a => self.draw_lines(canvas, .{ .right = .heavy }), - // '╻' - 0x257b => self.draw_lines(canvas, .{ .down = .heavy }), - // '╼' - 0x257c => self.draw_lines(canvas, .{ .left = .light, .right = .heavy }), - // '╽' - 0x257d => self.draw_lines(canvas, .{ .up = .light, .down = .heavy }), - // '╾' - 0x257e => self.draw_lines(canvas, .{ .left = .heavy, .right = .light }), - // '╿' - 0x257f => self.draw_lines(canvas, .{ .up = .heavy, .down = .light }), - - // '▀' UPPER HALF BLOCK - 0x2580 => self.draw_block(canvas, .upper, 1, half), - // '▁' LOWER ONE EIGHTH BLOCK - 0x2581 => self.draw_block(canvas, .lower, 1, one_eighth), - // '▂' LOWER ONE QUARTER BLOCK - 0x2582 => self.draw_block(canvas, .lower, 1, one_quarter), - // '▃' LOWER THREE EIGHTHS BLOCK - 0x2583 => self.draw_block(canvas, .lower, 1, three_eighths), - // '▄' LOWER HALF BLOCK - 0x2584 => self.draw_block(canvas, .lower, 1, half), - // '▅' LOWER FIVE EIGHTHS BLOCK - 0x2585 => self.draw_block(canvas, .lower, 1, five_eighths), - // '▆' LOWER THREE QUARTERS BLOCK - 0x2586 => self.draw_block(canvas, .lower, 1, three_quarters), - // '▇' LOWER SEVEN EIGHTHS BLOCK - 0x2587 => self.draw_block(canvas, .lower, 1, seven_eighths), - // '█' FULL BLOCK - 0x2588 => self.draw_full_block(canvas), - // '▉' LEFT SEVEN EIGHTHS BLOCK - 0x2589 => self.draw_block(canvas, .left, seven_eighths, 1), - // '▊' LEFT THREE QUARTERS BLOCK - 0x258a => self.draw_block(canvas, .left, three_quarters, 1), - // '▋' LEFT FIVE EIGHTHS BLOCK - 0x258b => self.draw_block(canvas, .left, five_eighths, 1), - // '▌' LEFT HALF BLOCK - 0x258c => self.draw_block(canvas, .left, half, 1), - // '▍' LEFT THREE EIGHTHS BLOCK - 0x258d => self.draw_block(canvas, .left, three_eighths, 1), - // '▎' LEFT ONE QUARTER BLOCK - 0x258e => self.draw_block(canvas, .left, one_quarter, 1), - // '▏' LEFT ONE EIGHTH BLOCK - 0x258f => self.draw_block(canvas, .left, one_eighth, 1), - - // '▐' RIGHT HALF BLOCK - 0x2590 => self.draw_block(canvas, .right, half, 1), - // '░' - 0x2591 => self.draw_light_shade(canvas), - // '▒' - 0x2592 => self.draw_medium_shade(canvas), - // '▓' - 0x2593 => self.draw_dark_shade(canvas), - // '▔' UPPER ONE EIGHTH BLOCK - 0x2594 => self.draw_block(canvas, .upper, 1, one_eighth), - // '▕' RIGHT ONE EIGHTH BLOCK - 0x2595 => self.draw_block(canvas, .right, one_eighth, 1), - // '▖' - 0x2596 => self.draw_quadrant(canvas, .{ .bl = true }), - // '▗' - 0x2597 => self.draw_quadrant(canvas, .{ .br = true }), - // '▘' - 0x2598 => self.draw_quadrant(canvas, .{ .tl = true }), - // '▙' - 0x2599 => self.draw_quadrant(canvas, .{ .tl = true, .bl = true, .br = true }), - // '▚' - 0x259a => self.draw_quadrant(canvas, .{ .tl = true, .br = true }), - // '▛' - 0x259b => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '▜' - 0x259c => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .br = true }), - // '▝' - 0x259d => self.draw_quadrant(canvas, .{ .tr = true }), - // '▞' - 0x259e => self.draw_quadrant(canvas, .{ .tr = true, .bl = true }), - // '▟' - 0x259f => self.draw_quadrant(canvas, .{ .tr = true, .bl = true, .br = true }), - - // '◢' - 0x25e2 => self.draw_corner_triangle_shade(canvas, .br, .on), - // '◣' - 0x25e3 => self.draw_corner_triangle_shade(canvas, .bl, .on), - // '◤' - 0x25e4 => self.draw_corner_triangle_shade(canvas, .tl, .on), - // '◥' - 0x25e5 => self.draw_corner_triangle_shade(canvas, .tr, .on), - - // '◸' - 0x25f8 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // left edge - self.rect( - canvas, - 0, - 0, - thickness_px, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - // '◹' - 0x25f9 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 0, - self.metrics.cell_width, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◺' - 0x25fa => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // left edge - self.rect( - canvas, - 0, - 1, - thickness_px, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◿' - 0x25ff => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 1, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - - 0x2800...0x28ff => self.draw_braille(canvas, cp), - - 0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp), - - octant_min...octant_max => self.draw_octant(canvas, cp), - - // '🬼' - 0x1fb3c => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#.. - \\##. - )), - // '🬽' - 0x1fb3d => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#\. - \\### - )), - // '🬾' - 0x1fb3e => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\#\. - \\##. - )), - // '🬿' - 0x1fb3f => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\##. - \\### - )), - // '🭀' - 0x1fb40 => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\#.. - \\##. - \\##. - )), - - // '🭁' - 0x1fb41 => try self.draw_smooth_mosaic(canvas, .from( - \\/## - \\### - \\### - \\### - )), - // '🭂' - 0x1fb42 => try self.draw_smooth_mosaic(canvas, .from( - \\./# - \\### - \\### - \\### - )), - // '🭃' - 0x1fb43 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\### - \\### - )), - // '🭄' - 0x1fb44 => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\.## - \\### - \\### - )), - // '🭅' - 0x1fb45 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\.## - \\### - )), - // '🭆' - 0x1fb46 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\./# - \\### - \\### - )), - - // '🭇' - 0x1fb47 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\..# - \\.## - )), - // '🭈' - 0x1fb48 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\./# - \\### - )), - // '🭉' - 0x1fb49 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\./# - \\.## - )), - // '🭊' - 0x1fb4a => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\.## - \\### - )), - // '🭋' - 0x1fb4b => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\..# - \\.## - \\.## - )), - - // '🭌' - 0x1fb4c => try self.draw_smooth_mosaic(canvas, .from( - \\##\ - \\### - \\### - \\### - )), - // '🭍' - 0x1fb4d => try self.draw_smooth_mosaic(canvas, .from( - \\#\. - \\### - \\### - \\### - )), - // '🭎' - 0x1fb4e => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\### - \\### - )), - // '🭏' - 0x1fb4f => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\##. - \\### - \\### - )), - // '🭐' - 0x1fb50 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\##. - \\### - )), - // '🭑' - 0x1fb51 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#\. - \\### - \\### - )), - - // '🭒' - 0x1fb52 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\\## - )), - // '🭓' - 0x1fb53 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\.\# - )), - // '🭔' - 0x1fb54 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\.## - )), - // '🭕' - 0x1fb55 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\..# - )), - // '🭖' - 0x1fb56 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\.## - \\.## - )), - - // '🭗' - 0x1fb57 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#.. - \\... - \\... - )), - // '🭘' - 0x1fb58 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\#/. - \\... - \\... - )), - // '🭙' - 0x1fb59 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#/. - \\#.. - \\... - )), - // '🭚' - 0x1fb5a => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\#.. - \\... - )), - // '🭛' - 0x1fb5b => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\#.. - \\#.. - )), - - // '🭜' - 0x1fb5c => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\#/. - \\... - )), - // '🭝' - 0x1fb5d => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\##/ - )), - // '🭞' - 0x1fb5e => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\#/. - )), - // '🭟' - 0x1fb5f => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\##. - )), - // '🭠' - 0x1fb60 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\#.. - )), - // '🭡' - 0x1fb61 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\##. - \\##. - )), - - // '🭢' - 0x1fb62 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\..# - \\... - \\... - )), - // '🭣' - 0x1fb63 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.\# - \\... - \\... - )), - // '🭤' - 0x1fb64 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.\# - \\..# - \\... - )), - // '🭥' - 0x1fb65 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\..# - \\... - )), - // '🭦' - 0x1fb66 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\..# - \\..# - )), - // '🭧' - 0x1fb67 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.\# - \\... - )), - - // '🭨' - 0x1fb68 => { - try self.draw_edge_triangle(canvas, .left); - canvas.invert(); - }, - // '🭩' - 0x1fb69 => { - try self.draw_edge_triangle(canvas, .top); - canvas.invert(); - }, - // '🭪' - 0x1fb6a => { - try self.draw_edge_triangle(canvas, .right); - canvas.invert(); - }, - // '🭫' - 0x1fb6b => { - try self.draw_edge_triangle(canvas, .bottom); - canvas.invert(); - }, - // '🭬' - 0x1fb6c => try self.draw_edge_triangle(canvas, .left), - // '🭭' - 0x1fb6d => try self.draw_edge_triangle(canvas, .top), - // '🭮' - 0x1fb6e => try self.draw_edge_triangle(canvas, .right), - // '🭯' - 0x1fb6f => try self.draw_edge_triangle(canvas, .bottom), - - // '🭰' - 0x1fb70 => self.draw_vertical_one_eighth_block_n(canvas, 1), - // '🭱' - 0x1fb71 => self.draw_vertical_one_eighth_block_n(canvas, 2), - // '🭲' - 0x1fb72 => self.draw_vertical_one_eighth_block_n(canvas, 3), - // '🭳' - 0x1fb73 => self.draw_vertical_one_eighth_block_n(canvas, 4), - // '🭴' - 0x1fb74 => self.draw_vertical_one_eighth_block_n(canvas, 5), - // '🭵' - 0x1fb75 => self.draw_vertical_one_eighth_block_n(canvas, 6), - - // '🭶' - 0x1fb76 => self.draw_horizontal_one_eighth_block_n(canvas, 1), - // '🭷' - 0x1fb77 => self.draw_horizontal_one_eighth_block_n(canvas, 2), - // '🭸' - 0x1fb78 => self.draw_horizontal_one_eighth_block_n(canvas, 3), - // '🭹' - 0x1fb79 => self.draw_horizontal_one_eighth_block_n(canvas, 4), - // '🭺' - 0x1fb7a => self.draw_horizontal_one_eighth_block_n(canvas, 5), - // '🭻' - 0x1fb7b => self.draw_horizontal_one_eighth_block_n(canvas, 6), - - // '🮂' UPPER ONE QUARTER BLOCK - 0x1fb82 => self.draw_block(canvas, .upper, 1, one_quarter), - // '🮃' UPPER THREE EIGHTHS BLOCK - 0x1fb83 => self.draw_block(canvas, .upper, 1, three_eighths), - // '🮄' UPPER FIVE EIGHTHS BLOCK - 0x1fb84 => self.draw_block(canvas, .upper, 1, five_eighths), - // '🮅' UPPER THREE QUARTERS BLOCK - 0x1fb85 => self.draw_block(canvas, .upper, 1, three_quarters), - // '🮆' UPPER SEVEN EIGHTHS BLOCK - 0x1fb86 => self.draw_block(canvas, .upper, 1, seven_eighths), - - // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK - 0x1fb7c => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK - 0x1fb7d => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK - 0x1fb7e => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK - 0x1fb7f => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK - 0x1fb80 => { - self.draw_block(canvas, .upper, 1, one_eighth); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮁' - 0x1fb81 => self.draw_horizontal_one_eighth_1358_block(canvas), - - // '🮇' RIGHT ONE QUARTER BLOCK - 0x1fb87 => self.draw_block(canvas, .right, one_quarter, 1), - // '🮈' RIGHT THREE EIGHTHS BLOCK - 0x1fb88 => self.draw_block(canvas, .right, three_eighths, 1), - // '🮉' RIGHT FIVE EIGHTHS BLOCK - 0x1fb89 => self.draw_block(canvas, .right, five_eighths, 1), - // '🮊' RIGHT THREE QUARTERS BLOCK - 0x1fb8a => self.draw_block(canvas, .right, three_quarters, 1), - // '🮋' RIGHT SEVEN EIGHTHS BLOCK - 0x1fb8b => self.draw_block(canvas, .right, seven_eighths, 1), - // '🮌' - 0x1fb8c => self.draw_block_shade(canvas, .left, half, 1, .medium), - // '🮍' - 0x1fb8d => self.draw_block_shade(canvas, .right, half, 1, .medium), - // '🮎' - 0x1fb8e => self.draw_block_shade(canvas, .upper, 1, half, .medium), - // '🮏' - 0x1fb8f => self.draw_block_shade(canvas, .lower, 1, half, .medium), - - // '🮐' - 0x1fb90 => self.draw_medium_shade(canvas), - // '🮑' - 0x1fb91 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .upper, 1, half); - }, - // '🮒' - 0x1fb92 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .lower, 1, half); - }, - // '🮔' - 0x1fb94 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .right, half, 1); - }, - // '🮕' - 0x1fb95 => self.draw_checkerboard_fill(canvas, 0), - // '🮖' - 0x1fb96 => self.draw_checkerboard_fill(canvas, 1), - // '🮗' - 0x1fb97 => { - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 3); - self.draw_horizontal_one_eighth_block_n(canvas, 6); - self.draw_horizontal_one_eighth_block_n(canvas, 7); - }, - // '🮘' - 0x1fb98 => self.draw_upper_left_to_lower_right_fill(canvas), - // '🮙' - 0x1fb99 => self.draw_upper_right_to_lower_left_fill(canvas), - // '🮚' - 0x1fb9a => { - try self.draw_edge_triangle(canvas, .top); - try self.draw_edge_triangle(canvas, .bottom); - }, - // '🮛' - 0x1fb9b => { - try self.draw_edge_triangle(canvas, .left); - try self.draw_edge_triangle(canvas, .right); - }, - // '🮜' - 0x1fb9c => self.draw_corner_triangle_shade(canvas, .tl, .medium), - // '🮝' - 0x1fb9d => self.draw_corner_triangle_shade(canvas, .tr, .medium), - // '🮞' - 0x1fb9e => self.draw_corner_triangle_shade(canvas, .br, .medium), - // '🮟' - 0x1fb9f => self.draw_corner_triangle_shade(canvas, .bl, .medium), - - // '🮠' - 0x1fba0 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true }), - // '🮡' - 0x1fba1 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true }), - // '🮢' - 0x1fba2 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true }), - // '🮣' - 0x1fba3 => self.draw_corner_diagonal_lines(canvas, .{ .br = true }), - // '🮤' - 0x1fba4 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true }), - // '🮥' - 0x1fba5 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .br = true }), - // '🮦' - 0x1fba6 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true, .br = true }), - // '🮧' - 0x1fba7 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true }), - // '🮨' - 0x1fba8 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .br = true }), - // '🮩' - 0x1fba9 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true }), - // '🮪' - 0x1fbaa => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true, .br = true }), - // '🮫' - 0x1fbab => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true, .br = true }), - // '🮬' - 0x1fbac => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .br = true }), - // '🮭' - 0x1fbad => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '🮮' - 0x1fbae => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }), - // '🮯' - 0x1fbaf => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - - // '🮽' - 0x1fbbd => { - self.draw_light_diagonal_cross(canvas); - canvas.invert(); - }, - // '🮾' - 0x1fbbe => { - self.draw_corner_diagonal_lines(canvas, .{ .br = true }); - canvas.invert(); - }, - // '🮿' - 0x1fbbf => { - self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }); - canvas.invert(); - }, - - // '🯎' - 0x1fbce => self.draw_block(canvas, .left, two_thirds, 1), - // '🯏' - 0x1fbcf => self.draw_block(canvas, .left, one_third, 1), - // '🯐' - 0x1fbd0 => self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ), - // '🯑' - 0x1fbd1 => self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ), - // '🯒' - 0x1fbd2 => self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ), - // '🯓' - 0x1fbd3 => self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ), - // '🯔' - 0x1fbd4 => self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ), - // '🯕' - 0x1fbd5 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ), - // '🯖' - 0x1fbd6 => self.draw_cell_diagonal( - canvas, - .upper_right, - .lower_center, - ), - // '🯗' - 0x1fbd7 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_left, - ), - // '🯘' - 0x1fbd8 => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .upper_right, - ); - }, - // '🯙' - 0x1fbd9 => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯚' - 0x1fbda => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯛' - 0x1fbdb => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_left, - ); - }, - // '🯜' - 0x1fbdc => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ); - self.draw_cell_diagonal( - canvas, - .lower_center, - .upper_right, - ); - }, - // '🯝' - 0x1fbdd => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ); - self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ); - }, - // '🯞' - 0x1fbde => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_center, - ); - self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ); - }, - // '🯟' - 0x1fbdf => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ); - self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ); - }, - - // '🯠' - 0x1fbe0 => self.draw_circle(canvas, .top, false), - // '🯡' - 0x1fbe1 => self.draw_circle(canvas, .right, false), - // '🯢' - 0x1fbe2 => self.draw_circle(canvas, .bottom, false), - // '🯣' - 0x1fbe3 => self.draw_circle(canvas, .left, false), - // '🯤' - 0x1fbe4 => self.draw_block(canvas, .upper_center, 0.5, 0.5), - // '🯥' - 0x1fbe5 => self.draw_block(canvas, .lower_center, 0.5, 0.5), - // '🯦' - 0x1fbe6 => self.draw_block(canvas, .middle_left, 0.5, 0.5), - // '🯧' - 0x1fbe7 => self.draw_block(canvas, .middle_right, 0.5, 0.5), - // '🯨' - 0x1fbe8 => self.draw_circle(canvas, .top, true), - // '🯩' - 0x1fbe9 => self.draw_circle(canvas, .right, true), - // '🯪' - 0x1fbea => self.draw_circle(canvas, .bottom, true), - // '🯫' - 0x1fbeb => self.draw_circle(canvas, .left, true), - // '🯬' - 0x1fbec => self.draw_circle(canvas, .top_right, true), - // '🯭' - 0x1fbed => self.draw_circle(canvas, .bottom_left, true), - // '🯮' - 0x1fbee => self.draw_circle(canvas, .bottom_right, true), - // '🯯' - 0x1fbef => self.draw_circle(canvas, .top_left, true), - - // (Below:) - // 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. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - - // '' - 0x0f5d0 => self.hline_middle(canvas, .light), - // '' - 0x0f5d1 => self.vline_middle(canvas, .light), - // '' - 0x0f5d2 => self.draw_fading_line(canvas, .right, .light), - // '' - 0x0f5d3 => self.draw_fading_line(canvas, .left, .light), - // '' - 0x0f5d4 => self.draw_fading_line(canvas, .bottom, .light), - // '' - 0x0f5d5 => self.draw_fading_line(canvas, .top, .light), - // '' - 0x0f5d6 => try self.draw_arc(canvas, .br, .light), - // '' - 0x0f5d7 => try self.draw_arc(canvas, .bl, .light), - // '' - 0x0f5d8 => try self.draw_arc(canvas, .tr, .light), - // '' - 0x0f5d9 => try self.draw_arc(canvas, .tl, .light), - // '' - 0x0f5da => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5db => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dc => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dd => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5de => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5df => { - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .bl, .light); - }, - - // '' - 0x0f5e0 => { - try self.draw_arc(canvas, .bl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e1 => { - try self.draw_arc(canvas, .br, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e2 => { - try self.draw_arc(canvas, .br, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5e3 => { - try self.draw_arc(canvas, .tl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e4 => { - try self.draw_arc(canvas, .tr, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e5 => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e6 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5e7 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5e8 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e9 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ea => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5eb => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ec => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ed => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ee => self.draw_branch_node(canvas, .{ .filled = true }, .light), - // '' - 0x0f5ef => self.draw_branch_node(canvas, .{}, .light), - - // '' - 0x0f5f0 => self.draw_branch_node(canvas, .{ - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f1 => self.draw_branch_node(canvas, .{ - .right = true, - }, .light), - // '' - 0x0f5f2 => self.draw_branch_node(canvas, .{ - .left = true, - .filled = true, - }, .light), - // '' - 0x0f5f3 => self.draw_branch_node(canvas, .{ - .left = true, - }, .light), - // '' - 0x0f5f4 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f5 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - }, .light), - // '' - 0x0f5f6 => self.draw_branch_node(canvas, .{ - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5f7 => self.draw_branch_node(canvas, .{ - .down = true, - }, .light), - // '' - 0x0f5f8 => self.draw_branch_node(canvas, .{ - .up = true, - .filled = true, - }, .light), - // '' - 0x0f5f9 => self.draw_branch_node(canvas, .{ - .up = true, - }, .light), - // '' - 0x0f5fa => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fb => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - }, .light), - // '' - 0x0f5fc => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fd => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - }, .light), - // '' - 0x0f5fe => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5ff => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - }, .light), - - // '' - 0x0f600 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f601 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - }, .light), - // '' - 0x0f602 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f603 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - }, .light), - // '' - 0x0f604 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f605 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - }, .light), - // '' - 0x0f606 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f607 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - }, .light), - // '' - 0x0f608 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f609 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60a => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60b => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60c => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60d => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - }, .light), - - // '𜰡' - SEPARATED BLOCK QUADRANT-1 - 0x1cc21 => try self.draw_separated_block_quadrant(canvas, "1"), - // '𜰢' - SEPARATED BLOCK QUADRANT-2 - 0x1cc22 => try self.draw_separated_block_quadrant(canvas, "2"), - // '𜰣' - SEPARATED BLOCK QUADRANT-12 - 0x1cc23 => try self.draw_separated_block_quadrant(canvas, "12"), - // '𜰤' - SEPARATED BLOCK QUADRANT-3 - 0x1cc24 => try self.draw_separated_block_quadrant(canvas, "3"), - // '𜰥' - SEPARATED BLOCK QUADRANT-13 - 0x1cc25 => try self.draw_separated_block_quadrant(canvas, "13"), - // '𜰦' - SEPARATED BLOCK QUADRANT-23 - 0x1cc26 => try self.draw_separated_block_quadrant(canvas, "23"), - // '𜰧' - SEPARATED BLOCK QUADRANT-123 - 0x1cc27 => try self.draw_separated_block_quadrant(canvas, "123"), - // '𜰨' - SEPARATED BLOCK QUADRANT-4 - 0x1cc28 => try self.draw_separated_block_quadrant(canvas, "4"), - // '𜰩' - SEPARATED BLOCK QUADRANT-14 - 0x1cc29 => try self.draw_separated_block_quadrant(canvas, "14"), - // '𜰪' - SEPARATED BLOCK QUADRANT-24 - 0x1cc2a => try self.draw_separated_block_quadrant(canvas, "24"), - // '𜰫' - SEPARATED BLOCK QUADRANT-124 - 0x1cc2b => try self.draw_separated_block_quadrant(canvas, "124"), - // '𜰬' - SEPARATED BLOCK QUADRANT-34 - 0x1cc2c => try self.draw_separated_block_quadrant(canvas, "34"), - // '𜰭' - SEPARATED BLOCK QUADRANT-134 - 0x1cc2d => try self.draw_separated_block_quadrant(canvas, "134"), - // '𜰮' - SEPARATED BLOCK QUADRANT-234 - 0x1cc2e => try self.draw_separated_block_quadrant(canvas, "234"), - // '𜰯' - SEPARATED BLOCK QUADRANT-1234 - 0x1cc2f => try self.draw_separated_block_quadrant(canvas, "1234"), - - else => return error.InvalidCodepoint, - } -} - -fn draw_lines( - self: Box, - canvas: *font.sprite.Canvas, - lines: Lines, -) void { - const light_px = Thickness.light.height(self.metrics.box_thickness); - const heavy_px = Thickness.heavy.height(self.metrics.box_thickness); - - // Top of light horizontal strokes - const h_light_top = (self.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 = (self.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 = (self.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 = (self.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 => self.rect(canvas, v_light_left, 0, v_light_right, up_bottom), - .heavy => self.rect(canvas, v_heavy_left, 0, v_heavy_right, up_bottom), - .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; - - self.rect(canvas, v_double_left, 0, v_light_left, left_bottom); - self.rect(canvas, v_light_right, 0, v_double_right, right_bottom); - }, - } - - switch (lines.right) { - .none => {}, - .light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom), - .heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom), - .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; - - self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top); - self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom); - }, - } - - switch (lines.down) { - .none => {}, - .light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height), - .heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height), - .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; - - self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height); - self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height); - }, - } - - switch (lines.left) { - .none => {}, - .light => self.rect(canvas, 0, h_light_top, left_right, h_light_bottom), - .heavy => self.rect(canvas, 0, h_heavy_top, left_right, h_heavy_bottom), - .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; - - self.rect(canvas, 0, h_double_top, top_right, h_light_top); - self.rect(canvas, 0, h_light_bottom, bottom_right, h_double_bottom); - }, - } -} - -fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.light.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 }, - .p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = @floatFromInt(self.metrics.cell_width), - .y = @floatFromInt(self.metrics.cell_height), - }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_light_diagonal_upper_right_to_lower_left(canvas); - self.draw_light_diagonal_upper_left_to_lower_right(canvas); -} - -fn draw_block( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, -) void { - self.draw_block_shade(canvas, alignment, width, height, .on); -} - -fn draw_block_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, - comptime shade: Shade, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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 => self.metrics.cell_width - w, - .center => (self.metrics.cell_width - w) / 2, - }; - const y = switch (alignment.vertical) { - .top => 0, - .bottom => self.metrics.cell_height - h, - .middle => (self.metrics.cell_height - h) / 2, - }; - - canvas.rect(.{ - .x = x, - .y = y, - .width = w, - .height = h, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); -} - -fn draw_corner_triangle_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime shade: Shade, -) void { - const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) { - .tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height }, - .br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - }; - - canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(x0), .y = @floatFromInt(y0) }, - .p1 = .{ .x = @floatFromInt(x1), .y = @floatFromInt(y1) }, - .p2 = .{ .x = @floatFromInt(x2), .y = @floatFromInt(y2) }, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))) catch {}; -} - -fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void { - self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - self.rect(canvas, x, 0, x + w, self.metrics.cell_height); -} - -fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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 = (self.metrics.cell_width * x) / x_size; - const x1 = (self.metrics.cell_width * (x + 1)) / x_size; - for (0..y_size) |y| { - const y0 = (self.metrics.cell_height * y) / y_size; - const y1 = (self.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); - } - } - } -} - -fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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 {}; - } -} - -fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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 {}; - } -} - -fn draw_corner_diagonal_lines( - self: Box, - canvas: *font.sprite.Canvas, - comptime corners: Quads, -) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2); - const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.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 draw_cell_diagonal( - self: Box, - canvas: *font.sprite.Canvas, - comptime from: Alignment, - comptime to: Alignment, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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, - }; - - self.draw_line( - canvas, - .{ .x = x0, .y = y0 }, - .{ .x = x1, .y = y1 }, - .light, - ) catch {}; -} - -fn draw_fading_line( - self: Box, - canvas: *font.sprite.Canvas, - comptime to: Edge, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.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..self.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..self.metrics.cell_width) |x| { - for (h_top..h_bottom) |y| { - canvas.pixel( - @intCast(x), - @intCast(y), - @enumFromInt(@as(u8, @intFromFloat(@round(color)))), - ); - } - color += inc; - } - }, - } -} - -fn draw_branch_node( - self: Box, - canvas: *font.sprite.Canvas, - node: BranchNode, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.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) - self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r))); - if (node.right) - self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom); - if (node.down) - self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height); - if (node.left) - self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom); - - 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 draw_circle( - self: Box, - canvas: *font.sprite.Canvas, - comptime position: Alignment, - comptime filled: bool, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.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(self.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; - } -} - -fn draw_line( - self: Box, - canvas: *font.sprite.Canvas, - p0: font.sprite.Point(f64), - p1: font.sprite.Point(f64), - comptime thickness: Thickness, -) !void { - canvas.line( - .{ .p0 = p0, .p1 = p1 }, - @floatFromInt(thickness.height(self.metrics.box_thickness)), - .on, - ) catch {}; -} - -fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void { - canvas.rect((font.sprite.Box(u32){ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = self.metrics.cell_width, - .y = self.metrics.cell_height, - }, - }).rect(), @as(font.sprite.Color, @enumFromInt(v))); -} - -fn draw_light_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x40); -} - -fn draw_medium_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x80); -} - -fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0xc0); -} - -fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8))); - const y = @min( - self.metrics.cell_height -| h, - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))), - ); - self.rect(canvas, 0, y, self.metrics.cell_width, y + h); -} - -fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_horizontal_one_eighth_block_n(canvas, 0); - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 4); - self.draw_horizontal_one_eighth_block_n(canvas, 7); -} - -fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void { - const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2; - const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2; - - if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y); - if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y); - if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height); - if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8); - var x_spacing: u32 = self.metrics.cell_width / 4; - var y_spacing: u32 = self.metrics.cell_height / 8; - var x_margin: u32 = x_spacing / 2; - var y_margin: u32 = y_spacing / 2; - - var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w; - var y_px_left: u32 = self.metrics.cell_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 <= self.metrics.cell_width); - assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height); - - const x = [2]u32{ x_margin, x_margin + w + x_spacing }; - const y = y: { - var y: [4]u32 = 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 sym = cp - 0x2800; - - // Left side - if (sym & 1 > 0) - self.rect(canvas, x[0], y[0], x[0] + w, y[0] + w); - if (sym & 2 > 0) - self.rect(canvas, x[0], y[1], x[0] + w, y[1] + w); - if (sym & 4 > 0) - self.rect(canvas, x[0], y[2], x[0] + w, y[2] + w); - - // Right side - if (sym & 8 > 0) - self.rect(canvas, x[1], y[0], x[1] + w, y[0] + w); - if (sym & 16 > 0) - self.rect(canvas, x[1], y[1], x[1] + w, y[1] + w); - if (sym & 32 > 0) - self.rect(canvas, x[1], y[2], x[1] + w, y[2] + w); - - // 8-dot patterns - if (sym & 64 > 0) - self.rect(canvas, x[0], y[3], x[0] + w, y[3] + w); - if (sym & 128 > 0) - self.rect(canvas, x[1], y[3], x[1] + w, y[3] + w); -} - -fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - 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, - ))); - - const x_halfs = self.xHalfs(); - const y_thirds = self.yThirds(); - - if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]); - if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]); - if (sex.ml) self.rect(canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); - if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, y_thirds[2]); - if (sex.bl) self.rect(canvas, 0, y_thirds[3], x_halfs[0], self.metrics.cell_height); - if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[3], self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - assert(cp >= octant_min and cp <= octant_max); - - // 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 this is - // static data that is embedded in the binary. - const octants_len = octant_max - octant_min + 1; - const octants: [octants_len]Octant = comptime octants: { - @setEvalBranchQuota(10_000); - - var result: [octants_len]Octant = @splat(.{}); - var i: usize = 0; - - const data = @embedFile("octants.txt"); - var it = std.mem.splitScalar(u8, data, '\n'); - while (it.next()) |line| { - // Skip comments - if (line.len == 0 or line[0] == '#') continue; - - const current = &result[i]; - i += 1; - - // Octants are in the format "BLOCK OCTANT-1235". The numbers - // at the end are keys into our packed struct. Since we're - // at comptime we can metaprogram it all. - const idx = std.mem.indexOfScalar(u8, line, '-').?; - for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; - } - - assert(i == octants_len); - break :octants result; - }; - - const x_halfs = self.xHalfs(); - const y_quads = self.yQuads(); - const oct = octants[cp - octant_min]; - if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]); - if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]); - if (oct.@"3") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]); - if (oct.@"5") self.rect(canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); - if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[3], self.metrics.cell_width, y_quads[4]); - if (oct.@"7") self.rect(canvas, 0, y_quads[5], x_halfs[0], self.metrics.cell_height); - if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[5], self.metrics.cell_width, self.metrics.cell_height); -} - -/// xHalfs[0] should be used as the right edge of a left-aligned half. -/// xHalfs[1] should be used as the left edge of a right-aligned half. -fn xHalfs(self: Box) [2]u32 { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); - return .{ half_width, self.metrics.cell_width - half_width }; -} - -/// Use these values as such: -/// yThirds[0] bottom edge of the first third. -/// yThirds[1] top edge of the second third. -/// yThirds[2] bottom edge of the second third. -/// yThirds[3] top edge of the final third. -fn yThirds(self: Box) [4]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); - const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); - return .{ - one_third_height, - self.metrics.cell_height - two_thirds_height, - two_thirds_height, - self.metrics.cell_height - one_third_height, - }; -} - -/// Use these values as such: -/// yQuads[0] bottom edge of first quarter. -/// yQuads[1] top edge of second quarter. -/// yQuads[2] bottom edge of second quarter. -/// yQuads[3] top edge of third quarter. -/// yQuads[4] bottom edge of third quarter -/// yQuads[5] top edge of fourth quarter. -fn yQuads(self: Box) [6]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); - const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); - const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); - return .{ - quarter_height, - self.metrics.cell_height - three_quarters_height, - half_height, - self.metrics.cell_height - half_height, - three_quarters_height, - self.metrics.cell_height - quarter_height, - }; -} - -fn draw_smooth_mosaic( - self: Box, - canvas: *font.sprite.Canvas, - mosaic: SmoothMosaic, -) !void { - const y_thirds = self.yThirds(); - const top: f64 = 0.0; - // We average the edge positions for the y_thirds boundaries here - // rather than having to deal with varying alignments depending on - // the surrounding pieces. The most this will be off by is half of - // a pixel, so hopefully it's not noticeable. - const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); - const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); - const bottom: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.metrics.cell_width); - - var path: z2d.StaticPath(12) = .{}; - path.init(); // 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 z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_edge_triangle( - self: Box, - canvas: *font.sprite.Canvas, - comptime edge: Edge, -) !void { - const upper: f64 = 0.0; - const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2); - const lower: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.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: z2d.StaticPath(5) = .{}; - path.init(); // 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 z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_arc( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime thickness: Thickness, -) !void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2; - const center_y: f64 = @as(f64, @floatFromInt((self.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 ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth(float_thick); - ctx.setLineCapMode(.round); - - switch (corner) { - .tl => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .tr => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - .bl => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .br => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - } - try ctx.stroke(); -} - -fn draw_dash_horizontal( - self: Box, - 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 (self.metrics.cell_width < count + gap_count) { - self.hline_middle(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 = @min(desired_gap, self.metrics.cell_width / (2 * count)); - const total_gap_width = gap_count * gap_width; - const total_dash_width = self.metrics.cell_width - total_gap_width; - const dash_width = total_dash_width / count; - const remaining = total_dash_width % count; - - assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width); - - // Our dashes should be centered vertically. - const y: u32 = (self.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: u32 = 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: u32 = 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; - } - self.hline(canvas, x, x1, y, thick_px); - // Advance by the width of the dash we drew and the width - // of a gap to get the the start of the next dash. - x = x1 + gap_width; - } -} - -fn draw_dash_vertical( - self: Box, - 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 (self.metrics.cell_height < count + gap_count) { - self.vline_middle(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 = @min(desired_gap, self.metrics.cell_height / (2 * count)); - const total_gap_height = gap_count * gap_height; - const total_dash_height = self.metrics.cell_height - total_gap_height; - const dash_height = total_dash_height / count; - const remaining = total_dash_height % count; - - assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height); - - // Our dashes should be centered horizontally. - const x: u32 = (self.metrics.cell_width -| thick_px) / 2; - - // We start at the top of the cell. - var y: u32 = 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: u32 = 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; - } - self.vline(canvas, y, y1, x, thick_px); - // Advance by the height of the dash we drew and the height - // of a gap to get the the start of the next dash. - y = y1 + gap_height; - } -} - -fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px); -} - -fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px); -} - -fn vline( - self: Box, - canvas: *font.sprite.Canvas, - y1: u32, - y2: u32, - x: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x + thickness_px, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn hline( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - x2: u32, - y: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y + thickness_px, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn rect( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - y1: u32, - x2: u32, - y2: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -// Separated Block Quadrants from Symbols for Legacy Computing Supplement -// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 -fn draw_separated_block_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime fmt: []const u8) !void { - comptime { - if (fmt.len > 4) @compileError("cannot have more than four quadrants"); - var seen = [_]bool{false} ** (std.math.maxInt(u8) + 1); - for (fmt) |c| { - if (seen[c]) @compileError("repeated quadrants not allowed"); - seen[c] = true; - switch (c) { - '1'...'4' => {}, - else => @compileError("invalid quadrant"), - } - } - } - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - const gap: f64 = @max(1.0, @as(f64, @floatFromInt(self.metrics.cell_width)) * 0.10) / 2.0; - const left: f64 = gap; - const right = @as(f64, @floatFromInt(self.metrics.cell_width)) - gap; - const top: f64 = gap; - const bottom = @as(f64, @floatFromInt(self.metrics.cell_height)) - gap; - const center_x = @as(f64, @floatFromInt(self.metrics.cell_width)) / 2.0; - const center_left = center_x - gap; - const center_right = center_x + gap; - const center_y = @as(f64, @floatFromInt(self.metrics.cell_height)) / 2.0; - const center_top = center_y - gap; - const center_bottom = center_y + gap; - - inline for (fmt) |c| { - const x1, const y1, const x2, const y2 = switch (c) { - '1' => .{ - left, top, - center_left, center_top, - }, - '2' => .{ - center_right, top, - right, center_top, - }, - '3' => .{ - left, center_bottom, - center_left, bottom, - }, - '4' => .{ - center_right, center_bottom, - right, bottom, - }, - else => unreachable, - }; - try ctx.moveTo(x1, y1); - try ctx.lineTo(x2, y1); - try ctx.lineTo(x2, y2); - try ctx.lineTo(x1, y2); - try ctx.closePath(); - } - - try ctx.fill(); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - var cp: u32 = 0x2500; - const end = 0x259f; - while (cp <= end) : (cp += 1) { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Box = .{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - }), - }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width); - try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height); - } -} - -fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { - // Box Drawing and Block Elements. - var cp: u32 = 0x2500; - while (cp <= 0x259f) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Braille - cp = 0x2800; - while (cp <= 0x28ff) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing. - cp = 0x1fb00; - while (cp <= 0x1fbef) : (cp += 1) { - switch (cp) { - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // 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. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - cp = 0xf5d0; - while (cp <= 0xf60d) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing Supplement: Quadrants - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - cp = 0x1cc21; - while (cp <= 0x1cc2f) : (cp += 1) { - switch (cp) { - 0x1cc21...0x1cc2f => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Symbols for Legacy Computing Supplement: Octants - cp = 0x1CD00; - while (cp <= 0x1CDE5) : (cp += 1) { - switch (cp) { - 0x1CD00...0x1CDE5 => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Geometric Shapes: filled and outlined corners - for ([_]u21{ '◢', '◣', '◤', '◥', '◸', '◹', '◺', '◿' }) |char| { - _ = try self.renderGlyph( - alloc, - atlas, - char, - ); - } -} - -test "render all sprites" { - // Renders all sprites to an atlas and compares - // it to a ground truth for regression testing. - - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // Even cell size and thickness (18 x 36) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - // Odd cell size and thickness (9 x 15) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 9.0, - .ascent = 12.0, - .descent = -3.0, - .line_gap = 0.0, - .underline_thickness = 1.0, - .strikethrough_thickness = 1.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - const ground_truth = @embedFile("./testdata/Box.ppm"); - - var stream = std.io.changeDetectionStream(ground_truth, std.io.null_writer); - try atlas_grayscale.dump(stream.writer()); - - if (stream.changeDetected()) { - log.err( - \\ - \\!! [Box.zig] Change detected from ground truth! - \\!! Dumping ./Box_test.ppm and ./Box_test_diff.ppm - \\!! Please check changes and update Box.ppm in testdata if intended. - , - .{}, - ); - - const ppm = try std.fs.cwd().createFile("Box_test.ppm", .{}); - defer ppm.close(); - try atlas_grayscale.dump(ppm.writer()); - - const diff = try std.fs.cwd().createFile("Box_test_diff.ppm", .{}); - defer diff.close(); - var writer = diff.writer(); - try writer.print( - \\P6 - \\{d} {d} - \\255 - \\ - , .{ atlas_grayscale.size, atlas_grayscale.size }); - for (ground_truth[try diff.getPos()..], atlas_grayscale.data) |a, b| { - if (a == b) { - try writer.writeByteNTimes(a / 3, 3); - } else { - try writer.writeByte(a); - try writer.writeByte(b); - try writer.writeByte(0); - } - } - } -} diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index af0c0af6a..25968e865 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -16,25 +16,154 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const wuffs = @import("wuffs"); +const z2d = @import("z2d"); const font = @import("../main.zig"); const Sprite = font.sprite.Sprite; -const Box = @import("Box.zig"); -const Powerline = @import("Powerline.zig"); -const underline = @import("underline.zig"); -const cursor = @import("cursor.zig"); + +const special = @import("draw/special.zig"); const log = std.log.scoped(.font_sprite); /// Grid metrics for rendering sprites. metrics: font.Metrics, +pub const DrawFnError = + Allocator.Error || + z2d.painter.FillError || + z2d.painter.StrokeError || + error{ + /// Something went wrong while doing math. + MathError, + }; + +/// A function that draws a glyph on the provided canvas. +pub const DrawFn = fn ( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) DrawFnError!void; + +const Range = struct { + min: u32, + max: u32, + draw: DrawFn, +}; + +/// Automatically collect ranges for functions with names +/// in the format `draw` or `draw_`. +const ranges = ranges: { + @setEvalBranchQuota(1_000_000); + + // Structs containing drawing functions for codepoint ranges. + const structs = [_]type{ + @import("draw/block.zig"), + @import("draw/box.zig"), + @import("draw/braille.zig"), + @import("draw/branch.zig"), + @import("draw/geometric_shapes.zig"), + @import("draw/powerline.zig"), + @import("draw/symbols_for_legacy_computing.zig"), + @import("draw/symbols_for_legacy_computing_supplement.zig"), + }; + + // Count how many draw fns we have + var range_count = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + range_count += 1; + } + } + + // Make an array and collect ranges for each function. + var r: [range_count]Range = undefined; + var names: [range_count][:0]const u8 = undefined; + var i = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + + const sep = std.mem.indexOfScalar(u8, decl.name, '_') orelse decl.name.len; + + const min = std.fmt.parseInt(u21, decl.name[4..sep], 16) catch unreachable; + + const max = if (sep == decl.name.len) + min + else + std.fmt.parseInt(u21, decl.name[sep + 1 ..], 16) catch unreachable; + + r[i] = .{ + .min = min, + .max = max, + .draw = @field(s, decl.name), + }; + names[i] = decl.name; + i += 1; + } + } + + // Sort ranges in ascending order + std.mem.sortUnstableContext(0, r.len, struct { + r: []Range, + names: [][:0]const u8, + pub fn lessThan(self: @This(), a: usize, b: usize) bool { + return self.r[a].min < self.r[b].min; + } + pub fn swap(self: @This(), a: usize, b: usize) void { + std.mem.swap(Range, &self.r[a], &self.r[b]); + std.mem.swap([:0]const u8, &self.names[a], &self.names[b]); + } + }{ + .r = &r, + .names = &names, + }); + + // Ensure there's no overlapping ranges + i = 0; + for (r, 0..) |n, k| { + if (n.min <= i) { + @compileError( + std.fmt.comptimePrint( + "Codepoint range for {s}(...) overlaps range for {s}(...), {X} <= {X} <= {X}", + .{ names[k], names[k - 1], r[k - 1].min, n.min, r[k - 1].max }, + ), + ); + } + i = n.max; + } + + break :ranges r; +}; + +fn getDrawFn(cp: u32) ?*const DrawFn { + // For special sprites (cursors, underlines, etc.) all sprites are drawn + // by functions from `Special` that share the name of the enum field. + if (cp >= Sprite.start) switch (@as(Sprite, @enumFromInt(cp))) { + inline else => |sprite| { + return @field(special, @tagName(sprite)); + }, + }; + + // Pray that the compiler is smart enough to + // turn this in to a jump table or something... + inline for (ranges) |range| { + if (cp >= range.min and cp <= range.max) return range.draw; + } + return null; +} + /// Returns true if the codepoint exists in our sprite font. pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool { - // We ignore presentation. No matter what presentation is requested - // we always provide glyphs for our codepoints. + // We ignore presentation. No matter what presentation is + // requested we always provide glyphs for our codepoints. _ = p; _ = self; - return Kind.init(cp) != null; + return getDrawFn(cp) != null; } /// Render the glyph. @@ -52,18 +181,10 @@ pub fn renderGlyph( } } - const metrics = self.metrics; - - // We adjust our sprite width based on the cell width. - const width = switch (opts.cell_width orelse 1) { - 0, 1 => metrics.cell_width, - else => |width| metrics.cell_width * width, - }; - // It should be impossible for this to be null and we assert that // in runtime safety modes but in case it is its not worth memory // corruption so we return a valid, blank glyph. - const kind = Kind.init(cp) orelse return .{ + const draw = getDrawFn(cp) orelse return .{ .width = 0, .height = 0, .offset_x = 0, @@ -73,217 +194,349 @@ pub fn renderGlyph( .advance_x = 0, }; - // Safe to ".?" because of the above assertion. - return switch (kind) { - .box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp), + const metrics = self.metrics; - .underline => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.underline_position, - metrics.underline_thickness, - ), + // We adjust our sprite width based on the cell width. + const width = switch (opts.cell_width orelse 1) { + 0, 1 => metrics.cell_width, + else => |width| metrics.cell_width * width, + }; - .strikethrough => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.strikethrough_position, - metrics.strikethrough_thickness, - ), + const height = metrics.cell_height; - .overline => overline: { - var g = try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - 0, - metrics.overline_thickness, - ); + const padding_x = width / 4; + const padding_y = height / 4; - // We have to manually subtract the overline position - // on the rendered glyph since it can be negative. - g.offset_y -= metrics.overline_position; + // Make a canvas of the desired size + var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y); + defer canvas.deinit(); - break :overline g; - }, + try draw(cp, &canvas, width, height, metrics); - .powerline => powerline: { - const f: Powerline = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.box_thickness, - }; + // Write the drawing to the atlas + const region = try canvas.writeAtlas(alloc, atlas); - break :powerline try f.renderGlyph(alloc, atlas, cp); - }, - - .cursor => cursor: { - var g = try cursor.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cursor_height, - metrics.cursor_thickness, - ); - - // Cursors are drawn at their specified height - // and are centered vertically within the cell. - const cursor_height: i32 = @intCast(metrics.cursor_height); - const cell_height: i32 = @intCast(metrics.cell_height); - g.offset_y += @divTrunc(cell_height - cursor_height, 2); - - break :cursor g; - }, + return font.Glyph{ + .width = region.width, + .height = region.height, + .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)), + .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = @floatFromInt(width), }; } -/// Kind of sprites we have. Drawing is implemented separately for each kind. -const Kind = enum { - box, - underline, - overline, - strikethrough, - powerline, - cursor, +/// Used in `testDrawRanges`, checks for diff between the provided atlas +/// and the reference file for the range, returns true if there is a diff. +fn testDiffAtlas( + alloc: Allocator, + atlas: *z2d.Surface, + path: []const u8, + i: u32, + width: u32, + height: u32, + thickness: u32, +) !bool { + // Get the file contents, we compare the PNG data first in + // order to ensure that no one smuggles arbitrary binary + // data in to the reference PNGs. + const test_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only }); + defer test_file.close(); + const test_bytes = try test_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(test_bytes); - pub fn init(cp: u32) ?Kind { - return switch (cp) { - Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) { - .underline, - .underline_double, - .underline_dotted, - .underline_dashed, - .underline_curly, - => .underline, + const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, "."); + defer alloc.free(cwd_absolute); - .overline, - => .overline, + // Get the reference file contents to compare. + const ref_path = try std.fmt.allocPrint( + alloc, + "./src/font/sprite/testdata/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(ref_path); + const ref_file = + std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| { + log.err("Can't open reference file {s}: {}\n", .{ + ref_path, + err, + }); - .strikethrough, - => .strikethrough, + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); - .cursor_rect, - .cursor_hollow_rect, - .cursor_bar, - => .cursor, - }, - - // == Box fonts == - - // "Box Drawing" block - // ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ - // ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ - // ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ - // ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ - 0x2500...0x257F, - - // "Block Elements" block - // ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ - 0x2580...0x259F, - - // "Geometric Shapes" block - 0x25e2...0x25e5, // ◢◣◤◥ - 0x25f8...0x25fa, // ◸◹◺ - 0x25ff, // ◿ - - // "Braille" block - 0x2800...0x28FF, - - // "Symbols for Legacy Computing" block - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - // (Octants) - 0x1CD00...0x1CDE5, - => .box, - - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - //           - //                     - //                     - //             - 0xF5D0...0xF60D => .box, - - // Separated Block Quadrants from Symbols for Legacy Computing Supplement - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - 0x1CC21...0x1CC2F => .box, - - // Powerline fonts - 0xE0B0, - 0xE0B1, - 0xE0B3, - 0xE0B4, - 0xE0B6, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0D2, - 0xE0D4, - => .powerline, - - else => null, + return true; }; + defer ref_file.close(); + const ref_bytes = try ref_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(ref_bytes); + + // Do our PNG bytes comparison, if it's the same then we can + // move on, otherwise we'll decode the reference file and do + // a pixel-for-pixel diff. + if (std.mem.eql(u8, test_bytes, ref_bytes)) return false; + + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); + + // Use wuffs to decode the reference PNG to raw pixels. + // These will be RGBA, so when diffing we can just compare + // every fourth byte. + const ref_rgba = try wuffs.png.decode(alloc, ref_bytes); + defer alloc.free(ref_rgba.data); + + assert(ref_rgba.width == atlas.getWidth()); + assert(ref_rgba.height == atlas.getHeight()); + + // We'll make a visual representation of the diff using + // red for removed pixels and green for added. We make + // a z2d surface for that here. + var diff = try z2d.Surface.init( + .image_surface_rgb, + alloc, + atlas.getWidth(), + atlas.getHeight(), + ); + defer diff.deinit(alloc); + const diff_pix = diff.image_surface_rgb.buf; + + const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); + + var differs: bool = false; + for (0..test_gray.len) |j| { + const t = test_gray[j]; + const r = ref_rgba.data[j * 4]; + if (t == r) { + // If the pixels match, write it as a faded gray. + diff_pix[j].r = t / 3; + diff_pix[j].g = t / 3; + diff_pix[j].b = t / 3; + } else { + differs = true; + // Otherwise put the reference value in the red + // channel and the new value in the green channel. + diff_pix[j].r = r; + diff_pix[j].g = t; + } } -}; + + // If the PNG data differs but not the raw pixels, that's + // a big red flag, since it could mean someone is trying to + // smuggle binary data in to the test files. + if (!differs) { + log.err( + "!!! Test PNG data does not match reference, but pixels do match! " ++ + "Either z2d's PNG exporter changed or someone is " ++ + "trying to smuggle binary data in the test files!\n" ++ + "test={s}, reference={s}", + .{ test_path, ref_path }, + ); + return true; + } + + // Drop the diff image as a PNG in the cwd. + const diff_path = try std.fmt.allocPrint( + alloc, + "./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(diff_path); + try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{}); + log.err( + "One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++ + "test={s}, reference={s}, diff={s}", + .{ i, i + 0xFF, test_path, ref_path, diff_path }, + ); + + return true; +} + +/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions +/// against reference files, logs errors and exposes a diff for any difference +/// between the reference and test atlas. +/// +/// Returns true if there was a diff. +fn testDrawRanges( + width: u32, + ascent: u32, + descent: u32, + thickness: u32, +) !bool { + const testing = std.testing; + const alloc = testing.allocator; + + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(width), + .ascent = @floatFromInt(ascent), + .descent = -@as(f64, @floatFromInt(descent)), + .line_gap = 0.0, + .underline_thickness = @floatFromInt(thickness), + .strikethrough_thickness = @floatFromInt(thickness), + }); + + const height = ascent + descent; + + const padding_x = width / 4; + const padding_y = height / 4; + + // Canvas to draw glyphs on, we'll re-use this for all glyphs. + var canvas = try font.sprite.Canvas.init( + alloc, + width, + height, + padding_x, + padding_y, + ); + defer canvas.deinit(); + + // We render glyphs in batches of 256, which we copy (including padding) to + // a 16 by 16 surface to be compared with the reference file for that range. + const stride_x = width + 2 * padding_x; + const stride_y = height + 2 * padding_y; + var atlas = try z2d.Surface.init( + .image_surface_alpha8, + alloc, + @intCast(stride_x * 16), + @intCast(stride_y * 16), + ); + defer atlas.deinit(alloc); + + var i: u32 = std.mem.alignBackward(u32, ranges[0].min, 0x100); + + // Try to make the sprite_face_test folder if it doesn't already exist. + var dir = testing.tmpDir(.{}); + defer dir.cleanup(); + const tmp_dir = try dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(tmp_dir); + + // We set this to true if we have any fails so we can + // return an error after we're done comparing all glyphs. + var fail: bool = false; + + inline for (ranges) |range| { + for (range.min..range.max + 1) |cp| { + // If we've moved to a new batch of 256, check the + // current one and clear the surface for the next one. + if (cp - i >= 0x100) { + // Export to our tmp dir. + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + i = std.mem.alignBackward(u32, @intCast(cp), 0x100); + @memset(std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf), 0); + } + + try getDrawFn(@intCast(cp)).?( + @intCast(cp), + &canvas, + width, + height, + metrics, + ); + canvas.clearClippingRegions(); + atlas.composite( + &canvas.sfc, + .src, + @intCast(stride_x * ((cp - i) % 16)), + @intCast(stride_y * ((cp - i) / 16)), + .{}, + ); + @memset(std.mem.sliceAsBytes(canvas.sfc.image_surface_alpha8.buf), 0); + canvas.clip_top = 0; + canvas.clip_left = 0; + canvas.clip_right = 0; + canvas.clip_bottom = 0; + } + } + + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + return fail; +} + +test "sprite face render all sprites" { + // Renders all sprites to an atlas and compares + // it to a ground truth for regression testing. + + var diff: bool = false; + + // testDrawRanges(width, ascent, descent, thickness): + // + // We compare 4 different sets of metrics; + // - even cell size / even thickness + // - even cell size / odd thickness + // - odd cell size / even thickness + // - odd cell size / odd thickness + // (Also a decreasing range of sizes.) + if (try testDrawRanges(18, 30, 6, 4)) diff = true; + if (try testDrawRanges(12, 20, 4, 3)) diff = true; + if (try testDrawRanges(11, 19, 2, 2)) diff = true; + if (try testDrawRanges(9, 15, 2, 1)) diff = true; + + try std.testing.expect(!diff); // There should be no diffs from reference. +} + +// test "sprite face print all sprites" { +// std.debug.print("\n\n", .{}); +// inline for (ranges) |range| { +// for (range.min..range.max + 1) |cp| { +// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) }); +// } +// } +// std.debug.print("\n\n", .{}); +// } test { - @import("std").testing.refAllDecls(@This()); + std.testing.refAllDecls(@This()); } diff --git a/src/font/sprite/Powerline.zig b/src/font/sprite/Powerline.zig deleted file mode 100644 index eaa7554b1..000000000 --- a/src/font/sprite/Powerline.zig +++ /dev/null @@ -1,564 +0,0 @@ -//! This file contains functions for drawing certain characters from Powerline -//! Extra (https://github.com/ryanoasis/powerline-extra-symbols). These -//! characters are similarly to box-drawing characters (see Box.zig), so the -//! logic will be mainly the same, just with a much reduced character set. -//! -//! Note that this is not the complete list of Powerline glyphs that may be -//! needed, so this may grow to add other glyphs from the set. -const Powerline = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const font = @import("../main.zig"); -const Quad = @import("canvas.zig").Quad; - -const log = std.log.scoped(.powerline_font); - -/// The cell width and height because the boxes are fit perfectly -/// into a cell so that they all properly connect with zero spacing. -width: u32, -height: u32, - -/// Base thickness value for glyphs that are not completely solid (backslashes, -/// thin half-circles, etc). If you want to do any DPI scaling, it is expected -/// to be done earlier. -/// -/// TODO: this and Thickness are currently unused but will be when the -/// aforementioned glyphs are added. -thickness: u32, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -inline fn sq(x: anytype) @TypeOf(x) { - return x * x; -} - -pub fn renderGlyph( - self: Powerline, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(self.height)); - - return font.Glyph{ - .width = self.width, - .height = self.height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(self.width), - }; -} - -fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - switch (cp) { - // Hard dividers and triangles - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - => try self.draw_wedge_triangle(canvas, cp), - - // Soft Dividers - 0xE0B1, - 0xE0B3, - => try self.draw_chevron(canvas, cp), - - // Half-circles - 0xE0B4, - 0xE0B6, - => try self.draw_half_circle(alloc, canvas, cp), - - // Mirrored top-down trapezoids - 0xE0D2, - 0xE0D4, - => try self.draw_trapezoid_top_bottom(canvas, cp), - - else => return error.InvalidCodepoint, - } -} - -fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p1_y: u32 = 0; - var p2_x: u32 = 0; - var p2_y: u32 = 0; - var p3_x: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B1 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - 0xE0B3 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle_outline(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, @floatFromInt(Thickness.light.height(self.thickness)), .on); -} - -fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p2_x: u32 = 0; - var p3_x: u32 = 0; - var p1_y: u32 = 0; - var p2_y: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B0 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - - 0xE0B2 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - 0xE0B8 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BA => { - p1_x = width; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BC => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = 0; - p3_y = height; - }, - - 0xE0BE => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, .on); -} - -fn draw_half_circle(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - const supersample = 4; - - // We make a canvas big enough for the whole circle, with the supersample - // applied. - const width = self.width * 2 * supersample; - const height = self.height * supersample; - - // We set a minimum super-sampled canvas to assert on. The minimum cell - // size is 1x3px, and this looked safe in empirical testing. - std.debug.assert(width >= 8); // 1 * 2 * 4 - std.debug.assert(height >= 12); // 3 * 4 - - const center_x = width / 2 - 1; - const center_y = height / 2 - 1; - - // Our radii. We're technically drawing an ellipse here to ensure that this - // works for fonts with different aspect ratios than a typical 2:1 H*W, e.g. - // Iosevka (which is around 2.6:1). - const radius_x = width / 2 - 1; // This gives us a small margin for smoothing - const radius_y = height / 2; - - // Pre-allocate a matrix to plot the points on. - const cap = height * width; - var points = try alloc.alloc(u8, cap); - defer alloc.free(points); - @memset(points, 0); - - { - // This is a midpoint ellipse algorithm, similar to a midpoint circle - // algorithm in that we only draw the octants we need and then reflect - // the result across the other axes. Since an ellipse has two radii, we - // need to calculate two octants instead of one. There are variations - // on the algorithm and you can find many examples online. This one - // does use some floating point math in calculating the decision - // parameter, but I've found it clear in its implementation and it does - // not require adjustment for integer error. - // - // This algorithm has undergone some iterations, so the following - // references might be helpful for understanding: - // - // * "Drawing a circle, point by point, without floating point - // support" (Dennis Yurichev, - // https://yurichev.com/news/20220322_circle/), which describes the - // midpoint circle algorithm and implementation we initially adapted - // here. - // - // * "Ellipse-Generating Algorithms" (RTU Latvia, - // https://daugavpils.rtu.lv/wp-content/uploads/sites/34/2020/11/LEC_3.pdf), - // which was used to further adapt the algorithm for ellipses. - // - // * "An Effective Approach to Minimize Error in Midpoint Ellipse - // Drawing Algorithm" (Dr. M. Javed Idrisi, Aayesha Ashraf, - // https://arxiv.org/abs/2103.04033), which includes a synopsis of - // the history of ellipse drawing algorithms, and further references. - - // Declare some casted constants for use in various calculations below - const rx: i32 = @intCast(radius_x); - const ry: i32 = @intCast(radius_y); - const rxf: f64 = @floatFromInt(radius_x); - const ryf: f64 = @floatFromInt(radius_y); - const cx: i32 = @intCast(center_x); - const cy: i32 = @intCast(center_y); - - // Our plotting x and y - var x: i32 = 0; - var y: i32 = @intCast(radius_y); - - // Decision parameter, initialized for region 1 - var dparam: f64 = sq(ryf) - sq(rxf) * ryf + sq(rxf) * 0.25; - - // Region 1 - while (2 * sq(ry) * x < 2 * sq(rx) * y) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - x += 1; - if (dparam < 0) { - const xf: f64 = @floatFromInt(x); - dparam += 2 * sq(ryf) * xf + sq(ryf); - } else { - y -= 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(ryf); - } - } - - // Region 2 - { - // Reset our decision parameter for region 2 - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam = sq(ryf) * sq(xf + 0.5) + sq(rxf) * sq(yf - 1) - sq(rxf) * sq(ryf); - } - while (y >= 0) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - y -= 1; - if (dparam > 0) { - const yf: f64 = @floatFromInt(y); - dparam -= 2 * sq(rxf) * yf + sq(rxf); - } else { - x += 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(rxf); - } - } - } - - // Fill - { - const u_height: u32 = @intCast(height); - const u_width: u32 = @intCast(width); - - for (0..u_height) |yf| { - for (0..u_width) |left| { - // Count forward from the left to the first filled pixel - if (points[yf * u_width + left] != 0) { - // Count back to our left point from the right to the first - // filled pixel on the other side. - var right: usize = u_width - 1; - while (right > left) : (right -= 1) { - if (points[yf * u_width + right] != 0) { - break; - } - } - - // Start filling 1 index after the left and go until we hit - // the right; this will be a no-op if the line length is < - // 3 as both left and right will have already been filled. - const start = yf * u_width + left; - const end = yf * u_width + right; - if (end - start >= 3) { - for (start + 1..end) |idx| { - points[idx] = 0xFF; - } - } - } - } - } - } - - // Now that we have our points, we need to "split" our matrix on the x - // axis for the downsample. - { - // The side of the circle we're drawing - const offset_j: u32 = if (cp == 0xE0B4) center_x + 1 else 0; - - for (0..self.height) |r| { - for (0..self.width) |c| { - var total: u32 = 0; - for (0..supersample) |i| { - for (0..supersample) |j| { - const idx = (r * supersample + i) * width + (c * supersample + j + offset_j); - total += points[idx]; - } - } - - const average = @as(u8, @intCast(@min(total / (supersample * supersample), 0xFF))); - canvas.rect( - .{ - .x = @intCast(c), - .y = @intCast(r), - .width = 1, - .height = 1, - }, - @as(font.sprite.Color, @enumFromInt(average)), - ); - } - } - } -} - -fn draw_trapezoid_top_bottom(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const t_top: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - }; - - const t_bottom: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - }; - - try canvas.quad(t_top, .on); - try canvas.quad(t_bottom, .on); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - const cps = [_]u32{ - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0B4, - 0xE0B6, - 0xE0D2, - 0xE0D4, - 0xE0B1, - 0xE0B3, - }; - for (cps) |cp| { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Powerline = .{ .width = 18, .height = 36, .thickness = 2 }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.width), glyph.width); - try testing.expectEqual(@as(u32, face.height), glyph.height); - } -} diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index a5ca7b290..b981449bc 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -81,19 +81,39 @@ pub const Canvas = struct { /// The underlying z2d surface. sfc: z2d.Surface, + padding_x: u32, + padding_y: u32, + + clip_top: u32 = 0, + clip_left: u32 = 0, + clip_right: u32 = 0, + clip_bottom: u32 = 0, + alloc: Allocator, - pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas { + pub fn init( + alloc: Allocator, + width: u32, + height: u32, + padding_x: u32, + padding_y: u32, + ) !Canvas { // Create the surface we'll be using. + // We add padding to both sides (hence `2 *`) const sfc = try z2d.Surface.initPixel( .{ .alpha8 = .{ .a = 0 } }, alloc, - @intCast(width), - @intCast(height), + @intCast(width + 2 * padding_x), + @intCast(height + 2 * padding_y), ); errdefer sfc.deinit(alloc); - return .{ .sfc = sfc, .alloc = alloc }; + return .{ + .sfc = sfc, + .padding_x = padding_x, + .padding_y = padding_y, + .alloc = alloc, + }; } pub fn deinit(self: *Canvas) void { @@ -109,30 +129,33 @@ pub const Canvas = struct { ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region { assert(atlas.format == .grayscale); - const width = @as(u32, @intCast(self.sfc.getWidth())); - const height = @as(u32, @intCast(self.sfc.getHeight())); + self.trim(); + + const sfc_width: u32 = @intCast(self.sfc.getWidth()); + const sfc_height: u32 = @intCast(self.sfc.getHeight()); + + // Subtract our clip margins from the + // width and height to get region size. + const region_width = sfc_width -| self.clip_left -| self.clip_right; + const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region const region = region: { - // We need to add a 1px padding to the font so that we don't - // get fuzzy issues when blending textures. - const padding = 1; - - // Get the full padded region + // Reserve a region with a 1px margin on the bottom and right edges + // so that we can avoid interpolation between adjacent glyphs during + // texture sampling. var region = try atlas.reserve( alloc, - width + (padding * 2), // * 2 because left+right - height + (padding * 2), // * 2 because top+bottom + region_width + 1, + region_height + 1, ); - // Modify the region so that we remove the padding so that - // we write to the non-zero location. The data in an Altlas - // is always initialized to zero (Atlas.clear) so we don't - // need to worry about zero-ing that. - region.x += padding; - region.y += padding; - region.width -= padding * 2; - region.height -= padding * 2; + // Modify the region to remove the margin so that we write to the + // non-zero location. The data in an Altlas is always initialized + // to zero (Atlas.clear) so we don't need to worry about zero-ing + // that. + region.width -= 1; + region.height -= 1; break :region region; }; @@ -140,38 +163,138 @@ pub const Canvas = struct { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); // Write the glyph information into the atlas - assert(region.width == width); - assert(region.height == height); - atlas.set(region, buffer); + assert(region.width == region_width); + assert(region.height == region_height); + atlas.setFromLarger( + region, + buffer, + sfc_width, + self.clip_left, + self.clip_top, + ); } return region; } + // Adjust clip boundaries to trim off any fully transparent rows or columns. + // This circumvents abstractions from z2d so that it can be performant. + fn trim(self: *Canvas) void { + const width: u32 = @intCast(self.sfc.getWidth()); + const height: u32 = @intCast(self.sfc.getHeight()); + + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + + top: while (self.clip_top < height - self.clip_bottom) { + const y = self.clip_top; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :top; + } + self.clip_top += 1; + } + + bottom: while (self.clip_bottom < height - self.clip_top) { + const y = height - self.clip_bottom -| 1; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :bottom; + } + self.clip_bottom += 1; + } + + left: while (self.clip_left < width - self.clip_right) { + const x = self.clip_left; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :left; + } + self.clip_left += 1; + } + + right: while (self.clip_right < width - self.clip_left) { + const x = width - self.clip_right -| 1; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :right; + } + self.clip_right += 1; + } + } + + /// Only really useful for test purposes, since the clipping region is + /// automatically excluded when writing to an atlas with `writeAtlas`. + pub fn clearClippingRegions(self: *Canvas) void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + + for (0..height) |y| { + for (0..self.clip_left) |x| { + buf[y * width + x] = 0; + } + } + + for (0..height) |y| { + for (width - self.clip_right..width) |x| { + buf[y * width + x] = 0; + } + } + + for (0..self.clip_top) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + + for (height - self.clip_bottom..height) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + } + + /// Return a transformation representing the translation for our padding. + pub fn transformation(self: Canvas) z2d.Transformation { + return .{ + .ax = 1, + .by = 0, + .cx = 0, + .dy = 1, + .tx = @as(f64, @floatFromInt(self.padding_x)), + .ty = @as(f64, @floatFromInt(self.padding_y)), + }; + } + /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) z2d.Context { - return .init(self.alloc, &self.sfc); + var ctx = z2d.Context.init(self.alloc, &self.sfc); + // Offset by our padding to keep + // coordinates relative to the cell. + ctx.setTransformation(self.transformation()); + return ctx; } /// Draw and fill a single pixel - pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void { + pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void { self.sfc.putPixel( - @intCast(x), - @intCast(y), + x + @as(i32, @intCast(self.padding_x)), + y + @as(i32, @intCast(self.padding_y)), .{ .alpha8 = .{ .a = @intFromEnum(color) } }, ); } /// Draw and fill a rectangle. This is the main primitive for drawing /// lines as well (which are just generally skinny rectangles...) - pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void { - const x0 = v.x; - const x1 = v.x + v.width; - const y0 = v.y; - const y1 = v.y + v.height; - - for (y0..y1) |y| { - for (x0..x1) |x| { + pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void { + var y = v.y; + while (y < v.y + v.height) : (y += 1) { + var x = v.x; + while (x < v.x + v.width) : (x += 1) { self.pixel( @intCast(x), @intCast(y), @@ -181,96 +304,226 @@ pub const Canvas = struct { } } + /// Convenience wrapper for `Canvas.rect` + pub fn box( + self: *Canvas, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Color, + ) void { + self.rect((Box(i32){ + .p0 = .{ .x = x0, .y = y0 }, + .p1 = .{ .x = x1, .y = y1 }, + }).rect(), color); + } + /// Draw and fill a quad. pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { - var path: z2d.StaticPath(6) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(6); // nodes.len = 0 path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1 path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2 path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3 path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4 path.close(); // +2, nodes.len = 6 - - try z2d.painter.fill( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); + try self.fillPath(path.wrapped_path, .{}, color); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { - var path: z2d.StaticPath(5) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(5); // nodes.len = 0 path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 path.close(); // +2, nodes.len = 5 + try self.fillPath(path.wrapped_path, .{}, color); + } + /// Stroke a line. + pub fn line( + self: *Canvas, + l: Line(f64), + thickness: f64, + color: Color, + ) !void { + var path = self.staticPath(2); // nodes.len = 0 + path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 + path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 + try self.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = thickness, + }, + color, + ); + } + + /// Create a static path of the provided len and initialize it. + /// Use this function instead of making the path manually since + /// it ensures that the transform is applied. + pub inline fn staticPath( + self: *Canvas, + comptime len: usize, + ) z2d.StaticPath(len) { + var path: z2d.StaticPath(len) = .{}; + path.init(); + path.wrapped_path.transformation = self.transformation(); + return path; + } + + /// Stroke a z2d path. + pub fn strokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) z2d.painter.StrokeError!void { + try z2d.painter.stroke( + self.alloc, + &self.sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, + } }, + path.nodes.items, + opts, + ); + } + + /// Do an inner stroke on a z2d path, right now this involves a pretty + /// heavy workaround that uses two extra surfaces; in the future, z2d + /// should add inner and outer strokes natively. + pub fn innerStrokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { + // On one surface we fill the shape, this will be a mask we + // multiply with the double-width stroke so that only the + // part inside is used. + var fill_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer fill_sfc.deinit(self.alloc); + + // On the other we'll do the double width stroke. + var stroke_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer stroke_sfc.deinit(self.alloc); + + // Make a closed version of the path for our fill, so + // that we can support open paths for inner stroke. + var closed_path = path; + closed_path.nodes = try path.nodes.clone(self.alloc); + defer closed_path.deinit(self.alloc); + try closed_path.close(self.alloc); + + // Fill the shape in white to the fill surface, we use + // white because this is a mask that we'll multiply with + // the stroke, we want everything inside to be the stroke + // color. + try z2d.painter.fill( + self.alloc, + &fill_sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = 255 } }, + } }, + closed_path.nodes.items, + .{}, + ); + + // Stroke the shape with double the desired width. + var mut_opts = opts; + mut_opts.line_width *= 2; + try z2d.painter.stroke( + self.alloc, + &stroke_sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, + } }, + path.nodes.items, + mut_opts, + ); + + // We multiply the stroke sfc on to the fill surface. + // The z2d composite operation doesn't seem to work for + // this with alpha8 surfaces, so we have to do it manually. + for ( + std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf), + std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf), + ) |*d, s| { + d.* = @intFromFloat(@round( + 255.0 * + (@as(f64, @floatFromInt(s)) / 255.0) * + (@as(f64, @floatFromInt(d.*)) / 255.0), + )); + } + + // Then we composite the result on to the main surface. + self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{}); + } + + /// Fill a z2d path. + pub fn fillPath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.FillOpts, + color: Color, + ) z2d.painter.FillError!void { try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, - path.wrapped_path.nodes.items, - .{}, - ); - } - - pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(3) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 - path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 - path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 - - try z2d.painter.stroke( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, - ); - } - - /// Stroke a line. - pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(2) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 - path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 - - try z2d.painter.stroke( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, + path.nodes.items, + opts, ); } + /// Invert all pixels on the canvas. pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } + + /// Mirror the canvas horizontally. + pub fn flipHorizontal(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[y * width + width - x - 1]; + } + } + std.mem.swap(u32, &self.clip_left, &self.clip_right); + } + + /// Mirror the canvas vertically. + pub fn flipVertical(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[(height - y - 1) * width + x]; + } + } + std.mem.swap(u32, &self.clip_top, &self.clip_bottom); + } }; diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig deleted file mode 100644 index d63db624a..000000000 --- a/src/font/sprite/cursor.zig +++ /dev/null @@ -1,65 +0,0 @@ -//! This file renders cursor sprites. -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const font = @import("../main.zig"); -const Sprite = font.sprite.Sprite; - -/// Draw a cursor. -pub fn renderGlyph( - alloc: Allocator, - atlas: *font.Atlas, - sprite: Sprite, - width: u32, - height: u32, - thickness: u32, -) !font.Glyph { - // Make a canvas of the desired size - var canvas = try font.sprite.Canvas.init(alloc, width, height); - defer canvas.deinit(); - - // Draw the appropriate sprite - switch (sprite) { - Sprite.cursor_rect => canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = height, - }, .on), - Sprite.cursor_hollow_rect => { - // left - canvas.rect(.{ .x = 0, .y = 0, .width = thickness, .height = height }, .on); - // right - canvas.rect(.{ .x = width -| thickness, .y = 0, .width = thickness, .height = height }, .on); - // top - canvas.rect(.{ .x = 0, .y = 0, .width = width, .height = thickness }, .on); - // bottom - canvas.rect(.{ .x = 0, .y = height -| thickness, .width = width, .height = thickness }, .on); - }, - Sprite.cursor_bar => canvas.rect(.{ - .x = 0, - .y = 0, - .width = thickness, - .height = height, - }, .on), - else => unreachable, - } - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - // HACK: Set the width for the bar cursor to just the thickness, - // this is just for the benefit of the custom shader cursor - // uniform code. -- In the future code will be introduced to - // auto-crop the canvas so that this isn't needed. - .width = if (sprite == .cursor_bar) thickness else width, - .height = height, - .offset_x = 0, - .offset_y = @intCast(height), - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} diff --git a/src/font/sprite/draw/README.md b/src/font/sprite/draw/README.md new file mode 100644 index 000000000..c6219b83f --- /dev/null +++ b/src/font/sprite/draw/README.md @@ -0,0 +1,50 @@ +# This is a *special* directory. +The files in this directory are imported by `../Face.zig` and scanned for pub +functions with names matching a specific format, which are then used to handle +drawing specified codepoints. + +## IMPORTANT +When you add a new file here, you need to add the corresponding import in +`../Face.zig` for its draw functions to be picked up. I tried dynamically +listing these files to do this automatically but it was more pain than it +was worth. + +## `draw*` functions +Any function named `draw` or `draw_` 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/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig new file mode 100644 index 000000000..27c6ae516 --- /dev/null +++ b/src/font/sprite/draw/block.zig @@ -0,0 +1,184 @@ +//! Block Elements | U+2580...U+259F +//! https://en.wikipedia.org/wiki/Block_Elements +//! +//! ▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏ +//! ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Shade = common.Shade; +const Quads = common.Quads; +const Alignment = common.Alignment; +const rect = common.rect; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +// Utility names for common fractions +const one_eighth: f64 = 0.125; +const one_quarter: f64 = 0.25; +const one_third: f64 = (1.0 / 3.0); +const three_eighths: f64 = 0.375; +const half: f64 = 0.5; +const five_eighths: f64 = 0.625; +const two_thirds: f64 = (2.0 / 3.0); +const three_quarters: f64 = 0.75; +const seven_eighths: f64 = 0.875; + +pub fn draw2580_259F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '▀' UPPER HALF BLOCK + 0x2580 => block(metrics, canvas, .upper, 1, half), + // '▁' LOWER ONE EIGHTH BLOCK + 0x2581 => block(metrics, canvas, .lower, 1, one_eighth), + // '▂' LOWER ONE QUARTER BLOCK + 0x2582 => block(metrics, canvas, .lower, 1, one_quarter), + // '▃' LOWER THREE EIGHTHS BLOCK + 0x2583 => block(metrics, canvas, .lower, 1, three_eighths), + // '▄' LOWER HALF BLOCK + 0x2584 => block(metrics, canvas, .lower, 1, half), + // '▅' LOWER FIVE EIGHTHS BLOCK + 0x2585 => block(metrics, canvas, .lower, 1, five_eighths), + // '▆' LOWER THREE QUARTERS BLOCK + 0x2586 => block(metrics, canvas, .lower, 1, three_quarters), + // '▇' LOWER SEVEN EIGHTHS BLOCK + 0x2587 => block(metrics, canvas, .lower, 1, seven_eighths), + // '█' FULL BLOCK + 0x2588 => fullBlockShade(metrics, canvas, .on), + // '▉' LEFT SEVEN EIGHTHS BLOCK + 0x2589 => block(metrics, canvas, .left, seven_eighths, 1), + // '▊' LEFT THREE QUARTERS BLOCK + 0x258a => block(metrics, canvas, .left, three_quarters, 1), + // '▋' LEFT FIVE EIGHTHS BLOCK + 0x258b => block(metrics, canvas, .left, five_eighths, 1), + // '▌' LEFT HALF BLOCK + 0x258c => block(metrics, canvas, .left, half, 1), + // '▍' LEFT THREE EIGHTHS BLOCK + 0x258d => block(metrics, canvas, .left, three_eighths, 1), + // '▎' LEFT ONE QUARTER BLOCK + 0x258e => block(metrics, canvas, .left, one_quarter, 1), + // '▏' LEFT ONE EIGHTH BLOCK + 0x258f => block(metrics, canvas, .left, one_eighth, 1), + + // '▐' RIGHT HALF BLOCK + 0x2590 => block(metrics, canvas, .right, half, 1), + // '░' + 0x2591 => fullBlockShade(metrics, canvas, .light), + // '▒' + 0x2592 => fullBlockShade(metrics, canvas, .medium), + // '▓' + 0x2593 => fullBlockShade(metrics, canvas, .dark), + // '▔' UPPER ONE EIGHTH BLOCK + 0x2594 => block(metrics, canvas, .upper, 1, one_eighth), + // '▕' RIGHT ONE EIGHTH BLOCK + 0x2595 => block(metrics, canvas, .right, one_eighth, 1), + // '▖' + 0x2596 => quadrant(metrics, canvas, .{ .bl = true }), + // '▗' + 0x2597 => quadrant(metrics, canvas, .{ .br = true }), + // '▘' + 0x2598 => quadrant(metrics, canvas, .{ .tl = true }), + // '▙' + 0x2599 => quadrant(metrics, canvas, .{ .tl = true, .bl = true, .br = true }), + // '▚' + 0x259a => quadrant(metrics, canvas, .{ .tl = true, .br = true }), + // '▛' + 0x259b => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }), + // '▜' + 0x259c => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .br = true }), + // '▝' + 0x259d => quadrant(metrics, canvas, .{ .tr = true }), + // '▞' + 0x259e => quadrant(metrics, canvas, .{ .tr = true, .bl = true }), + // '▟' + 0x259f => quadrant(metrics, canvas, .{ .tr = true, .bl = true, .br = true }), + + else => unreachable, + } +} + +pub fn block( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, +) void { + blockShade(metrics, canvas, alignment, width, height, .on); +} + +pub fn blockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, + comptime shade: Shade, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const w: u32 = @intFromFloat(@round(float_width * width)); + const h: u32 = @intFromFloat(@round(float_height * height)); + + const x = switch (alignment.horizontal) { + .left => 0, + .right => metrics.cell_width - w, + .center => (metrics.cell_width - w) / 2, + }; + const y = switch (alignment.vertical) { + .top => 0, + .bottom => metrics.cell_height - h, + .middle => (metrics.cell_height - h) / 2, + }; + + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(y), + .width = @intCast(w), + .height = @intCast(h), + }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); +} + +pub fn fullBlockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + shade: Shade, +) void { + canvas.box( + 0, + 0, + @intCast(metrics.cell_width), + @intCast(metrics.cell_height), + @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade))), + ); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} diff --git a/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig new file mode 100644 index 000000000..91d78d2b2 --- /dev/null +++ b/src/font/sprite/draw/box.zig @@ -0,0 +1,947 @@ +//! Box Drawing | U+2500...U+257F +//! https://en.wikipedia.org/wiki/Box_Drawing +//! +//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏ +//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟ +//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯ +//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿ +//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏ +//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟ +//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯ +//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Quads = common.Quads; +const Corner = common.Corner; +const Edge = common.Edge; +const Alignment = common.Alignment; +const rect = common.rect; +const hline = common.hline; +const vline = common.vline; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +/// Specification of a traditional intersection-style line/box-drawing char, +/// which can have a different style of line from each edge to the center. +pub const Lines = packed struct(u8) { + up: Style = .none, + right: Style = .none, + down: Style = .none, + left: Style = .none, + + const Style = enum(u2) { + none, + light, + heavy, + double, + }; +}; + +pub fn draw2500_257F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '─' + 0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }), + // '━' + 0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }), + // '│' + 0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }), + // '┃' + 0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }), + // '┄' + 0x2504 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┅' + 0x2505 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┆' + 0x2506 => dashVertical( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┇' + 0x2507 => dashVertical( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┈' + 0x2508 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┉' + 0x2509 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┊' + 0x250a => dashVertical( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┋' + 0x250b => dashVertical( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┌' + 0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }), + // '┍' + 0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }), + // '┎' + 0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }), + // '┏' + 0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }), + + // '┐' + 0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }), + // '┑' + 0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }), + // '┒' + 0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }), + // '┓' + 0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }), + // '└' + 0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }), + // '┕' + 0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }), + // '┖' + 0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }), + // '┗' + 0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }), + // '┘' + 0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }), + // '┙' + 0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }), + // '┚' + 0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }), + // '┛' + 0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }), + // '├' + 0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }), + // '┝' + 0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }), + // '┞' + 0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }), + // '┟' + 0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }), + + // '┠' + 0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), + // '┡' + 0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), + // '┢' + 0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), + // '┣' + 0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), + // '┤' + 0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }), + // '┥' + 0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }), + // '┦' + 0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }), + // '┧' + 0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }), + // '┨' + 0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), + // '┩' + 0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), + // '┪' + 0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), + // '┫' + 0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), + // '┬' + 0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }), + // '┭' + 0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }), + // '┮' + 0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }), + // '┯' + 0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), + + // '┰' + 0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }), + // '┱' + 0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), + // '┲' + 0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), + // '┳' + 0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), + // '┴' + 0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }), + // '┵' + 0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }), + // '┶' + 0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }), + // '┷' + 0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), + // '┸' + 0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }), + // '┹' + 0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), + // '┺' + 0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), + // '┻' + 0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), + // '┼' + 0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), + // '┽' + 0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), + // '┾' + 0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), + // '┿' + 0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), + + // '╀' + 0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), + // '╁' + 0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), + // '╂' + 0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), + // '╃' + 0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), + // '╄' + 0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), + // '╅' + 0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), + // '╆' + 0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), + // '╇' + 0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), + // '╈' + 0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╉' + 0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), + // '╊' + 0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), + // '╋' + 0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╌' + 0x254c => dashHorizontal( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.light.height(metrics.box_thickness), + ), + // '╍' + 0x254d => dashHorizontal( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╎' + 0x254e => dashVertical( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╏' + 0x254f => dashVertical( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + + // '═' + 0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }), + // '║' + 0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }), + // '╒' + 0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }), + // '╓' + 0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }), + // '╔' + 0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }), + // '╕' + 0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }), + // '╖' + 0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }), + // '╗' + 0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }), + // '╘' + 0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }), + // '╙' + 0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }), + // '╚' + 0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }), + // '╛' + 0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }), + // '╜' + 0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }), + // '╝' + 0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }), + // '╞' + 0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }), + // '╟' + 0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }), + + // '╠' + 0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }), + // '╡' + 0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }), + // '╢' + 0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }), + // '╣' + 0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }), + // '╤' + 0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }), + // '╥' + 0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }), + // '╦' + 0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }), + // '╧' + 0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }), + // '╨' + 0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }), + // '╩' + 0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }), + // '╪' + 0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), + // '╫' + 0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), + // '╬' + 0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), + // '╭' + 0x256d => try arc(metrics, canvas, .br, .light), + // '╮' + 0x256e => try arc(metrics, canvas, .bl, .light), + // '╯' + 0x256f => try arc(metrics, canvas, .tl, .light), + + // '╰' + 0x2570 => try arc(metrics, canvas, .tr, .light), + // '╱' + 0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas), + // '╲' + 0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas), + // '╳' + 0x2573 => lightDiagonalCross(metrics, canvas), + // '╴' + 0x2574 => linesChar(metrics, canvas, .{ .left = .light }), + // '╵' + 0x2575 => linesChar(metrics, canvas, .{ .up = .light }), + // '╶' + 0x2576 => linesChar(metrics, canvas, .{ .right = .light }), + // '╷' + 0x2577 => linesChar(metrics, canvas, .{ .down = .light }), + // '╸' + 0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }), + // '╹' + 0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }), + // '╺' + 0x257a => linesChar(metrics, canvas, .{ .right = .heavy }), + // '╻' + 0x257b => linesChar(metrics, canvas, .{ .down = .heavy }), + // '╼' + 0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }), + // '╽' + 0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }), + // '╾' + 0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }), + // '╿' + 0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }), + + else => unreachable, + } +} + +pub fn linesChar( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + lines: Lines, +) void { + const light_px = Thickness.light.height(metrics.box_thickness); + const heavy_px = Thickness.heavy.height(metrics.box_thickness); + + // Top of light horizontal strokes + const h_light_top = (metrics.cell_height -| light_px) / 2; + // Bottom of light horizontal strokes + const h_light_bottom = h_light_top +| light_px; + + // Top of heavy horizontal strokes + const h_heavy_top = (metrics.cell_height -| heavy_px) / 2; + // Bottom of heavy horizontal strokes + const h_heavy_bottom = h_heavy_top +| heavy_px; + + // Top of the top doubled horizontal stroke (bottom is `h_light_top`) + const h_double_top = h_light_top -| light_px; + // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`) + const h_double_bottom = h_light_bottom +| light_px; + + // Left of light vertical strokes + const v_light_left = (metrics.cell_width -| light_px) / 2; + // Right of light vertical strokes + const v_light_right = v_light_left +| light_px; + + // Left of heavy vertical strokes + const v_heavy_left = (metrics.cell_width -| heavy_px) / 2; + // Right of heavy vertical strokes + const v_heavy_right = v_heavy_left +| heavy_px; + + // Left of the left doubled vertical stroke (right is `v_light_left`) + const v_double_left = v_light_left -| light_px; + // Right of the right doubled vertical stroke (left is `v_light_right`) + const v_double_right = v_light_right +| light_px; + + // The bottom of the up line + const up_bottom = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_bottom + else if (lines.left != lines.right or lines.down == lines.up) + if (lines.left == .double or lines.right == .double) + h_double_bottom + else + h_light_bottom + else if (lines.left == .none and lines.right == .none) + h_light_bottom + else + h_light_top; + + // The top of the down line + const down_top = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_top + else if (lines.left != lines.right or lines.up == lines.down) + if (lines.left == .double or lines.right == .double) + h_double_top + else + h_light_top + else if (lines.left == .none and lines.right == .none) + h_light_top + else + h_light_bottom; + + // The right of the left line + const left_right = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_right + else if (lines.up != lines.down or lines.left == lines.right) + if (lines.up == .double or lines.down == .double) + v_double_right + else + v_light_right + else if (lines.up == .none and lines.down == .none) + v_light_right + else + v_light_left; + + // The left of the right line + const right_left = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_left + else if (lines.up != lines.down or lines.right == lines.left) + if (lines.up == .double or lines.down == .double) + v_double_left + else + v_light_left + else if (lines.up == .none and lines.down == .none) + v_light_left + else + v_light_right; + + switch (lines.up) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + 0, + @intCast(v_light_right), + @intCast(up_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + 0, + @intCast(v_heavy_right), + @intCast(up_bottom), + .on, + ), + .double => { + const left_bottom = if (lines.left == .double) h_light_top else up_bottom; + const right_bottom = if (lines.right == .double) h_light_top else up_bottom; + + canvas.box( + @intCast(v_double_left), + 0, + @intCast(v_light_left), + @intCast(left_bottom), + .on, + ); + canvas.box( + @intCast(v_light_right), + 0, + @intCast(v_double_right), + @intCast(right_bottom), + .on, + ); + }, + } + + switch (lines.right) { + .none => {}, + .light => canvas.box( + @intCast(right_left), + @intCast(h_light_top), + @intCast(metrics.cell_width), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(right_left), + @intCast(h_heavy_top), + @intCast(metrics.cell_width), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_left = if (lines.up == .double) v_light_right else right_left; + const bottom_left = if (lines.down == .double) v_light_right else right_left; + + canvas.box( + @intCast(top_left), + @intCast(h_double_top), + @intCast(metrics.cell_width), + @intCast(h_light_top), + .on, + ); + canvas.box( + @intCast(bottom_left), + @intCast(h_light_bottom), + @intCast(metrics.cell_width), + @intCast(h_double_bottom), + .on, + ); + }, + } + + switch (lines.down) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + @intCast(down_top), + @intCast(v_light_right), + @intCast(metrics.cell_height), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + @intCast(down_top), + @intCast(v_heavy_right), + @intCast(metrics.cell_height), + .on, + ), + .double => { + const left_top = if (lines.left == .double) h_light_bottom else down_top; + const right_top = if (lines.right == .double) h_light_bottom else down_top; + + canvas.box( + @intCast(v_double_left), + @intCast(left_top), + @intCast(v_light_left), + @intCast(metrics.cell_height), + .on, + ); + canvas.box( + @intCast(v_light_right), + @intCast(right_top), + @intCast(v_double_right), + @intCast(metrics.cell_height), + .on, + ); + }, + } + + switch (lines.left) { + .none => {}, + .light => canvas.box( + 0, + @intCast(h_light_top), + @intCast(left_right), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + 0, + @intCast(h_heavy_top), + @intCast(left_right), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_right = if (lines.up == .double) v_light_left else left_right; + const bottom_right = if (lines.down == .double) v_light_left else left_right; + + canvas.box( + 0, + @intCast(h_double_top), + @intCast(top_right), + @intCast(h_light_top), + .on, + ); + canvas.box( + 0, + @intCast(h_light_bottom), + @intCast(bottom_right), + @intCast(h_double_bottom), + .on, + ); + }, + } +} + +pub fn lightDiagonalUpperRightToLowerLeft( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = float_width + 0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = -0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalUpperLeftToLowerRight( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = -0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = float_width + 0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalCross( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + lightDiagonalUpperRightToLowerLeft(metrics, canvas); + lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} + +pub fn arc( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime thickness: Thickness, +) !void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2; + const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2; + + const r = @min(float_width, float_height) / 2; + + // Fraction away from the center to place the middle control points, + const s: f64 = 0.25; + + var path = canvas.staticPath(4); + + switch (corner) { + .tl => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .tr => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + .bl => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .br => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + } + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, + .on, + ); +} + +fn dashHorizontal( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +------------+ + // | | + // | | + // | | + // | | + // | -- -- -- | + // | | + // | | + // | | + // | | + // +------------+ + // Our dashed line should be made such that when tiled horizontally + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have half-sized + // gaps on the left and right so that it is centered properly. + + // For N dashes, there are N - 1 gaps between them, but we also have + // half-sized gaps on either side, adding up to N total gaps. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_width < count + gap_count) { + hlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count))); + const total_gap_width: i32 = gap_count * gap_width; + const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width; + const dash_width: i32 = @divFloor(total_dash_width, count); + const remaining: i32 = @mod(total_dash_width, count); + + assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width); + + // Our dashes should be centered vertically. + const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2); + + // We start at half a gap from the left edge, in order to center + // our dashes properly. + var x: i32 = @divFloor(gap_width, 2); + + // We'll distribute the extra space in to dash widths, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + for (0..count) |_| { + var x1 = x + dash_width; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + x1 += 1; + } + hline(canvas, x, x1, y, thick_px); + // Advance by the width of the dash we drew and the width + // of a gap to get the the start of the next dash. + x = x1 + gap_width; + } +} + +fn dashVertical( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +-----------+ + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // +-----------+ + // Our dashed line should be made such that when tiled vertically it + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have an extra gap + // gap at the bottom. + // + // A single full-sized extra gap is preferred to two half-sized ones for + // vertical to allow better joining to solid characters without creating + // visible half-sized gaps. Unlike horizontal, centering is a lot less + // important, visually. + + // Because of the extra gap at the bottom, there are as many gaps as + // there are dashes. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_height < count + gap_count) { + vlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_height: i32 = @intCast(@min(desired_gap, metrics.cell_height / (2 * count))); + const total_gap_height: i32 = gap_count * gap_height; + const total_dash_height: i32 = @as(i32, @intCast(metrics.cell_height)) - total_gap_height; + const dash_height: i32 = @divFloor(total_dash_height, count); + const remaining: i32 = @mod(total_dash_height, count); + + assert(dash_height * count + gap_height * gap_count + remaining == metrics.cell_height); + + // Our dashes should be centered horizontally. + const x: i32 = @intCast((metrics.cell_width -| thick_px) / 2); + + // We start at the top of the cell. + var y: i32 = 0; + + // We'll distribute the extra space in to dash heights, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + inline for (0..count) |_| { + var y1 = y + dash_height; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + y1 += 1; + } + vline(canvas, y, y1, x, thick_px); + // Advance by the height of the dash we drew and the height + // of a gap to get the the start of the next dash. + y = y1 + gap_height; + } +} diff --git a/src/font/sprite/draw/braille.zig b/src/font/sprite/draw/braille.zig new file mode 100644 index 000000000..c756ff369 --- /dev/null +++ b/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 = std.debug.assert; +const Allocator = std.mem.Allocator; + +const font = @import("../../main.zig"); + +/// A braille pattern. +/// +/// Mnemonic: +/// [t]op - . . +/// [u]pper - . . +/// [l]ower - . . +/// [b]ottom - . . +/// | | +/// [l]eft, [r]ight +/// +/// Struct layout matches bit patterns of unicode codepoints. +const Pattern = packed struct(u8) { + tl: bool, + ul: bool, + ll: bool, + tr: bool, + ur: bool, + lr: bool, + bl: bool, + br: bool, + + fn from(cp: u32) Pattern { + return @bitCast(@as(u8, @truncate(cp))); + } +}; + +pub fn draw2800_28FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + var w: i32 = @intCast(@min(width / 4, height / 8)); + var x_spacing: i32 = @intCast(width / 4); + var y_spacing: i32 = @intCast(height / 8); + var x_margin: i32 = @divFloor(x_spacing, 2); + var y_margin: i32 = @divFloor(y_spacing, 2); + + var x_px_left: i32 = + @as(i32, @intCast(width)) - 2 * x_margin - x_spacing - 2 * w; + + var y_px_left: i32 = + @as(i32, @intCast(height)) - 2 * y_margin - 3 * y_spacing - 4 * w; + + // First, try hard to ensure the DOT width is non-zero + if (x_px_left >= 2 and y_px_left >= 4 and w == 0) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + // Second, prefer a non-zero margin + if (x_px_left >= 2 and x_margin == 0) { + x_margin = 1; + x_px_left -= 2; + } + if (y_px_left >= 2 and y_margin == 0) { + y_margin = 1; + y_px_left -= 2; + } + + // Third, increase spacing + if (x_px_left >= 1) { + x_spacing += 1; + x_px_left -= 1; + } + if (y_px_left >= 3) { + y_spacing += 1; + y_px_left -= 3; + } + + // Fourth, margins (“spacing”, but on the sides) + if (x_px_left >= 2) { + x_margin += 1; + x_px_left -= 2; + } + if (y_px_left >= 2) { + y_margin += 1; + y_px_left -= 2; + } + + // Last - increase dot width + if (x_px_left >= 2 and y_px_left >= 4) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + assert(x_px_left <= 1 or y_px_left <= 1); + assert(2 * x_margin + 2 * w + x_spacing <= width); + assert(2 * y_margin + 4 * w + 3 * y_spacing <= height); + + const x = [2]i32{ x_margin, x_margin + w + x_spacing }; + const y = y: { + var y: [4]i32 = undefined; + y[0] = y_margin; + y[1] = y[0] + w + y_spacing; + y[2] = y[1] + w + y_spacing; + y[3] = y[2] + w + y_spacing; + break :y y; + }; + + assert(cp >= 0x2800); + assert(cp <= 0x28ff); + const p: Pattern = .from(cp); + + if (p.tl) canvas.box(x[0], y[0], x[0] + w, y[0] + w, .on); + if (p.ul) canvas.box(x[0], y[1], x[0] + w, y[1] + w, .on); + if (p.ll) canvas.box(x[0], y[2], x[0] + w, y[2] + w, .on); + if (p.bl) canvas.box(x[0], y[3], x[0] + w, y[3] + w, .on); + if (p.tr) canvas.box(x[1], y[0], x[1] + w, y[0] + w, .on); + if (p.ur) canvas.box(x[1], y[1], x[1] + w, y[1] + w, .on); + if (p.lr) canvas.box(x[1], y[2], x[1] + w, y[2] + w, .on); + if (p.br) canvas.box(x[1], y[3], x[1] + w, y[3] + w, .on); +} diff --git a/src/font/sprite/draw/branch.zig b/src/font/sprite/draw/branch.zig new file mode 100644 index 000000000..ac7220390 --- /dev/null +++ b/src/font/sprite/draw/branch.zig @@ -0,0 +1,505 @@ +//! Branch Drawing Characters | U+F5D0...U+F60D +//! +//! Branch drawing character set, used for drawing git-like +//! graphs in the terminal. Originally implemented in Kitty. +//! Ref: +//! - https://github.com/kovidgoyal/kitty/pull/7681 +//! - https://github.com/kovidgoyal/kitty/pull/7805 +//! NOTE: Kitty is GPL licensed, and its code was not referenced +//! for these characters, only the loose specification of +//! the character set in the pull request descriptions. +//! +//!                 +//!                 +//!                 +//!               +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Edge = common.Edge; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const arc = @import("box.zig").arc; + +const font = @import("../../main.zig"); + +/// Specification of a branch drawing node, which consists of a +/// circle which is either empty or filled, and lines connecting +/// optionally between the circle and each of the 4 edges. +const BranchNode = packed struct(u5) { + up: bool = false, + right: bool = false, + down: bool = false, + left: bool = false, + filled: bool = false, +}; + +pub fn drawF5D0_F60D( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '' + 0x0f5d0 => hlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d1 => vlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d2 => fadingLine(metrics, canvas, .right, .light), + // '' + 0x0f5d3 => fadingLine(metrics, canvas, .left, .light), + // '' + 0x0f5d4 => fadingLine(metrics, canvas, .bottom, .light), + // '' + 0x0f5d5 => fadingLine(metrics, canvas, .top, .light), + // '' + 0x0f5d6 => try arc(metrics, canvas, .br, .light), + // '' + 0x0f5d7 => try arc(metrics, canvas, .bl, .light), + // '' + 0x0f5d8 => try arc(metrics, canvas, .tr, .light), + // '' + 0x0f5d9 => try arc(metrics, canvas, .tl, .light), + // '' + 0x0f5da => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5db => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dc => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dd => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5de => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5df => { + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .bl, .light); + }, + + // '' + 0x0f5e0 => { + try arc(metrics, canvas, .bl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e1 => { + try arc(metrics, canvas, .br, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e2 => { + try arc(metrics, canvas, .br, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5e3 => { + try arc(metrics, canvas, .tl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e4 => { + try arc(metrics, canvas, .tr, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e5 => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e6 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5e7 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5e8 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e9 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ea => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5eb => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ec => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ed => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ee => branchNode(metrics, canvas, .{ .filled = true }, .light), + // '' + 0x0f5ef => branchNode(metrics, canvas, .{}, .light), + + // '' + 0x0f5f0 => branchNode(metrics, canvas, .{ + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f1 => branchNode(metrics, canvas, .{ + .right = true, + }, .light), + // '' + 0x0f5f2 => branchNode(metrics, canvas, .{ + .left = true, + .filled = true, + }, .light), + // '' + 0x0f5f3 => branchNode(metrics, canvas, .{ + .left = true, + }, .light), + // '' + 0x0f5f4 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f5 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + }, .light), + // '' + 0x0f5f6 => branchNode(metrics, canvas, .{ + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5f7 => branchNode(metrics, canvas, .{ + .down = true, + }, .light), + // '' + 0x0f5f8 => branchNode(metrics, canvas, .{ + .up = true, + .filled = true, + }, .light), + // '' + 0x0f5f9 => branchNode(metrics, canvas, .{ + .up = true, + }, .light), + // '' + 0x0f5fa => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fb => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + }, .light), + // '' + 0x0f5fc => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fd => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + }, .light), + // '' + 0x0f5fe => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5ff => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + }, .light), + + // '' + 0x0f600 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f601 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + }, .light), + // '' + 0x0f602 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f603 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + }, .light), + // '' + 0x0f604 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f605 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + }, .light), + // '' + 0x0f606 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f607 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + }, .light), + // '' + 0x0f608 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f609 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60a => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60b => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60c => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60d => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + }, .light), + + else => unreachable, + } +} + +fn branchNode( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + node: BranchNode, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // We calculate the center of the circle this way + // to ensure it aligns with box drawing characters + // since the lines are sometimes off center to + // make sure they aren't split between pixels. + const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; + const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; + // The radius needs to be the smallest distance from the center to an edge. + const r: f64 = @min( + @min(cx, cy), + @min(float_width - cx, float_height - cy), + ); + + var ctx = canvas.getContext(); + defer ctx.deinit(); + ctx.setSource(.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + } }); + ctx.setLineWidth(float_thick); + + // These @intFromFloat casts shouldn't ever fail since r can never + // be greater than cx or cy, so when subtracting it from them the + // result can never be negative. + if (node.up) canvas.box( + @intCast(v_left), + 0, + @intCast(v_right), + @intFromFloat(@ceil(cy - r + float_thick / 2)), + .on, + ); + if (node.right) canvas.box( + @intFromFloat(@floor(cx + r - float_thick / 2)), + @intCast(h_top), + @intCast(metrics.cell_width), + @intCast(h_bottom), + .on, + ); + if (node.down) canvas.box( + @intCast(v_left), + @intFromFloat(@floor(cy + r - float_thick / 2)), + @intCast(v_right), + @intCast(metrics.cell_height), + .on, + ); + if (node.left) canvas.box( + 0, + @intCast(h_top), + @intFromFloat(@ceil(cx - r + float_thick / 2)), + @intCast(h_bottom), + .on, + ); + + if (node.filled) { + ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.fill() catch return; + } else { + ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.stroke() catch return; + } +} + +fn fadingLine( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime to: Edge, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // If we're fading to the top or left, we start with 0.0 + // and increment up as we progress, otherwise we start + // at 255.0 and increment down (negative). + var color: f64 = switch (to) { + .top, .left => 0.0, + .bottom, .right => 255.0, + }; + const inc: f64 = 255.0 / switch (to) { + .top => float_height, + .bottom => -float_height, + .left => float_width, + .right => -float_width, + }; + + switch (to) { + .top, .bottom => { + for (0..metrics.cell_height) |y| { + for (v_left..v_right) |x| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + .left, .right => { + for (0..metrics.cell_width) |x| { + for (h_top..h_bottom) |y| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + } +} diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig new file mode 100644 index 000000000..2f608180e --- /dev/null +++ b/src/font/sprite/draw/common.zig @@ -0,0 +1,244 @@ +//! This file contains a set of useful helper functions +//! and types for drawing our sprite font glyphs. These +//! are generally applicable to multiple sets of glyphs +//! rather than being single-use. + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +const log = std.log.scoped(.sprite_font); + +// Utility names for common fractions +pub const one_eighth: f64 = 0.125; +pub const one_quarter: f64 = 0.25; +pub const one_third: f64 = (1.0 / 3.0); +pub const three_eighths: f64 = 0.375; +pub const half: f64 = 0.5; +pub const five_eighths: f64 = 0.625; +pub const two_thirds: f64 = (2.0 / 3.0); +pub const three_quarters: f64 = 0.75; +pub const seven_eighths: f64 = 0.875; + +/// The thickness of a line. +pub const Thickness = enum { + super_light, + light, + heavy, + + /// Calculate the real height of a line based on its + /// thickness and a base thickness value. The base + /// thickness value is expected to be in pixels. + pub fn height(self: Thickness, base: u32) u32 { + return switch (self) { + .super_light => @max(base / 2, 1), + .light => base, + .heavy => base * 2, + }; + } +}; + +/// Shades. +pub const Shade = enum(u8) { + off = 0x00, + light = 0x40, + medium = 0x80, + dark = 0xc0, + on = 0xff, + + _, +}; + +/// Applicable to any set of glyphs with features +/// that may be present or not in each quadrant. +pub const Quads = packed struct(u4) { + tl: bool = false, + tr: bool = false, + bl: bool = false, + br: bool = false, +}; + +/// A corner of a cell. +pub const Corner = enum(u2) { + tl, + tr, + bl, + br, +}; + +/// An edge of a cell. +pub const Edge = enum(u2) { + top, + left, + bottom, + right, +}; + +/// Alignment of a figure within a cell. +pub const Alignment = struct { + horizontal: enum { + left, + right, + center, + } = .center, + + vertical: enum { + top, + bottom, + middle, + } = .middle, + + pub const upper: Alignment = .{ .vertical = .top }; + pub const lower: Alignment = .{ .vertical = .bottom }; + pub const left: Alignment = .{ .horizontal = .left }; + pub const right: Alignment = .{ .horizontal = .right }; + + pub const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; + pub const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; + pub const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; + pub const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; + + pub const center: Alignment = .{}; + + pub const upper_center = upper; + pub const lower_center = lower; + pub const middle_left = left; + pub const middle_right = right; + pub const middle_center: Alignment = center; + + pub const top = upper; + pub const bottom = lower; + pub const center_top = top; + pub const center_bottom = bottom; + + pub const top_left = upper_left; + pub const top_right = upper_right; + pub const bottom_left = lower_left; + pub const bottom_right = lower_right; +}; + +/// Fill a rect, clamped to within the cell boundaries. +/// +/// TODO: Eliminate usages of this, prefer `canvas.box`. +pub fn rect( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + x1: u32, + y1: u32, + x2: u32, + y2: u32, +) void { + canvas.box( + @intCast(@min(@max(x1, 0), metrics.cell_width)), + @intCast(@min(@max(y1, 0), metrics.cell_height)), + @intCast(@min(@max(x2, 0), metrics.cell_width)), + @intCast(@min(@max(y2, 0), metrics.cell_height)), + .on, + ); +} + +/// Centered vertical line of the provided thickness. +pub fn vlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + vline( + canvas, + 0, + @intCast(metrics.cell_height), + @intCast((metrics.cell_width -| thick_px) / 2), + thick_px, + ); +} + +/// Centered horizontal line of the provided thickness. +pub fn hlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + hline( + canvas, + 0, + @intCast(metrics.cell_width), + @intCast((metrics.cell_height -| thick_px) / 2), + thick_px, + ); +} + +/// Vertical line with the left edge at `x`, between `y1` and `y2`. +pub fn vline( + canvas: *font.sprite.Canvas, + y1: i32, + y2: i32, + x: i32, + thickness_px: u32, +) void { + canvas.box(x, y1, x + @as(i32, @intCast(thickness_px)), y2, .on); +} + +/// Horizontal line with the top edge at `y`, between `x1` and `x2`. +pub fn hline( + canvas: *font.sprite.Canvas, + x1: i32, + x2: i32, + y: i32, + thickness_px: u32, +) void { + canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on); +} + +/// xHalfs[0] should be used as the right edge of a left-aligned half. +/// xHalfs[1] should be used as the left edge of a right-aligned half. +pub fn xHalfs(metrics: font.Metrics) [2]u32 { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); + return .{ half_width, metrics.cell_width - half_width }; +} + +/// Use these values as such: +/// yThirds[0] bottom edge of the first third. +/// yThirds[1] top edge of the second third. +/// yThirds[2] bottom edge of the second third. +/// yThirds[3] top edge of the final third. +pub fn yThirds(metrics: font.Metrics) [4]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); + const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); + return .{ + one_third_height, + metrics.cell_height - two_thirds_height, + two_thirds_height, + metrics.cell_height - one_third_height, + }; +} + +/// Use these values as such: +/// yQuads[0] bottom edge of first quarter. +/// yQuads[1] top edge of second quarter. +/// yQuads[2] bottom edge of second quarter. +/// yQuads[3] top edge of third quarter. +/// yQuads[4] bottom edge of third quarter +/// yQuads[5] top edge of fourth quarter. +pub fn yQuads(metrics: font.Metrics) [6]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); + const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); + const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); + return .{ + quarter_height, + metrics.cell_height - three_quarters_height, + half_height, + metrics.cell_height - half_height, + three_quarters_height, + metrics.cell_height - quarter_height, + }; +} diff --git a/src/font/sprite/draw/geometric_shapes.zig b/src/font/sprite/draw/geometric_shapes.zig new file mode 100644 index 000000000..d95a4fd2f --- /dev/null +++ b/src/font/sprite/draw/geometric_shapes.zig @@ -0,0 +1,200 @@ +//! Geometric Shapes | U+25A0...U+25FF +//! https://en.wikipedia.org/wiki/Geometric_Shapes_(Unicode_block) +//! +//! ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ +//! ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ +//! ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● +//! ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ +//! ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯ +//! ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◸ ◹ ◺ ◻ ◼ ◽︎◾︎◿ +//! +//! Only a subset of this block is viable for sprite drawing; filling +//! out this file to have full coverage of this block is not the goal. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; + +const font = @import("../../main.zig"); + +/// ◢ ◣ ◤ ◥ +pub fn draw25E2_25E5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◢ + 0x25e2 => try cornerTriangleShade(metrics, canvas, .br, .on), + // ◣ + 0x25e3 => try cornerTriangleShade(metrics, canvas, .bl, .on), + // ◤ + 0x25e4 => try cornerTriangleShade(metrics, canvas, .tl, .on), + // ◥ + 0x25e5 => try cornerTriangleShade(metrics, canvas, .tr, .on), + + else => unreachable, + } +} + +/// ◸ ◹ ◺ +pub fn draw25F8_25FA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◸ + 0x25f8 => try cornerTriangleOutline(metrics, canvas, .tl), + // ◹ + 0x25f9 => try cornerTriangleOutline(metrics, canvas, .tr), + // ◺ + 0x25fa => try cornerTriangleOutline(metrics, canvas, .bl), + + else => unreachable, + } +} + +/// ◿ +pub fn draw25FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + try cornerTriangleOutline(metrics, canvas, .br); +} + +pub fn cornerTriangleShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime shade: Shade, +) !void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.fillPath( + path.wrapped_path, + .{}, + @enumFromInt(@intFromEnum(shade)), + ); +} + +pub fn cornerTriangleOutline( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, +) !void { + const float_thick: f64 = @floatFromInt(Thickness.light.height(metrics.box_thickness)); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, .on); +} diff --git a/src/font/sprite/octants.txt b/src/font/sprite/draw/octants.txt similarity index 100% rename from src/font/sprite/octants.txt rename to src/font/sprite/draw/octants.txt diff --git a/src/font/sprite/draw/powerline.zig b/src/font/sprite/draw/powerline.zig new file mode 100644 index 000000000..24fce454b --- /dev/null +++ b/src/font/sprite/draw/powerline.zig @@ -0,0 +1,396 @@ +//! Powerline + Powerline Extra Symbols | U+E0B0...U+E0D4 +//! https://github.com/ryanoasis/powerline-extra-symbols +//! +//!                 +//!               +//!     +//! +//! We implement the more geometric glyphs here, but not the stylized ones. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; + +const box = @import("box.zig"); + +const font = @import("../../main.zig"); +const Quad = font.sprite.Canvas.Quad; + +///  +pub fn drawE0B0( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height / 2 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = 0, .y = float_height / 2 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B8( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B9( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0BA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BB( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BC( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BD( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0B1( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + var path = canvas.staticPath(3); + path.moveTo(0, 0); + path.lineTo(float_width, float_height / 2); + path.lineTo(0, float_height); + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = @floatFromInt( + Thickness.light.height(metrics.box_thickness), + ), + }, + .on, + ); +} + +///  +pub fn drawE0B3( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B1(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +///  +pub fn drawE0B5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(4); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_width = @floatFromInt(metrics.box_thickness), + .line_cap_mode = .butt, + }, .on); +} + +///  +pub fn drawE0B6( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B4(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B7( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B5(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0D2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + const float_thick: f64 = @floatFromInt(metrics.box_thickness); + + // Top piece + { + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.lineTo(float_width, 0); + path.lineTo(float_width / 2, float_height / 2 - float_thick / 2); + path.lineTo(0, float_height / 2 - float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } + + // Bottom piece + { + var path = canvas.staticPath(6); + path.moveTo(0, float_height); + path.lineTo(float_width, float_height); + path.lineTo(float_width / 2, float_height / 2 + float_thick / 2); + path.lineTo(0, float_height / 2 + float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } +} + +///  +pub fn drawE0D4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0D2(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} diff --git a/src/font/sprite/draw/special.zig b/src/font/sprite/draw/special.zig new file mode 100644 index 000000000..3d75360e3 --- /dev/null +++ b/src/font/sprite/draw/special.zig @@ -0,0 +1,328 @@ +//! This file contains glyph drawing functions for all of the +//! non-Unicode sprite glyphs, such as cursors and underlines. +//! +//! The naming convention in this file differs from the usual +//! because the draw functions for special sprites are found by +//! having names that exactly match the enum fields in Sprite. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const font = @import("../../main.zig"); +const Sprite = font.sprite.Sprite; + +pub fn underline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_double( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // We place one underline above the underline position, and one below + // by one thickness, creating a "negative" underline where the single + // underline would be placed. + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position -| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position +| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_dotted( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this now that we can go out of bounds, just + // make sure that adjacent versions of this glyph align. + const dot_width = @max(metrics.underline_thickness, 3); + const dot_count = @max((width / dot_width) / 2, 1); + const gap_width = std.math.divCeil( + u32, + width -| (dot_count * dot_width), + dot_count, + ) catch return error.MathError; + var i: u32 = 0; + while (i < dot_count) : (i += 1) { + // Ensure we never go out of bounds for the rect + const x = @min(i * (dot_width + gap_width), width - 1); + const rect_width = @min(width - x, dot_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_dashed( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + const dash_width = width / 3 + 1; + const dash_count = (width / dash_width) + 1; + var i: u32 = 0; + while (i < dash_count) : (i += 2) { + // Ensure we never go out of bounds for the rect + const x = @min(i * dash_width, width - 1); + const rect_width = @min(width - x, dash_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_curly( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this using z2d, this is pretty cool code and all but + // it doesn't need to be highly optimized and z2d path drawing + // code would be clearer and nicer to have. + + const float_width: f64 = @floatFromInt(width); + // Because of we way we draw the undercurl, we end up making it around 1px + // thicker than it should be, to fix this we just reduce the thickness by 1. + // + // We use a minimum thickness of 0.414 because this empirically produces + // the nicest undercurls at 1px underline thickness; thinner tends to look + // too thin compared to straight underlines and has artefacting. + const float_thick: f64 = @max( + 0.414, + @as(f64, @floatFromInt(metrics.underline_thickness -| 1)), + ); + + // Calculate the wave period for a single character + // `2 * pi...` = 1 peak per character + // `4 * pi...` = 2 peaks per character + const wave_period = 2 * std.math.pi / float_width; + + // The full amplitude of the wave can be from the bottom to the + // underline position. We also calculate our mid y point of the wave + const half_amplitude = 1.0 / wave_period; + const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; + + // Offset to move the undercurl up slightly. + const y_off: u32 = @intFromFloat(half_amplitude * 0.5); + + // This is used in calculating the offset curve estimate below. + const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min( + 1.0, + half_amplitude * wave_period, + ); + + // follow Xiaolin Wu's antialias algorithm to draw the curve + var x: u32 = 0; + while (x < width) : (x += 1) { + // We sample the wave function at the *middle* of each + // pixel column, to ensure that it renders symmetrically. + const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; + // Use the slope at this location to add thickness to + // the line on this column, counteracting the thinning + // caused by the slope. + // + // This is not the exact offset curve for a sine wave, + // but it's a decent enough approximation. + // + // How did I derive this? I stared at Desmos and fiddled + // with numbers for an hour until it was good enough. + const t_u: f64 = t + std.math.pi; + const slope_factor_u: f64 = + (@sin(t_u) * @sin(t_u) * offset_factor) / + ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); + const slope_factor_l: f64 = + (@sin(t) * @sin(t) * offset_factor) / + ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); + + const cosx: f64 = @cos(t); + // This will be the center of our stroke. + const y: f64 = y_mid + half_amplitude * cosx; + + // The upper pixel and lower pixel are + // calculated relative to the center. + const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; + const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; + const y_upper: u32 = @intFromFloat(@floor(y_u)); + const y_lower: u32 = @intFromFloat(@ceil(y_l)); + const alpha_u: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_u - @floor(y_u)))), + ); + const alpha_l: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_l - @ceil(y_l)))), + ); + + // upper and lower bounds + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_upper -| y_off), + @enumFromInt(alpha_u), + ); + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_lower -| y_off), + @enumFromInt(alpha_l), + ); + + // fill between upper and lower bound + var y_fill: u32 = y_upper + 1; + while (y_fill < y_lower) : (y_fill += 1) { + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_fill -| y_off), + .on, + ); + } + } +} + +pub fn strikethrough( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.strikethrough_position), + .width = @intCast(width), + .height = @intCast(metrics.strikethrough_thickness), + }, .on); +} + +pub fn overline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.overline_position), + .width = @intCast(width), + .height = @intCast(metrics.overline_thickness), + }, .on); +} + +pub fn cursor_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); +} + +pub fn cursor_hollow_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + // We fill the entire rect and then hollow out the inside, this isn't very + // efficient but it doesn't need to be and it's the easiest way to write it. + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); + canvas.rect(.{ + .x = @intCast(metrics.cursor_thickness), + .y = @intCast(metrics.cursor_thickness), + .width = @intCast(width -| metrics.cursor_thickness * 2), + .height = @intCast(height -| metrics.cursor_thickness * 2), + }, .off); +} + +pub fn cursor_bar( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + + // We place the bar cursor half of its thickness over the left edge of the + // cell, so that it sits centered between characters, not biased to a side. + // + // We round up (add 1 before dividing by 2) because, empirically, having a + // 1px cursor shifted left a pixel looks better than having it not shifted. + canvas.rect(.{ + .x = -@as(i32, @intCast((metrics.cursor_thickness + 1) / 2)), + .y = 0, + .width = @intCast(metrics.cursor_thickness), + .height = @intCast(height), + }, .on); +} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig new file mode 100644 index 000000000..a17ddb494 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -0,0 +1,1431 @@ +//! 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 = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Alignment = common.Alignment; +const Corner = common.Corner; +const Quads = common.Quads; +const Edge = common.Edge; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yThirds = common.yThirds; +const rect = common.rect; + +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, + ))); + + const x_halfs = xHalfs(metrics); + const y_thirds = yThirds(metrics); + + if (sex.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_thirds[0]); + if (sex.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_thirds[0]); + if (sex.ml) rect(metrics, canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); + if (sex.mr) rect(metrics, canvas, x_halfs[1], y_thirds[1], metrics.cell_width, y_thirds[2]); + if (sex.bl) rect(metrics, canvas, 0, y_thirds[3], x_halfs[0], metrics.cell_height); + if (sex.br) rect(metrics, canvas, x_halfs[1], y_thirds[3], metrics.cell_width, metrics.cell_height); +} + +/// 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 y_thirds = yThirds(metrics); + const top: f64 = 0.0; + // We average the edge positions for the y_thirds boundaries here + // rather than having to deal with varying alignments depending on + // the surrounding pieces. The most this will be off by is half of + // a pixel, so hopefully it's not noticeable. + const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); + const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); + const bottom: 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); + + 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; + + const x: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + const w: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + rect(metrics, canvas, x, 0, x + w, metrics.cell_height); +} + +/// 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; + + const h = @as( + u32, + @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + ); + const y = @min( + metrics.cell_height -| h, + @as( + u32, + @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * + @as(f64, @floatFromInt(metrics.cell_height)) / 8), + ), + ), + ); + rect(metrics, canvas, 0, y, metrics.cell_width, y + h); +} + +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); + } + } + } +} + +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/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig new file mode 100644 index 000000000..0a57a0439 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -0,0 +1,193 @@ +//! Symbols for Legacy Computing Supplement | U+1CC00...U+1CEBF +//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement +//! +//! 𜰀 𜰁 𜰂 𜰃 𜰄 𜰅 𜰆 𜰇 𜰈 𜰉 𜰊 𜰋 𜰌 𜰍 𜰎 𜰏 +//! 𜰐 𜰑 𜰒 𜰓 𜰔 𜰕 𜰖 𜰗 𜰘 𜰙 𜰚 𜰛 𜰜 𜰝 𜰞 𜰟 +//! 𜰠 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +//! 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿 +//! 𜱀 𜱁 𜱂 𜱃 𜱄 𜱅 𜱆 𜱇 𜱈 𜱉 𜱊 𜱋 𜱌 𜱍 𜱎 𜱏 +//! 𜱐 𜱑 𜱒 𜱓 𜱔 𜱕 𜱖 𜱗 𜱘 𜱙 𜱚 𜱛 𜱜 𜱝 𜱞 𜱟 +//! 𜱠 𜱡 𜱢 𜱣 𜱤 𜱥 𜱦 𜱧 𜱨 𜱩 𜱪 𜱫 𜱬 𜱭 𜱮 𜱯 +//! 𜱰 𜱱 𜱲 𜱳 𜱴 𜱵 𜱶 𜱷 𜱸 𜱹 𜱺 𜱻 𜱼 𜱽 𜱾 𜱿 +//! 𜲀 𜲁 𜲂 𜲃 𜲄 𜲅 𜲆 𜲇 𜲈 𜲉 𜲊 𜲋 𜲌 𜲍 𜲎 𜲏 +//! 𜲐 𜲑 𜲒 𜲓 𜲔 𜲕 𜲖 𜲗 𜲘 𜲙 𜲚 𜲛 𜲜 𜲝 𜲞 𜲟 +//! 𜲠 𜲡 𜲢 𜲣 𜲤 𜲥 𜲦 𜲧 𜲨 𜲩 𜲪 𜲫 𜲬 𜲭 𜲮 𜲯 +//! 𜲰 𜲱 𜲲 𜲳 𜲴 𜲵 𜲶 𜲷 𜲸 𜲹 𜲺 𜲻 𜲼 𜲽 𜲾 𜲿 +//! 𜳀 𜳁 𜳂 𜳃 𜳄 𜳅 𜳆 𜳇 𜳈 𜳉 𜳊 𜳋 𜳌 𜳍 𜳎 𜳏 +//! 𜳐 𜳑 𜳒 𜳓 𜳔 𜳕 𜳖 𜳗 𜳘 𜳙 𜳚 𜳛 𜳜 𜳝 𜳞 𜳟 +//! 𜳠 𜳡 𜳢 𜳣 𜳤 𜳥 𜳦 𜳧 𜳨 𜳩 𜳪 𜳫 𜳬 𜳭 𜳮 𜳯 +//! 𜳰 𜳱 𜳲 𜳳 𜳴 𜳵 𜳶 𜳷 𜳸 𜳹 +//! 𜴀 𜴁 𜴂 𜴃 𜴄 𜴅 𜴆 𜴇 𜴈 𜴉 𜴊 𜴋 𜴌 𜴍 𜴎 𜴏 +//! 𜴐 𜴑 𜴒 𜴓 𜴔 𜴕 𜴖 𜴗 𜴘 𜴙 𜴚 𜴛 𜴜 𜴝 𜴞 𜴟 +//! 𜴠 𜴡 𜴢 𜴣 𜴤 𜴥 𜴦 𜴧 𜴨 𜴩 𜴪 𜴫 𜴬 𜴭 𜴮 𜴯 +//! 𜴰 𜴱 𜴲 𜴳 𜴴 𜴵 𜴶 𜴷 𜴸 𜴹 𜴺 𜴻 𜴼 𜴽 𜴾 𜴿 +//! 𜵀 𜵁 𜵂 𜵃 𜵄 𜵅 𜵆 𜵇 𜵈 𜵉 𜵊 𜵋 𜵌 𜵍 𜵎 𜵏 +//! 𜵐 𜵑 𜵒 𜵓 𜵔 𜵕 𜵖 𜵗 𜵘 𜵙 𜵚 𜵛 𜵜 𜵝 𜵞 𜵟 +//! 𜵠 𜵡 𜵢 𜵣 𜵤 𜵥 𜵦 𜵧 𜵨 𜵩 𜵪 𜵫 𜵬 𜵭 𜵮 𜵯 +//! 𜵰 𜵱 𜵲 𜵳 𜵴 𜵵 𜵶 𜵷 𜵸 𜵹 𜵺 𜵻 𜵼 𜵽 𜵾 𜵿 +//! 𜶀 𜶁 𜶂 𜶃 𜶄 𜶅 𜶆 𜶇 𜶈 𜶉 𜶊 𜶋 𜶌 𜶍 𜶎 𜶏 +//! 𜶐 𜶑 𜶒 𜶓 𜶔 𜶕 𜶖 𜶗 𜶘 𜶙 𜶚 𜶛 𜶜 𜶝 𜶞 𜶟 +//! 𜶠 𜶡 𜶢 𜶣 𜶤 𜶥 𜶦 𜶧 𜶨 𜶩 𜶪 𜶫 𜶬 𜶭 𜶮 𜶯 +//! 𜶰 𜶱 𜶲 𜶳 𜶴 𜶵 𜶶 𜶷 𜶸 𜶹 𜶺 𜶻 𜶼 𜶽 𜶾 𜶿 +//! 𜷀 𜷁 𜷂 𜷃 𜷄 𜷅 𜷆 𜷇 𜷈 𜷉 𜷊 𜷋 𜷌 𜷍 𜷎 𜷏 +//! 𜷐 𜷑 𜷒 𜷓 𜷔 𜷕 𜷖 𜷗 𜷘 𜷙 𜷚 𜷛 𜷜 𜷝 𜷞 𜷟 +//! 𜷠 𜷡 𜷢 𜷣 𜷤 𜷥 𜷦 𜷧 𜷨 𜷩 𜷪 𜷫 𜷬 𜷭 𜷮 𜷯 +//! 𜷰 𜷱 𜷲 𜷳 𜷴 𜷵 𜷶 𜷷 𜷸 𜷹 𜷺 𜷻 𜷼 𜷽 𜷾 𜷿 +//! 𜸀 𜸁 𜸂 𜸃 𜸄 𜸅 𜸆 𜸇 𜸈 𜸉 𜸊 𜸋 𜸌 𜸍 𜸎 𜸏 +//! 𜸐 𜸑 𜸒 𜸓 𜸔 𜸕 𜸖 𜸗 𜸘 𜸙 𜸚 𜸛 𜸜 𜸝 𜸞 𜸟 +//! 𜸠 𜸡 𜸢 𜸣 𜸤 𜸥 𜸦 𜸧 𜸨 𜸩 𜸪 𜸫 𜸬 𜸭 𜸮 𜸯 +//! 𜸰 𜸱 𜸲 𜸳 𜸴 𜸵 𜸶 𜸷 𜸸 𜸹 𜸺 𜸻 𜸼 𜸽 𜸾 𜸿 +//! 𜹀 𜹁 𜹂 𜹃 𜹄 𜹅 𜹆 𜹇 𜹈 𜹉 𜹊 𜹋 𜹌 𜹍 𜹎 𜹏 +//! 𜹐 𜹑 𜹒 𜹓 𜹔 𜹕 𜹖 𜹗 𜹘 𜹙 𜹚 𜹛 𜹜 𜹝 𜹞 𜹟 +//! 𜹠 𜹡 𜹢 𜹣 𜹤 𜹥 𜹦 𜹧 𜹨 𜹩 𜹪 𜹫 𜹬 𜹭 𜹮 𜹯 +//! 𜹰 𜹱 𜹲 𜹳 𜹴 𜹵 𜹶 𜹷 𜹸 𜹹 𜹺 𜹻 𜹼 𜹽 𜹾 𜹿 +//! 𜺀 𜺁 𜺂 𜺃 𜺄 𜺅 𜺆 𜺇 𜺈 𜺉 𜺊 𜺋 𜺌 𜺍 𜺎 𜺏 +//! 𜺐 𜺑 𜺒 𜺓 𜺔 𜺕 𜺖 𜺗 𜺘 𜺙 𜺚 𜺛 𜺜 𜺝 𜺞 𜺟 +//! 𜺠 𜺡 𜺢 𜺣 𜺤 𜺥 𜺦 𜺧 𜺨 𜺩 𜺪 𜺫 𜺬 𜺭 𜺮 𜺯 +//! 𜺰 𜺱 𜺲 𜺳 +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yQuads = common.yQuads; +const rect = common.rect; + +const font = @import("../../main.zig"); + +const octant_min = 0x1cd00; +const octant_max = 0x1cde5; + +/// Octants +pub fn draw1CD00_1CDE5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + // Octant representation. We use the funny numeric string keys + // so its easier to parse the actual name used in the Symbols for + // Legacy Computing spec. + const Octant = packed struct(u8) { + @"1": bool = false, + @"2": bool = false, + @"3": bool = false, + @"4": bool = false, + @"5": bool = false, + @"6": bool = false, + @"7": bool = false, + @"8": bool = false, + }; + + // Parse the octant data. This is all done at comptime so + // that this is static data that is embedded in the binary. + const octants_len = octant_max - octant_min + 1; + const octants: [octants_len]Octant = comptime octants: { + @setEvalBranchQuota(10_000); + + var result: [octants_len]Octant = @splat(.{}); + var i: usize = 0; + + const data = @embedFile("octants.txt"); + var it = std.mem.splitScalar(u8, data, '\n'); + while (it.next()) |line| { + // Skip comments + if (line.len == 0 or line[0] == '#') continue; + + const current = &result[i]; + i += 1; + + // Octants are in the format "BLOCK OCTANT-1235". The numbers + // at the end are keys into our packed struct. Since we're + // at comptime we can metaprogram it all. + const idx = std.mem.indexOfScalar(u8, line, '-').?; + for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; + } + + assert(i == octants_len); + break :octants result; + }; + + const x_halfs = xHalfs(metrics); + const y_quads = yQuads(metrics); + const oct = octants[cp - octant_min]; + if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]); + if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]); + if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); + if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]); + if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); + if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]); + if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height); + if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height); +} + +// Separated Block Quadrants +// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +pub fn draw1CC21_1CC2F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + // Struct laid out to match the codepoint order so we can cast from it. + const Quads = packed struct(u4) { + tl: bool, + tr: bool, + bl: bool, + br: bool, + }; + + const quad: Quads = @bitCast(@as(u4, @truncate(cp - 0x1CC20))); + + const gap: i32 = @intCast(@max(1, width / 12)); + + const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2)); + const mid_gap_y: i32 = gap * 2 + @as(i32, @intCast(height % 2)); + + const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2); + const h: i32 = @divExact(@as(i32, @intCast(height)) - gap * 2 - mid_gap_y, 2); + + if (quad.tl) canvas.box( + gap, + gap, + gap + w, + gap + h, + .on, + ); + if (quad.tr) canvas.box( + gap + w + mid_gap_x, + gap, + gap + w + mid_gap_x + w, + gap + h, + .on, + ); + if (quad.bl) canvas.box( + gap, + gap + h + mid_gap_y, + gap + w, + gap + h + mid_gap_y + h, + .on, + ); + if (quad.br) canvas.box( + gap + w + mid_gap_x, + gap + h + mid_gap_y, + gap + w + mid_gap_x + w, + gap + h + mid_gap_y + h, + .on, + ); +} diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm deleted file mode 100644 index 6082475af7d3e2264cf04061e9f63d7c6e6fdc9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048593 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 z 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 z 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..8e54879661a7e7ff268d9ee5819a4dd253c98633 GIT binary patch literal 534 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|Vn-PZ!6KiaBo%7&0<2 zFdSO&)jyY8Ujw873H*>`U|{(F|IOu#%MJ%fv|RjN|EgB+TC%u!q(_3t& Z++hvq|84tv-WeN^#h$KyF6*2UngE#jpY;F$ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd28ba0d428b099b2f709b64e4458d7770942d6 GIT binary patch literal 1022 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfr0s`r;B4q#hf<>3>g_1 z7!ED?>YvN4uK`kk1a?R=Ffjc8|K_kEF9QQZ!-9Wnbsus{Ga;#9U|=xdf~aZaVPIf5 zpiuu>IcC8&7F0D4Six$pIC3=yFdPYZU;nZ!@P*%s5H?Qz2gla8D6Zf>P`Jk~rs_%c zz8FWL{z>|Tt42I9ALDk^)`GFX8C73=hMh_fJpZh=q2kt3Mj~@s@ z_Y|hb59pw)!Spz$nzIl1(E|B^FuFOlJ?If}0GhgRhfRSX*c9obixWh8ngSby85lC6 WKOK2{rEn1_U3j|sxvX9m z?p%L3fu;Yrp^Oq&t7B^rSL?y^T#O&qb2Bjf|NrK&Auj_1L&Ji9YyX~QkY<9J1px(& z5H*aPU^V~OOHXP-Q^LcFrsTIE7syLU;Day&nwr~!8X8Cn7#J81xP#TaJ>19x;wjX> g)|OeYjfH_Bf%A2D-GueIETtf2p00i_>zopr0BHeV%K!iX literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b2aef99d9f2940399c7b29f5191bfef318ed62 GIT binary patch literal 1275 zcmXZceN>WH90%|V4?ezuFQr1x%9XwBgoImVGr%+*J!#pC7PHi6$68A)vDq;3Bxq%= zki|~9vgS-(GgCQp6$f7cIbvoS>432$C=%7b-#xp18=K@6 z>;(WIA)DiN0stHU;2a(R0MLfCy#s*IfyBim=U*K)s&8sS{Wd;wx{_|_;@x)et_rXG z+*~lfuzMfBjV63;&KPR4{Ke#-^5s{OLK;~CQB;o1swt^RVBZ-n>%ARIj$U~wlp{mc zzJyZ4z4I-8RE}d%eopBP1Awk-$dC+~(psMvN%p16P_>>BwR_x4yx_+XyCsw(vrein z*AA#x(cWDSTz;fL-{s~VRQMeo05Cp@1Hk1PHOBaKV7&L03R;EVMs3t-_0)XwF1O=m z|LF%!bw=~Y+=NBJ6iR-(hL`=uOPrL-%*YvKK`;!{K4le^bfyQ<=?+g6k%l4tN@L3> zjp@tx80=$CQ!0vDpIM(ULMn=`NUC-x$+i?U<0%w5b^U%Y6FkkwL68w1*6Sh{%zqr` zan-Oq77JN+%uG@)WO5CsPcT>s)5#Hdx&yJ2!7SFGK6)yYxbcz8mDRHUw0~j%b-K?S z@BZLW5RG`^gX?~LvwD;ELU=45UZh`AKB|> zac{X186M8Jxfhv2ktnBl86sU%U;Ss{(+ivlRGcKY!pSUE?hZLE>M{Tw#tLz zxt!0c!w_k^Zlv6vYj~dBT`Lla$lk_8@n!Ggud)a*O!H}PpY)v5#rTxWb>MFr;=zFl z>Q?1P^?B(nx%%ldPs1GVa?7#nZcuLsx8=Yvd<066h8>EnEpN;l+wvjWI%9ve%Q@qqYfF#cR%^88B*44trrc9GZhzpm+cKzRk2pUPCcr0;emF>oy#MXgne-k)3?Wh8*0gV>y-bT1xF{+ni)$nL@3L8F2f77{k3GbZ0014o%rrDiZ2d+klgVVffiw#ChXDYDa-3Xu@_J_FF*$Zb M;$#-hdl0f3HG3jhEB literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..0a616d72cc695dac524733880abe56fa572797a1 GIT binary patch literal 1870 zcmZWq3s93+7XC>>2r&r(!UCaogAEp?lti!*v+Vw15lEE~At>^a5(cwjpj4~~tcLuN zmxv%K_yFBsc2{GN0PH-Ff5;oLX?j1^aznmV3zzcG14k1x+f58W7QQ4$BTv%V^(Ksp`9 zD2GSBX!h=s#{K-9d6zvO26fp(7>hCIJ9B zR{d|MA3xIM)xCJ&@c9pg9^X{hA9VNpSiz0r#9S+=nljAl$+4Bw2`kk-U!iORh%D61 zT|v5bgz>w9?XvH)hJOzLfXOudn2zY}1q&%>8p9anu{ki0s-p=-rdo?E{P zL=X3yl>p*@>A5j6u6{PF#}57mpJ+~UMnX*&VqOfb)+AnX5j@2E=+We(NPMd}@15L= zgKzhumx^(T6lAWwnSR;F8}~O5?$dDS|GX9nogz!+3!yP&oRO2zkaoF^ehSPE)D+d& zoPRYj{Z6--j&NJ;_LCgA_dU z=ZIo4f)C$wa;KrOXT=EV5fUubDZ+uzF zxl-xQ+Sp!SHD-(ZN3I1F=v((frn~2_u~yn2Tz}0=epTa|u2Ix|=kTOCp8}1{xE8<} z*)7&eQ+vhqu&!oz<*oU8FW{#aE5m=`ZJyrN>w>zvR6r-S`Aoz;SdSwsM2@)<5 z+52{Mx2I*jIOihF2qW*MH_m=*Zg!l;MHr?RHBiNG;=%R*v%32>U03Wpm^%_O>K#E5qqh4X zYBw#kcC6yVGq{yEnX;aN%>zl31floWR?W_4D)$guHH{O8Uti6Xe)(nIMnlA6NEdCg zQ| literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..ecbb650a1c0b08f8038ff65ecd1a70d91f5b8e37 GIT binary patch literal 3404 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 B*17-y literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6cc75fa6d12fd7fe88e1caa1bfc6382b39a733 GIT binary patch literal 1101 zcmV-T1hV^yP)h{!1 z$T8kDiEZeN)WaeSJGm5oeKD^(;msG!+G>wCZE$8+Twgz<& zLTDu+vFVwR@O>vdAao@lgiy9neFcONS~>s#0F-n5&tz7=3$G6iXAnXtTgV?08HUC1 z%iK`Z4+tStF7(hvS@&H3a@_(#-NW8O8-xZG5c)GA007vCHvj+t|NrcjF%AMT3A@wj%$fykn0uvCrOb7q~ppvzDc~_Ku<6dBD9fT076bfSg9}Jn^=V5;j2qA<} zfsix^sWVgP_@H4xNIi@b0ssJz;e5-kO{NSCIkQgAb}ZAW#PKnc_wl-LI~{^s+OH0a?Un=4o>R*2qAPr zXy(-yLsp@No{j@T|1VT-5e(r!LC68296|s90H-h$$uX~#&q$X;NWI*=P(KW8ZNKUa z5$eMTA(UHa^o?dwB@jYrGxI{>FIO}OR6r=15C8zg`2YX_|Nrck!45(p3A!{u zAykZAE)oPl2%%ySLZ}#o)P(@x&3pg=0RR8(!vP5Z004lX|F7E-C@25`008_2;B=jV T=N* literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..58afe3de7ab7579b86fd68a5e0dfff03db55dd65 GIT binary patch literal 5450 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..784622dd0d6d0db26ac98c6342373459b20fad90 GIT binary patch literal 5724 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e488525135da1459ef46642d2ccb21bb9d1018 GIT binary patch literal 9997 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 F`(L$nMXLY+ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..9be9501ba18ef61d9715e67027bf5b9ef23e861e GIT binary patch literal 4298 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 bjno~6&hMTW%=PxaPYN{^O-#9xW!V1#Cja!) literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png b/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..100d9fa18d6f3b5d73c93c83ca1e32288da0a875 GIT binary patch literal 2223 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png b/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..304cc711f6c5ca26da0ce6c5066931775cbb542e GIT binary patch literal 1547 zcmY+Ee>~H99LGQNqiKW=M^|U+A(G5$U6GOb@hHl+$`LK;9@pyTT%jMwu+77dx}+b@ z&rPU@v)03pXf#HfJg$}>O`)?257tp=Y|9Muy~lSvy1PG~|GwToUhmiY3YoZX4eA>d z0ANkf{(!>(ARGWN@HGJd028&G4S?~>pa4SXubfHQ15}%@Wnj-DM5?)#;V{0cO@3!A zl!ecH+IBu+0}wGHV&0V|YG)RcuM?>a!6YhPT5wRyVxC05?&n06%Q%#B*@od%uLZn7 z#rF~TEdv(5@}%0p3l;~_7arI zN+{$h^V(SY4St>HZP{*yG}n~(S)N9N#lg86u4LwmZ#CI002zw z&xH}&s?#ENIzQR!Igz#g4K)=j(?J+hf& z_zLe+x-YsoV6R8W+``#BIjZj#r92WYSNr&;*Ub+&_Y*#UyfDj)@*HK_Q=cP^w$1iU zLDls544NJ$)Hm1`lMK$HoVOM0E)+oHHUcZ4rZA>I>kI@xGkGl zm7JbX=Dsl-vmfDQFHtXHwF<(nJ7-X=`ZHR@w5wCc zjrT=tcXS8t?n^HmW_;6;R)^^N%KoLwZix-A3kK0zGiws150XAiLQNAQPN}HjhH)?6 zat-xD002COeq2|F#|`vMc4h}FIvb>zg2@9r^X$>5Nw%+{QF-r|(FWPV5TC^V?n&!}_RlXZO1?XgNVRTJ zWh2$22ek+UJLUK>jXI5{k*Co#pfRL>X2ff}3Tlh_@}0IRYmR$g_#)-+uJ2}QfPhN( zH8T=HP!hTpX|@01xA#Q{O#E?D`(8r;+{3hXb*--!RS#N^pi;(iOq>1+YITT?sxQj( zL0=$J${r?+7*v!PPSP4QjKd#Pz0IR6d>regvfVhi!*E;C$B*}NANl?SV+EM*^N1w3 VL35k)9u4r*3koC#RQpAz{R0dG@)ZC8 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png b/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..012a5b952848f82e54e56fbb0568b14e15cbd914 GIT binary patch literal 2490 zcmYk73pCUHAICql&1KA#lA;hImrN?5vX!2wr{=CklBbqyxkM|MhLCuwV^prK>wl#w zVQ%F?(f^?_<<@9!ixU4_D?Hl&Y-9ib@0{29owKv^{d|7!&-?TKrZ^tjhsCVH007p( z-p&~S!2m$|$O8b7j0V*K6yhB0h_2M!(GGJA4X=de@`dk0!j(Xi{BeXiD;8l2h0^sFQH4A0U7@`S(SlLk)Bd5}n$Kvb6 z1ZOP=yl-cNl2t!3tS&)tN$zh1P^YJ^zL{S&5-5B$=HkI*k&fPB?}|v|&57%}Y?4-_ zIm)BB^SzbEc7AEEqPXv57XoJxi_K|Nk1j{Irr;apZ>eHSKecUbyW!I5=&1}L9Hdy0ZX%`zFTb~TzPHlA5T)z8%Yy4>J|1~GcaHo$ zLui|cr36%6xNES3erkvk$vdr6M z<@5U-ix|nqidPu}*Ngx{8sx+u!Yjfg;+4>jLSey2FI0+t7u8CRKUtELW3qZg((-uF zr@{_~MVUvTt+y9RH)2w-RN zesjIz*|VU_l=lnO?~^z3c3m1BY;dh^$Bg*T2HHEeT&o%#aahNu)g_xU#*hC%wSAI| zs7FUR5f386t@%@1yQ?GOoBlDkvm8r;Oe}aZRdw2)cI*_7~U-z4Q>dwv1JaCK{s9YV^nJwT%68dw-Xmys|Yu&)7W&iN4 zE$ZRTWG_z!+bSZCH`Im>$U(w*?|#zxSPOsrnbHE|$u@LAlglcmDd{MKy(4-|GP`b~ zC}2b;5`tp@_$)!jH%mHd%A>L|cUZEdop-RHqk?YzRrL^o zN_zG?zqof)@YV?@iTMf!83#4|Vxdk`>*hXGDsFm0^h!2bk}c%DRGvhDBW}Oy>w$TG0^+`^vaT{@?Sv60IZbXM~dc5>>k-Q24M8HTgi3x_N3&%8!I zkOI*C&yG0Hxf_Um&&nC~O*GxZg{x4KMr_tf+U*fXa#HJ$i(hSeS!xd^rZhvI^pkFC z6LA2@u_u05>*C_5ZL@FdJoyonu;QcKaY_}VpTnM01kKRnJDi3Id0q_i5^JH)CGxj4 z&s|L0=~GvwSA*!9SO+xQuk?D!m+;InU@pFWE)JwUX@$ zCE62C=%vY)9}l~`@^MyAerBMu_XjtZb!>Efaxih?xXl7Oc5%%+%`uno>X%yQdwFEX z>*{xBsW^7_K+7}EXSo-aC&SZB)X(L!%E@{h~1y#LYj6dmGJBR~&Rsv{$5zD4gf6r${TD%kf1V9<;)pKk0>`_s(4to#T J74G(l|1bGGWx)Ud literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..563f23d093b48bd455c244ca49e3b3448efbab10 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=GUMo-U3d6?5L)aO67V zz{7gs-v3LYEaFXzC6=lF+_PQNglDq2eUu#+4iaGbIrb~-^0wd?_GCCt|QQ{ zul&QzYiFl28D6h?P;KA3``t-y76t}^?bExtUBdUP@06=JZoGE3Hmj1)oj5%`0R{#K zFugeB-)-ypnRn-?o-C=?J8*6G(^K<)HC2CNVq`G5&dk8@|3An%%?<)A2mb%J(x31^ zWruC9XvGHui?fdsZy#1H-5V3D5OjCX#pCB)^v$_kcI}E=2QvMDFIXmQ=f2PH=XA#9 ziFE8$i;ezN{)_43lK5TEk4$e6{U^i0@L=t$s-3$Wmi(%lbe(;kh49`x8Um?cA)A*q zlb2JoXSyyMp3gRMU3_1Wp&|d+*P_Q_N>`7%$k(e^?O5CD zasBG_Qrq-3#G0u@eyg(C9mL0)xdV%V|b_0v~Og;P%MnpmS&WwHD2%eZ%M zzn}fIWck0t#lM&)LKGUo6i$5g)J?t~Y|^E*uTF>AF4q@$x$kGJN*$-f!{dz%4D)K? zLC0*!(em*3{ax%DLSbxkeg5(P+Tan9`ddK#U3h$l$<)uwmaOc!y-xcv^Fytz^6*`S_!j9gPMnC(B#gOKee)<6vNT5L&o1=8i)_>Ay)|*~@K`@5W>+v{?RE z{U<-?{$z6jwYTb-k7Dxnvwn17U|?9kO5bJHDND->AxCnrPyN_fF!kKMcQwa$SvpO+ zUCVRe*6x2-sz0$cZTzd^JLmMW}P}oq-AqFTej`-n0APHS6z8oHI^Fon$`X eWHF)RM68|Cr4Ns$rfU_0@|<pUXO@geCw+GruYT literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1569bfaba5e3e5faea55947ed3bdf3456e6736 GIT binary patch literal 1104 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=Aydo-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C_^xybKHs4GaFQ)qTh*&4i?afq}t*6QZV(hk=3N zfI|IiZJ7nzSWwj@a6!~Ca)QnIzg~J$6Phu1SkaXH7UTl?00|U`L!1*4yYY(x&rwF9 zFelYDE!qo(v=}+H89Ap0aC)p@QF$_3FV;gNwfeOA;aZFIW&AT+4xH57{{w2ygY}1B zcqiWab3gC+)1(JR|1MRZ$Z7rR{H|qV$K}4dDM3%3O>9knCTDf7kLy{m%i0n*mt(zK z@?>3gSl>^&v-Hdq5?bA%Wd34+cO_73##gqtv{2>_459HkEX>dPkcYgy;iNLOL*GV8+)^!weZh; z$(4LMw5!Hm)>`c6&;V&jd2>YrVkK6I-u%gNIXiDGyEm#(?l`hVpO^P|mnt-=5M0^0wzXG~VH>D;QDb8FFQ_hmQLq?FIx`LeYz!%H8@A3dvD7Nmq*&~gLK~~T3YRmGK#6J{L{L9o+MJbgo6kDkL4jr z^!8cKW(NV*2SM9Jyf?H=nIxohG)U*Dn3IqSOPOtJ!{>ecHg9EWI^OXw=4M!tentQL zx4Zh`htvaK_iQ`VIQ`v+AhUw~ia&pRT$1o0F#6frj0d?98?N%$@NzGk)OdYz*Vcpw zd(Bm4Z^dbS_?u~`J2T$2ez$U+_B+nwdk@cyf4Qg9B7dpq_G#yC>mGgE{@Kqa=-d`q l`uCCpyYMX{FCt|$b;fYz@5)a&<;_6KJzf1=);T3K0RVa7(p3Ne literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png b/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..b77aee11eedc8bb85ce379c2fc4844ee22653e64 GIT binary patch literal 1251 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png b/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d09df5e457b30258dda2c21cd7dcd49516ef62a5 GIT binary patch literal 1114 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=Axao-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C_^xybKHs4GaFQ)qTh*&4i?afq}t*6QZV(hk=3N zfI|IiZJ7nzSWwj@a6!~Ca)QnIzg~J$6Phu1SkaXH7UTl?00|U`L!ARR=C+^)nwt-V zLe$JO^!w}}&|1u(;$^{LD#q^ca>9xg8WWE)3h?qywU)BtYf$y#Yj8>JWKIYyW_S@) z(BP=DZBFHG>(`2YNztE|#P6S1mTojZ^6fg?Z|75tX8NdI_HfywIsBJJ(Z zynCaO#LoHW>(Mv=_9>N3I52#M~lh+2R`gG{c%auyomZY3i z4RMm>+@7??rDJBG)`REiY3I+2ZnRAG)>|xd>ckGwWYvT-Jwc52c1G2eD?8>2rGCAy zd9q_!fSd8Fu-1)<2`7AdZ8pFB{rKz4f6pF0Dae2KozGt{@7?@3`+ct^`T5^WU$;&` zOaJ*#BdyPxJ-(NImH*dTHih3R^y?c2Fjeikw)itL;- zQ;=yt^P7oTp3I6LPIrfevaR`$S#j_%SO5QYToTs|))Z7mPkPF**;Z%zwkEq-mk+xV{XvWk_^=hYny2gWSWA#${7Vtzc_uAIZvv9UsBQFj_Si@mQD#3(GBID zf=q{hE9}wj6Og#~<6-pyj>d7QyE%jT{91F{JH4nq~B}9eu&=B zjOG$+SiJIvMjGF-Wu@zN*1maDRsOw7EZgAc({Gt`wxwQRD(DW$5iw>@J}tFlP0p?d zaW{B&wciQz-Mj3#y8plGRX2{d$i>fH)v-KFEqvp_GYdoBD2D%Ee@LL(sz)Fx`GDOa z2QAqRMN_j{vv<7RG^gU40jv7wkWZ85WqenhTQEIq_eUR_-6EWmrw1Q3*S+ewSa_cE z+J3R~mM(`p%+=0JHaHqjEjmOLkKjy!k<<4V$NcG?bWuAodKh#zw z*M!TKzSYdze_`I`@4@nBhJUa8PCi|H{acdo^vTkJe@nj`uc`dN)xYK0)u4*Zm49@X zwK}bJjg2k}^zPi!?)^^HV9vDehg)_j8qArdoh8AbFqwJY1{-?;Yy0=04Cv|V=d#Wz Gp$Pz$r`l}* literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png b/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..f88688c1409204a3158c8c30902d1e1ead9afce1 GIT binary patch literal 1423 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..3a60a59d5b294d62b39cf4f00c0a75aa1c9d8981 GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E80JY5_^D(1Yoc96H( zfTuOlEWyO>14qvhjh+dCMop`?WzLFo;E7ykHBtkdoKbyIeKxa5!Ux$Boc){cGNfBfU}(9@AcH#e44Fd9Vj|G6dhK2?I*WVV@(11&W!2w^W z8be;NntyB8K4g?;LREB!1!4|dP3}WhbTu(dXlisHa^h0c$iu+Ea6qB{wQ|gYZ7isE wNid?hS$a|vnschbO5P&8^;?h&6ptX#!z#vB!P|9P?mvj<>FVdQ&MBb@0NY@q&;S4c literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png b/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..8b3134e52adbba897a0f480d92636471d713cd7b GIT binary patch literal 636 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|S$@r;B4q#hf?SALh0g z@ErY^bBLqo07uV(3`xz!6)fSR0#|~rPTTQW^2`6)8q>}%A&u)r7#QC9g$Aq2O)L)# zxRNR&GNmG5LSqw8V>xeV=<5j@f>Yx?FMpf1+eYnTzR}l|o0Fb?G?qL3Ys#uOXZvocFCI?-%66Pb-PvShCx5o9&6+JCEgVpLn}$gY~~SZdXS(E+ep0 zL{b(gfcyvn9`Css82 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png b/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..64b1a80b8d1d89ee9a062a167cde4161afa90df2 GIT binary patch literal 1218 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq|vd)5S5QV$PeH4|AIh zL|Sfk2-+|yr;4~$DjRsYRfJv@ySdDzK(|i3`2KpA>7T9Vs!qCbSfNG0i9@kPz-hvx zclRbY9hlR*;L;DF8Jh(|q~e*1&6>O(dncT83&^bDJadynt5?6#QrdCpM^%G#;Kc|(uiLvL|{TmujqgWGbew`|wQna*b!uqgv>QDEJ za(rf5Ei$_;anpT~O22A>N#RSJDr@%E<%m}LRVzHJx-duk^}Fb`CyMVeZ@kiM6TWP| zdGNX%9q)A_a@n@&k3Ytq|2L=GMqoYrT6yo|hehW#AKiO)j(w$qmFBwpyqA>w{s&z5_^`_XpAU^@l|hX4QHTr}imP+&OX@PA$52jPW0<({HnoY*A4gzZ1L zVCDUU3s>?DGG3WWn0%GyVFE<~6g23|K(rrbxEkMw?C5INqnpF~ zkQ-f%H&jg{4+8_k0fqY4+%gNdv7kCAjG)gCgrF zL4eJ{YC+3>mgz5qG#V#q990$gx%A1AgSU=&%-k6=e z)wF!}_JuJ~`Ks}WxqH`E{yVi`{ try drawSingle(alloc, width, line_thickness), - .underline_double => try drawDouble(alloc, width, line_thickness), - .underline_dotted => try drawDotted(alloc, width, line_thickness), - .underline_dashed => try drawDashed(alloc, width, line_thickness), - .underline_curly => try drawCurly(alloc, width, line_thickness), - .overline => try drawSingle(alloc, width, line_thickness), - .strikethrough => try drawSingle(alloc, width, line_thickness), - else => unreachable, - }; - defer canvas.deinit(); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - .width = width, - .height = @intCast(region.height), - .offset_x = 0, - // Glyph.offset_y is the distance between the top of the glyph and the - // bottom of the cell. We want the top of the glyph to be at line_pos - // from the TOP of the cell, and then offset by the offset_y from the - // draw function. - .offset_y = @as(i32, @intCast(height -| line_pos)) - offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} - -/// A tuple with the canvas that the desired sprite was drawn on and -/// a recommended offset (+Y = down) to shift its Y position by, to -/// correct for underline styles with additional thickness. -const CanvasAndOffset = struct { font.sprite.Canvas, i32 }; - -/// Draw a single underline. -fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a double underline. -fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - // Our gap between lines will be at least 2px. - // (i.e. if our thickness is 1, we still have a gap of 2) - const gap = @max(2, thickness); - - const height: u32 = thickness * 2 * gap; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - canvas.rect(.{ - .x = 0, - .y = thickness * 2, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = -@as(i32, @intCast(thickness)); - - return .{ canvas, offset_y }; -} - -/// Draw a dotted underline. -fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dot_width = @max(thickness, 3); - const dot_count = @max((width / dot_width) / 2, 1); - const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count); - var i: u32 = 0; - while (i < dot_count) : (i += 1) { - // Ensure we never go out of bounds for the rect - const x = @min(i * (dot_width + gap_width), width - 1); - const rect_width = @min(width - x, dot_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a dashed underline. -fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dash_width = width / 3 + 1; - const dash_count = (width / dash_width) + 1; - var i: u32 = 0; - while (i < dash_count) : (i += 2) { - // Ensure we never go out of bounds for the rect - const x = @min(i * dash_width, width - 1); - const rect_width = @min(width - x, dash_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a curly underline. Thanks to Wez Furlong for providing -/// the basic math structure for this since I was lazy with the -/// geometry. -fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const float_width: f64 = @floatFromInt(width); - // Because of we way we draw the undercurl, we end up making it around 1px - // thicker than it should be, to fix this we just reduce the thickness by 1. - // - // We use a minimum thickness of 0.414 because this empirically produces - // the nicest undercurls at 1px underline thickness; thinner tends to look - // too thin compared to straight underlines and has artefacting. - const float_thick: f64 = @max(0.414, @as(f64, @floatFromInt(thickness -| 1))); - - // Calculate the wave period for a single character - // `2 * pi...` = 1 peak per character - // `4 * pi...` = 2 peaks per character - const wave_period = 2 * std.math.pi / float_width; - - // The full amplitude of the wave can be from the bottom to the - // underline position. We also calculate our mid y point of the wave - const half_amplitude = 1.0 / wave_period; - const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; - - // This is used in calculating the offset curve estimate below. - const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(1.0, half_amplitude * wave_period); - - const height: u32 = @intFromFloat(@ceil(half_amplitude + float_thick + 1) * 2); - - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - // follow Xiaolin Wu's antialias algorithm to draw the curve - var x: u32 = 0; - while (x < width) : (x += 1) { - // We sample the wave function at the *middle* of each - // pixel column, to ensure that it renders symmetrically. - const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; - // Use the slope at this location to add thickness to - // the line on this column, counteracting the thinning - // caused by the slope. - // - // This is not the exact offset curve for a sine wave, - // but it's a decent enough approximation. - // - // How did I derive this? I stared at Desmos and fiddled - // with numbers for an hour until it was good enough. - const t_u: f64 = t + std.math.pi; - const slope_factor_u: f64 = (@sin(t_u) * @sin(t_u) * offset_factor) / ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); - const slope_factor_l: f64 = (@sin(t) * @sin(t) * offset_factor) / ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); - - const cosx: f64 = @cos(t); - // This will be the center of our stroke. - const y: f64 = y_mid + half_amplitude * cosx; - - // The upper pixel and lower pixel are - // calculated relative to the center. - const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; - const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; - const y_upper: u32 = @intFromFloat(@floor(y_u)); - const y_lower: u32 = @intFromFloat(@ceil(y_l)); - const alpha_u: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_u - @floor(y_u))))); - const alpha_l: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_l - @ceil(y_l))))); - - // upper and lower bounds - canvas.pixel(x, @min(y_upper, height - 1), @enumFromInt(alpha_u)); - canvas.pixel(x, @min(y_lower, height - 1), @enumFromInt(alpha_l)); - - // fill between upper and lower bound - var y_fill: u32 = y_upper + 1; - while (y_fill < y_lower) : (y_fill += 1) { - canvas.pixel(x, @min(y_fill, height - 1), .on); - } - } - - const offset_y: i32 = @intFromFloat(-@round(half_amplitude)); - - return .{ canvas, offset_y }; -} - -test "single" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 2, - ); -} - -test "strikethrough" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .strikethrough, - 36, - 18, - 9, - 2, - ); -} - -test "single large thickness" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // unrealistic thickness but used to cause a crash - // https://github.com/mitchellh/ghostty/pull/1548 - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 200, - ); -} - -test "curly" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline_curly, - 36, - 18, - 9, - 2, - ); -} diff --git a/typos.toml b/typos.toml index fafc38858..a8b296755 100644 --- a/typos.toml +++ b/typos.toml @@ -32,6 +32,8 @@ extend-ignore-re = [ # Ignore typos in test expectations "testing\\.expect[^;]*;", "kHOM\\d*", + # Ignore "typos" in sprite font draw fn names + "draw[0-9A-F]+(_[0-9A-F]+)?\\(", ] [default.extend-words] From c96af1b3b15ceb03c39178b1dda2c86422247db3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 15:59:33 -0600 Subject: [PATCH 02/23] font/sprite: add separated sextants from sflc supplement --- ...ymbols_for_legacy_computing_supplement.zig | 80 ++++++++++++++++++ .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 0 -> 559 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 0 -> 744 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 0 -> 1388 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 0 -> 400 bytes 5 files changed, 80 insertions(+) create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 0a57a0439..9f7e8815d 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -191,3 +191,83 @@ pub fn draw1CC21_1CC2F( .on, ); } + +/// 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, + ); +} diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d47d83b743de43e11332c1f080a2a0c9e8f8b167 GIT binary patch literal 559 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E9lJY5_^D(1X7V93b8 zz;I~6SN~jYeGQNTB(Ot@fq~)w|2KyXc^McO8W#LptNV~snh8k-0|SErCs@r*N3I3~ z9+m_D_gl&BnX<@h?8y@ zaxp0KxLo{wzv{MYK!z&!Aey{Odw*-8T4hDX2%mr$TNr zZ%gt+esG9f+Q`Ykz`=a*@AfkGT%R4x+}83Yu6pk$?x@@8ydmM>X|N~ZfWrV9){LCs zu>QaPw;&fN3X#AYR!DTfmE0E8K#Q{t;$UOmBBIPzmk}Oi+}i(5(>hbPo&p)_>FVdQ I&MBb@090ti>i_@% literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..6366a0ff6030ac12f05c3b87c8e1a3b9469c5b90 GIT binary patch literal 744 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V1~PZ!6KiaBo%7&0<2 zFdSO&)jyY8Ujw873H*>`U|{(F|IJ}TUIqq+h6VrD>OSO@W8*g(ML;=kQ~jGSvcME>aRFQ0SRKF%SF4Jevut-dZk5|{|u z12emn#f1Zn4;IJHf3y;2%udIxD&{MuJz={l1d2`j3P^Y%A~*LTD|+OvCn5pB)Zj}1 z^PuK1a)J}m|Mksf8fd9)16avhgc9jVO|YcK&3KOeJww`p3pYXO#M9N!Wt~$(69BUs B6fFP% literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..72b744510b2285d16fc8f85960ac1350758c79f8 GIT binary patch literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$j*8nL%0y`ub7#RNle{c9J62>3#yt2tPnMfoDg%?zw2m1GbVr;Malp5zXiEKK0pE+#GuZB8uNesZ9xq* zHy?;Zck^Q|ITHc1D_*#BC=WZrklq<0s{){2?u&#!d< zXU?}r=XsRhx&U4E@DBsr|*8bmJKK)Ab>;6l3RoxUC7x8nc1Wa>aa+MeHSfJC$ z(q*SH!QsHpE6a~>`gyXhzR7jLqtpYOVvSwX8Py|LwBp$2BnYmkkSu6=YAyhFyx~zzCK~QT2<7XWxLZ6EY080T=DPyJKtqo^YxeRJt@H=XcFZi z;J8`9_g_$jC3cQX0Sl+2xH`LxqtgY^lic9+ zbjy&d#X!XM;@|gGyE0W42sstmF8{)P{`m2bjSH03rE8ixS!HcNSpWng9v}PLdRqHG zJHMX&iOT7C;u@9|7|o3Sl|T0jW9{Q#x^r@>-HrtyAOHUkiiQRU5$A*dc5h?mSGt)p zbLX=S@^V@e9PEWW7QAR?;+!Qmo1tB{8Gf=>z=t|B$;72p10Gj#Xj@g@oo+CFv-2%_p(v#52ha46(Ph%7h9|X}ogz4!&=uYW< n$c*ONH%uUl-XdHJQ*uCiPNQFMo8J9EP*U-9^>bP0l+XkKc_-Ju literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..ca37ee32883f64d6d02a278c8c2e60c3c6f854e2 GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=E9no-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE5~z@5U|{(F|IOu#Yz&G5tQY?Oe--&HSLK*Z$`#8srcW;|twAWZ z(N5a_>Hqb&4`*I%59WC0^?j>FNz=xAF?o_TrcU*9)p(L~2%S0k8Oo4efpDeUAw+ukI~619IB1B1Rv zcu>{OZ%-FFT*!>+RSmGH&=Gj^YhP{9xmYm{hJ>{{CtW(dRbQO7$?tKNPUE42$&Crl zpXQs%bA<_<&}L@{sf4)Tupuu414F}te`|XmGD|ZdJOKhVOkh7F)c8JR$FAlrLe1KT cj36}|g!tR4_m-WQ0`e+@r>mdKI;Vst05b=Wr~m)} literal 0 HcmV?d00001 From 4f9d7c565a13a38d21ac1b0f1fe16c79be3fc0d0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 16:11:55 -0600 Subject: [PATCH 03/23] font/sprite: add explicit underline cursor Resolves #7651 - uses cursor thickness rather than underline thickness. --- src/font/sprite.zig | 1 + src/font/sprite/draw/special.zig | 18 ++++++++++++++++++ src/renderer/generic.zig | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 4be06a918..cf86fa6dd 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -32,6 +32,7 @@ pub const Sprite = enum(u32) { cursor_rect, cursor_hollow_rect, cursor_bar, + cursor_underline, test { const testing = std.testing; diff --git a/src/font/sprite/draw/special.zig b/src/font/sprite/draw/special.zig index 3d75360e3..e41cac487 100644 --- a/src/font/sprite/draw/special.zig +++ b/src/font/sprite/draw/special.zig @@ -326,3 +326,21 @@ pub fn cursor_bar( .height = @intCast(height), }, .on); } + +pub fn cursor_underline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position), + .width = @intCast(width), + .height = @intCast(metrics.cursor_thickness), + }, .on); +} diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fba577231..0e97808af 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3098,7 +3098,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .block => .cursor_rect, .block_hollow => .cursor_hollow_rect, .bar => .cursor_bar, - .underline => .underline, + .underline => .cursor_underline, .lock => unreachable, }; From e691404a57b242c598a7617acb8ced27a06af6b9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 16:58:34 -0600 Subject: [PATCH 04/23] prettier format --- src/font/sprite/draw/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/font/sprite/draw/README.md b/src/font/sprite/draw/README.md index c6219b83f..d16035996 100644 --- a/src/font/sprite/draw/README.md +++ b/src/font/sprite/draw/README.md @@ -1,20 +1,24 @@ -# This is a *special* directory. +# 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` or `draw_` 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 @@ -44,6 +48,7 @@ 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 From 2084d5f256c2260c3ad6dc7d750c46b339585f0c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 21:32:22 -0600 Subject: [PATCH 05/23] font/sprite+renderer: never constrain sprite glyphs This was creating problems with the branch drawing glyphs at some sizes. In the future the whole "foreground modes" thing needs to be reworked, so this is just a stopgap until that gets turned in to something nicer. --- src/font/Glyph.zig | 3 +++ src/font/sprite/Face.zig | 3 ++- src/renderer/generic.zig | 25 ++++++++++++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig index 5449e2440..fa29e44fa 100644 --- a/src/font/Glyph.zig +++ b/src/font/Glyph.zig @@ -20,3 +20,6 @@ atlas_y: u32, /// horizontal position to increase drawing position for strings advance_x: f32, + +/// Whether we drew this glyph ourselves with the sprite font. +sprite: bool = false, diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 25968e865..8c39daef4 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -216,7 +216,7 @@ pub fn renderGlyph( // Write the drawing to the atlas const region = try canvas.writeAtlas(alloc, atlas); - return font.Glyph{ + return .{ .width = region.width, .height = region.height, .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)), @@ -224,6 +224,7 @@ pub fn renderGlyph( .atlas_x = region.x, .atlas_y = region.y, .advance_x = @floatFromInt(width), + .sprite = true, }; } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 0e97808af..810e17686 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3039,15 +3039,22 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } - const mode: shaderpkg.CellText.Mode = switch (fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; + // We always use fg mode for sprite glyphs, since we know we never + // need to constrain them, and we don't have any color sprites. + // + // Otherwise we defer to `fgMode`. + const mode: shaderpkg.CellText.Mode = + if (render.glyph.sprite) + .fg + else switch (fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, + }; try self.cells.add(self.alloc, .text, .{ .mode = mode, From 61b7dffcaa1d4668937f2adce16ad2844ddfe7b0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:01:07 -0600 Subject: [PATCH 06/23] deps: update z2d We need to use this version of z2d so that we can get reproducible PNG exports in CI for testing, since previously the PNG export was affected by the CPU arch / features because it depended on vector width. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- src/build/SharedDeps.zig | 11 +++++------ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 51e2e4538..68d65fbe9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - .hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", + .url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + .hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 1d95ed93a..3099ca823 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -124,10 +124,10 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": { + "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - "hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=" + "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + "hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=" }, "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index fffc639b4..133284201 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -282,11 +282,11 @@ in }; } { - name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW"; + name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; - hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; + url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz"; + hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index d032711e5..bb0a27105 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -31,4 +31,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025c https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz +https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index ec97a9c9f..f173e4856 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -405,12 +405,11 @@ pub fn add( })) |dep| { step.root_module.addImport("xev", dep.module("xev")); } - if (b.lazyDependency("z2d", .{})) |dep| { - step.root_module.addImport("z2d", b.addModule("z2d", .{ - .root_source_file = dep.path("src/z2d.zig"), - .target = target, - .optimize = optimize, - })); + if (b.lazyDependency("z2d", .{ + .target = target, + .optimize = optimize, + })) |dep| { + step.root_module.addImport("z2d", dep.module("z2d")); } if (b.lazyDependency("ziglyph", .{ .target = target, From 8b6e1fe5b143a81cd4158cddf58d4fd135ad9231 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:14:47 -0600 Subject: [PATCH 07/23] font/sprite: update reference PNGs to match new z2d export --- .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 403 -> 402 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 534 -> 534 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 1022 -> 1025 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 316 -> 316 bytes .../testdata/U+1CD00...U+1CDFF-11x21+2.png | Bin 1275 -> 1280 bytes .../testdata/U+1CD00...U+1CDFF-12x24+3.png | Bin 1870 -> 1870 bytes .../testdata/U+1CD00...U+1CDFF-18x36+4.png | Bin 3404 -> 3411 bytes .../testdata/U+1CD00...U+1CDFF-9x17+1.png | Bin 1101 -> 1103 bytes .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 559 -> 562 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 744 -> 741 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1388 -> 1388 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 400 -> 399 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 5450 -> 5448 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 5724 -> 5724 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 9997 -> 9973 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 4298 -> 4295 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 2223 -> 2220 bytes .../testdata/U+2500...U+25FF-12x24+3.png | Bin 2638 -> 2635 bytes .../testdata/U+2500...U+25FF-18x36+4.png | Bin 4541 -> 4570 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 1848 -> 1844 bytes .../testdata/U+2800...U+28FF-11x21+2.png | Bin 1022 -> 1022 bytes .../testdata/U+2800...U+28FF-12x24+3.png | Bin 1547 -> 1541 bytes .../testdata/U+2800...U+28FF-18x36+4.png | Bin 2490 -> 2501 bytes .../testdata/U+2800...U+28FF-9x17+1.png | Bin 917 -> 917 bytes .../testdata/U+E000...U+E0FF-11x21+2.png | Bin 1104 -> 1102 bytes .../testdata/U+E000...U+E0FF-12x24+3.png | Bin 1251 -> 1252 bytes .../testdata/U+E000...U+E0FF-18x36+4.png | Bin 2228 -> 2220 bytes .../testdata/U+E000...U+E0FF-9x17+1.png | Bin 895 -> 894 bytes .../testdata/U+F500...U+F5FF-11x21+2.png | Bin 1114 -> 1114 bytes .../testdata/U+F500...U+F5FF-12x24+3.png | Bin 1423 -> 1421 bytes .../testdata/U+F500...U+F5FF-18x36+4.png | Bin 2470 -> 2473 bytes .../testdata/U+F500...U+F5FF-9x17+1.png | Bin 871 -> 872 bytes .../testdata/U+F600...U+F6FF-11x21+2.png | Bin 493 -> 495 bytes .../testdata/U+F600...U+F6FF-12x24+3.png | Bin 636 -> 637 bytes .../testdata/U+F600...U+F6FF-18x36+4.png | Bin 1218 -> 1210 bytes .../testdata/U+F600...U+F6FF-9x17+1.png | Bin 394 -> 393 bytes 36 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png index 6623bd0ff4d42cbfb869a8aaa315e820d8c2d582..581b0bbf0547c2526878bb36c8ecbb8277689956 100644 GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E8so-U3d6?5JkFl1z4 zU^ukktA8$+y#`1D64)WZz`*eT|C`Gjxf&dJm@oYQ|H^8ItX7NP!S4JZH3yXwB zB({RAgn))KtY8}sGjcL8Ff=Uqzy7x%7s%mApg<7f3WS>5f*NQ}J>UveW5^3O=il13 z4;iJIkW67o-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C`H>Tnz>S%m@DO-zB$4RP=C@z#PVdqT5y4tL{Z- z+t%2`>;2cfyOw8d!G>tbsD}yLjkX**n3wt@F!9Icw@EIKpR~MY diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png index 8e54879661a7e7ff268d9ee5819a4dd253c98633..852fc999be5a707fc47bbad639495a07ed7b56b8 100644 GIT binary patch delta 268 zcmbQnGL2<|c|E7U1_LViA<4kN@c;jt%Ykf%10-55{$2m;+rs|9V8LH2i#5J>8@fGW z2WvP`e9kic?*0Gvwm#J7sZ2I64%zdhW@!@$6B kK%xG%^qd9TSRncuL<9PN+m_uC7X39GC-(U4ONKP(eQjUJW&A{;g|C_^x zybKHs4GaFQt>tBuW~Xk6yh$Z|_*fj|D3O6dJd2s9XrqnBWl2B9x&e;-T_K6g6aSO delta 395 zcmZqV_{Tm$Mg6F!i(^Q|oHqvy85tND4lVfVpUbVU!GH>ONKV#cQVurYVqp0H|IOh> z9tH-60}A!8m17ocV?kB=fEBFfiX&HZ0K<`h_w_H!0$=#82w~&Ye{gJli{c9I1BH9+ zVyd20KW?91`ETM$qltIft(hM<&Z*!1ecqau=k`mh{nI#9E`(@IaEN9R%Fq(=SfIzopr0PwMOQ2+n{ diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png index 630d26dbfaf79d560f364845536935b414b94378..ecdb2ce10121405cfa22222a848c0ce9c99f775b 100644 GIT binary patch delta 174 zcmdnPw1;Vee&7KG0p@`J|Cj5QOb(jBd+8j6ZO&*VLQ&nXz*y_lB^(q1_p-z|KA)o6Fmf_X>`KscX8>{E9&Y4eU|=|)Q2$z6X2CWV28IOA*WGm!*5|U6 Pf^>Ph`njxgN@xNAX469e diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png index d9b2aef99d9f2940399c7b29f5191bfef318ed62..8d7de36aceef89fbd945cf8a56477f1936953b56 100644 GIT binary patch delta 1144 zcmV-;1c&?k34jWaB!9$d<#gyH@DpC&dy33l=Q+h2YQ{JL4?` z3l_XK-s0zi0RRBFO7GWWefCse^1msU7A#nBGtZ9AyF%lR#ea48-ym49;Pb&dOK%6C zxafCQSh(p^9~UfGaPD9L008p)0{{U3|LoU64#FT1K+*f3x-Od#K*n|q)4rd$m@3HY zjvGGnYwB_KkhLWb4GauC-(fg5dxgd;t)8zpcAo(Q0|OTsufM@!FAEqL_z7T1k|ase z+w0vFS1*$*Yk%{0{%tKVFmQ2jZyt`#Kh4Kbt0!0`eOUnv3|tl*G63HO{$Ierz_r17 zHS|r!#5NHEr?rU?Sdt`3NgZdyZ%5CXwV@x6&k{aWi~tP0Gx!4l0RR8&*1>XxAPhj! z|Nm;cbjk=M3O;bo-9-n?X*UlzM(7!WW2(!gn_EUtM1Ph91_lOB8g7j5|5rNA1`ioW zwg?FrcvOp!I^ae200ssIj)5gfk~FpI&#$W&=dEDa3Y0JjgPDsD-QPcA43 z?GYFl_=Dg#KK!fij{HmSF#@l#7BKK2@aM->_5cP3z6q8jNz$y2v+CR0vmMWFxil~^ za1@-XxPK)zKDnSEv`66o{Q&>~|NrdP-3r1m41iJJ|E1^UAUJ6of12!kcR^Zm6r{kw z)xqt@)VEb}1m0sUVBlqNu(uy~*#j6Dco8f~lB92SoINfcV)4_fw_1&}z!jDT1|9}a z6^>1fnG0V2NP{CVFfj0az$F9j9&q%fvENm@|p(3P%TssAr{zPWhVtGxO4S_c6b z7=QR|a8t#lTeiF%v$dYbeE|al#{{oiYss5O#5|7@qY(+-2e;>oL9FQ^B?Pd128af1}sUEqz#Q8 zT~FU-9nZF08W;$&he-z`(%K*rQ5t{?D`5c*-8Y zz`%=ONs=V(YV_!O`Y!8uw&l{mz`)_*`*Hq6xX09(>3Rz=Fz|=KnWfX6@h;M00960?AAFFgD?z0(fi*zRZ=jdE<4z%_XTRTA|%38Cz8k5tlM2=&Z-3Sz5e1x^=~hjwLQf@}l$> zS>tTxvYBC`{SGKnSM>> z4KTw)PJ1Hf8?d+#f-lo7S9q zOXqfmeSjBBjLA!Cqa(YPE5U(IlHX(|DGIZOH9A}iLwcUC0utlvsmLBcmM4@o|p#3h8a>ka`a65ynrh`LB{u+jChlxji|Y(O0*Jc_Nui zCS%G4>u>PK5gq6`#eMHEBIKex-ekUZyZ`_}a7ZSiGHciyi)zi?f0j>v@uefTyct0P zvHj=qO7E8S5hI6&U?K?eZ1jq9e5uzRpDMT~OMhTwK{wIwW`7gJN07{752omJYvb>Q z&+@x;8t*Cyf+~vR61sU!iWB7c(tQ)UvEKNQt~DF*vEW~r!AzSq_W26~D5VE;C zMo?{D{7D)Kf@oI#{hOwTyilu>!yf!eW16ra651zyyEP}dH(R-L>p6Apb#|5E!GUO? zw=NTcpzC0?hcIpUm^s7JfY!(;*ox>}7hOa<=7_LhRw zU-iB)jJ?Zu)ud`noN+>0B>sOJe68e$Htfyy6qGjd8#-&Z{jEQeS~&Pc@?aQi=o$MC zXG#455WH%bb-=`{$<2S0RO=a|hM$njw{`m4S;7|I=}kw2NQVv9TbRy+5LBUXSs^Xr z^a4SGL6~e1{uSRCsh;M1Jk)V8Wh?7B;l_!9pq~{n>PvhCNg<7UeGjefPPD2EIWu`s zitUsX39j@Y9V7NpDx*kT#>`v#gOyu(O0Wa~VBp97wzl>2pYz3Hu{diwi2@uRzJwr| VDE%q5K38J6U`Bix-W~=Q{|6!q5+ncs diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png index 0a616d72cc695dac524733880abe56fa572797a1..ab6bec96d48174c49b4822c5bfe87c766207399a 100644 GIT binary patch literal 1870 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 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png index ecbb650a1c0b08f8038ff65ecd1a70d91f5b8e37..43035aefbcdfade74b5a321ad0330850f531af8f 100644 GIT binary patch literal 3411 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 Yfq)`SS=TGv*?K2{kY46AXH>{4k(*OVf literal 3404 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 B*17-y diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png index 1a6cc75fa6d12fd7fe88e1caa1bfc6382b39a733..fc111e2d77136c5735c7665615301ff09dd5f436 100644 GIT binary patch delta 1021 zcmV@s7?rm41p^Ds}dsgBB3cEJ(g96K#0%` zApih?Qpyhik?SIV3u0b%!kaIcx%ae_GYBD+Eo9Ph7|!;Ch;U(Cd$JWFgc60srU;dR z5JF2cn3aT^K?tEJ0001#;=e7;>f~`6Z}X2A8>mGQLMT}%Xc`9*Th8qxYz^ulgwRSt zV$(Ar;rmW_KnQs9flwi?Z&y{^hy_gt~{lg*FHcDj@V{LI42Jmp1?a0RR8&l|c>yArJ)p|EKZ9 zVIsuIXqE!15)Zv=Ctd7;8-A@belvVP?x4phL?8EOp(LT443%COIu8mdv{dMTP&x&2 zP>3GR6AEg72L!+bg)S2k5fNEQ>%2x+RD3hMz-S$X6j~{iB>X=aD!tFc{vH%kNTCHn zXcVGzrqJ}khCv~EI8I1JLC3F5Cs4Kt8<gb>;+KG|aNX=Y6S*28gmw#6%gzuvXPZ6;C-r`W5IP|= z^XiLHA*)bBPsahF?+cY%1Vi{w5OP2$hY$b&z$wf`a?C5`Gt%V{QZF|z)DHt&+pju9 zg!(W-2;~+UeWO`a34{>Z%)C(e%M}d*6%a}$1OV`1{*zh*B7a`Mw2>V9N_j@Q975{l z@g35_|vv0RR8( r!!Zp2004lX{;dt+3mXCe00016;B=jV40`aj00000NkvXXu0mjfBHO&G delta 1019 zcmVR|t;rnrKl%gnmINcxfh#fe@k2 zyiN!J008%M{H8m8aL&AE>T-l;FW0iCNP$qC5DXbwdl4cuLP#i6E-Tb!a9`cnGPz!N z5hB!C2mk;8&*k_Fa@}~;o~g?bn!Q{~Xt%K*iW7n%L!ipJDj`BI5*iZHW0`dbgb0lg z0ssIw;}4PTB7c1`uR7t)7tGvy+Q}J&5Xu%Z={O8$`$0swFs?n>iV#AHLSj>d%0LLA zr5Vgh!p$IrP!s?F07~)SmS%PGIE}aY$BPZrq6i_BEEF`2gNQBX_7S!Qbr3>mB_Xlt znUL^(Cp;i@B_M=QworWqgb-Rf0001#bNtU_R=*3c4}T435JD(h$R83JhQ;v9+)&gH z2q9E1^w33F_gw#S-2y`0!`?z0ga#E5`ZFN_0N98(00030|Lm194gxU@1o{7`LzTk9 z5tc#cwKF48>>VrZt&RW2eW2Wt9*2I@O; z!w4aiTWIu+W>F;<EGdLg6o0Gze5cD47rd0L1x|TmvG1@=P1av9FY8q{|_sUM??W zhmE7puR247aTp}2AbTVxu$G$o^ z6S;bvLkOWxAv87=+Y?0N~Ah00030|Lwy8 p2><{9fS~`c+Yu-z00000`~~22oq_1@evkkF002ovPDHLkV1mW(wXy&J diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index d47d83b743de43e11332c1f080a2a0c9e8f8b167..ed0e8381614585f5bbb2eac395badc97154b2740 100644 GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E7vJzX3_D(1X7V93b8 zz;I~6SN~itdkv5RB(Ot*fq~)w|2Kylc^DWN4k*;W)|OeYjRi>s0|P?>7g)_rN3IqF z9@Y!L=UZ)_;j5|Dvf#%@TY;V#llbo*Wd`f`kQ!S0@#FuZAO0EnPcAT-S1GrP?YPLj z`t7_er%rO-*3F%&%*0(2=jSKzW90#mjy-{ob{)xEc~RPO%Yoi|ClI?nj8 z&|>Lv+eDDL?l%*hEbUB3Mj-O*Yw7q4b z^YcQrW16-c!geCdzwA&41;T=p|2}Qm&#!vUZJJulj9wkz9R}PsFXN}~%dOaZDJaJg zZ1JD|5By+9URub@z`((L@b7lFM+di@oO}4A=jGR2#&-~P$aImTWAUI@TU;`T@ zxEVPa7#JEB{9pfDkP8%xNFav=qQsCFtmNO?wGSDknUEAPFfc?jg4Dc47$ZHY38sXZ XQTxAXnkq|03P`1=tDnm{r-UW|F|^Ys literal 559 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E9lJY5_^D(1X7V93b8 zz;I~6SN~jYeGQNTB(Ot@fq~)w|2KyXc^McO8W#LptNV~snh8k-0|SErCs@r*N3I3~ z9+m_D_gl&BnX<@h?8y@ zaxp0KxLo{wzv{MYK!z&!Aey{Odw*-8T4hDX2%mr$TNr zZ%gt+esG9f+Q`Ykz`=a*@AfkGT%R4x+}83Yu6pk$?x@@8ydmM>X|N~ZfWrV9){LCs zu>QaPw;&fN3X#AYR!DTfmE0E8K#Q{t;$UOmBBIPzmk}Oi+}i(5(>hbPo&p)_>FVdQ I&MBb@090ti>i_@% diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index 6366a0ff6030ac12f05c3b87c8e1a3b9469c5b90..bfc7882152eff8b889fff1fbf733c0dd89746c07 100644 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V0`U|{(F|IJ}TUIqq+h6VrDo_)YC&4i?afq|ib1x-yYFC)4d z14gi#%NN;>IEXM``2YXaZvNjP$t{K^uVki`hPrI21DpB5z~hMrEN#72^g#4=c4gVlsoUyw(x+$hh)8G{1SBjt-~iTq;2J;Jl3R{k zOojq37k}Ts#jH^_gY${-J!bR08eL71uKSI%>djX^eZqCMsHio|a;MYQIWY^P{MH5N zviE)XKE1J#jZI1-BVj>flcK9)i(0clTvi4Kh8+h^1)n+f z`NUde6V$_3Oq;A7a=KCZo89I;{=ff4%!}P-c;SEpGczxbiGjeMjjx$^ex0PfrR*mp zP;VaOJZvD~a`E48KSs_q9jyO^-pf8W;OVZN#GE?m&Vr~((jneO%~_nFAl@={`HZR0 zCq`EnHD}rGblf`c&Vnfaynwap<}0o-ufOs8z^6TLf2aS{zj>^YjnAZDK_fG-3`gCq zubTH}Jz=|wWSVDQfG#M){XRir1QD~o582UUZ9NgO?~kr#?^_0RbHt%)7(sq%Snz+n m^rR-#M0CIhtmG|LC39HLvA<^sljH`U|{(F|IJ}TUIqq+h6VrD>OSO@W8*g(ML;=kQ~jGSvcME>aRFQ0SRKF%SF4Jevut-dZk5|{|u z12emn#f1Zn4;IJHf3y;2%udIxD&{MuJz={l1d2`j3P^Y%A~*LTD|+OvCn5pB)Zj}1 z^PuK1a)J}m|Mksf8fd9)16avhgc9jVO|YcK&3KOeJww`p3pYXO#M9N!Wt~$(69BUs B6fFP% diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png index 72b744510b2285d16fc8f85960ac1350758c79f8..2dae7ab4a259d960f5fb5ba98df06d39145915c1 100644 GIT binary patch literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$>*8nL%0y`ub7#RNle{;Bzhk=3NfI|IiZJ7nzSddgOFfcsehNv;*Wnf@v zSnzLcEia=q6RH{mMu-|lPOzH)>!l|(p(&Zef~MrQpaz+!}56pFYa%Fg#_z^9tm+24y?@?Q%cw@A$B3 zPP*kzr>%2h7DoB43(!>$Uoma6cF5_IqN|=h;ksH>)S3m7mU-ZM?f$!alUE-2>Yw@k zqy)2MhRFp7wppnP%)M+<9j*P-_A=`f{VPkKex>>O{7Y|ylqWb`WM>u9@K$JC#LuM?FwKFyLl>H2v>?PO-+W>8$DzELw45a}oqsR74guxh{B=dXRHkwgT9R@8lryam$d4 z$x(#m;_v&n&bWw`{H#gu4o^tpw)K7J@|c-{fx+kVQRlY)`Fr;NcqrO3uiAE}4UW`b%%7@HTNo>8f-r zh~-eq*g8R>sZ6nB!BwXw4km*W*JM?8R+j(&&mq(#H%F#`h0~GWkI$khCGr9!S>6K0 zF@wv+-}iUzR`PIAZTV4~e^ax^`9i1)-wWrHZ2by_4*Gtu5Wlvgp#I^P>Hqo7_3U3% zVsja~!x_zt>dK$1t>!u|zjWuxi_kc5)&-i%Bl-< W8vT0T@TA@bxysYk&t;ucLK6VA$lsX& literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$j*8nL%0y`ub7#RNle{c9J62>3#yt2tPnMfoDg%?zw2m1GbVr;Malp5zXiEKK0pE+#GuZB8uNesZ9xq* zHy?;Zck^Q|ITHc1D_*#BC=WZrklq<0s{){2?u&#!d< zXU?}r=XsRhx&U4E@DBsr|*8bmJKK)Ab>;6l3RoxUC7x8nc1Wa>aa+MeHSfJC$ z(q*SH!QsHpE6a~>`gyXhzR7jLqtpYOVvSwX8Py|LwBp$2BnYmkkSu6=YAyhFyx~zzCK~QT2<7XWxLZ6EY080T=DPyJKtqo^YxeRJt@H=XcFZi z;J8`9_g_$jC3cQX0Sl+2xH`LxqtgY^lic9+ zbjy&d#X!XM;@|gGyE0W42sstmF8{)P{`m2bjSH03rE8ixS!HcNSpWng9v}PLdRqHG zJHMX&iOT7C;u@9|7|o3Sl|T0jW9{Q#x^r@>-HrtyAOHUkiiQRU5$A*dc5h?mSGt)p zbLX=S@^V@e9PEWW7QAR?;+!Qmo1tB{8Gf=>z=t|B$;72p10Gj#Xj@g@oo+CFv-2%_p(v#52ha46(Ph%7h9|X}ogz4!&=uYW< n$c*ONH%uUl-XdHJQ*uCiPNQFMo8J9EP*U-9^>bP0l+XkKc_-Ju diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png index ca37ee32883f64d6d02a278c8c2e60c3c6f854e2..cf8d5afc7e5294fbbe63568eb4fbcfdba549ca52 100644 GIT binary patch delta 309 zcmbQh+|N8gr9RBl#WAE}&YJ^H(hLj?|NpT3G4M7G1%m@Dezoi}$w6bt6BfqUq&Vg?N_q~c$mrl=)_q%vP;c=Eu z;~_^^<@gDA+MhWey8O&$k>Q+lGk_-$C|NpdE z1XF8sm)k#uo&0Cpn?zZn_Ag^#&^HMWs@nPO=^}>br>mdKI;Vst0N*uup8x;= diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png index 58afe3de7ab7579b86fd68a5e0dfff03db55dd65..ecf6eae26b9a2cfcd2d027d2c15f9373eb6c0d9e 100644 GIT binary patch delta 5419 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 F{{VArY|{V$ delta 5421 zcmXw*cQhO9`^IAvdsC&g8!>`dMNn0uMjNeCRFv8(lu|1xL5CMG zv&BnekD9;J_x=7p|2*fp&U2sVkL%p`eRWCwkct;Wv%_!eJn+xjBCmdd!a4&uDt%s zbg0v&m(K#3!jIJ=Q4Efjx{yma4vg2tuj=ZPDXFMz(JsgZf0fztt8oW!jdfi#6p%L9>PH`S`zQ|QR#8S4ct3=@H;MZjv*K!*6Z_+E$NjoqaYR#E{KH|M2VZT)> zYh4&IRm2!LJGi&hdc%QQh?ke_qYF#@vkcA_`zmRi7gB&}h}B+2JHY0IWqw$3H9csp zjs;|F@f26!yn^vN{AaNZ&JSA`Tob{>hRP3f}n?|q^=FAg>Pj% z@0ybB57uu<8y%ca2a7~jSy+i*^4U8T-OF^pMK6qHt5le5b8D}S67aIy%9wWb5gU;B z3Rb9Mth1@10$gWuLLXSyVLx`|dx z0X-)zmWfD`J6OXX&qB3D)!GICtaX0*HT+3s^+U20I~Y83p+K@qAoB=fzOE&D1Fo~2 zdCQ)oVf(EA`e;4mwIRh;KsC#f%(8nEbrZ#4LMJOsMiazU1;0>#rfO!%Vo7A#|7Cey zwTBF(&e&k>@68^%@s6BhBb{;HZ3L_lJxF-~fCUb&==Hp;=ME)0(wgpnn%fD6814sD5tu~HMU`(131P&`!e za}QR|xnGy?SU&>AH_U<2-Q+Io`#~4(>7SM=G?-^tD|!YZQW#$uMgVlBnfxzJb1ku~ z!g&z6Y@OpUwmJ5T{Xa?ZI7~L*wuC1?whTR3faHi&GmKeQ(&$r|`J`BmAkwHYJ~OzBp2j zRzpC$PzlX1StNfT5Kab`vk6kkS2lUG!4A*FVG9}L{wzYZtlqQ{~r z2F?qBc8)oYDP=mE8>ok&<2uaiTGjM)COyG2>=%QfxK&fC_oqLKM&Mds4TFAY8yOEAYh1kwuySgMRYTQP!u8%h{ghnh(ce6R5G z1T4ZVXC1;h(0&?&%&Gt11E@Rgt_&L91eJ6b&N;oc=h3vlEP$4uNZfy88kcm* z^sdV4`c;SY8a9KAuwTu4G&!W>>^hwLdEYCE`i&lPR5X?--bu@0u@}{R`h2<>x$Ie! zl$@ri&!MB);LOfIJo(tvHHlx?3Yk9qTl(9fEf!6%#Cl2QHpR}SabA$ucRIDu&4b5B ztn+ZA>1n6!u)rokTXsoy33$Xm;Xu~si(4i$CZuCAABA7a-hRT75Mzavl#ge0{|=O_ z^Hv|(Du;^3Qy&N9m4#nJqDk%dBnSGU)vKW|HKfz-k&VbH3!kg zoW8~=QWm6`QNAqFzgYTZim8lz}WiV<<4mj-eMKBPD4!{(2NmLVELQ?I(S zVhhXZcX7YX{zDZmunMICN2Ob0|3+5P%Rw@g%BhUmyK>dvEaVC!D9His+tAQR@M=fKc4#B zhKZ}>dr81%+c7iHC#WOJ~LW=+dF$= zQ)}B1$AfZSGP~FKLX&{bJeUY&ZHhYYG>g5KNpd)SW^^9jdwy~s0Q-e?HW?}+E?k%e zm@O~)lg1};*#`c%*7A06lLK)on7rVWD61M5e}iH0qJHRgKF%w zY)TW#vH(%|Kq|#>tYouUAY>@}gJ19$t9#n=((OSsM_jck#5_t~#)~R{HWwJpg~$Tq zt}O@>QbcgtP)1|qM`Nic4hPU0v>=4piAE6_d(4oq zQ)a-UKP?HxelwI1o?JqBkdb4G;lDqRr53WM+V&d(u}dx7C?>pyX2HoJs~8IN@y(am-3Qisr(FGvs~Z@uO|roqfF&UfvCDKe*6foGua_PFxvf4(_L1JRT!_n#l#HMRL#mnvm5M+7$4)cdTRd*+I{LZaD30~6 z%StVm6~bSjH(A~8j%OQxu+#2_#C&>t@fms9JTbe_!YqSAOT*?DF3|rZH&wLxc_puL z+C%LrLvJXzTz?edcCGG)al35D32JU<#e9VG#CJJr(9V40SI&W0L%GrEvJ$3bqGalZ zO=Ht}3RU<(Yngy4mf>4sFnR-C4=`dGHzznACVsJeaNiGIJ(nj;e^&pIV0B)fh)8B* zt`-Zle50Q;I#O!)-o-wukgzhry{qoXEn_zEIMD=C(bk8lXtPG{ZvamO98xSlMJRN` z&WmUa&#KJo0q?|FZdf4qH)LCOl%ATW-`NZRC;BjuY@Se(jD&8?pd-i2>M~{bHe_|I zjxfdArLJo5Q!s_@ggGI*nbU7K-x8rNbfI$c;FO}W{^v8rQ~?S8sbRIP23Bj_GrHqJ zg6Kn^uTb5D?e^z;OazC4YWl2=!aU}~BOfZ9jm)9c%e(H~T z;=At7V!f8&!Q!5dzTvCIemqFXtCb-dPv`3RF$@%_Q8e83x}A#I%RDsX^jt5&#L&sT z#S?NHru;l8OYuKTO$oYiu4_}K>ILifG^&;=l=ytwo75Rm2YPQzFl9<2YR|z)S$SSQ zruyA>AnHIVq7lHVBm(kML`r+zM?%&*gblVg#u=qYL^G^4+B*(>7Q#ej(g1t=e9ZNh zv&PS&!R!(QsOG!36-p3kuR%(BexIb^iAxG)o0dEfuQsB{jX6v1=@17mzt0H1YtOSj z^qh5>XQ&I?b65Fz)?ae#e+e5in(ic^_Nlvf! z?3U(Ybj^G8+COpu|5#?!%3ndNC@*b(Z47UaWtpT=37Ww(SU-?9`~W%kfX^jvi|M=A zybQ8CbgOT?*ZV|8u3&+KXeE5H0Ga9 z8_oV|1$?}4mpWx4^4a5=25<98WbNTf!KihnUAcmcr;mJ*2jp?!E+U|`4^^p9^89~7 zC4#oJ50YIeX`4+fSWnaEKy=d3qJlpgRkAzlca`inb|B>P9l z>`9HoYWq9f=Nz+jqQ2xi&{2EUD$)`A6(Rg1zkGYm2;G?Aun&vx-u1tl{+`n`sNeF3 z=Q1Nz+))*_Seoa0@gBG5Nm?D$H)M`u^{)kK1%E)twH`+tuu7e6DcXmfpEmLMs|@UK zau65IqJI-zF{6uX+tzt@$4qhR#;y?$Ehw-L>pvqa_5zCoEvT>qi@4TIe|o>CEN*}~ zmxIxFi)ulc8Ld|^N5e7)^6tR_KsUfy&5HUJX4^vkU*)ufEn-ui8BzV8U*$jyXWZaW z_cnTP@Txfl>XUC4YlXExe&gmy{j$n`+rXyfZ4rd6fa>OPvNpw~HUmglJRh4JPkE@# zkQ}cI%*9--p-$cta-<;%1v+l(fqx5+2M!xj3k(Em$PIlQ6F39>%bEC+3a=HcIs_+#Ic!19YtgiZBm>u9<#b9bDEyk=qD zp0jG6{)0rp6mdxBv&@P9sg5?l6MQW}cgICs7X)^;7+GW(?{t?H#VCO4h z%oFk#X&oD@tdJVY5m@!_q=`ZOwqPrs`C_O+n!sDNx@v2`XtGK1_s?b8%Wsy}riOib zjC&3j+}z*X&6cKNQSq+)+M_NpG;O`s4Gl4$k%M}t_R4F`qDIQIgWocHy~>o*n>6`s zlB5wl>)a`pl^F#SL0=JPxR^SA5mKMBM=ETFsnLVrqqElPj^$OPiJvJnP9HBtkafAo z_^#CzgP3e~-^#Nz@zn-U>d=DBSj);oK8#b-R zQ$h$#nNQ3BYt@;$b9p&w;sAWN-%VIYRI+6>GpBS0>h5HV-V)&m$#;@4nbKCMnK%H0 z``v`$#Q4GI?!S-6Hs)|9A>m*Z=IO~t>FToy+)3Fw3jQxx@gC3mUdiOY%t<1smIZ$5 z^YE2FVg5JuX7_n4L)gM+U#piJ8i1ti?xGl^guh#R8!$-e2;QdI)Df=@b7r5ESZ(At z)HTGsra@~VDMk_vV0)g9m4g}=)#HU~Puo9y@x0)JIqWC|m06z)$u90Se%0?w%`q){ zH2QXAeQ#AHCQ6=(wCvd^eKpnfte|8aus}a@ysW1d@%0hpLV6qSeV^)=s~t-5om=mimU6S8n!r55cu)E5oW)OdiM+*>pz|6GBHi23%$e66 zGzs>IOd!aZB_WF*nli6*>5WV&;JK53Bu7(L;XXY2F)Z;K7RkPW3yHD=(@RRIntpOC zAfO+7vgX2QQ8yx*xEOlJd)|NEAzR9C+F2W~T2d7nJ-olKaDl9~VxL7NmOkTCWG7654EZ27Giewwgn^U#1{{o7rEe52u2S|hG@Xc4Mi4 z`es;w_4hZdSD+pGwgJ~nq6vEU%SyNBz|m+I97FNnY+8U&10>Wmt*sAVt|{vq3fVq= zQ0kOAjeYT_D!LDlUsm%`C{D){-f%?9wk@Y`v0h!DSaFy5tYSZEE_Q6nwmE=}F$!r} zaDTg3vUOrL$=Yada>&})FI||S(u_on_S@cVu+=kZf3J7kq~Ug3vk`9$YvEM?gf^b8 zmU0_L>Nz~8>T6bN8CNsArq;7+M=CA|2IkaJgQ5q|1;FK=I-f$ zU2t&fLTWZ~csTK8T4j|_=OF_R4p&7);Ks9)Wqwu=xnAK8KM85^<7(_=8e}~5-pl!B zEnKyAbwsbN92KM)dH#X4SkCSC3T<_7$tb4p#1pBV+I4ZK&VqrXH6SI4paB#jz4tCnnu0(G9TXBe0z&8_U5Zo@5aE>; z5R@(mB1KeshkNw>alh~0ojv=UeP(yhoHIMKb9V0f+;xH!maa#JBYJG%a-7wQHb#%j zkbC{{V8rsT*8#JD#eyxL@4rst%C{zXCYlW&VrKQ`kzO|fshj?rNJ}pEm#+MjtP%Lr znG*;658>6RLJ-}{5_Tj*OQE}zfG(Y#7r@Z{h(n9qTrft z{Q;NX*%@{n#cs?ECQ;D+E8#3_lmNix<&V$0biD|Aim}rIm1(XXQldr+wT&cbI@7}y z=W>f!hWPw>{_|6j*-W+<1OFdtKokG~0PyI^@~`RdZ?R4n3x^%2`=#t+k8qLyF}Qur zsv0@^ocH+PaKe8TNkK=$0v3Z}5X1-s8O~dmU0EcI*K+OAuf)Ch{Zq*fWm9JBl5ZWc)F+;E z(qHoU*^K6go&3DNpmeCr&_wA1e1-`8;m_p(8lsTJZA^WapI|c!1b*Y*<@EpLZSjg* zg!;KdY@YLH=Czt^Lo@q zATPE?h(wcrAj4p-O{7%AZk1HL#@7~%RXCX_Ma8T14agf=+PT71GDz3D8spb$IG)9N zfPTo|DuoC@vexuLLSRcvYrAsTtD@u6r?y2JWv2e|<%PC!_Xy1HZkGXUS&mfxQ!@o| z_mY|22g35KioE6`l-K_d1asftOAxPO=8=T^SiW1x*wR9HFV?omnpRcxd+Cs!3qkq`uoE8p=oiK1H`bLR4e8%ZDuHlsbDqhWy>roiw!R5$jj#eF2vexJ+v`S^O_B4W z5XA6rcko!vxld)YSZgRdp*fHyWPC1@N)e0`k|(gawW~jUR)`>Xw7BkoY#c{{l%4o= z^mAs%iAqd4`|4z+xcy2eu7JUVrU&p)Ya)`4Wjv7&q@OcAPE@M>WrRk+WUyi5nA<|6 zH;T!kGUeAroO@0hA@Sxj!(*8u^SPoK(Pb^Ku{Wx0h5g&-+4`cJ9-JMuA(*xd^i%Eg z_TpqwwNx_28K?Kb3mwN;4cD)zv&I(1(MH|a`|=_I?yO{;BX?@q+un8k^VQwaC86T zD;mrL zni&0Wj-l3nHbdOf@66=lwb%Fn0PLT5wjN7ahSJ}qwv5z8i;H&)m!zi?AO?xLzOp2U3cNT8X~MIr+wzrtjL-N^CrF=Eeaa!cm(<`0W9&Dg7^|z<7 zi#poqGM}o2TZ}qUgq#gy8R}3jZiv--3fhgXjZ0aK0cG)0v^y_8eT-nWPA)u=WZdYs z1Wg-SHFk)mWZgAGYXTBmZg=&?BywI_9!5U{`0SrY(FEl!Q%#< zbgPR@5)Vo#(PATk7Dm`0Lhq6F7TVWJ~@&Va?H5-+Jiex5cjMR94gm+ku#rK03W2+X_z>P^78g6s*|} z{BWt%ULGgo5Jkv=oqW z)ZU!qc+@j!QuV?)s>31=6I)UNQ2cS-W7&#l6K=ySu;3? z?I;a)MBZ3B;#kZMZb6dAo*Fxdt}TIp9=^H>Jih&jLaN?S0!kWd(R3jRzdSHLE3&dZ$Z{XU5igKOm5d%Tpe!=x&_gXdmwhYIHU1Co(h2Z zdI8#gH{g3W6Mh=xJIT}%^O(QxO@{rZPD`Q|z`lnwRX!d$th;zCccL`_rBc2C#h?nM zMXYkZ?(E+j<$gTqE4*Jr8{)jFuJLyK^}-6ng{WcyP`<=GUm5-kZ2l_|4mMxc;@A_3 zUf?CdhIag(g+T`FO-Ep}4tY92eT+jbW4QKcyT5tzR?+i>4Hs76333^z2{f zNJbq^3zON?o+!x--~0;?Dey+lCn0W>+t*`*=D ztu*_Kl6rxToyQ0O4JS~>lj?Xc%9P>x8yTJ8Mia(Yd!$`)is<)(&}cVlXfz#wL{lI7 zO6AbEGfk1!3@aT=6hE(z637+vLo91Di>9S9mWU*V$OfC2-@?BU-pQh-$>O3S+E?HP zQ?#iKbQwCiVfsnlaYWAZ0Kk&NBv6RGvW56Oh-hC=2eR1Z7zm=t8GA?#F1v9DAux{< zPjRQ_8s{NE>aV5pF(4Q4bTE4t@(o(KJ1-^S1avmm-XJNTOYO4A;`AWfFA&g_E>Y&p z&W$dyf=LdJsBtqPNAPqo_g`c$?PTh^RuI+7+CLinnfh-Mc8gJS2M@?txmy$(R}j^$ z?FuY(F{na}*kAarsW?2PKGQjz4APje-b?*7QVqlaWNSzlqhvd8Nb`PoZSM0#014Rr z;Z=*vQH}scbHIt_#7Y<7-}(xf8Yu&gy_Y;PR_-yUnf5y^UZ?vsa|$;ytE|MZ_6||K z+fN`%{KaWbP6~{$@X6yRXVFipB3t`_sP=J^MMRvCan*>B@9oeca>=;7naq{vZ&0qS zDrCgCScdTzKkD?0(qb_xsFH1Rrqb3K2{M6W`Xm zQem>mSX{g*w70j# ztV(m;S!(^L&wTwS-DeXC0XTIO)dsz8$y-#X_n>&0-h4I?qvYNr#4bWP6=&cZ(!Z5L` zn^r-im8!!J*=y*A_=02oeEtY;RnStc8|`i15*05ahVfVE^6Tz3^hEHWB~D&WU87LI zX%1lrXcd$`sy;;SyU6PmhDCt;C7$1`17F@@o8YBerhp(7C_TLM-G4?(K=V*z05bY3 zImu!kFZ5oRN9MN@F9NBuQDXp3d3qO>m6!F`Fb%$U;tZ2mI-KMrD1?9zRdi zuB_905iB#*3`(_)A2Sr=hA!G~1*o{yj$gxKb{Dfe*{_qWi?c8hs2uJ>c_xn*RSI34 zvn1K~gvr*$_Zs@4XV+CLmJ==`>PB7TBrpwrx|tweGV!u8z>+h+jY_x?baPj=&;`CC z+eWNqCPZ>E5Qz6~Q^;-OL`(WIfeO9axg^T_2ti}{U&vLmyT1+n$ZVnWfZAbh|G;yT~>7_}^gNXZV9a4t4!b8*P`7bnn~i zGO3*OXu5d*<}OC0{>ze8ts`T9MWuir4{akKLVd}v(5RlV1BqGMJ39Qla@YFky-LpYJaQGW-AkOq}(kO2v+`-h}`DgW7~mS z8p~5wDjOT$sS&_ok=tzlhjYU;lQ+evj@ZN)%EZdmei+zA+hLl?{+puVO0TuO&yV*v zN-~s5maF{mPXpQx^Cq-9Ms?(#f?hX#r0SaJsDK-|A>e-Z@zsh&Qa@-uo1N(bVe>A?j{4#smwxSI3%{MOJ{FdpT%sp*u+>-z8g@vE& z>Kwmcl}t9kAXoaX6Fuq%Nw$$((os=AXMjT!-Od109>O2GYVhIx6Gk0=7zm29o35Jl z5vB*LM_M0IX}{(dx$O=;Wc&HxPE`6VIIUQoa0*^~a3|^owjZm0`#$uLW9h-2sBChf zf?@^2WAHw_3^^e|qj=j3dT1Qm4`Xn!ai~}JAa9lKRZVsW?{lROrCb#|-k^Qe3u8!} zaXf>|-u5Q0REiET_RGg}^~os1A0azEN*o_pMP=LbX7xT5vXx`mgIE9cutl}WX6@#` z=?eR-JLw&iUQ1jcj@yrzE}3Cq@F6Ibl|zwYIl)D0WbINkDV<*(ZaXy<+jX0HHp#RK zbi#IB!;9auV87B%_$kWh zHRd|L8!Xnvs%z@126u8yudS7^fR2@!wW+IY;x3L75IUOBfFn0t00*$}N*x+*`Uml6A-61NuQcug|uz-O5iTiy1~XazwlKB5^q1NM}tu` z{Lx_48K;l2bKl3}I3&)Sh)}{1uP(5!G5wpJhrQlG!7;ki5UUTF1c((xnDjoVUf67g zN<<)~?`>4D@00XH?G(w_rSDHv<2r{lPSetYn8mToLg>Fo(`>VAuOA*~9L@DRy3XO7 z^0&^pJMkd|V_%8}U)!z=rr|Ifm)Zx> zsa13i!Ew_~a^cT-iVsU|v}pDQf1fID_6DlE6lljUib${Qm&f;?A6v#r%-4X8Ob@rP zm$$c^lXuAbVYIRGhQT(9H$M04SK3BkZGW)DMzpv2ybgbu(Lke8zN9>_ne&+^>gi*z z=RFyE_A~>Pr>hu*R3s25XU8=-D}nk5X`B+LU*DPxtNneYEs?l*B~K>%-A+&b?pvVgi1JZ5E!rI1L0{i6xN9=-_=>p&&+&{h=w6Akw%r=b*ft2g8r z2X!0iXNdH+_)HbgAy@DkvaLcU;cf}tq1F8W$3RXgx8-P)299 z58+t?_;pphb->Y+L79C1sx%Y{93YY6Bj%nUCv?B1(12$G{Z}EvEpUOo3Z#!o?~Geu zLI~+%=*1$st=V|MDcWd%Hqd337}9Y!^6Bcj49Ufm^PTG>VJu{TsV&qm9+OMKxpiny zyer-NVsy5r}Nl1}guUUh07C zs>SLzBx`B|5X7Ou?o?7lh_*UdD&@C1_~(U=%8)CD3+3tdEQ>mjnQ?Yv*>8;0lV7;~ zRX|GolNR!)99CND;1x{ff7@d3@vr$qeO3kS0V4(}vQwiPqjBlSHM&Uc;H!j^Un z^E(TO>w}n$^EV$n!~I)+Hgq?$Jd!O~`94x996`ez&O{M3O zVagyhkyKDEsupdzV-qzjk&VN_cvWk)zd{I=(WbMmzNv! zCeJko6mTquMwZd`!9+)!^=Owx<{JuBjX#;mrPmD|5t}ySi#hoI9H7O+;V7m)-zrI> z7hj^?Bs`F+(R;E52iltZ24tJbIvz(|EzB4&LcD-ze}9sJ4gg9{mH|q41t;Vu)}fbj{z?f#f9lx3oecXo(6)ME p3;+Nif_PC~y+n2u2mk47>fV^ delta 5516 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 Xn7M|6v_=2?WP)zpyshyLV;lNEGRz~% diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png index d4e488525135da1459ef46642d2ccb21bb9d1018..e1a9e7841def9008a0c8cc31a8f5b5936e76ea1c 100644 GIT binary patch literal 9973 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 MsOTtHDOv{q2N{SX!2kdN literal 9997 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 F`(L$nMXLY+ diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png index 9be9501ba18ef61d9715e67027bf5b9ef23e861e..3fa06a8fc969f3641e2f6d2a02871c11b4bf20ff 100644 GIT binary patch delta 2951 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 x1XfBWLjjc%A%qY@2qAZFy7x{ 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 P#UXOUy4Y1=0?7XdBHz?M delta 2048 zcmX|>dpOez7{|A{Z8o>%mS&cbl*@|9T|#b&bWCe%>Qoe(Tjp~5F~e~QOS-w$LF&w% zxn#+<}O=`A`H28u=A+%obNyH_j#Z9ectE!KA-2UP^wnS(Lr5xbv$%}Qt@!j++G1~JAk1fWd#e?qjNbF&o%$QO} zPG1f7z%^B+c$9NM?EvKh?QlF3V?Z_1W{tRSLA}hYTN6{yD^zK4%puBCKPMnJLaIiwUM$Wk-HC!1zP%@7C$!y zJuWihH!L$#p;*+WascfJzcN8Y<;=5tg}#8%C#y&0vGc!fOvQ;=O*13=M|$fkIQpqp zZ@m`A>o|awJ^;9P0S)>>mr-9(07+p=Yf^#8>WNl?S516HB6@Hk*(sZJ-vrn z+3(OVS8^||KC;XFGzDR+z?NzUMZv#|nBQVxnZnw@meazLP24TUZ$V`>z|5m3ly9uY zeG8fjS~R855RCxZsian#vAueL$*OrW7z6^5WaplxKFq6|N;gZ9y6lfBkl;WsN?!KI z00mrRKyAhYo%}ddwswD-6^&rRkniI;i7uAwnk~-I@;cYw9p>rQg>Zi{(dzSDnB(M? zN4`(fTfcG5plO?LPwuQt>xihqk5*X%#b%Q09p#`uk$`!`TN9UV5>m7?aU7xB=>S@^ z@-^^}Ant~2l;99u(JX^}(h{_#5*VSFS1maC}{?0Q^1Btj;~H z{bRy};nH@qL)%V%w9xS&+K@k(<&x_ptjL`L1B)7o%SoC8pTb*+Mz z<$ouq!zFJadA3FJ<<0h_1u!c;9z8wAZG0}_>uz2O`oq^cI8Ua6nuEL@8xWi)GxE4q z>45&(K7VH9R;DjLeNU%!Pv4AkRd!JEtR~P0g63bGUcDSH5X<}NMhXatJ}}LJM03Fy zC^tb$10XMKR&|&1hNFx{rHYg@qG(#T;26YKb5+lQByc3BC>~T{*+UVmm%^Q{H%gm2 zDz9G8npf2vw|Qb+oZ8Vw*K6(SJ4uK)F@;F<3mT=>e~4n(yjg-*Xma6qZDUxPN&Kga z3A(N~bX#zK;y+0`*^_J_vAnd&hFjBB%Q@IJs zrNM(PLnM;cON(xo&tsa9DT)7AKp+qln{^XDIT=w{ruydf!Q;}iW8V4{mR$dwdXd7& z&@(U55PMqP`OMhLW{gM<1QG+{nf#Nri#SB`i~EX@2Z|6~FUSK$gfNw~JJAcesh5FW zqr`Yy{u#0KawOFDU)^}z|umIb+8KMZ)OqvJ` z#XV~|bti}v6p8~-Pm&GY{Tif_2B~u@ZS$N)ME)|nr-gK_fy0J(EwFo=1UeMz8(B#8 z-#ttc9vSaPG+jlo{?R#_fU()L1Lec8UBj!g^xAXK#hDwjDcaIchulzY%Z*dX#a;c9 zS8j9;4gaZz;Nk!T7l#-R$G9b{FYm2=9NG?#f&Qw0rrS2s=U$16=%Md){`G$3G1zn0 zX`^64WnW_FA2xbMlW>`-5lDptu4fFhCCg;l$?H>7Po=ZVB<2M ZbpZE~je9oBQ?s`aa&_`_ta2bx{{;)y(zyTt diff --git a/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png b/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png index ee9a9d9053b66f4852f4c3d176f42e1cead1a9cb..89011df49d34f3da3b765d0df7097c5914331991 100644 GIT binary patch literal 2635 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 GGyenQadeyj literal 2638 zcmZvc3pA8l8^>Q`hB4zZjS+Gi_mPB9LcB9gF1f^sBQgld(apgecbV5E6mjGTVJ0fM zCby#8<&wfglA{>W$jBjb&3wzD^?m28Z?9+V^;>&CYp>`3?7ja9wl-!$s68kE0E8^e z$v*)A4;}zuAQAuo0J-=v8vsyZ7G$D*@NG`UxJ?^~#(I4fp>yUG_9L|fjjEL>KPd*+6D`Q-7U(uAQikrt zTS6_AWi+az1npl*lIhKy8&Gb=RuHZ;?+8wn95I5q_pCJ;D`#9arQ|99#U z=ODI9Z3L$koSzIVl|j$0&8KBp(O@tH9EHST2zXnN9tG0!X{Juv*^K)pb0+F#@1&>P zU*_cb^r@4|>qRZohs% zwj_D(aEZc?PVTf+WY>VO%9)v!>go#1<*qK{smU5+Pcl<=66IjoEmrhLIP>+{sa)>! zNW*8n4LZAsMB-|PuU=VbkD|FM5-R|aA$%&{TxlEWPO$9@R6A zf~F6pkGh^971+GQrGnMUwWNWxwzyhJh!jUpktcW|Q9FnPfU%GrIa#!eZFs?B_037Y zr%J@89yo659wpV{5Rk$5u!44PuQJ!tf!a2frz$6=w?e}sLuYzLGV^7kzp4QR>pb{Y z5kN8$==_TpjFSTnY}`i6Ohg=^GCDNx{t@=sH|X|pVX1QtzVaxbpi{IIupeM0bP7bR z6nkl^m=<-NPg&|^39AP@(R=D?6svc3d>L_2xYn82 zp@H%*#EANY7=>%oFP4eI;cMehzlA=x zG>yIL;0+Y+6$K{yfJZN=NJ#f&{dD0UM)J;fCa;AIkS4(mx3_1}uBnzLIS z8M;ttdKAVCVlg*IFKuBWutwVn|1pcB*CPKDrZ7VJIS);6n1|c?l!b>wT5S z?Alrv23ZawXL{;H6*>w;eFN?;m$`P+;+1h7ossRSmR6P2wi_llLev_bIF2=2vMm;M zT{(@gHTd6#l4V!cs)dBlU71mRmRID=xr?$+%v>$+NiLj?fhVo5efugm$msSK2vs%6 zg-mj|4Vp(uwj1hF`_z0+pRfuvLX6aWDA3Wx;XYhm99!-PkCQ5gsjVi2vc^KGi{kBq z-{GbHGp%M8=q1sr#L^q(${GVUeFoJHzylw~Z_pdYl9=-`G*z_fMTslQxiFhf@%nyF ztjbo0xZYNG$n7%28~T<8$|xsGNN@j7oan1KS@KLjtlHh0p3w5(YDq4E?S%0HQ=DXo z-c)$zpc#72mLI}+4eiljKN5z^v*ARCa3Vj1(RJ+8Fu=sn)gW0W4G+&zW1rjltLIQK zp!J8nRtU6$7=&HM6w@WkGJx*Y{O!A_TWT=xap8(qn;K&P09@LPW^olQ&Ty-F4XK3++pcKd8-Ms;^(K z=C2bC6(A_thn&rq~^m079Xw#NkXr| ztFdoN7ml)%&8KlB%){TEtUL`9O4R4WUXAW4RFsoN zAK^{vc+tbD0q?8i(w_4uYlm_;?(#!mYkanZj7#9dl=F0Xr7kqtO=jOmDfac@1=#p6 z{ODCagXniv?i=r$N#cBUA-901P1EiSH1DXEgUFaQ94s&b;`2 zF-GrBGvQVI%U6k%kWIJU?kVV;Q$S;o>LTb4A5l6@;?j24QqM9K2Vk`_c| zi)>k19FyqShGdsLMZ~;U>73KK-rw(!_x|I#?&tgc+}C%xKlkT;o-1Ta6MmF93IG6p zQ<9Ml0Kgys;5d#1006L38rcQ_m>g3h&@LonCKVKDROdsiv#oA%IGUY)%dqe$@{|iB zsmBHD`0n_=004jkMn)bJy^^{T=adD&(V6BV9Yo?IvDniBwDf3bob2d7fT)^vuVC~W z1o2kQy00k0z){i_-rvs_a&gn1W%2qOB&?j44?QcWAk~hepsquX(MQR}pfdi?heR^# z50=Fso`w#Ps>#q6(@OP!wx4-4J`vhc3hge$=DA&mw2HdOmbf;MX=O*NhQzeKoLU+j zKx}Z((%EP<9k-FTpBxws73m_qvZA4z8Dancu-P-cr9AZnG)Kec(;)8H*QSD+Q&j24 zIf8>2$~Q@B%MgZ=K^Q@ZD>5sm2tUjGHZ?7_7&o6+k{NnVhx#GkFoUqzDQyXbObIHI zYnCqw{t^F;MtqYxpK-%_c36cuyeIVqadoHX zOC-voc0sqB5GkFhml;#6S}Z*`y*hrUIgKSSRF*(6;R+i(7usoGs9mh%-DfZ&nqJ;f z8B;K;c_!!7dW7_|tPOuhhe_uRw{u4)UO98#i&w6lY?7xoay(%di%pt;4i=^;QyY1( zmXI$zMi`$#7)GEJ$vS_V`bB?`0C;c2-s;j#@OzdVZ1yyZTQ14=n1VzAayhSXI7Aip zIsyIRa?trAN7f(Chz{r0$i+iiv@Wg!9LeW6>VKX*pccEI@C!$NZi+i2Ej6WOc73d+ zog*@BAF!7g1F3-Acc=Rj6ifue!)+1;%)RZ#o?__GNOEkzakpHhp=(fDq}=2p4h zi@3=nh-;D^ABIw1ct`KE(hOBZ)B5uF{qGNEB^3FSD-Eb99ADJ%N~D*bDBYnF&pi|z zkoYpv4o5w#_!m3oZYh4aK5^P=@C61TM1nF1a2`7u1_6EuDaRm)zDaM)HMcK$zW28$cj`RK#z+2TRSyfC=?h|aWOXMb0RsmTz{ zczv_W#Org-lKbksG)=MQiEwTQ{T={AFbrWxsIKOTa1_v~2TTD7#vqKujf+|qAdKB4 zi{KJ@4#9+eOv>&htUT9~9$AjpG#$!aT5H{XBx)RKtQP(lZWgu2fGcL4-*S|`d5 zfKvi@5XN9gs3jbL7wQ49t`I>25XcR2LEpFRy8?js8VP6wGm?EU@(4{m=mgwNX-TRw z_Hc!5?=``#Zq%>rQwy699@cYlu8T}(PdzzBrHOaKIrrpoqP$4F*e-@BzEDPmOx}MC zf-XKLfc(Dzstsl%&v`M|ZuP29c$;6|+vhV+3J#X&bbiLZ!ooh@XU(8tm-RR$v-V42 zTLqOz?H#Ng90tx-)O{9t@sc-eE6X=O6xg_7fZh4f_;!#NHb*es-qwA>r+l@Rl}hM+ zszNObb`YaVosCWC^FMpu_5m}?-aP#4hDXJ*MXt9R?S-r}-TH7R{iZwa>DEtK2rnKv zrf0=w)_C1?!U3zbu~RP!(gwH5Gfn?bMG(dhH2f661(U3;ly1&iH>snIrbzNt*t8TO zyV!%e3NkWg0Z;d8Y5U(`ZNI=6vFr$2mA!Hyi2?RZ3feM=&Q?a>iRf&T-_Naj)UEY~fm;N5e&SJQQ7e0~EZIj!xtF-3x;JgJ(W+h`XcE?#e=(+9Jvpld>C4SIG?iC_vy1k zpxnL!HS33H?fL7_SS|PI`f1=O^47{`IGF$O>xCR)s@esWZQ{Y-_7lC2Etgvgyq)h& zn@#NhJX==dtrw1cKN=w%BG@9}(_zZVvcyef!KDjys~#~B?WL?o&K=ST9JP{Tqkj;E zv|59#j0I0ja!6Xv;c5=$>euP|49&bM&r#mKMhf6l09&PZmGITek!W%x+PEwzq*eIK zTe#Ia;s=>CV6oBaXyb(Un3XoE2H8pea)h@?;O@wzoRkHr-;g)GFNH=Z+gWVxN7FJy z?r0!33UNP$>&J-lvsxUzDRH16R(59DX^IP4|4k-@c!IzJWHiaSRtb{FC{ z*6gdqEBfEwci3*rWL=WZp+up(WG0K763AKvQoXF&}QhmG+&aER`3O&3SEwuPNo z0W@v8_GZausbiPyV;{39=PkZwoIXQW=i^G~D0L7~$`NW&>rOnpIj-m(67mkuqw{Jk zUp!^?DEK%;^q#K`K9j(fpZwU;=R`^0C+V%^+`qo$ujtWenG*mvWyn&T>KDa?hVz-Z z*TPr6WJ!Wh!FbD@_V2HR3dLrqwv(&6uQ5;84GrJx3q4ld8POnqG!yXz8K&BF-tF4S z*O#)T&hgWnAGT;7UXd9xEo6v|N~%ay-h&o4bb5s02e|hQwHM>SFLXfAm6xL%CBpyY zkw=H!dUBpah^E&*o|NqAyXwviG0&_6?N+IWZ>Y8L66#<>sf0*Z zzn)l)$BL`sh1@nXJWNpSqS!d~NX8SH7*F0<&KriO1{lO#PUDeRmTR_@#}Fmt>)m(R z#AMFq+*cd4FWB_o@KTO*_7!)S9-!l7WqotvO`$eaGUxXEMytx2qsGsR=%**E2reg9 ziAWhbnuwI)TIB!1!Ndj(2ad1lOLny+@Z6emS-R87`D;(A;?D0wF@mdFnALE;QgLO% zK14Mual+|AF^*H;7#zmqY3KP-Uf}oH4$hcd2)UU9f-!g!m@Fs7Fd z(@G>)b$D66^V}m=z{lgKPfqz1EkAg+T}qei(#!R7*z2V>S>T9mIn}Q+y-Zq{?2^y* z662+`ha69~y-W3@rSfU1MKZu3TM}EZiBV}&K?b>~#Uxgd^C1_}@6b((sAp)&1bjTkWi#*eVrb7$gO)`by zu{cELezK8o(?uL2<`NE3dx-h-i#G=Y07#C~Im11%ob`5c?jJxu29N*C3jjd9*-wB5 z|BncpMMc9fiC;rJ_!;7FfX^`JzW^EVtsMUbs0{P`3-DkI@KyuWMBp=elRGuJH{C|Q zJUA#-%M7!@%@r*%JpAdGoPcl13^D8b5s{`}m!sn@nTBG@8uuu11jco!soY}lQR=aP3#Z5RqqlDDl3CdLQ zVvoCTX-`UM=TN27(e1SnsByK;;#ThGBI2k~Y+wJP>wpIC9+Ql_!yJt7_z>nsu*)eW z6f)n%3yyfgN)ava9HN7sLEJ}ISSeBgP9^H;8C<=^ZM6Fa5`mhXS^Z6wP5OlPgxkkx z2SscP7A2dzDliUF+*?m#q)$mT+2MpLC_*k~U*o)$(4nreQlmp*&#*Umv1vPQbK7sL z6Ulz=2vt(VRC=rq+WJ{z5h%lOW8QLIcQe&E8o_mh6ae+#D!=Gzhgfnbxy_-Ed$`9u zcPGQJ2F<^Uk-*BtClTgWt+d`>>LSzknZc_#0!l4+k5dmxkSuUJK+H$EXq%%?u5=+9 zWmHSewY?@^DkR+|sB`T~eQ`0+sn^2>i{!e}tTa{mw4^S_jdNyLOyq zY4*=|e!YHwq2c$`GJ@gxL*%7{al_H`O{%=hZ;!;FiI>@<99vhmoh&hjhXZI&%H}-B0pz3NmXB%IA8x(8VyyGi{ zqr?cp#fk3GkL5h{Y5t-HD9U9E{-h+vXLP^)suAJ5uNNbUUeiiQ7~Ku~{H6*d3N1jsL6e?{A*6cvstGA|ijQ8MuHw6uPQk%Lob}ct%Z~q%srR9ai z+X)^H+!GR%3iLL}YjT&rw%Kn|jEWbGem ze1v=kh^wAEB~7nc$h+2Ve}aA{X=Nhs6e8x)N&MrY=w79%#!I+pQNMEf8*5WB1H{co zjEcKS@n+QvrVI_<(Eg;U!aUwORlcKbte)=+SC#&ViWx9*pDfJdb!nceV6c6+OI{Cf w&9!_Q^Q~1g=KCc}M-SBeIH(fMxyMN`AgFsxJ`-Q}UFrZ+V@soA;&Ix400^P)U;qFB literal 4541 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 I>zopr097~6{{R30 delta 991 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 J74G(l|1bGGWx)Ud diff --git a/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png index 563f23d093b48bd455c244ca49e3b3448efbab10..4308c536144da75a46f915481571c3344ff02b81 100644 GIT binary patch delta 750 zcmbQrK9zmK=K2fwm>C%U|9^ACk?XL50P}(W|99yd9KHSM#3`O96An)jRbxNxJZo3n zIz54vX8B(}8ecr!%QSJ_y6LOASr`~LtOPNRgkCHEXPv%ix-{#fxXJ5ue_GczTD+Y8 z&hn#bLaRMH6GO#nhynZNKlyb)7_4xSPpp2pc*o@WdHI(_djBahocO>9aYrB*lOYew zh2QsYaYv+No6lYLzEM8tl-hz9J>j#;_QvdPwK(PT%wwOh?VN`4UF{ER@93QWaz)=} z`F$sr+xM4fO8wJnXCMvTN18HLooHiYeW&m+w4c&Zw*t z$HDL*>~Vd)YqnpV`chN%?&EUvr|2IEOfz!-H21|rmOZCKQvYu9TX@%L%I)~&nbNfr z86H&0Ge8`9kn50v0Lz8{|FhpWD#hknN#0}Tmvbub2r8=yE8QCts}NMX|Kw|Ke;Z-j zI~oG1uc~(La>)5!H+fmc7k4Ixh$~-RR;-7TTtx-9+h@1 z?`urRvGWvuUi2AaLA^agI>_N~ryFt|FyLYS|Nr$l*`Aex&kEivGz%&U#VMVCzrx-{ za%sMoq-xIYuxremQ~P`6>e+MiL_A)6v99BNyl(&3b9)__(l&*CV%Q+Sr=nCeon7hD Whezy;-Z2ae3=E#GelF{r5}E)%Z(eEu delta 750 zcmbQrK9zmK=6Zwc%nS_w|G&B6$kps1z;fXKe=Ge74^(#8=89H)Ft9lLDDn1T#nQbo zu?j(V_gp-F-bLS>%VpQDxOI903=9W+!7^bx_kDgpr!y{3q+_pIZ1kt{UrZmD#P523 zWO{?>KN${&2Wwwd?cC+C+JI^g!kUj5J&|J+0?(RnY=9H3qRw5Jb4C)Ljt)D z83?dk_*D*03=R3mz7{bm)fS^b7J}ZTzTRT zwS<;OtqctJ>a+f?U-9fodC)3LO}XmYOZx=eZ=RGnS-z-U_(R+>?bq8&xgLuxy8F3w z*0zh{oD3_zF+v=95aP)H|Fhq>2JOl+lDx;vFXvR=5meUq|H{{*(ngQ3HIu)K&#O?Y z+7YO53M>?nR}b>5I}^i>6|bMZVk(?+a##Jt8nr5m-FIKcy?gup?58Em{~a#=#WWG3 z(g>z<;;W}_^7UY&F0FlaI?Q&tzQD_UKVwzuI3*q)Z)9MYR|^k3WrDDwD?U~|IDb*#~-!q zXf#+kS>D=SVvB+t2Lr={(88TDcN_{z|4sVJUT%|oHzr%5#qz)EKlwTLCz}hXy;aYA z6qC1~^`ip=1H<}N`Yy9hSz2BQIg)#Q>c_@{spsy!t2wsI(rMD|TAl;9cK^Fl{fVt< z<6jluIj7%l2V1b?M>;q>-%j7iLl|4CyNOkCt~fCE`4}3 UHC?Nifq{X+)78&qol`;+03c&|iU0rr diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png index aa1569bfaba5e3e5faea55947ed3bdf3456e6736..addd1f49696bacc14482dfbb2ae7e0d4973d46ad 100644 GIT binary patch delta 627 zcmcb>agJkxiiW7Ci(^Q|oHqvy85tND4lVfVpUY*h!GH>ONH9z+(2_IcWnf@vSnzMH z?HWdDCR8OG7$#0pWQt*#xLvV+gCGL~!~g$pBH}-OQQ$ekDCFm)I;UlF0O!<(mdOn* zUMpBsLYRa;`S#1HOey+1HNS28-I;bNLQJ!D>py~A0RjcQZt+}~pZzbh))Qx*{;~g_ zbDH)V@twlQl>Dr#G?$(@tIU1=jBUvrSvZMj0t37_c_(Pl4Bl)ta&Tg7+z zknhqjGS>fQoff%NUpJA(J$T~#$>LGIPejypR&T7ndWKWp=(6asX{$wk)>)V7e0nvdirxKRy5?r@U%r#sm>dLV73r;v44Jjg!{*bONHH)l{Qv*vupuu414F}t ze`|Fga!NCyDl*`l=%Y1pmNe5H)`{B{>kGsodLm*seo^2#$|w}(q`Ia>d!djPBd0ba z=hOgBj}plbZW~KuvzI{_qR$#9M#v=N*5V^uXxf zrRozotzVtrwQTIT+*dax=*hE*t?AF?tj_guJqvbOTjJ(&tanSEtg8;|`^-sqmY$hn z{Ly)$zVsHUniZ#~U0z-@Q}Wn@?|sFaO|A%~#7NX7g=pOl7O+cM=G~(!UGijixs7{h zd*;JsK~;US^=DGKUf$pD(X@EwiSH-5*QynD2~WFvV{g{87XFzpxsp$ZcGcL+T8sUB zJb6E(uH?oW2h~5jHht(;UzU@n8xqCt{x4l~v-dC0$*fEc0yB#ARz`-*TIXT&Y2_4^ zKT4BBnN%6OCs#8`GQOWYl}TRpzJT_B?HQ9*Y&y5<=Gb%Zv$Yuyaw9fe z<+0)AUN))m`sA*y2@m#~tIFPr)B5l?(@uA0ylMSz<+{n=m{pCv6poS>5Pd7xS8Jk zDscIAov)xzkZaPk$pMq1yjN^I(CQ?(<;mt#O{^0q3SXYY9@4!c=hcIkGnF5$%aWVn za5XBZm%CM|VcM_YXJ3z$Kby#WUfiHD!)1A}a{~XG`T!G&jtGynp@Gh!8kf}lh3dH% zstY6~O`IPc(0In1*J9Tol{D|af(Oq(l zX|>ZtU*2g!XX?HFf4>o!@^G4+lJ&Lct6e#jH)!_1oXOLov*g;UO{+9SUiP{;E#&)h zRMacQ$y3$oYew;+H#a1VttU55XuT5k^|>C)gYMM=0)_Kgd&^VAn9e%!F>5;Uu^U*< zZtQBk5_RfKf{=@wkDud_TG21}CWdls30uDBqEjcQ%Z{5oAv#OyuW32&3(zo1nySiG zc>l%J3CgntGK58Pws4t>C|v1bJ0%vBz0hzqm#f>FZQ{On|ai!%`JTW zvD+28;oYq~YcIvEz5Q_3x7J_jXTQart*?9|{9f+&p4m_4_SFh}@9)d5l9dnrQ2YMD zhu?nR(sz_T_&$Bk{^iy(eC>xH7Fa;286Wt;$@0!w-ev`B6}ja`4~o ze{W1J1XwmpZfNkn{+2VXd4EjN*{od|@>}2Dvx7Mk4DM8ewZ6rgN9V9OcVy(GF!R0x Psr7XAb6Lmil+XkKul&Ko delta 1070 zcmaFD`IvKpis&j&7srr_Id2XaGBPkQ99r}l^kF~Q}SC7UC9P9Muv&o z-I#P(Co40$*2^pEm2aB<+H&Wr7kRqfoAIoa zSy!mSu_0Ez&*T3s#&d>Fj+csryn?bhO~rNuXf#c$35(pIU{sV8^{F9CaH_AYjcu{Z zozSz)!YiV@R~0S{V7U-fe`?RXALnd}9{gld6X<DqQeotz;OQamm?ORh2PcADtRJ87D>;PIBZZu1VX zS}9pSD)8)3HgH#N5@Sx}Ub*$ant+txX0@kY`kP+an90nHi&>z&$@WA=UWA5Wqkw&P zWZv?`CrbuOXtod_mtzmYPM=5vY0(Jw4K~Iq4i2sXqafL(%y+4 z8dnDimY^UTDpQhK*}!N{&qP|8GGD%{&RxcrNp>Rw{z^>ro&vH7p5zgm2Xs?Tg9GJ9-(t3FZfH=?4Iien$!zJUL0Zr)q{Q@_9V(cR?v<&*bZ-pK#x-^D`T^^X^G z?RzgPQ+NO5Kc4M;fBsGS{Jo_8@WTQN89pd=ys@4EoILNGg4 z#YRpOR~+&W&hg{;P%X4wreoe0& zU62-LdA5kvDwb7uU-s)${(sc#e<;gsY?)r-G~542fR9C%icFaBI%^^O`ouCuuv>2~ z&E_m&yvN-)R==i|4CJc^V7by%G{ WGIAza2X6%_^>p=fS?9!*&;$U!3A#D} diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png b/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png index faf502d01950cf3ca08beab990dde84bd641f639..787cbcb6c290adf9186a18cf8fcd89c4e8c60d2b 100644 GIT binary patch literal 2220 zcmai$dpO%?8^?b%a6DcicNdwXBcAI~43>wdo9`@XLGel8P_x*=d1 zFaQ7$1pE;n0Fa^sz^>B}0001Qlc#C`0Es0W!TBc81T3r+005nIk@x3NZ;TjtEAFZT z0I;$jP~U}~2coDvMVM1+7V zh|ZwcNB>%R2^?plk_Xs9|` zJPdrv>Q1id`EQUd{6UQ_l~WFRMm0Z;e`ZKn7HV{Oc;FvB_emzkp)F5F2ZguoXGG@TAQ(y>3lP+tK>7DXYiG z3!ZDLVazW+I=&q z^n@iDrW%At;qNLygPG(6sQSf7Ly;0iB?4dCnYW26k3Z3rwUFs`U8B+GLt#My7N_+f zKuTC-vEHl@XVIwZJO-j1ys?oJq2iE3;z<>&nwb&|CqA|&H~H6P^q6T+wvZYND`Yet zQ(_rb;mbx=r1`&QWsiBSL zIoctnF+dpQNlUr|a@@E&h+f?^9ORp3YSM+}ZKAQZDf$n_lVgmWi-iX-o`I;T&rVPE znoZ`a_}{b~6K;5=+hkR`?!S|=OAuE+(V7=(hnTho8B&TSKaTV;ZSa#vM^YUY*H(r( z_=b#vSxxdXq0;i0UQU8WI}{FwL%+&a(`Vy(nWgmRQX=%MK+|cN@1v_!kX6GUXM#ks zQNH?~_t~P8iSl!)k;bJL3VE*|*_O%b)b;f+#|6vd)8Q%;?Do>~Uo%M{yJ>}<)2N~I zC0`-E6Ni*=1MwiG-qaIZH}_cW;$q-pzcUvP8w#;6D*fdi-_*WdYD3+zJMk8F{?erL zhbOCAmB=~5km%mMD|qOa^_2=Lc`r%H58d|Uqhby(sEMr4KK#^Qe9zPnwMBMj=H#PZ z9L8vg9Ai3bI>dL*ZV@}7{fA%jQ7}7_jtHO9{#zj)Dl_Kf+SJNG1aY_ng|c-?x-G0% zR?+7fNtS+8lJa1nZpW!X>6gh{NxpJbtb4~IXBmEvD4xSdxbxw$D|;8Cv4{2!3{wWG z0+TjUNNXvN0;gh#o9`Wo;B8{^HWUCLtcI1 z@p1#pG#L{j{r7Z4A#3O=1`)z2VQJdEIl8y#c41n{9DQDH#R^6$h*5DB@NqbMpZ?m- z33r@*2={T8y_WKj{=+(SP&{iIc&$Q9lh3c_C-K;(vSFE&^xpIInusVK7e z)^($bHX1i?1nzt2VrvuLyg6n}`0mRoFG9&vqbSml){J>9=YMW)y5*9S{~z0774p8V t*sw$V`*&alD$eozuZ{S82C=g>S*nY>Zy4xAWygp+LvT5Iq|WJd+VA02W%&RA literal 2228 zcmai$c{tRGAIHBlM#eB`vYLZ+M9vB&xmq)haV*C0OG>eFW)Zn}D#krI#u)c8BXT^> zS{k7oP3_ROk}IUls&TGFt=W)$`mw)XO~2>Y=kM3^e%|lT=XpM#Pb$G44~L;)006+P zt;~r40Ad1wkOve10Kh?G8VLZoQ3k}z8T@n7uupG<0}Ht?GawGc+;3w3Uv zfzvS?sK7MN%3>*n+; ztsHxwU9Kd#yVu>_`;gpBg+}i{5fU?>2fhop2*2>~vibWJQ&)9AJcW4Mn4tNjFne7T zWziO0;-wdGuEyJ5TV`m{t`2UomUQj%&n)4y2keHiiyYhhTO#KzjN=UvZ^c-smqwDa zzWm`mp>Ox+=}Qk6-EnX)LPtj4&mghBTap~2@b?erz7-DSMg0y^ci;>ZxlIy=f+9-X z!g3<(!+x-e@k-^rH=VFT_Y{tzhgG**cbc1ZP7!J^;)1hziqN``Aok@_5k#^|2jZ4= za74Bp`qYIvO6-%qZw2m^*5#jA)1hO%?xnI1(beTKzc#=R&NhtkMH1w+>=4gyqEgBr z2z9UJ`Xx`bc+P{t>T{fD4DbRS<^-v8{4U=4) z6D%hXL1@!Nv4hH@kY`vN(pI4qx9de4b~B+&3tr=deBwU4wLaF8!ns=aR)h?K{1wZmr zHAA7FBwei5_gna055urH^4R@5PiNSEtEthLSCnMPX!-hxM5GS=hGM$CM0g@A7=O)x zVjWf7yvM#wIkue=+ZfV^k`S(|n#Qy$B^&+SD4kH4kD~+&k$>mO&R%;8E5hg%p8YU znB(8lz?R1qeFJ?M6jIJAl?#~~w{bC17T7@6RS)mcoij?1#M_S3KpsuOM_<{=c8q2WG|dt8NSV!0nIi5z#DQ7t>_&hLL&!GtN}zTeC>j$TGsmzkdC)E(X15~o@4 zSM-yv@Y9@d?lh+L*ibpT)<=&rcYw-h^pD%<*fgmXCVH4u>xG=C^~TEtDDiHxZm;EB zY(*C3xCGqu)vYu6Am6gy`RYJ$^usNcD_`AszT3p)4N*Wr%51>#*%ez&th8c!&GY!v z3i@4<|7@X;WojpzJ=x)TlXM|a?jKglu`5!VV6q4=9_RM zBh|KU^=IF`mV(jni~d#2U5#n4^yYQdW@9?jo(@`K0bt`$669jq81H0t;q<>U4bJ8k zim9*O?siNokc>$3{FStFlpg9cn8KhRo@m&3$sB9-Qx$nM;%=nkf3&|{-TLAN+b+FO z$j#^ZO&(PF?whZk$7Lx&B#CG`|wJK;=Y4 zPcy5L;dVZ2#=R%YU8U-8vwt~q_||gQQ*Yh++qWKHW)r%_dTK_~wQGsWW&XxzE^TmS zV_9habCt%6iJRwXXv`H@`b_hQ=yjhNr>L{UW4cjL$mSt_< z->;)$zAoa2L4&2_;s+d_zr@#^^qNv>G9gB4W#r{ePW|P(m#xx0f8p`xfa0)sQg%=0 z*>B96*TZ|?`T69k|9&eg%eU^(9$4%JV^C{d4zE{>f*UB4 z$*fKjmdvo#oRk`M2Kvl!GRYOM4hW3Fj>=;rw4O eJxL0bat;JtH8zodTxoF+WWT4YpUXO@geCxwBsJCm delta 679 zcmeyz_MdHnO1+z>i(^Q|oHqvy85tND4lVfVpUbVU!GH=XBpDbO{{Me-*pQcjfuUi+ zzqPs#Ii;CU6%}wo)HL!iFfbfYsDG^-vtSzws+t5=u$t-l8`l`{9DTh&XyPIf=_hBM z{?7^ewz1^ims-pHGp#r8%$+<(t@V(-6UW30qAdR#-^ovW-CMuz#In16RW%CimzQN& zFgoUW6>eG~t{SybSgb?2BJXdz35%mGAmHBmJ`fjojI4KbM$yT&YZUc9edz_KE7I9nx9)0YWJ{ z#||>hoDtnVx8dHC-AcXnZ<(teCBD6^^t5d8@x!;0gXd}8n(JlanEiT#;O^y;X_+^a zn42!lt_cLc-M8+FoD#ab?`w9R_}h0y?EUXjO~3C?+y3^$mWra{bvqZD z|CZy5wEwwn<43lKN|IV`?gOJ8+_vrT$G6W?*nX{FtaJW`!(*e`t2su@ zzDHY=ujVK*zjgoibgK9Yzq_)lYTADJ?cDV};Qo>T^V45_Z`0Y#DiK>=}-Kmitilc`e?bv0Rz z-I$HkA?xRd*_|Cl>>z=^zL?$LSHuny`16z5g9AnEAc5cCnLRpE#10bp?Ty*vV@2#B zfnQ#jJv~*#mXs{Hgi0tQ;Z}^52r)BC1^oQX?D@GOHm7XKWt2l18Mk7r1dN$cD&WUQ zW-l)lu~VX~{yQZZ(G<#mXhz+tFg6SfnW>RyHhq7*KHcA3oE+}W{odip#m)WG>&JIJ zfqIMrA&Qu}ND0?q9`}@w6 zX~8Z9dzgIi$Z~+$DOE~`R)Tho#}b@Fa7yVe0Ad+^xp=@$1MVRs1ctjL+%!iB3~vkY zE-^x2cprqf#SsF-8$Wyx7$Gox(}6EO!vHp!OeT}bWHN0>+iDkV?ef?YBm<6={eSfY z$$%qe|6e^pl5ARk5EV_g;jEa=iZchKrSKcC)3y{WR2fahMlo|ZHeWPba=+CIzv4S> zOThwgG!+}g%)!{S?vi<=L7-CO{5khPX*hd&hr2|&3 zQmt?ew_TPK&l;xSWS&Z1(i81Iq!aE^Z8x5YXO|*j@^Y&9rlmS*bi#9}_0i%vhe(*r zQpJy@?k+Jsy?Bfbc)ew*WWXD;>nuwp1KyBbXIU!s6xd`k^;EwA00960?VCXgz%U2| z|NrXrSlWYstx?0q%s>yah%AIul<)xnz%<7-j~I4@TY_On7{H$KEmX&tR~>cjDQ>^e zp40bJKQ=M_)c0e{d)M}36PJ@4QT=t#Gpu>=3Xinr)lztAsr)ULg+i@GU0d={ONq3H tTka(=oR*8>w7MZMoR%wU7yuxX3!bn%KckTSqyPX4002ovPDHLkV1fuyi!T5G delta 872 zcmV-u1DE{T2-*mcH#rM0C;$Ke000000000000000xEcTe0RR8&!!Zp2004lX{;e(H z2^#_c00000000000000005A!WOe&F@8v@(~k+&fNa2Ju3Vaj7`$|cmFD;ln**p)v0{3&A|MJ8e3rIRe|sBSe3W8 z3MBb)03j5)oS6{QAXeDSnJeJ@bf>TO&0T9dR%f$n38gLI8Au!w};ifr4 zV0c@AcZm@K!}}n-EshWv-uU5rzzBiin+|;O83wS)WHOmdCX;D9+E%+@YnR8KAQ^C^ z?EkALNCq4!`~T_*l4R3@f2e4>4QIt{R-8E?Ers8BowlW5p~`3~Hj0_UvH7CelKZVz z_!Zx2TM8C{qp8>^W)8-tb+0U~?Cg4X_j(p1+xb3o>c-AllC-YSDlAdVyA)RoSYP<1FJi8PLlb2J)H!amkqZ6J(t&bMZ zIYh!_mMVTMt?v@k(~HO0fV(YAB?I1&?XoPD40uDf%d%AJDX__8GIgt800030|LvR6 z3V<*OMgRY+*JIR!e`V^rbLT)0auK;OmMG%`0QjHnnnw&f!X?45BMe~A_$I1jd{-TH z?I~`*(4N!xQy(@l{nYzo%X8QEViT8>8&Umr_cN?{@CuK#=G9VoX{r1zriDVSMO|BR yQA>%mhfD4yFr1c);k3FTFr1bvY8U_@lLMZxJUXFs9e8>3C&2~hwSJ^ zLe`M$KLlxOCZMOp3L zlfUfQ*G;>>SKYnMVvpht7-+ceT<@Y^eMlwAz3#(#_=3Rk=;m|Jjf2BhPgU&O&NemmC6d zEx7ED&r;}lq;mGtrguGdk<&JQ@tmabTYpynhw3b@eVcH@ePz~6qnYvh%{#Qqo7a7gH`eeC z-p0Ff#~i-t!D{EblLG9;?e+b6f?fw+vR?D(#Al`RQ@ra79p9?0(avrAHRX-hy015Q zCds;&sZN(Z#d=ktY<*|UtGQnfMf==~{1lfx<>ry6KE)x;H&RrJRtEQn$h_Jpsj^(o zQc_Yq{NVmMOX{EA(Mf+DD6MjT&y(9XSMW}H|L)V@x+`oa*V}*MzPN&S(tZA?=5G6W ztC+#bWY)vnW&?p%TTe+v<&>753xQT#i%VGCK5~RCI@s|30iXK1J5PQu`S(J2&eQ3W zb00|7f8yOzDpoz%Cl-w$dauq zlLAUlhO{xQ3>X2)`? znkPFCs^3waYRqQ4ibZirUfh)QuTAUUf2z|o%~&7brzKt%@G>i?XWOGWvDSyqbxmIH z6()CSGh6GUzbgcC&s**O-7?o!+x)~S=c;l;)v3!>Ipy#DRNE5VZ!tG2E(-WL*k_i(SVT1rA)EXH17qLmCTv+F5&oKEKi%NY;QQ`!KW|lOz zR6p@*)AuHhN4?J089l$Oo^8b0KG}3-&zY06R8zM;SvT+Une(Uo4U5B-RW`bn^i0s?`qh<}a^AM9dwfa#er|W>n|)GwQu}uv_I+%jTJCqdYU#{X u^($_xIT&~}I5P1FC4elIU}2D8xNwM@jXf`T5wk%($cdh=elF{r5}E*Ph$>hB delta 1245 zcmeC>?&qGMA{yrD;uumf=gk2_Mg|6kLkqt8=W^>$v{b6seaI=z#DD?{xEL7z|9^A1 zk%xhS;ebN@Yi*eY+gMPPFeGq5)G%@~FfcSM_`iO!JQo+Lk^@X=N`4EXE7>5%$S`rc z8>7y|f6Dbb5cM;3Cp>ZxXuYl+uu;U0skQ%@dvl+=aBu&y<1Pyy8wOtdcu{2A`UiC$ zzf7mETxtEz<6ijIXK$D6e*X52&-oAS0!|!?Eoj7q{{n)Qx5M=c|E+x#vWWZo>rlOrs0gc#VoAe@K>C&7HF8_FJ1rIqx^u|9cz$y)t*>p?4CS>(jK)H8{@jw%YT{ z=hm(>n@u_we@KrOZV8;TBr@*W+-vHmji%ap3MKz@{jB`QahB8hKd*oPKeoO~Zu2BB ze#7iKw>r_~zhAq5n-?Ut^WPH3rrJ-l3xDr*jH;9< ziT-2Se90vAvcu8T(j9AzV)WbU9skBu?cHN=V$E@n_Xb|guUW58IsNZZ)6Ur|Vl%C` z_&;3tB&vVDc&%I#I08Tu!9_Aia5NMsbL{d>XrKRUWpjDUqOAfhK!I+c18_&+I zW#<&l-Tyv*-Ur1vzlNvbW*!Rr6HmOCsXw9sNsBKg-!qc9ySL-pwh*4IMJ7wOFHH&% zUaGiPSF~A5W4fYW;f^;`3Z=@ubiJn4`=4cr>WXRGUS#Gr`}B(9?&5o@_a=S%7m&rI zF6qVP1d=s55p5~G_-tw1b0K-og*!^ln5Hh>RKHQe)O~Rp(|OU%3Hw2sU++0L;f+LC z>Yn!BJGA64x0NoMyD4SQww3zlgNx_+YApJsr~U2ax;?d7D~s)>uaLR?USd+w-@N)I zu7&!gHK&W4^((xzRM~7-vnVdfi<=t$wQ2qPPjxFzU#y5frxjin@HQ`~ciZDB?_?h) z96wpEs?GoMX0y}R%8<^ow5jv#9zHm8ig(ke3HO41JfCKI9$N0dPx9@xGcS|YKbxzzB4XuD|7SJ7Qhe1O7Hm?RI?*z6^O7~w?p!UN^)vFPX4<=~ z7L{uprEV)GdzM_y{`TwF{Ttu?F4#0Z+hr9xYwh0;ES>w_+^aP2y&Hbm_py2Tj!c>5 zPtIjVuQv()%3pe$g@cXLz@x#DiAN}50z)$c14EQp1tS}KUU28;@Bctf^mO%eS?83{ F1OPonF{1zg diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png b/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png index feb4c4f8b0f4d013b581e94a49982f8ca577de80..e7415b7aef7801266881cf7c865815fcc67c41c3 100644 GIT binary patch literal 2473 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 aj)B3tI%j^tac3S-()D!pb6Mw<&;$Swkz~;T delta 826 zcmaFC_MB~kO1++^i(^Q|oHqvy85tND4lVfVpUbVU!GH=XBpDbO{{Me-*pQcjfuUi+ zzqPs#Ii;CU6%}wo)HL!iFfbfYsDG^-vtSzws+t5=h#E#th&k&g)=SpUFwA@9AkbFJ z9M53u_hF0arQHWtFRU=HnCd#c&q(&&rCnambERDt#cJxZc3Cj2%rHMBv1k8Bi{_Lw zQvdwB4sEdB{BQ4@H}5|53pjBowt$Eyeg$jGCfBaZFzM0y{8W15IrR+3`TIV4KNsGz zfdBrFwVyeoF6_?#bMYsO?u)ba=Ko&RtkJZW=5Cg{FHq~kE5RGcRrR&E-fF&%roFl( z&#WJ9FO<0sWpo5x=+6Ih{pXC*pgsAM*Zj5Yyt8uYFO~<&98DSTPDI*okJNj1`D4a} z=;n(RR`ELK$FHT^e-pMVPy4KJF73k=Ufp*$iqc#rUw!u^xQsD*`z#luQ<6-)3+nym zu|Hj45WB4IUBa$e#{;>V;wAt5dop#AjEGo>+S+wXo|>{$G8nbYIX$J>>D1<+f5y7$ zmM5-EIK1_D^SR}lYdO9J-AJw0;_$OB;Cr=F!J_|==ho#-4~{XEcV1u-bmCA zp>{=kQ&eS3+2X(p%G^pV0#0&`yKmcRYLx5}REeLu?!qTtQ2JWj&U5bEn+sD88s9N6 zN&UdX!Rsq&<6Pz(oF>qsIW;-sL9p%A&le6%_YTo}%I0iv)-hD_6?0M6Uq@RxlUEA^ k`6uR|*N|Xc%wW&JU|pRvU$K$pAt)Jpy85}Sb4q9e0K5@qEdT%j diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png index 3a60a59d5b294d62b39cf4f00c0a75aa1c9d8981..b216174e42ec037c12671a7cc87a0452fff178fd 100644 GIT binary patch delta 327 zcmaFM{GNG&O8quZ7srr_Id86AT-$8G)A}&yKt|64L5a>l3B|=JEb({Ed@qX&9BO0v z_x|mPLkXN6&I}FzR>TSaop)-ZDf4VwG3ME&b+-;|TA>&8=!g7q?NyqRrE%u3PuI-f z;b$^g=CTP-u-3-?uiAd>w0NfRUT)*X14qvhjh+dCMop`?WzLFo;E7ykHBtkdoKbyIeKxa5!Ux$Boc){cGNfBfU}(9@AcH#e44Fd9Vj|G6dhK2?I*WVV@(11&W!2w^W z8be;NntyB8K4g?;LREB!1!4|dP3}WhbTu(dXlisHa^h0c$iu+Ea6qB{wQ|gYZ7isE wNid?hS$a|vnschbO5P&8^;?h&6ptX#!z#vB!P|9P?mvj<>FVdQ&MBb@0NY@q&;S4c diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png b/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png index 8b3134e52adbba897a0f480d92636471d713cd7b..2243248b07668a08bcd7ba0e5d146238fb1b7b57 100644 GIT binary patch delta 402 zcmeyv@|R_TO1+Dxi(^Q|oHy4W=C&E|v^>l?#L;trqvt?|q~_uZmT*ylD?wMMW%N3} z|NJ>i=w(Rb{vZa13ae1xsTD5wTpa_a2L=Q@a&%eA3rg^)2?`&(f{rEjfgUcvjLSqw;NXh~QW>!W97&!3Oj$yJMqY~qq$$^Z@+H05? z82D~o delta 362 zcmey%@`q)DO1-nEi(^Q|oHy4W=C&B{9Q~Mch@j@fyQ{z1^f19@3 zM(tw0(btrllb(JwmOK1w%BnYK`*umo+dU5Yo&43~R?M?+%9T&jd%9mNRDT@zW0s6? zP}QchC%VsHylnaFyyf+@n141a{y#Md_A{Pu`}|T5_kYK>ocFCI?-%66Pb-PvShCx5 zo9&6+JCEgVpLn}$gY~~SZdXS(E+ep8L{b(gFu;Mwd+x~(7?qgTuuSG+RASVbtjnmZ zro+U*@c;jt!;L%)3=9Vp>R)TiEZD{Z*8>I?Oq0tPGZ;lC-)B^2@@E9;Hd@aB()ac- oBPRm`L&Jjq>!l|(F+sJ-u*g?FVdQ&MBb@0Ap^FIRF3v diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png b/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png index 64b1a80b8d1d89ee9a062a167cde4161afa90df2..f3a887d20c1f9b7646b2e588ac2110d5089c3005 100644 GIT binary patch literal 1210 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq|vb)5S5QV$PeH4|5MI zh_vjT=$^phc1ok?5J%4>!MM<^y$shU?zpx`c;EZ*)%xjid?!86Y~xgH5pd#AY!O&8 zW9Hfvff?z_E3WKkd3KXS>#KfarCF2LU+;u@y$gc=3*nLKNaud}xkz(Oa#y6JX>l6! zjm@=_%V%cu9{RUM{Cw;)ujYXGwR`8Csh94^;oWI$nHPO#zhr8gF~>nGr)0VKdiy@N znGbnfUWt2ct$bVeuGi?XWXC1_l&zii|Bu{~QCe!|yX;Kd^9dU-pZRGeo~m$9|H?kU znfp@}X57D!CCPF3(TV)RGxDoerz*_YKW*K#g9-lp4~PMW895mk7#bG* zU;kT>3lv*OU=1tS4uq21f*Kk~@(c_NAH<->81jOR`L}lMLq=&PR7LuX5Od&aav!py zt67b1j_yNFf@;n_;73>EjqWY!NlmEEIS`Ak1k>lmP#41k2h-)$A!Sr|m zH(H|beaMdHUJNC?54q8lY=C;Hk%xhS;ebN@Yvq^)+gMPNUjhrrqPK{MLYNY~mZ9so SQSWLskOogzKbLh*2~7Yo)_@oQ delta 663 zcmdnRd5Cj@N`0rNi(^Q|oHsKc<~AFMwA}0vv|&+OnHt=$*2)!zHbD2wlZk>4X z{q-)>KU>dLopj@{LW_VChhmF>(}YLw?oDnwFsFCHr5{2wHVcMG#WNL~HF-VuPB`Zl zkXgfd<|c<$uYRMYwByo`ss`!GD@^vWJj-F3dThGG9Nn|?PoJ${DW6&xe@-iR6JyV_ z`!_V6MzJQ={5n-UrD$o>g!N(T)SvDb<@n6BT4Z)x;->o|m44L%lfsucRo3jS%Mq>g zt5$ecbzzS7>vz#>PZZx{-gu?iCVbg^^Wb$kI^OF<LwesG_ z4~x!gKDzhp9Q#TIE6sKHdFwAJ_x%w({%7f}+3Za}(=+qsc%Ipp?X6UJ#P9HKal#RK z#e41tJM3Hb_cJxc7JQ8BGK~K;@$tjy4}Wtxt+qVdai1;sg7>5Ciotda3=IGOzqx40 z%b>t;#Nq$C!Vkg=dCEOSzc{f;ehJ%uaKXy^2^X&98)UpPmoWJ%&BMe10-Ruyp+R3} zawU`EWNs$T$%~n!nbxpOKER~N{6T91QCrwoIPLA~HFig+q{(uXl@pOcq_s(>8o>FVdQ&MBb@ E0Cq|KJ^%m! diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png b/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png index a768ba7aab599f6660e0f8a6f6425e3a89942d42..82ce82468166189d95acac312068a368f06f667b 100644 GIT binary patch delta 260 zcmeBT?qr^zVjAG-;uumf=gqaVoktZ!*aECKw1hi(`EX84@SHYDNaoY=GpAg4__UOt zQ>)pj5!Wd7E-gU-|spZ>JpgFSVb&?tI+OW%^(K)p6W& zem_6r;PN{xxjq9 zoe}fH6~e?GOle^F+dnZ|5Ewc>(0mh^pAh>_piX7 z_IvgloBVfjzMFER`o+rc{}r1uJ)F-UXFz}tulF%9F#P}j<}f2C0|P_Dg8%D(3vzKG z6o9}7!HFMKR9PYF40*xo{;lnO$Slo-s_+lfWKl*Xy&5Kv(zghuz7N?!N)Kq7DDQhR SHF{16NSmjtpUXO@geCxNO=P10 From 05eeaddb0445fb3b2aedd74da00f3b284b329cac Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:21:50 -0600 Subject: [PATCH 08/23] update flatpak hash --- flatpak/zig-packages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 81024bb26..08fa9568b 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -151,9 +151,9 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - "dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", - "sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45" + "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + "dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg", + "sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e" }, { "type": "archive", From a00a727e779f674b1d79068803dd336d42e372f7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:37:26 -0600 Subject: [PATCH 09/23] test(font/Atlas): add test case for `setFromLarger` --- src/font/Atlas.zig | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index aac2e7e8d..7b31e2794 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -587,6 +587,35 @@ test "writing data" { try testing.expectEqual(@as(u8, 4), atlas.data[66]); } +test "writing data from a larger source" { + const alloc = testing.allocator; + var atlas = try init(alloc, 32, .grayscale); + defer atlas.deinit(alloc); + + const reg = try atlas.reserve(alloc, 2, 2); + const old = atlas.modified.load(.monotonic); + // zig fmt: off + atlas.setFromLarger(reg, &[_]u8{ + 8, 8, 8, 8, 8, + 8, 8, 1, 2, 8, + 8, 8, 3, 4, 8, + 8, 8, 8, 8, 8, + }, 5, 2, 1); + // zig fmt: on + const new = atlas.modified.load(.monotonic); + try testing.expect(new > old); + + // 33 because of the 1px border and so on + try testing.expectEqual(@as(u8, 1), atlas.data[33]); + try testing.expectEqual(@as(u8, 2), atlas.data[34]); + try testing.expectEqual(@as(u8, 3), atlas.data[65]); + try testing.expectEqual(@as(u8, 4), atlas.data[66]); + + // None of the `8`s from the source data outside of the + // specified region should have made it on to the atlas. + try testing.expectEqual(null, std.mem.indexOfScalar(u8, atlas.data, 8)); +} + test "grow" { const alloc = testing.allocator; var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border From 95fbeb5b821cfd3cfdbb3340a8638c0031eb7336 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:44:21 -0600 Subject: [PATCH 10/23] style(font/sprite): annotate type for value --- src/font/sprite/Face.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 8c39daef4..1463fb38b 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -54,7 +54,7 @@ const Range = struct { /// Automatically collect ranges for functions with names /// in the format `draw` or `draw_`. -const ranges = ranges: { +const ranges: []const Range = ranges: { @setEvalBranchQuota(1_000_000); // Structs containing drawing functions for codepoint ranges. @@ -137,7 +137,11 @@ const ranges = ranges: { i = n.max; } - break :ranges r; + // 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 { From 114c3f56656354ebee0cf434a1aabf56681522f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 12:06:37 -0700 Subject: [PATCH 11/23] Fix abnormal exit detection on macOS I made an oopsie with #7705 and omitted the check entirely on macOS when the original logic only omitted the exit code check. --- src/Surface.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 390adf91b..cef71f265 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1002,10 +1002,11 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: { // On macOS, our exit code detection doesn't work, possibly // because of our `login` wrapper. More investigation required. - if (comptime builtin.target.os.tag.isDarwin()) break :runtime; + if (comptime !builtin.target.os.tag.isDarwin()) { + // If the exit code is 0 then we it was a good exit. + if (info.exit_code == 0) break :runtime; + } - // If the exit code is 0 then we it was a good exit. - if (info.exit_code == 0) break :runtime; log.warn("abnormal process exit detected, showing error message", .{}); // Update our terminal to note the abnormal exit. In the future we From fbdaea745698d20c994853524b9be5076954e8ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 12:15:45 -0700 Subject: [PATCH 12/23] Update src/Surface.zig Co-authored-by: Gregory Anders --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index cef71f265..dc7b0e3bf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1003,7 +1003,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // On macOS, our exit code detection doesn't work, possibly // because of our `login` wrapper. More investigation required. if (comptime !builtin.target.os.tag.isDarwin()) { - // If the exit code is 0 then we it was a good exit. + // If the exit code is 0 then it was a good exit. if (info.exit_code == 0) break :runtime; } From dd9ca556f953e257899811b513c76ad681b1b95c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 15:57:40 -0600 Subject: [PATCH 13/23] font/sprite: add sflc supplement circle pieces --- ...ymbols_for_legacy_computing_supplement.zig | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 9f7e8815d..01258b041 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -192,6 +192,60 @@ pub fn draw1CC21_1CC2F( ); } +/// 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), + // 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE + 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2), + // 𜰲 UPPER CENTRE RIGHT TWELFTH CIRCLE + 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2), + // 𜰳 UPPER RIGHT TWELFTH CIRCLE + 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2), + // 𜰴 UPPER MIDDLE LEFT TWELFTH CIRCLE + 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2), + // 𜰵 UPPER LEFT QUARTER CIRCLE + 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1), + // 𜰶 UPPER RIGHT QUARTER CIRCLE + 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1), + // 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE + 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2), + // 𜰸 LOWER MIDDLE LEFT TWELFTH CIRCLE + 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2), + // 𜰹 LOWER LEFT QUARTER CIRCLE + 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1), + // 𜰺 LOWER RIGHT QUARTER CIRCLE + 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1), + // 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE + 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2), + // 𜰼 LOWER LEFT TWELFTH CIRCLE + 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2), + // 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE + 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2), + // 𜰾 LOWER CENTRE RIGHT TWELFTH CIRCLE + 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2), + // 𜰿 LOWER RIGHT TWELFTH CIRCLE + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2), + else => unreachable, + } +} + /// Separated Block Sextants pub fn draw1CE51_1CE8F( cp: u32, @@ -271,3 +325,93 @@ pub fn draw1CE51_1CE8F( .on, ); } + +fn circlePiece( + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, + x: f64, + y: f64, + w: f64, + h: f64, +) !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); + + if (xp < wdth) { + if (yp < hght) { + // Upper left arc. + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth - cw - xp, + ht - yp, + ht - xp, + hght - ch - yp, + ht - xp, + hght - yp, + ); + } else { + // Lower left arc. + 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, + ); + } + } else { + if (yp < hght) { + // Upper right arc. + 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, + ); + } else { + // Lower right arc. + 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); +} From 0414e9e2819dfa08bf47a9aca8c3cd8ea5d7eafe Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:19:15 -0600 Subject: [PATCH 14/23] font/sprite: add (some) sflc supplement box drawing chars --- ...ymbols_for_legacy_computing_supplement.zig | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 01258b041..9ae92cc72 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -61,6 +61,8 @@ const xHalfs = common.xHalfs; const yQuads = common.yQuads; const rect = common.rect; +const box = @import("box.zig"); + const font = @import("../../main.zig"); const octant_min = 0x1cd00; @@ -246,6 +248,81 @@ pub fn draw1CC30_1CC3F( } } +/// 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, + } +} + +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, From b4d83e6349e34022f73ef550cfccaf8f361a3533 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 18:46:49 -0600 Subject: [PATCH 15/23] font/sprite: align quadrants better with other glyphs Use `xHalfs` and `yHalfs` so that the dimensions of each quadrant are appropriately aligned with block elements like the one half block, which could be 1px taller than the bottom quadrants before this change. This is in line with what we do for sextants, the fact that on odd-sized cells there's a 1px overlap is considered acceptable there so I assume it's acceptable here too. --- src/font/sprite/draw/block.zig | 14 ++++++++------ src/font/sprite/draw/common.zig | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig index 27c6ae516..f7faacea7 100644 --- a/src/font/sprite/draw/block.zig +++ b/src/font/sprite/draw/block.zig @@ -15,6 +15,8 @@ const common = @import("common.zig"); const Shade = common.Shade; const Quads = common.Quads; const Alignment = common.Alignment; +const xHalfs = common.xHalfs; +const yHalfs = common.yHalfs; const rect = common.rect; const font = @import("../../main.zig"); @@ -174,11 +176,11 @@ fn quadrant( canvas: *font.sprite.Canvas, comptime quads: Quads, ) void { - const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; - const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + const x_halfs = xHalfs(metrics); + const y_halfs = yHalfs(metrics); - if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); - if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); - if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); - if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); + if (quads.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_halfs[0]); + if (quads.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_halfs[0]); + if (quads.bl) rect(metrics, canvas, 0, y_halfs[1], x_halfs[0], metrics.cell_height); + if (quads.br) rect(metrics, canvas, x_halfs[1], y_halfs[1], metrics.cell_width, metrics.cell_height); } diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index 2f608180e..d10128cdf 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -204,6 +204,14 @@ pub fn xHalfs(metrics: font.Metrics) [2]u32 { return .{ half_width, metrics.cell_width - half_width }; } +/// yHalfs[0] should be used as the bottom edge of a top-aligned half. +/// yHalfs[1] should be used as the top edge of a bottom-aligned half. +pub fn yHalfs(metrics: font.Metrics) [2]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const half_height: u32 = @intFromFloat(@round(0.5 * float_height)); + return .{ half_height, metrics.cell_height - half_height }; +} + /// Use these values as such: /// yThirds[0] bottom edge of the first third. /// yThirds[1] top edge of the second third. From adace942d014072470ba170d036be53f38f153a7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 13:20:10 -0600 Subject: [PATCH 16/23] font/sprite: update reference images --- .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 402 -> 1032 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 534 -> 1295 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 1025 -> 2193 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 316 -> 794 bytes .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 562 -> 632 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 741 -> 819 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1388 -> 1492 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 399 -> 443 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 2220 -> 2225 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 1844 -> 1853 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png index 581b0bbf0547c2526878bb36c8ecbb8277689956..e04e7726b965fab41da5ed0c4f7e3501709db29f 100644 GIT binary patch literal 1032 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=GWYJzX3_D(1Yoq0M*L zK%&L*{{Ksw+ADUu)~#4AIW6?S|Aiis%pm0;AXYtZ{o~uMlfMbc92aVO9N|K#OMd;^-$%bI|M2D0PiB8XWa(s1dk42l1BL^3`V0&V|Np<4oxOIcfxz*?1{M_s zMote0CLyhdV3Lv3V@1nNo%P=7%hEH|e>dLwufOwT<2Hl$ex7>&-mH*)QCj?e;;W$h zJHAbIa(>veb-jobSNfXTr56G$lHY2#nrZSJ$Zu11ySDTxr;hGsJzs}JjXC?a*f4TA zCu;DtGJ?JELw?KR`0dvwhuLq7SK~A3u0ET7^Sa)AH=c7QZ|ozoXTK^DzAVM{|B|BG zgegxKeg3#V_=le5*Gd1`%i~}Fc#*0&`_NUV+9*a(fA69-^EwY*k*~}BttoJ}G-;E` z9TyW>wpaiDr5&E0-NV)Y=w$rN3?JUDjZNKOR>v+_aC~czj{C#3Uv?bw^VO1ORRkSg zp)4O8zgmh*Q(fcA%MLlFO2cavTuLR{x~kv!Es(>pX80xDPG0~yJZiB z&gQ%tFthCPr#(Ll*sFehkbD#RZpJ~AkS<46-Kxv=KPF9Wa;>h~EvY|M>hdy!{+xHy z<5;3(BT6k()4b=d$v^dW_u*zaVX3)cmtWV)drfP~ZMYU8KTm4&8eZk@U&dN958e8w zCR-nVV`bv1N2b~5{GC!dkFJ{eR7xq(Gi%<_#QBG7u3uWUw_vHpvUlCDj_bYK!gM|O z$0pHooiU+a{f`kS{s{)=XcYl zZ55lze`-zY$4RPId|iu*mK(_HRA!v~KY7xwVy?iDk3Lhc1w8ccDx3EBscZGDOnuWg z55-mYI;Xyf^0_-{q4)2F3#`}h)UI49Z}#P3%%U~_?iYPBm|MZ4uIqP=^L){^EpszZ zpTE?!#(2-e*NwgpKdo2gl8n#fx!<0{HNBocxblxIA0#z1Fg)jnq{zdDybKHs4GaFQ z^?k@L%>JZH3yXwB zB({RAgn))KtY8}sGjcL8Ff=Uqzy7x%7s%mApg<7f3WS>5f*NQ}J>UveW5^3O=il13 z4;iJIkW67v}eGzaNYY4FCVXx#8HxoG9UX@$dUx?6#b$m4CWoRu*SO@yasKCHbvC5tS?5vs56So)$v|eWva$^#j(7>Y7!=j>~2xhrBsm$3wqp{%S zl9JPd`|`aNpYt4DYR?~tA@UjAZcgO0Ss>keEdvPxHGK1--v^V?Y7yE;O&{iut|8nq+qo)o$tTf6>Oji?Zt z;k&bGipTYtOx0(<63SHW_piEfxla4`Rl)-eIjv zwq;5w6!2Fc517ScTFbM&=#=n#KGt1mC4<{JAc=xNCbboc;sk0 zQ*XCYm-*wTyJY|B7Hz**>-g$S&G*w9a~9fifB9wWqrN+&PT0fZz{IbL57wu7Y${yv zv)%RcUgND@F59Mke08h()HBgGWx)rB1(whCEqr}gp(s$7Icavs#5k+Rd38q?24!xa z^!~}M-9N9kh%qlp%&sh$TiO=($G~$Z^XV6V{>fE#I4`{<@ze8RiH_R-;`#t8=_7C<;H#J1fbXu9? zR1tRUqL$7}(Oo56`90~a4ofdBiCY=6yu`aQ#4AiI?=|O=;6MH!9wfJ1dM;?&SYjuy z`txV;v_y$EY-nOVBq1L*_-*{+?w(R{|*y4l{Cs-TQyN^rR+K=e}Wq7y?)ETaXJ>fFZyKVX%kZ t9&Y4eU|=|)Q2$z6X2CWVgaQVJc?|DM9vx1tS`V^@!PC{xWt~$(699W|8YKV# literal 534 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|Vn-PZ!6KiaBo%7&0<2 zFdSO&)jyZhUjw873H*>`U|{(F|IOtzkAOAf4*iz?+{q2}3&Q#wtqbE6i(-U1Y zRKwODa1|AN%YD+oWOD4GM3==ppAs%Cl%b+#jZN|BxmxprCb$}N!kev;Wln%*-DLZ# zYFAUEGWM$1;c><*N!#o1W5a_c z%WQ9B(>%-i0~;Nud)k>W|K_@KyJ^r&E)T-f(MB9>J$VHW%s&#wWD~?#X#CQDmEYB^`dfyZlBg zx7|nmZf4n?*k|G(E4Vs~lMORYXROZfwCb=wd*O=AutIdeojyD1IN-HyED-+#KGnea zxYBOV;h3klHN{DA!)A`@&a*$_oQLFRV`&?edFbR?DtW6jnXzHqHggn^oNWyXaP@z8 z12tf}t2)o(`n!D_+B0(sf3W%1=HMx|-6li-aq$WTW$!;(@x3)Dcl2awg&&ENep_!4 ztcv#bXI)8yoy$UC`z*WRaX3|z!~VLzC8H$tqu~Y>M$A_(<8I##N-9UAS896pNbtQO zcW>K@mOn61n!Dsa6J+CQp|?zsCkcZuwJegNpGDr2ACEw`PZd<;6}BkY0bfUtRO0wh~5`M!sLeN{{)YrYh`w^(}vfH%HmSruxuh2=}qRX$h<}Eft%j zbbLPX^H(EcIorr)JtwJcy?ILODIW-?)+?f6;0d_lsmNzX#F~H;k~pT_Lq4R6703W{ zQr6e5{mmkEt8?-jb8h?2x;(;L3VL~0l6cclGW~Zk#nPSlG(Ny6RUyo|3rpZRn&Nijk<(hWYYA)k>#n_k|E?B1Zu17fdc3LH@ z+AX*_(W9LSFU>p56gnLv2iZ9u3F=dXFKM#SSxcM0IQ?iAkCuC=xJA|Bw#B znH(xUKGE=)BZqR@bu(d|hEW1WBR$v&awSVA@sYLaC(*^1=yNX<61TJ{UaV;5#8_)% zdQV}4zwgN0d9n5H0fWi75@KVecMT29h=MV4#&-I`U{TD0u$2BOv;DWGv{(3gK@iIy z=d!qtcYF-Jv|#}ot0IUwCT$~K!o=N-F?`cuWoKZ}Nge4gw#qa9LQ4{-6L#zcYfvR0 zx*J;CT<(_NN)WEfaD1E!rIMzkQQstLl0HCzw(daK|S6p{?!@R?s`bB>(B)!cHG6y63G6b<}{y{gNA&mhKpgj zUII~Kl73DB(NDk54)AotxnDC|xav=-=Et4PS{DF-aM4Ugh8$uH{WzZIVmoAR+$ka=x332vLss4}_}&0DyE|SpRxokr-T3h`Epjf|%EmrHqMKDy4BLF`H$4Z7Csj zQ70z#ksp3%i=nk(XQ9e$R*ODi>F>(|R5ScVPo&Q3hN*I`rG}*@>d4y;T{T310r}Ju z?bRtudlJ4(-H*ECL5w<~?pVrDC%$k@Q{Un*9DgeF>7WG4HJ92MgsZjfi$jn-N4#BZ c{by2FgVP)9c-}#b>mRnto#H{xbYVsO3$o~Ewg3PC literal 1025 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfr0sir;B4q#hf<>3>g_1 z7!ED?>YvN$uK`kk1a?R=Ffjc8|K@Nb4+8_k0fqY4+A<5au^_2nU|@K_4N+sr%fP_U zu;Aa?T3$wJCR8;Bj9@ia9JvlTFdV(`dw!I7)9$86KQ4Y?x?2);NPUYQ!~MtK9;|hr zXC7BE^SQBqUclMO(jlLpuwAv;>1awkm!YoXb^iA^C%r2E23u7dIV&_Sb82L{r8L1I zc%cJRuAs+)l>rKk+c;D%glJ4~h-MMW&=T=bc(8vbGnyxKA9A95Vh$5pU`Wrw2n-8| zM-MY{f&=6Kdg)0`Xkn3$uH?5M7bvQbz#3MFO>k3g3u>Un%?B}P)WCy%?L&-U&xTqB zSCjjY72Q|s(api)^VR5T&OYErH^&=Y&EB^R=xT(aYT*9D^m;LR=wN#N3^a7$zQXkR zff#hx^gd)pbIk!AbTybh$5eyq^8+wn!QJ!sEV|DRu%JaKMzSb?nSwiP4)}o`EPZrw jf=EwOpaGZL!3PWs8PT7PEVpxC07^cdu6{1-oD!M<4z3?y diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png index ecdb2ce10121405cfa22222a848c0ce9c99f775b..459822e6359f512fa3654227681071c1847c59e0 100644 GIT binary patch delta 684 zcmdnPG>dJ5ay`>SPZ!6KiaBqtUu0u86ll9x`+s_#=FSe^&P>(%H9M^0alJL=LwX2o3G4Zapmuq zZRYdOKTP$T*BR#*@T;il(sXTB$7Sz+Tm1}>d{ZYgnP0W3tG>Bu6^khQbXLX;E?%jeBTUvj1@E^uC~&*^JTmHlA%ZefIx}%=GpDn+oPFPAfOiD?}=&+C6j|D@VK{qpH?*W0BeaZNHqjBLwTe<)K<4-hK2?I*Go@oVuBe30X(c| zN`4D+ft-s3J_s|Qsktqvp@F1;fq~(GKU9q&FW9PoYu7$xlt#1b4-3egw+QDz&GBb^ W-CZ|9lza7RkWrqlelF{r5}E+AP+o-q diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index ed0e8381614585f5bbb2eac395badc97154b2740..03305c81c24482574aa134051787ecb75293f0ab 100644 GIT binary patch literal 632 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=B;6o-U3d6?5L)Fyv}5 z5OFzp=l`Xs2#$?Wj)^~w?@kh_%)Q10Qq91?kg(@*(fPM2yZ^j@ywAYk$KN$77Bi-@ zuy3C9AR$ah=8RWUbEId1fi@>!n(D!WYrc4J**R$SaC3g?99W@YOz3~74>+KaGuFmThR(+N6QujHdx?ATFGg$3|eW#zR z_8t9y{vBu6v3sW$vhr1Fw~JZ4Xx;pDj?2@O_NP# z+eN;F@#z*^h_?KqVQ~K&GuXbH8@X5&d0a03|Nr{BUXxQ#$6;&nZ8MY-^&(Q+-@mWh zDxhZ5KbN;9pHsQ<+-9Gi^C*m+@1-nenXOA`%U;IC!Z%1K6FHHgYl;@Gu|zyZs!SqDyb9t6mUDgWLUZSqv0`-ou00KF6*2UngF19_2~cr literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E7vJzX3_D(1X7V93b8 zz;I~6SN~itdkv5RB(Ot*fq~)w|2Kylc^DWN4k*;W)|OeYjRi>s0|P?>7g)_rN3IqF z9@Y!L=UZ)_;j5|Dvf#%@TY;V#llbo*Wd`f`kQ!S0@#FuZAO0EnPcAT-S1GrP?YPLj z`t7_er%rO-*3F%&%*0(2=jSKzW90#mjy-{ob{)xEc~RPO%Yoi|ClI?nj8 z&|>Lv+eDDL?l%*hEbUB3Mj-O*Yw7q4b z^YcQrW16-c!geCdzwA&41;T=p|2}Qm&#!vUZJJulj9wkz9R}PsFXN}~%dOaZDJaJg zZ1JD|5By+9URub@z`((L@b7lFM+di@oO}4A=jGR2#&-~P$aImTWAUI@TU;`T@ zxEVPa7#JEB{9pfDkP8%xNFav=qQsCFtmNO?wGSDknUEAPFfc?jg4Dc47$ZHY38sXZ XQTxAXnkq|03P`1=tDnm{r-UW|F|^Ys diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index bfc7882152eff8b889fff1fbf733c0dd89746c07..c17e08c39e013a569c43f4e933fd0280ecbf106a 100644 GIT binary patch literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V19PZ!6KiaBpCY~*TC zU~mom|9^RQCTF{}2xrl`A1)WH*+6QcV8gY-ow}cY*1TTNzD_MbK*Q$e9tU}y+d>)~ zA}m}?tc^_$jtdk5DnwtfFfjc8|K`RBM1I&=X=TAH07IY1;gq;yy^i02=xzt z{yENJJi2nL!#Fw2X~k{`>#`%aiTJuei28y&{@*dZqT2X~E$ubeFFS zi00e#s7Cj_UUcU3<3;6>$qNoRG&V9Zv$8RPHM`nFEV*@%lgUxQ<>H^sW(-H;wj|me zu70qT-!*W<@{9evTldetQdHEMWx3O7>ztT{QGV+Jbk)OGOk;Nc@%!`v2WDno9utEL z2OK_JytcgXwQBf^vXi2#VvAa{rpGM21>!O?FfizSc%u32(M_4G9df!+xom#q zKmY3g5p}!Q7+!#xDNwWVYvY`~dmwJSiR8vsxh+TEf4H}i&-LP)dC#gn^8$2F@~%?d z>9|$Je8sdUY*&RC7#JR`pKEh;j!plaTa%?jUZ3P$_4Ntc)vBWAEZd!qTjhRq-esS5 z{`S23Y2PQA@|hGYXk_M<$#~%K;nKC`71@)dL%fTcvycpCVPIegvxUZ+Aul-I{;l2n zmO&aEln5XWRRfP(=}AqfN)Gs-E5VHZJFJit0=H@HLq;^44#a?sd5cif`;Zx-W;#Q_ WYUdN5JCfZ&Zu4~Yb6Mw<&;$SxGBgVS literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V0`U|{(F|IJ}TUIqq+h6VrDo_)YC&4i?afq|ib1x-yYFC)4d z14gi#%NN;>IEXM``2YXaZvNjP$t{K^uVki`hPrI21DpB5z~hMrEN#72^g#4=c4gVlsoUyw(x+$hh)8G{1SBjt-~iTq;2J;Jl3R{k zOojq37k}Ts#jH^_gY${-J!bR08eL71uKSI%>djX^eZqCMsHio|a;MYQIWY^P{MH5N zviE)XKE1J#jZI1-BVj>flcK9)i(0clTvi4Kh8+h^1)n+f z`NUde6V$_3Oq;A7a=KCZo89I;{=ff4%!}P-c;SEpGczxbiGjeMjjx$^ex0PfrR*mp zP;VaOJZvD~a`E48KSs_q9jyO^-pf8W;OVZN#GE?m&Vr~((jneO%~_nFAl@={`HZR0 zCq`EnHD}rGblf`c&Vnfaynwap<}0o-ufOs8z^6TLf2aS{zj>^YjnAZDK_fG-3`gCq zubTH}Jz=|wWSVDQfG#M){XRir1QD~o582UUZ9NgO?~kr#?^_0RbHt%)7(sq%Snz+n m^rR-#M0CIhtmG|LC39HLvA<^sljH3>g_1 z7!ED?>YvN$uK`kk1a?R=Ffjc8|K_40*8v3qhJ*k2TWMZa5fDu|D7oR&x}9I)N*e;N zKYsfB`-ks**(_bc8h#6Q%uMFqGSQ)s=_sd&dcYhQ<-!G{fb85kHE7W`Y= z`;b|h3GPlXn8Spo#`hsRK{dP&xzW|gFrt}L%gcza#s}itD~?<(1|lv8f6uQYoh%*l`U%@r ztDTNp{qkVkxvy=YemTh5U?AXf@!#%k44v$}k8>*CTeb7nYYvAtB{5_D$Vgj@9XQ0d;X;U|IgCx&^*0KJ%UAQkBH4dr3(+1KHwB< zWYcH$-(bTAinxZ|kSM)n$aUC2fc4_<`&O~D5*LE`!XGulskG8>#Mz^Qu-)*6?s#?8K^> zA;zki5wb#o_3R=A)?O~rB?=7EGjBPnb?^TD-`ybaLa#cz%)uw8o8+Jg*^sLxK)~hT z@B6pTxCod|nCTw6WA6pC$#tE~o-!HPMh}El__kO`e&C#Rn;DWw85sUNI{3Twbo@E_ zIGef)pVz9Jueb&Z^OL-*zCK~QT2<7XWxLaH>%2P)qWtp$z|w~yY3<5E&K3s&my3Tk zn>Br0;2YWd{@}b<2E6mH_A5RA|L)DCstJ52x40Q@z z$!$Rm)UxV;Ke}tzK19!qZ=e>z%LdGR{|CKj!1Qwt)I*Itpz=nc{*8nL%0y`ub7#RNle{;Bzhk=3NfI|IiZJ7nzSddgOFfcsehNv;*Wnf@v zSnzLcEia=q6RH{mMu-|lPOzH)>!l|(p(&Zef~MrQpaz+!}56pFYa%Fg#_z^9tm+24y?@?Q%cw@A$B3 zPP*kzr>%2h7DoB43(!>$Uoma6cF5_IqN|=h;ksH>)S3m7mU-ZM?f$!alUE-2>Yw@k zqy)2MhRFp7wppnP%)M+<9j*P-_A=`f{VPkKex>>O{7Y|ylqWb`WM>u9@K$JC#LuM?FwKFyLl>H2v>?PO-+W>8$DzELw45a}oqsR74guxh{B=dXRHkwgT9R@8lryam$d4 z$x(#m;_v&n&bWw`{H#gu4o^tpw)K7J@|c-{fx+kVQRlY)`Fr;NcqrO3uiAE}4UW`b%%7@HTNo>8f-r zh~-eq*g8R>sZ6nB!BwXw4km*W*JM?8R+j(&&mq(#H%F#`h0~GWkI$khCGr9!S>6K0 zF@wv+-}iUzR`PIAZTV4~e^ax^`9i1)-wWrHZ2by_4*Gtu5Wlvgp#I^P>Hqo7_3U3% zVsja~!x_zt>dK$1t>!u|zjWuxi_kc5)&-i%Bl-< W8vT0T@TA@bxysYk&t;ucLK6VA$lsX& diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png index cf8d5afc7e5294fbbe63568eb4fbcfdba549ca52..a822c4f58a76d4becfb6991ca4974fe3c528e2d2 100644 GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=E7-o-U3d6?5KPH{@zC z;BmNk@BgK)ZUv1UB6bh=u01=+K1~;-m?2@_<0AXGdilwF-fv|!i|76|4iWqVVnHU)U|9^AGkc&ZqgZ0Ai`&JxMYuE}k+P^Cta(LmqHVUE+1U6W^ z&tEfnXT192l6RT4mW>P6e~6pxy40|yFC|bZVB^O~1qOx(yBWde-EibOsvy98;rIPt z@(2BO%w+F3>&5W+EZEiYKJ)C%^_iEK{`Gw^A{ z&}Fgq&;Q!$inK2eMVfYAs@3J2n@}ZlbfN3Qv=32=3=9t}Q3Fj;p@NJGX(jH7JteLKzPbz&S032S#wx^#N0zBp@>A6Wmv zxxxfaXtOieJYoX-=dd9!0|P_Df`4m$AF@j`Av^&BZ_w57KIBGMQ^N={=j~xePOv%u e*Dsdm0tGiWe_Qolf1!XtkU^fVelF{r5}E+BhllY1 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png b/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png index 00b508dcb221d03ea7ebb416569824a41637229b..9ae5b45ff498b398202e44aa19158721e7bd2b29 100644 GIT binary patch delta 184 zcmZ1@xKVI|3S-4a)kN0%)4WWEJPwXti%q;1n`}{cRuK4Hs8n|EY)7ey`FrU<%LFQ7 zKa{r|ec;dZ&-#OZ%Yg%}54aQyG%K0}Br0YecAli-sox#_RwDPY=B;DJB|Gf-*MCc$ zCnHy#o>?bv@FBS%o|y*(_DJlgee!L)h$gGtkIy^>zmGO3ZhiGKEkQ5=3?5i*Q1P6k j;^|ewxLJ^`ok=9@hsdp;hADjv3=9mOu6{1-oD!MiGO`-N_|qZhyV=z*F(V zz6b9)CLf&7@~8a4e2&IO;e(P+4?;dLDH?n*Yj0QaoODlY`z=$eW6>o&-(P&HkrVsv zHt&sGbb98;vllpcVw!JDC4j+$tq*?n$JyRd3YGU?^Xdi<49sDirs6qC#go~VVY3)p cJCn!)p>0}mN-Y~07#J8lUHx3vIVCg!0QNpiM*si- diff --git a/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png b/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png index ea100d7f117b4c4c506f6b174c9cdc11e9a108ee..cf96b7d15bd266af18ffb33076d8e614b44d4018 100644 GIT binary patch delta 688 zcmV;h0#E(44!sVLBLW8mu_b#1f6WPmFc5&zl(wa9X_h8AOz(SxAA2YX0Z#(byo z`-rx{exV-zp&q7Cwo9@NNq!@7=q}?ELj?6-EJ%!1Dnjk@f1POXp5C8xG0000000017Hv<3w|NrdVK@Gz&4259`g-|Gie=rDzFbIV( z2!SvNg%SvbuxWD~+wEzGX|eQw(keOXp>I~xoXigZ0000000000000000001f@6s7N|;~>Xur9S*kf#phX9JyB0}+qGd@H zsA?eBqR^n;qW?VKw{A9ie|_zXpXa~ZO>3S&({6$GNmrktYIo%bwEE%ry+xr2w~fyJ z`p}d^gIps=XD>&1ZH^7HS8Z@^`wAOd*z!b85vUvkwNFp@D2XLQAb$i1hl6v2^guDT zaty@&=?IKaME=^T z1IUD@BKarr#gvwZN&NbR1br|Y%?!4e4i}53!b^g}+n%Ju#iC&thG7^Vp&w2To5lhj}Kf`HF&E4kukLN>*0`*dB+g{a$l88E@f_8Ri>+Z(zdJTn#5(Deitq zi~c=cP-#$ZQ4}-{e`(P+;RVe!oSciGpVd8h;fq|>XMYntn7u5VNa$7)^v6Rn4foT* z1uZa9>)X|(p{__6!d{m4rDp%rK=w=+!n8?~ zqnS_|$W(*T4=1^Y&_I0$jD9%DU7iN&3}N)cN#?^fFz*hdA|Fn&J0%U@_tOuW-S%i_ce-*z) z!~)y9HXL0W&ThLe$vC8VM`1{par|8zmJZ5aEJ%= z2%9>`vE80_m=;U_pR`JedgwQ+sZZty00000000000000000000-}l!qN{0<^$6`x2 zdU)#>r4b}*wm|LvQk^L^@Oqu1BI`F{Spf8Dg^2{i2%XrFZV8CLDC9D!Cp9KV++EW&N0v%fwRl50HHa!7DyY4v6X8eY2U5| z(w1*%M;rYD00960?VZsL!Y~X(*?e*&=ER9N9hQRe^5k|IC0_tGT~Gt zzt2!w9VYSlghVOG8|+28TwFX9Y|vpN&>&qdE*gem7>4l^+Tmokj*M;X!RoC(oLsZT zZk`EizPun8!%3GGC9A6eY>)S+o$oQ6^do$oVSWL4fZ3nsQaI^oaa+2S=(^wqQGsHK zyr7{=iPixxXw1XOe~|>8X7}QaA99=D{i||kHrhCzy;?!gg@Z&OZ&=nF&MaHlcGA9ZEyi)nK&4 zN$w#u Date: Tue, 1 Jul 2025 15:04:21 -0600 Subject: [PATCH 17/23] font/sprite: introduce `Fraction` enum for cell fractions I've included a compatibility test here to make sure that the numbers from this are in line with the numbers produced by xHalfs, yThirds, etc. After this commit I'll introduce a helper function that fills based on a span specified with this enum to replace any uses of xHalfs and friends. Once I do that I'll remove them and the compatibility test, this should be a much cleaner interface for this and make it easier to consistently align block elements with each other. --- src/font/sprite/draw/common.zig | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index d10128cdf..ad9788b94 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -122,6 +122,162 @@ pub const Alignment = struct { 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 + one_eighth, + two_eighths, + three_eighths, + four_eighths, + five_eighths, + six_eighths, + seven_eighths, + + // Names based on quarters + one_quarter, + two_quarters, + three_quarters, + + // Names based on thirds + one_third, + two_thirds, + + // Names based on halves + one_half, + half, + + // Alternative names for 1/2 + center, + middle, + + // Names for the max edge + end, + right, + bottom, + one, + full, + + /// 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)); + } + + pub inline fn fraction(self: Fraction) f64 { + return switch (self) { + .start, + .left, + .top, + .zero, + => 0.0, + + .one_eighth, + => 0.125, + + .one_quarter, + .two_eighths, + => 0.25, + + .one_third, + => 1.0 / 3.0, + + .three_eighths, + => 0.375, + + .one_half, + .two_quarters, + .four_eighths, + .half, + .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, + }; + } +}; + +test "sprite font fraction" { + const testing = std.testing; + + for (4..64) |s| { + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(s), + .ascent = @floatFromInt(s), + .descent = 0.0, + .line_gap = 0.0, + .underline_thickness = 2.0, + .strikethrough_thickness = 2.0, + }); + + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); + } +} + /// Fill a rect, clamped to within the cell boundaries. /// /// TODO: Eliminate usages of this, prefer `canvas.box`. From 190c744a6fb547f9ca3b0424f549b258ac0d2617 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 1 Jul 2025 15:20:19 -0500 Subject: [PATCH 18/23] linux: add install target to systemd user service This will allow users to enable Ghostty startup on login. Users will need to explicitly enable startup on login via this command: ```sh systemctl enable --user com.mitchellh.ghostty.service ``` --- dist/linux/systemd.service.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index b0ef3d59a..3ff848ddd 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,7 +1,11 @@ [Unit] Description=@NAME@ +After=graphical-session.target [Service] Type=dbus BusName=@APPID@ ExecStart=@GHOSTTY@ --launched-from=systemd + +[Install] +WantedBy=graphical-session.target From c838d3d7d251f1420d7bdaa1c6428caf9f6416d6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:00:35 -0600 Subject: [PATCH 19/23] font/sprite: remove `yHalfs` and friends, use `Fraction` Introduces `fill`, which fills between two `Fraction`s, use this instead of `yHalfs` and friends wherever they're used, which also means we can remove `rect`. This commit does change alignment of the vertical/horizontal eighths in certain cell sizes, but the change is for the better IMO. Also changes the center-point alignment of smooth mosaics for odd cell widths, but the change is no more than half a pixel at worst and is probably an improvement ultimately. --- src/font/sprite/draw/block.zig | 15 +- src/font/sprite/draw/box.zig | 15 -- src/font/sprite/draw/common.zig | 168 +++++++----------- .../draw/symbols_for_legacy_computing.zig | 66 +++---- ...ymbols_for_legacy_computing_supplement.zig | 22 +-- typos.toml | 2 - 6 files changed, 108 insertions(+), 180 deletions(-) diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig index f7faacea7..571f25a79 100644 --- a/src/font/sprite/draw/block.zig +++ b/src/font/sprite/draw/block.zig @@ -15,9 +15,7 @@ const common = @import("common.zig"); const Shade = common.Shade; const Quads = common.Quads; const Alignment = common.Alignment; -const xHalfs = common.xHalfs; -const yHalfs = common.yHalfs; -const rect = common.rect; +const fill = common.fill; const font = @import("../../main.zig"); const Sprite = @import("../../sprite.zig").Sprite; @@ -176,11 +174,8 @@ fn quadrant( canvas: *font.sprite.Canvas, comptime quads: Quads, ) void { - const x_halfs = xHalfs(metrics); - const y_halfs = yHalfs(metrics); - - if (quads.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_halfs[0]); - if (quads.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_halfs[0]); - if (quads.bl) rect(metrics, canvas, 0, y_halfs[1], x_halfs[0], metrics.cell_height); - if (quads.br) rect(metrics, canvas, x_halfs[1], y_halfs[1], metrics.cell_width, metrics.cell_height); + 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/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig index 91d78d2b2..f14e5a3f9 100644 --- a/src/font/sprite/draw/box.zig +++ b/src/font/sprite/draw/box.zig @@ -24,7 +24,6 @@ const Quads = common.Quads; const Corner = common.Corner; const Edge = common.Edge; const Alignment = common.Alignment; -const rect = common.rect; const hline = common.hline; const vline = common.vline; const hlineMiddle = common.hlineMiddle; @@ -695,20 +694,6 @@ pub fn lightDiagonalCross( lightDiagonalUpperLeftToLowerRight(metrics, canvas); } -fn quadrant( - metrics: font.Metrics, - canvas: *font.sprite.Canvas, - comptime quads: Quads, -) void { - const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; - const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; - - if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); - if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); - if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); - if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); -} - pub fn arc( metrics: font.Metrics, canvas: *font.sprite.Canvas, diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index ad9788b94..67b9dc778 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -135,6 +135,7 @@ pub const Fraction = enum { zero, // Names based on eighths + eighth, one_eighth, two_eighths, three_eighths, @@ -144,17 +145,19 @@ pub const Fraction = enum { 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 - one_half, half, + one_half, // Alternative names for 1/2 center, @@ -167,6 +170,43 @@ pub const Fraction = enum { 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. @@ -196,6 +236,19 @@ pub const Fraction = enum { 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, @@ -204,23 +257,26 @@ pub const Fraction = enum { .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, - .half, .center, .middle, => 0.5, @@ -248,52 +304,21 @@ pub const Fraction = enum { } }; -test "sprite font fraction" { - const testing = std.testing; - - for (4..64) |s| { - const metrics: font.Metrics = .calc(.{ - .cell_width = @floatFromInt(s), - .ascent = @floatFromInt(s), - .descent = 0.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }); - - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); - } -} - -/// Fill a rect, clamped to within the cell boundaries. -/// -/// TODO: Eliminate usages of this, prefer `canvas.box`. -pub fn rect( +/// 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, - x1: u32, - y1: u32, - x2: u32, - y2: u32, + x0: Fraction, + x1: Fraction, + y0: Fraction, + y1: Fraction, ) void { canvas.box( - @intCast(@min(@max(x1, 0), metrics.cell_width)), - @intCast(@min(@max(y1, 0), metrics.cell_height)), - @intCast(@min(@max(x2, 0), metrics.cell_width)), - @intCast(@min(@max(y2, 0), metrics.cell_height)), + x0.min(metrics.cell_width), + y0.min(metrics.cell_height), + x1.max(metrics.cell_width), + y1.max(metrics.cell_height), .on, ); } @@ -351,58 +376,3 @@ pub fn hline( ) void { canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on); } - -/// xHalfs[0] should be used as the right edge of a left-aligned half. -/// xHalfs[1] should be used as the left edge of a right-aligned half. -pub fn xHalfs(metrics: font.Metrics) [2]u32 { - const float_width: f64 = @floatFromInt(metrics.cell_width); - const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); - return .{ half_width, metrics.cell_width - half_width }; -} - -/// yHalfs[0] should be used as the bottom edge of a top-aligned half. -/// yHalfs[1] should be used as the top edge of a bottom-aligned half. -pub fn yHalfs(metrics: font.Metrics) [2]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const half_height: u32 = @intFromFloat(@round(0.5 * float_height)); - return .{ half_height, metrics.cell_height - half_height }; -} - -/// Use these values as such: -/// yThirds[0] bottom edge of the first third. -/// yThirds[1] top edge of the second third. -/// yThirds[2] bottom edge of the second third. -/// yThirds[3] top edge of the final third. -pub fn yThirds(metrics: font.Metrics) [4]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); - const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); - return .{ - one_third_height, - metrics.cell_height - two_thirds_height, - two_thirds_height, - metrics.cell_height - one_third_height, - }; -} - -/// Use these values as such: -/// yQuads[0] bottom edge of first quarter. -/// yQuads[1] top edge of second quarter. -/// yQuads[2] bottom edge of second quarter. -/// yQuads[3] top edge of third quarter. -/// yQuads[4] bottom edge of third quarter -/// yQuads[5] top edge of fourth quarter. -pub fn yQuads(metrics: font.Metrics) [6]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); - const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); - const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); - return .{ - quarter_height, - metrics.cell_height - three_quarters_height, - half_height, - metrics.cell_height - half_height, - three_quarters_height, - metrics.cell_height - quarter_height, - }; -} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index a17ddb494..19e62cf4b 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -28,13 +28,12 @@ const z2d = @import("z2d"); 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 xHalfs = common.xHalfs; -const yThirds = common.yThirds; -const rect = common.rect; +const fill = common.fill; const box = @import("box.zig"); const block = @import("block.zig"); @@ -121,16 +120,12 @@ pub fn draw1FB00_1FB3B( const sex: Sextants = @bitCast(@as(u6, @intCast( idx + (idx / 0x14) + 1, ))); - - const x_halfs = xHalfs(metrics); - const y_thirds = yThirds(metrics); - - if (sex.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_thirds[0]); - if (sex.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_thirds[0]); - if (sex.ml) rect(metrics, canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); - if (sex.mr) rect(metrics, canvas, x_halfs[1], y_thirds[1], metrics.cell_width, y_thirds[2]); - if (sex.bl) rect(metrics, canvas, 0, y_thirds[3], x_halfs[0], metrics.cell_height); - if (sex.br) rect(metrics, canvas, x_halfs[1], y_thirds[3], metrics.cell_width, metrics.cell_height); + 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 @@ -465,17 +460,12 @@ pub fn draw1FB3C_1FB67( else => unreachable, }; - const y_thirds = yThirds(metrics); const top: f64 = 0.0; - // We average the edge positions for the y_thirds boundaries here - // rather than having to deal with varying alignments depending on - // the surrounding pieces. The most this will be off by is half of - // a pixel, so hopefully it's not noticeable. - const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); - const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); + 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 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const center: f64 = Fraction.half.float(metrics.cell_width); const right: f64 = @floatFromInt(metrics.cell_width); var path = canvas.staticPath(12); // nodes.len = 0 @@ -571,13 +561,14 @@ pub fn draw1FB70_1FB75( const n = cp + 1 - 0x1fb70; - const x: u32 = @intFromFloat( - @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + fill( + metrics, + canvas, + Fraction.eighths[n], + Fraction.eighths[n + 1], + .top, + .bottom, ); - const w: u32 = @intFromFloat( - @round(@as(f64, @floatFromInt(metrics.cell_width)) / 8), - ); - rect(metrics, canvas, x, 0, x + w, metrics.cell_height); } /// Horizontal one eighth blocks @@ -593,21 +584,14 @@ pub fn draw1FB76_1FB7B( const n = cp + 1 - 0x1fb76; - const h = @as( - u32, - @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + fill( + metrics, + canvas, + .left, + .right, + Fraction.eighths[n], + Fraction.eighths[n + 1], ); - const y = @min( - metrics.cell_height -| h, - @as( - u32, - @intFromFloat( - @round(@as(f64, @floatFromInt(n)) * - @as(f64, @floatFromInt(metrics.cell_height)) / 8), - ), - ), - ); - rect(metrics, canvas, 0, y, metrics.cell_width, y + h); } pub fn draw1FB7C_1FB97( diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 9ae92cc72..fd193a0d5 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -57,9 +57,7 @@ const common = @import("common.zig"); const Thickness = common.Thickness; const Corner = common.Corner; const Shade = common.Shade; -const xHalfs = common.xHalfs; -const yQuads = common.yQuads; -const rect = common.rect; +const fill = common.fill; const box = @import("box.zig"); @@ -122,17 +120,15 @@ pub fn draw1CD00_1CDE5( break :octants result; }; - const x_halfs = xHalfs(metrics); - const y_quads = yQuads(metrics); const oct = octants[cp - octant_min]; - if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]); - if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]); - if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]); - if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); - if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]); - if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height); - if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height); + 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 diff --git a/typos.toml b/typos.toml index a8b296755..1fb54ecc6 100644 --- a/typos.toml +++ b/typos.toml @@ -39,8 +39,6 @@ extend-ignore-re = [ [default.extend-words] Pn = "Pn" thr = "thr" -# Should be "halves", but for now skip it as it would make diff huge -halfs = "halfs" # Swift oddities Requestor = "Requestor" iterm = "iterm" From ffe06f1ccdc82a4f0effa13729ab066640537615 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:26:57 -0600 Subject: [PATCH 20/23] font/sprite: add sixteenth blocks from slfc supplement --- ...ymbols_for_legacy_computing_supplement.zig | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index fd193a0d5..40c330d2c 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -55,6 +55,7 @@ const z2d = @import("z2d"); const common = @import("common.zig"); const Thickness = common.Thickness; +const Fraction = common.Fraction; const Corner = common.Corner; const Shade = common.Shade; const fill = common.fill; @@ -399,6 +400,88 @@ pub fn draw1CE51_1CE8F( ); } +/// 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, From 2fa4fc89027be57890e50973fff01a50f956b8b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 14:58:39 -0700 Subject: [PATCH 21/23] reload configuration on SIGUSR2 This is done at the apprt-level for a couple reasons. (1) For libghostty, we don't have a way to know what the embedding application is doing, so its risky to create signal handlers that might overwrite the application's signal handlers. (2) It's extremely messy to deal with signals and multi-threading. Apprts have framework access that handles this for us. For GTK, we use g_unix_signal_add. For macOS, we use `DispatchSource.makeSignalSource`. This is an awkward API but made for this purpose. --- macos/Sources/App/macOS/AppDelegate.swift | 34 +++++++++++++++++++++++ src/apprt/gtk/App.zig | 23 +++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 734fcbc20..418005927 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -112,6 +112,9 @@ class AppDelegate: NSObject, /// The observer for the app appearance. private var appearanceObserver: NSKeyValueObservation? = nil + /// Signals + private var signals: [DispatchSourceSignal] = [] + /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? = nil { didSet { @@ -249,6 +252,9 @@ class AppDelegate: NSObject, // Setup our menu setupMenuImages() + + // Setup signal handlers + setupSignals() } func applicationDidBecomeActive(_ notification: Notification) { @@ -406,6 +412,34 @@ class AppDelegate: NSObject, return dockMenu } + /// Setup signal handlers + private func setupSignals() { + // Register a signal handler for config reloading. It appears that all + // of this is required. I've commented each line because its a bit unclear. + // Warning: signal handlers don't work when run via Xcode. They have to be + // run on a real app bundle. + + // We need to ignore signals we register with makeSignalSource or they + // don't seem to handle. + signal(SIGUSR2, SIG_IGN) + + // Make the signal source and register our event handle. We keep a weak + // ref to ourself so we don't create a retain cycle. + let sigusr2 = DispatchSource.makeSignalSource(signal: SIGUSR2, queue: .main) + sigusr2.setEventHandler { [weak self] in + guard let self else { return } + Ghostty.logger.info("reloading configuration in response to SIGUSR2") + self.ghostty.reloadConfig() + } + + // The signal source starts unactivated, so we have to resume it once + // we setup the event handler. + sigusr2.resume() + + // We need to keep a strong reference to it so it isn't disabled. + signals.append(sigusr2) + } + /// Setup all the images for our menu items. private func setupMenuImages() { // Note: This COULD Be done all in the xib file, but I find it easier to diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7786f976a..c61254fbd 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -373,6 +373,13 @@ pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void { .{}, ); + // Setup a listener for SIGUSR2 to reload the configuration. + _ = glib.unixSignalAdd( + std.posix.SIG.USR2, + sigusr2, + self, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -1508,6 +1515,22 @@ pub fn quitNow(self: *App) void { self.running = false; } +// SIGUSR2 signal handler via g_unix_signal_add +fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int { + const self: *App = @ptrCast(@alignCast(ud orelse + return @intFromBool(glib.SOURCE_CONTINUE))); + + log.info("received SIGUSR2, reloading configuration", .{}); + self.reloadConfig(.app, .{ .soft = false }) catch |err| { + log.err( + "error reloading configuration for SIGUSR2: {}", + .{err}, + ); + }; + + return @intFromBool(glib.SOURCE_CONTINUE); +} + /// This is called by the `activate` signal. This is sent on program startup and /// also when a secondary instance launches and requests a new window. fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void { From 0cd95a791f15dfc17d257c5d40b92a1fe1a26ec9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:52:17 -0600 Subject: [PATCH 22/23] font/sprite: add sflc supplement circle/ellipse glyphs --- .../draw/symbols_for_legacy_computing.zig | 2 +- ...ymbols_for_legacy_computing_supplement.zig | 129 +++++++++++++----- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index 19e62cf4b..164aa1ac3 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -1367,7 +1367,7 @@ fn checkerboardFill( } } -fn circle( +pub fn circle( metrics: font.Metrics, canvas: *font.sprite.Canvas, comptime position: Alignment, diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 40c330d2c..f43949eb9 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -61,6 +61,7 @@ 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"); @@ -210,37 +211,37 @@ pub fn draw1CC30_1CC3F( ) !void { switch (cp) { // 𜰰 UPPER LEFT TWELFTH CIRCLE - 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 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), + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2, .br), else => unreachable, } } @@ -285,6 +286,62 @@ pub fn draw1CC1B_1CC1E( } } +/// 𜸀 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, @@ -491,6 +548,7 @@ fn circlePiece( 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; @@ -516,9 +574,8 @@ fn circlePiece( var path = canvas.staticPath(2); - if (xp < wdth) { - if (yp < hght) { - // Upper left arc. + switch (corner) { + .tl => { path.moveTo(wdth - xp, ht - yp); path.curveTo( wdth - cw - xp, @@ -528,8 +585,19 @@ fn circlePiece( ht - xp, hght - yp, ); - } else { - // Lower left arc. + }, + .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, @@ -539,21 +607,8 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } - } else { - if (yp < hght) { - // Upper right arc. - 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, - ); - } else { - // Lower right arc. + }, + .br => { path.moveTo(wdth * 2 - ht - xp, hght - yp); path.curveTo( wdth * 2 - ht - xp, @@ -563,7 +618,7 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } + }, } try canvas.strokePath(path.wrapped_path, .{ From cff6860fd936b3265adf01db068f8b86a7458aa6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 17:03:10 -0600 Subject: [PATCH 23/23] font/sprite: update reference images --- .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 632 -> 1006 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 819 -> 1255 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1492 -> 2247 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 443 -> 751 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 5448 -> 5427 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 5724 -> 5718 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 9973 -> 9974 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 4295 -> 4334 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index 03305c81c24482574aa134051787ecb75293f0ab..b3cda82d1391bf8a59367dae98436e1ed243ef7f 100644 GIT binary patch delta 943 zcmeyt@{WCiay|1_PZ!6KiaBp?T+BVJAi$PjmcZh6f}!>VN6!R7BPQhvmi&PJ0~~UV zf)Wqczgiaf_Li4}$)l?O-yg~J`R!z2OXd-rm~r5nv%R0yKUu_e+}E@!s^Z1ui7rXy7us^vZ(0)b^3dkzRmT#`+LJKVgAzeC>IwO z7ZsL*`wX+c)|J*KSBLAgvR-dj+W4R;Kt(J@pzBBzx6{Ok0!IxYZaogyq=T#rJ#=;` zh;*`wwM`T z#eVbDLQ%d=1>5IL&ak|hXu0%akxOFh_tPi&xavbCKvvmj?a?dTE|Q|%9%gYOjL-Ij z90S-THx0R13^`aY{Jvjbb^B&3ON{denYn>_>oQYJYVjBSXWozfUvk<-OkY#zZ}E%CCQNiAlWb z;mZ0@Ex#WS$6fR0Yc}9%JNSG4ti07bLgn2W^WV|z|8!gbDE0hq;o6p8J<2T-p|6y!!w8Xoyofm4ULV@n`Ffd3<^RX zADDX#%rc9Zq>@>6I(D7j{QB~~G5>+ Kb6Mw<&;$VCm8w<% literal 632 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=B;6o-U3d6?5L)Fyv}5 z5OFzp=l`Xs2#$?Wj)^~w?@kh_%)Q10Qq91?kg(@*(fPM2yZ^j@ywAYk$KN$77Bi-@ zuy3C9AR$ah=8RWUbEId1fi@>!n(D!WYrc4J**R$SaC3g?99W@YOz3~74>+KaGuFmThR(+N6QujHdx?ATFGg$3|eW#zR z_8t9y{vBu6v3sW$vhr1Fw~JZ4Xx;pDj?2@O_NP# z+eN;F@#z*^h_?KqVQ~K&GuXbH8@X5&d0a03|Nr{BUXxQ#$6;&nZ8MY-^&(Q+-@mWh zDxhZ5KbN;9pHsQ<+-9Gi^C*m+@1-nenXOA`%U;IC!Z%1K6FHHgYl;@Gu|zyZs!SqDyb9t6mUDgWLUZSqv0`-ou00KF6*2UngF19_2~cr diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index c17e08c39e013a569c43f4e933fd0280ecbf106a..e076da7c52f1c09e553d278e5025d464d17985db 100644 GIT binary patch literal 1255 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|U!CPZ!6KiaBp?<`=dY z2(T8&C@2a|XkbxMc&OsSG)ak(bK(Y}ICWMl-KRJFrq4Z7`2PFSxsTH<<$k(nNGT~T zs%G5q`*|u)@T9lLy=Sq_-+bmk#J|1+Z;ILEb44EfzRRkzjZeL+p+~WY$)){M+##Mf z%uLS;{?5F$N6h`(iTj=!XVs7Y)1GkQc#-0x2?5*HrtE&5y<79`^Ns&M1m2o(I5sa? z$W!m{b#In}0(DO7cVz1=0|lM#D#IJO^sxLWAJ$+x`E95bSWBUgu% ze)_Gy!-TD*FZPWHdnW7hN>=eMF1xHh%<^I93wJ-3F+U@6rzo`Ga@@h|b}nG>C9UCa z_Kx}IkL>4@uX%n=VnP$6=D(tkJTIF3^ST)|!F*)mkjMpg1_p-z|KHp&GCcbbEv0w|WCpdpYQtp6}0Cj$dR z!-D_oZwqQ@z+@r7Kp0}j<%?`b97LEe{Qv)IH~(*oTLx{*O8HLj(o%}D2P z`~Ux|)%VX04qu_Wd|g1aU+ltdb9Oo9T9&rnDtaLJI(uii<7~qpOW{YuPxG$+AJk|qF$u()HU{^tVz-#-bKw>o_PVf zCwW(??sVMB!oa{#vDMn&uza4o-`kVCtG+&AyINJ$oMpSyaqGN03!?aKlD;=S{lD#f z|I_U$z0ABa84nzo*`+K5Vs8D?oHy$U+to>T7DOQ#%*gQI5G3NR8*(u@3b0=MJwHmB zdFw*4){PUtCrtMH7JTyuLtS>w_PWYH+Ry}`a6!?D9h?*t0t8+NENpgg1hF(YN;)n; zSt2Y<3=9t@eHVXFQE7QkPEXGM$4fD|_Q|Tz)6YogL~cttmT+>Dsxh1QG~Jno5u0*O z_8e?VEr-P4^^Lp@20W}6|88H#-{G8Du;jbghwG(sCdp^|`2Wq+x^QB3G&o5fI(Oyi z+32`~zteyG(0F!rBO;xCsemRtP>K{6hZuXLdmO+{cp@M;dp@0!B4NFgI oLRImH1*GCF!jQEO8IcV+5ShrfSW)2KULlZqp00i_>zopr0E+Sig8%>k literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V19PZ!6KiaBpCY~*TC zU~mom|9^RQCTF{}2xrl`A1)WH*+6QcV8gY-ow}cY*1TTNzD_MbK*Q$e9tU}y+d>)~ zA}m}?tc^_$jtdk5DnwtfFfjc8|K`RBM1I&=X=TAH07IY1;gq;yy^i02=xzt z{yENJJi2nL!#Fw2X~k{`>#`%aiTJuei28y&{@*dZqT2X~E$ubeFFS zi00e#s7Cj_UUcU3<3;6>$qNoRG&V9Zv$8RPHM`nFEV*@%lgUxQ<>H^sW(-H;wj|me zu70qT-!*W<@{9evTldetQdHEMWx3O7>ztT{QGV+Jbk)OGOk;Nc@%!`v2WDno9utEL z2OK_JytcgXwQBf^vXi2#VvAa{rpGM21>!O?FfizSc%u32(M_4G9df!+xom#q zKmY3g5p}!Q7+!#xDNwWVYvY`~dmwJSiR8vsxh+TEf4H}i&-LP)dC#gn^8$2F@~%?d z>9|$Je8sdUY*&RC7#JR`pKEh;j!plaTa%?jUZ3P$_4Ntc)vBWAEZd!qTjhRq-esS5 z{`S23Y2PQA@|hGYXk_M<$#~%K;nKC`71@)dL%fTcvycpCVPIegvxUZ+Aul-I{;l2n zmO&aEln5XWRRfP(=}AqfN)Gs-E5VHZJFJit0=H@HLq;^44#a?sd5cif`;Zx-W;#Q_ WYUdN5JCfZ&Zu4~Yb6Mw<&;$SxGBgVS diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png index 6f4a004d497fbb023e9bfb3d94b4268f823015e4..366be38671868fa0eeefedb0a19356ef4d3c2b59 100644 GIT binary patch literal 2247 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 literal 443 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 IHQSK?0qw~kI{*Lx diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png index 35504e45bc06f179958280e2cec69348399d1fef..2eff59c76114698bf12ffc228db36a935a03c16e 100644 GIT binary patch delta 1862 zcmV-M2f6s%EY>WLBmo_GP0%W<>-+#q-&&P7_)n}p1*l82 zJhO2!A+pvKaUinNiZEzZ2)#W(eVXNwt+&>NN}cLL30j@-h!q1DV(|8V0J${F6PvfK z3za(6g)+1{*%2!SF2vYv0b=LB*^hT@|F%4RO`Hx$k3_{0H_sz!i6|DG!-xHS2bo9n@~%~Ao1~3_NM+rkpQcC{_{UGMIo?B zd)kFz#d`1kNJ|H4@$pmwQ*)u#-JuylazK=Y=y=kedR@O_J@d|5rlq6U#CR%uQ;(E< ziFF_qCftYc&B6Y3mh6R~wc{X5 z3qPJhKxjMyLWp_-VFZABh~=BzzK7$LSF|l0D02(pGijDGbsKkPp%r$fq0#g0u_d2;;n&%@DM7sBx1WPvK*p0&%#N`G`vWCKMGs2?ceU~j zGU-5Ws^O<2a@Xi~2%6E)0U(^LDnNF!iWAyt2kIuPFrk&(K(<5BjJ^*5VY)*CGU*OX ziA+0Cm+pX+h&qw{)(|xMQ6VPGG@zC+!=&1R1DS*wCe`SFuF?BnRI4wKDc1p@nsUXo z?1BTClq;rX*}jL{L(t$yg_yJ-0Xo58Vt>hjYITy>XZs#*4?%+;72>kb5KzH^OG8Tz zRI3k{hC-oGC=?2XLZMJ7^qAP187aHA+Wo>W9Zq^lZGEKx6o6C-YyTji(G6X#k;!Yd z15g0cw>DD^`@hDZ`;Y~oPee`r3zM1(9DmTk&f7UP4d~23t>I(H0LAZ&tO4jcK&|65 zWPswSku?Ba8L0KaOa=&0KS0CPWPo0N0?=lFhTB6{PWMg$dU^+_JKZ}aNs=T<`a!Kf zb>DkZu2HX5Z&|NhU*qv${ja`n`Ft$`1n5IRdjb>!6ao}D_RIhZGl0U(jezC?6dnQ; zx)IREHHJ=|AE@^>kO2bp9?($%3IPfMT9!Wmvr`Lo2Yd%NizlaaZCS*{^u!R zsJ^N4Q2m_Wgj5T1-W0|`CCuZN{t@~76fnmo^V|p0#Cx7&&+wa&Zb8ybVdSc0e(lx2 zIuXg^mVf@yd*!EyIY>H=roYU8PI>qSpkt_%@=fRis(MIE-?Un^W*q2E%m!byDIddZyLoezhE}1Blwwo3NVO<)^ zmpQhCGtOWuAiUgVeWXnnilptP1wmNX&hllBZ+`+oBE~k8QThnmb`(h4TP(=D5*h51 zKpH_f!*nD{ADs9(OsL6%%uBV=K8YuyHD{dAhUp{!VkzqOHVZN?Plp?Ac9cOl!y}PE zebC{P!-U!_$hdwVZnWuP80Cx()k5_F$CnNhYP29#7o4e)<5b2O9LomlLqF~@p;il0 zXMdKEsZ|H^j59dc2Wn`Umj~6BoE2C=`?(+^6@?_^vJWo}@v=`wRnlRcL1_r7p=Dkk z)I~TPQ@6`SLkoL`#*2%%q*xaPIcHv6>{Vl6U|?WiU|?WiU|_(9^Z&c=?<%U3kgggi zf%sI8RMIcAfk;K4UOq=}{&+C%vTf`9cz?|6wk@4(LIj$NKt?g%-I;IF(giHBeADex{!E5KhT^1yUzV*p&1mo3}!UoBF*1seg7G zX6k~?|v0{{U3|LoX73Iah4MA7@7TDNMam3%9Z#eXXoL7IY@p486+1%XHG6bxwHBL-g5 zr(i(qUee&2ug5|Gt-I#IhhjMv3u$QEhay-tPRW4Qtr~COLOXZ4fQ9zUd~2bt&`M+~ zAXJH5=bM#C^$)aEn+gb(YS;N@saC+X>`*|gmc7k)YuN%W_J;yu#r|!+TkO|={qdu7 zrhGt;7yln-?k15WNs=T41@f~=Y delta 1912 zcmV-;2Z#99EZi)RBmpC_C2I$N-<8}f48_w)N(r5$w3Lt%mQGTNbF#}Hp?jqT#yAhF zd(QW;{=hT(MaUoqIsOZULZL4vT$^d^Mf9-uMJ^2pI!=|ZJVbD;#iDm-F^<3bF7-X0*8W_jZ9 zwsfJ=rnyjtUX>lO!f_$SZVM2r|0e%Dar|GpP;FB%B1BwZX@%FkOg}+XBRDP^t^JmmrZY)Y`m*NYJZ?Z)%0%LL3~0W8S)mRWa2U z?k-{@U8uErCy}97p8!yQD-0Ln+|X3Kwcgd3*=#~Bl|kb3sjN-?hav%1@%-oi%oK%z z&9tZA75}29`wc#6z86*cpSr`M)w5Q!y?^w^gvzBS86q_-h%G%T` zC4EIYkQFEh3@db-_8^XSWoEO9Xr-9nH$IqvXfGgy0MH1re6l+CaJ}-1wuOSCwZ-qxh8O_^Lkx$Zy#y74AvO-f zj}O!59Wv{sU~X_!x8kY<2%kj6khIpMUsx3H>NSI-NQjMM(J^!Lw`}UY}bpW-L zE2d=^97v>GF)ho^Jsb}~&W{Q)Y3~85U@)=2K zR+P%$=3qRCd^pe)jtpZR0GOp16&jIqj(A{!P zUTYnI0+988&E{JF&lq$LSpfP()a1Va00960?AJk(f-n$O2PY< z*n!U5IX4aH#6Yd{$B+Sv?-^MG&~<=XZ=4|m6igp{OGR6l_0Kj#-ARf3$I!Z;{}dEGKU zay~x=o@0@D?h9$;J62%Wr{2U_GBtd3VZ4@W* zM6~u9C$wSw$iG;Mdb~}7jOOVu&}LT|#AkRV637p_d~%3Tn*u?5TQm1QhC9d8o5qoK7(u7V1DSwJtEY9DnaVb5;C>wLZ0~yF7^Q%TIS_J6_T?8 z3m88aWTc{ygk1LFr6FGS$*4*?%x6#Am~&o;tmW*%vAB2lSoIZ;sae^JVCL;qp_S zy`P}z%TVh5zRGNVbGYubyw>%#^!pQ~taIA76tKUJ+5E9l)XfsMCH=_atvJMgMg3laRJjckbwBd)FFl1}!L0=82X<6gZ~@8w zU#0XbyOkh44awB5j!;owC)iDFX>ZbKFP18v+0T0B{z*S!#lu9g2Ja00006UunmM7NgfcnB|sC@Y}bzr+q?WV0q+I z+v_!AdUK|Hwd{?5z36eIMjzvaY6dB}hBkJE*UjJ97sxZ4mNQIYQD}H4-tdP(<+H7< zgAs#h8pF=X8<-cUFq~Y|Say+7gl)ksPli{o88mJ`_CLheu*vqH${P!AkNS1FOPE*K zGF(k(u#{y;&HbnHhCyQ?b4D`5s_Am9C5#LV|Np|eQldK^m^=M^gp{{`0$Rx)knJ9nDt>*cwx?`Jdp zRk={aq^o|RsJW2=3@U6H7#RL$_r0?H?y+`3gUh zNV$6HUxvyrHK{i%>gM06=dfPa&fs-pc5>?W$5EH#l|JgsiIic|5EST{dUvL)?@eX7 zb@nU)k7h3QpA%!^@Y#Kx{v%cq;kO-!CoyfZew5+MG)-he>|}@R%^R0Ayt$Qg%T=`A zo9o28MOqu&wlut1Gq0{Vko&|rowwVcA8mN^sO?*WH}46(4Z8(R!{X}B+g$u6tQ*vO z^0^X!S`NF*9se~RwHH$2=GolZA}vsCzfh?qaIHh7+`_XBb-#ZyfkW@MAy!-r==KYJm!&-9w`HeXr4{J|S@tmaMd6N}v z-C;vs1_p+P1^?FSKID{U0*CcxP*DFzk~;fzopr0BnRY5dZ)H delta 700 zcmez7`_*@X3ge!Qs;7nOZv}E4HsEP~`1}4Yb`8O48=4jOC-^g^-u@ntak_m%>qPx4 ztBMv~e!6JR{^cJtVsobSF};v!n8v=~DPu+@!=}0me?|V^>Emsf#K01eDX_qqA#2wi zKUM|F1}#;GG~qmkDJ&rN(^J-*+g~tnF<;1Gzp$TC!}@ssq2BO@s+SD)S^N&NvJU^* z7UZ1GKg8Rx$#g%bc^qR<`mYj(*fSy**cll9|9^8;k?(*3N882U_p8#@B{sJS9I5{1 zzv73>>^u&YfI8t9F)UrowYnd-{@K0Kb-@gWt%Y--IOhHPlDPl-)h?(uP7}_sY@8;| z!@>Xp2VOIRP2T)L*hR4Z%1q5a3}F7OK)%BYJkIayyJAb@YeLp$I>|JziMl1<>vG?v zdG5OV`rqXb^}SYX5c>8k;^x~O*GfE9?zI|sJ2R*r4rT5#YdsH4V%n4~l#y#TO=LpsY~S50lIlGfZ^nkOUh5E> zF!$=^e7!8jn=_aG{kBJ9LhPLjas?|HZ?cy0FFJPW^!u6Wx9S$Tt}^@?^Wi@)#RiYqw7SMT{* zK7zxDBXjcI$uf$k`h@rB#nkh0tmRgB@7VRBLH|xUa}xLWgMv#6>>sFW-)*>Y(yM@F z<{F+K7sJgJ%D#xoIJ(8#@Bi?1+x{EeFXo=#YWet}`Xm+4Nh+S{a*!ZAY{<*Nz|gSZ z-&);=oYG9-;NFfdb@l;2RLY-$Ve$qw1+_*VP_!u2zm}e}U>geqLj~j4u%!tK9;~q- O6Fgo0T-G@yGyworh&BTN diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png index 3fa06a8fc969f3641e2f6d2a02871c11b4bf20ff..062a7da810b835f0f27d0c9b540ce22f3207e157 100644 GIT binary patch delta 4142 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 delta 4081 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