mirror-ghostty/src/font/sprite/draw/box.zig

930 lines
34 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//! Box Drawing | U+2500...U+257F
//! https://en.wikipedia.org/wiki/Box_Drawing
//!
//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏
//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟
//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯
//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿
//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏
//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟
//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯
//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿
//!
const std = @import("std");
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const common = @import("common.zig");
const Thickness = common.Thickness;
const Shade = common.Shade;
const Quads = common.Quads;
const Corner = common.Corner;
const Edge = common.Edge;
const Alignment = common.Alignment;
const hline = common.hline;
const vline = common.vline;
const hlineMiddle = common.hlineMiddle;
const vlineMiddle = common.vlineMiddle;
const font = @import("../../main.zig");
/// Specification of a traditional intersection-style line/box-drawing char,
/// which can have a different style of line from each edge to the center.
pub const Lines = packed struct(u8) {
up: Style = .none,
right: Style = .none,
down: Style = .none,
left: Style = .none,
const Style = enum(u2) {
none,
light,
heavy,
double,
};
};
pub fn draw2500_257F(
cp: u32,
canvas: *font.sprite.Canvas,
width: u32,
height: u32,
metrics: font.Metrics,
) !void {
_ = width;
_ = height;
switch (cp) {
// '─'
0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }),
// '━'
0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }),
// '│'
0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }),
// '┃'
0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }),
// '┄'
0x2504 => dashHorizontal(
metrics,
canvas,
3,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┅'
0x2505 => dashHorizontal(
metrics,
canvas,
3,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┆'
0x2506 => dashVertical(
metrics,
canvas,
3,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┇'
0x2507 => dashVertical(
metrics,
canvas,
3,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┈'
0x2508 => dashHorizontal(
metrics,
canvas,
4,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┉'
0x2509 => dashHorizontal(
metrics,
canvas,
4,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┊'
0x250a => dashVertical(
metrics,
canvas,
4,
Thickness.light.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┋'
0x250b => dashVertical(
metrics,
canvas,
4,
Thickness.heavy.height(metrics.box_thickness),
@max(4, Thickness.light.height(metrics.box_thickness)),
),
// '┌'
0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }),
// '┍'
0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }),
// '┎'
0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }),
// '┏'
0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }),
// '┐'
0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }),
// '┑'
0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }),
// '┒'
0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }),
// '┓'
0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }),
// '└'
0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }),
// '┕'
0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }),
// '┖'
0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }),
// '┗'
0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }),
// '┘'
0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }),
// '┙'
0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }),
// '┚'
0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }),
// '┛'
0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }),
// '├'
0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }),
// '┝'
0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }),
// '┞'
0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }),
// '┟'
0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }),
// '┠'
0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }),
// '┡'
0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }),
// '┢'
0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }),
// '┣'
0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }),
// '┤'
0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }),
// '┥'
0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }),
// '┦'
0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }),
// '┧'
0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }),
// '┨'
0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }),
// '┩'
0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }),
// '┪'
0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }),
// '┫'
0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }),
// '┬'
0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }),
// '┭'
0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }),
// '┮'
0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }),
// '┯'
0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }),
// '┰'
0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }),
// '┱'
0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }),
// '┲'
0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }),
// '┳'
0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }),
// '┴'
0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }),
// '┵'
0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }),
// '┶'
0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }),
// '┷'
0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }),
// '┸'
0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }),
// '┹'
0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }),
// '┺'
0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }),
// '┻'
0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }),
// '┼'
0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }),
// '┽'
0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }),
// '┾'
0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }),
// '┿'
0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }),
// '╀'
0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }),
// '╁'
0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }),
// '╂'
0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }),
// '╃'
0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }),
// '╄'
0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }),
// '╅'
0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }),
// '╆'
0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }),
// '╇'
0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }),
// '╈'
0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }),
// '╉'
0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }),
// '╊'
0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }),
// '╋'
0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }),
// '╌'
0x254c => dashHorizontal(
metrics,
canvas,
2,
Thickness.light.height(metrics.box_thickness),
Thickness.light.height(metrics.box_thickness),
),
// '╍'
0x254d => dashHorizontal(
metrics,
canvas,
2,
Thickness.heavy.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '╎'
0x254e => dashVertical(
metrics,
canvas,
2,
Thickness.light.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '╏'
0x254f => dashVertical(
metrics,
canvas,
2,
Thickness.heavy.height(metrics.box_thickness),
Thickness.heavy.height(metrics.box_thickness),
),
// '═'
0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }),
// '║'
0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }),
// '╒'
0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }),
// '╓'
0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }),
// '╔'
0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }),
// '╕'
0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }),
// '╖'
0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }),
// '╗'
0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }),
// '╘'
0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }),
// '╙'
0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }),
// '╚'
0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }),
// '╛'
0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }),
// '╜'
0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }),
// '╝'
0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }),
// '╞'
0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }),
// '╟'
0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }),
// '╠'
0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }),
// '╡'
0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }),
// '╢'
0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }),
// '╣'
0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }),
// '╤'
0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }),
// '╥'
0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }),
// '╦'
0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }),
// '╧'
0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }),
// '╨'
0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }),
// '╩'
0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }),
// '╪'
0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }),
// '╫'
0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }),
// '╬'
0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }),
// '╭'
0x256d => try arc(metrics, canvas, .br, .light),
// '╮'
0x256e => try arc(metrics, canvas, .bl, .light),
// '╯'
0x256f => try arc(metrics, canvas, .tl, .light),
// '╰'
0x2570 => try arc(metrics, canvas, .tr, .light),
// ''
0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas),
// '╲'
0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas),
// ''
0x2573 => lightDiagonalCross(metrics, canvas),
// '╴'
0x2574 => linesChar(metrics, canvas, .{ .left = .light }),
// '╵'
0x2575 => linesChar(metrics, canvas, .{ .up = .light }),
// '╶'
0x2576 => linesChar(metrics, canvas, .{ .right = .light }),
// '╷'
0x2577 => linesChar(metrics, canvas, .{ .down = .light }),
// '╸'
0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }),
// '╹'
0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }),
// '╺'
0x257a => linesChar(metrics, canvas, .{ .right = .heavy }),
// '╻'
0x257b => linesChar(metrics, canvas, .{ .down = .heavy }),
// '╼'
0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }),
// '╽'
0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }),
// '╾'
0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }),
// '╿'
0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }),
else => unreachable,
}
}
pub fn linesChar(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
lines: Lines,
) void {
const light_px = Thickness.light.height(metrics.box_thickness);
const heavy_px = Thickness.heavy.height(metrics.box_thickness);
// Top of light horizontal strokes
const h_light_top = (metrics.cell_height -| light_px) / 2;
// Bottom of light horizontal strokes
const h_light_bottom = h_light_top +| light_px;
// Top of heavy horizontal strokes
const h_heavy_top = (metrics.cell_height -| heavy_px) / 2;
// Bottom of heavy horizontal strokes
const h_heavy_bottom = h_heavy_top +| heavy_px;
// Top of the top doubled horizontal stroke (bottom is `h_light_top`)
const h_double_top = h_light_top -| light_px;
// Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`)
const h_double_bottom = h_light_bottom +| light_px;
// Left of light vertical strokes
const v_light_left = (metrics.cell_width -| light_px) / 2;
// Right of light vertical strokes
const v_light_right = v_light_left +| light_px;
// Left of heavy vertical strokes
const v_heavy_left = (metrics.cell_width -| heavy_px) / 2;
// Right of heavy vertical strokes
const v_heavy_right = v_heavy_left +| heavy_px;
// Left of the left doubled vertical stroke (right is `v_light_left`)
const v_double_left = v_light_left -| light_px;
// Right of the right doubled vertical stroke (left is `v_light_right`)
const v_double_right = v_light_right +| light_px;
// The bottom of the up line
const up_bottom = if (lines.left == .heavy or lines.right == .heavy)
h_heavy_bottom
else if (lines.left != lines.right or lines.down == lines.up)
if (lines.left == .double or lines.right == .double)
h_double_bottom
else
h_light_bottom
else if (lines.left == .none and lines.right == .none)
h_light_bottom
else
h_light_top;
// The top of the down line
const down_top = if (lines.left == .heavy or lines.right == .heavy)
h_heavy_top
else if (lines.left != lines.right or lines.up == lines.down)
if (lines.left == .double or lines.right == .double)
h_double_top
else
h_light_top
else if (lines.left == .none and lines.right == .none)
h_light_top
else
h_light_bottom;
// The right of the left line
const left_right = if (lines.up == .heavy or lines.down == .heavy)
v_heavy_right
else if (lines.up != lines.down or lines.left == lines.right)
if (lines.up == .double or lines.down == .double)
v_double_right
else
v_light_right
else if (lines.up == .none and lines.down == .none)
v_light_right
else
v_light_left;
// The left of the right line
const right_left = if (lines.up == .heavy or lines.down == .heavy)
v_heavy_left
else if (lines.up != lines.down or lines.right == lines.left)
if (lines.up == .double or lines.down == .double)
v_double_left
else
v_light_left
else if (lines.up == .none and lines.down == .none)
v_light_left
else
v_light_right;
switch (lines.up) {
.none => {},
.light => canvas.box(
@intCast(v_light_left),
0,
@intCast(v_light_right),
@intCast(up_bottom),
.on,
),
.heavy => canvas.box(
@intCast(v_heavy_left),
0,
@intCast(v_heavy_right),
@intCast(up_bottom),
.on,
),
.double => {
const left_bottom = if (lines.left == .double) h_light_top else up_bottom;
const right_bottom = if (lines.right == .double) h_light_top else up_bottom;
canvas.box(
@intCast(v_double_left),
0,
@intCast(v_light_left),
@intCast(left_bottom),
.on,
);
canvas.box(
@intCast(v_light_right),
0,
@intCast(v_double_right),
@intCast(right_bottom),
.on,
);
},
}
switch (lines.right) {
.none => {},
.light => canvas.box(
@intCast(right_left),
@intCast(h_light_top),
@intCast(metrics.cell_width),
@intCast(h_light_bottom),
.on,
),
.heavy => canvas.box(
@intCast(right_left),
@intCast(h_heavy_top),
@intCast(metrics.cell_width),
@intCast(h_heavy_bottom),
.on,
),
.double => {
const top_left = if (lines.up == .double) v_light_right else right_left;
const bottom_left = if (lines.down == .double) v_light_right else right_left;
canvas.box(
@intCast(top_left),
@intCast(h_double_top),
@intCast(metrics.cell_width),
@intCast(h_light_top),
.on,
);
canvas.box(
@intCast(bottom_left),
@intCast(h_light_bottom),
@intCast(metrics.cell_width),
@intCast(h_double_bottom),
.on,
);
},
}
switch (lines.down) {
.none => {},
.light => canvas.box(
@intCast(v_light_left),
@intCast(down_top),
@intCast(v_light_right),
@intCast(metrics.cell_height),
.on,
),
.heavy => canvas.box(
@intCast(v_heavy_left),
@intCast(down_top),
@intCast(v_heavy_right),
@intCast(metrics.cell_height),
.on,
),
.double => {
const left_top = if (lines.left == .double) h_light_bottom else down_top;
const right_top = if (lines.right == .double) h_light_bottom else down_top;
canvas.box(
@intCast(v_double_left),
@intCast(left_top),
@intCast(v_light_left),
@intCast(metrics.cell_height),
.on,
);
canvas.box(
@intCast(v_light_right),
@intCast(right_top),
@intCast(v_double_right),
@intCast(metrics.cell_height),
.on,
);
},
}
switch (lines.left) {
.none => {},
.light => canvas.box(
0,
@intCast(h_light_top),
@intCast(left_right),
@intCast(h_light_bottom),
.on,
),
.heavy => canvas.box(
0,
@intCast(h_heavy_top),
@intCast(left_right),
@intCast(h_heavy_bottom),
.on,
),
.double => {
const top_right = if (lines.up == .double) v_light_left else left_right;
const bottom_right = if (lines.down == .double) v_light_left else left_right;
canvas.box(
0,
@intCast(h_double_top),
@intCast(top_right),
@intCast(h_light_top),
.on,
);
canvas.box(
0,
@intCast(h_light_bottom),
@intCast(bottom_right),
@intCast(h_double_bottom),
.on,
);
},
}
}
pub fn lightDiagonalUpperRightToLowerLeft(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
const slope_x: f64 = @min(1.0, float_width / float_height);
const slope_y: f64 = @min(1.0, float_height / float_width);
canvas.line(.{
.p0 = .{
.x = float_width + 0.5 * slope_x,
.y = -0.5 * slope_y,
},
.p1 = .{
.x = -0.5 * slope_x,
.y = float_height + 0.5 * slope_y,
},
}, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}
pub fn lightDiagonalUpperLeftToLowerRight(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
const slope_x: f64 = @min(1.0, float_width / float_height);
const slope_y: f64 = @min(1.0, float_height / float_width);
canvas.line(.{
.p0 = .{
.x = -0.5 * slope_x,
.y = -0.5 * slope_y,
},
.p1 = .{
.x = float_width + 0.5 * slope_x,
.y = float_height + 0.5 * slope_y,
},
}, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}
pub fn lightDiagonalCross(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
) void {
lightDiagonalUpperRightToLowerLeft(metrics, canvas);
lightDiagonalUpperLeftToLowerRight(metrics, canvas);
}
pub fn arc(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
comptime corner: Corner,
comptime thickness: Thickness,
) !void {
const thick_px = thickness.height(metrics.box_thickness);
const float_width: f64 = @floatFromInt(metrics.cell_width);
const float_height: f64 = @floatFromInt(metrics.cell_height);
const float_thick: f64 = @floatFromInt(thick_px);
const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2;
const r = @min(float_width, float_height) / 2;
// Fraction away from the center to place the middle control points,
const s: f64 = 0.25;
var path = canvas.staticPath(4);
switch (corner) {
.tl => {
path.moveTo(center_x, 0);
path.lineTo(center_x, center_y - r);
path.curveTo(
center_x,
center_y - s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.lineTo(0, center_y);
},
.tr => {
path.moveTo(center_x, 0);
path.lineTo(center_x, center_y - r);
path.curveTo(
center_x,
center_y - s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.lineTo(float_width, center_y);
},
.bl => {
path.moveTo(center_x, float_height);
path.lineTo(center_x, center_y + r);
path.curveTo(
center_x,
center_y + s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.lineTo(0, center_y);
},
.br => {
path.moveTo(center_x, float_height);
path.lineTo(center_x, center_y + r);
path.curveTo(
center_x,
center_y + s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.lineTo(float_width, center_y);
},
}
try canvas.strokePath(
path.wrapped_path,
.{
.line_cap_mode = .butt,
.line_width = float_thick,
},
.on,
);
}
fn dashHorizontal(
metrics: font.Metrics,
canvas: *font.sprite.Canvas,
count: u8,
thick_px: u32,
desired_gap: u32,
) void {
assert(count >= 2 and count <= 4);
// +------------+
// | |
// | |
// | |
// | |
// | -- -- -- |
// | |
// | |
// | |
// | |
// +------------+
// Our dashed line should be made such that when tiled horizontally
// it creates one consistent line with no uneven gap or segment sizes.
// In order to make sure this is the case, we should have half-sized
// gaps on the left and right so that it is centered properly.
// For N dashes, there are N - 1 gaps between them, but we also have
// half-sized gaps on either side, adding up to N total gaps.
const gap_count = count;
// We need at least 1 pixel for each gap and each dash, if we don't
// have that then we can't draw our dashed line correctly so we just
// draw a solid line and return.
if (metrics.cell_width < count + gap_count) {
hlineMiddle(metrics, canvas, .light);
return;
}
// We never want the gaps to take up more than 50% of the space,
// because if they do the dashes are too small and look wrong.
const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count)));
const total_gap_width: i32 = gap_count * gap_width;
const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width;
const dash_width: i32 = @divFloor(total_dash_width, count);
const remaining: i32 = @mod(total_dash_width, count);
assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width);
// Our dashes should be centered vertically.
const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2);
// We start at half a gap from the left edge, in order to center
// our dashes properly.
var x: i32 = @divFloor(gap_width, 2);
// We'll distribute the extra space in to dash widths, 1px at a
// time. We prefer this to making gaps larger since that is much
// more visually obvious.
var extra: i32 = remaining;
for (0..count) |_| {
var x1 = x + dash_width;
// We distribute left-over size in to dash widths,
// since it's less obvious there than in the gaps.
if (extra > 0) {
extra -= 1;
x1 += 1;
}
hline(canvas, x, x1, y, thick_px);
// Advance by the width of the dash we drew and the width
// of a gap to get the 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;
}
}