feat(font): port font metrics, atlas and the sprite font

Ports ZIG/src/font/{Metrics,Atlas,Glyph}.zig and the whole sprite font
(font/sprite/**): box drawing, block elements, braille, branch/git
graph symbols, geometric shapes, powerline, and the symbols for legacy
computing (+ supplement, incl. the octants table).

Ghostty draws sprites with z2d; we use tiny-skia for path coverage,
composited onto an alpha8 canvas, while following the Zig geometry
(positions, thickness, rounding). Comptime discovery of draw functions
is replaced by explicit, overlap-checked codepoint range tables.

Upstream reference sheets are vendored in testdata/ and compared with a
tolerance: braille and U+1CD00 are pixel-identical; other sheets differ
only in antialiasing on thin diagonals/curves. Review renders are written
to target/sprite_sheets/.

The crate temporarily drops its config/unicode/terminal deps until the
rest of the font engine is ported (Phase 3).

Test Plan:
- cargo test -p ghostty-font (49 tests incl. reference sheet comparison)
- inspect target/sprite_sheets/review-all-18x36.png
This commit is contained in:
ddidderr committed 2026-10-11 21:53:29 +02:00
1 parent b2b248e003
commit 1388f117d3
58 files changed
+9331 -6

No files matched your search

+1778
View File
@@ -0,0 +1,1778 @@
//! Box Drawing | U+2500...U+257F
//! <https://en.wikipedia.org/wiki/Box_Drawing>
//!
//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏
//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟
//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯
//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿
//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏
//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟
//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯
//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿
//!
//! Port of `sprite/draw/box.zig`.
#![allow(clippy::needless_update)]
use super::common::{Corner, DrawFn, Range, Thickness, hline, hline_middle, vline, vline_middle};
use crate::metrics::Metrics;
use crate::sprite::canvas::{Canvas, CapMode, Color, DrawError, Line, Path, Point, StrokeOptions};
pub const RANGES: &[Range] = &[Range {
min: 0x2500,
max: 0x257f,
draw: draw2500_257f as DrawFn,
}];
/// Style of a line from an edge to the center.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Style {
#[default]
None,
Light,
Heavy,
Double,
}
/// Specification of a traditional intersection-style line/box-drawing char,
/// which can have a different style of line from each edge to the center.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Lines {
pub up: Style,
pub right: Style,
pub down: Style,
pub left: Style,
}
impl Lines {
pub const NONE: Lines = Lines {
up: Style::None,
right: Style::None,
down: Style::None,
left: Style::None,
};
}
pub fn draw2500_257f(
cp: u32,
canvas: &mut Canvas,
_width: u32,
_height: u32,
metrics: &Metrics,
) -> Result<(), DrawError> {
let light = Thickness::Light.height(metrics.box_thickness);
let heavy = Thickness::Heavy.height(metrics.box_thickness);
let gap4 = 4u32.max(light);
match cp {
// '┄' '┅' '┆' '┇' '┈' '┉' '┊' '┋'
0x2504 => dash_horizontal(metrics, canvas, 3, light, gap4),
0x2505 => dash_horizontal(metrics, canvas, 3, heavy, gap4),
0x2506 => dash_vertical(metrics, canvas, 3, light, gap4),
0x2507 => dash_vertical(metrics, canvas, 3, heavy, gap4),
0x2508 => dash_horizontal(metrics, canvas, 4, light, gap4),
0x2509 => dash_horizontal(metrics, canvas, 4, heavy, gap4),
0x250a => dash_vertical(metrics, canvas, 4, light, gap4),
0x250b => dash_vertical(metrics, canvas, 4, heavy, gap4),
// '╌' '╍' '╎' '╏'
0x254c => dash_horizontal(metrics, canvas, 2, light, light),
0x254d => dash_horizontal(metrics, canvas, 2, heavy, heavy),
0x254e => dash_vertical(metrics, canvas, 2, light, heavy),
0x254f => dash_vertical(metrics, canvas, 2, heavy, heavy),
// '╭'
0x256d => arc(metrics, canvas, Corner::Br, Thickness::Light)?,
// '╮'
0x256e => arc(metrics, canvas, Corner::Bl, Thickness::Light)?,
// '╯'
0x256f => arc(metrics, canvas, Corner::Tl, Thickness::Light)?,
// '╰'
0x2570 => arc(metrics, canvas, Corner::Tr, Thickness::Light)?,
// '╱'
0x2571 => light_diagonal_upper_right_to_lower_left(metrics, canvas),
// '╲'
0x2572 => light_diagonal_upper_left_to_lower_right(metrics, canvas),
// '╳'
0x2573 => light_diagonal_cross(metrics, canvas),
// All the remaining codepoints are intersection-style lines.
0x2500 => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2501 => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2502 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x2503 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x250c => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x250d => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
right: Style::Heavy,
..Lines::NONE
},
),
0x250e => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
right: Style::Light,
..Lines::NONE
},
),
0x250f => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2510 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Light,
..Lines::NONE
},
),
0x2511 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Heavy,
..Lines::NONE
},
),
0x2512 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
left: Style::Light,
..Lines::NONE
},
),
0x2513 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
left: Style::Heavy,
..Lines::NONE
},
),
0x2514 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2515 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
right: Style::Heavy,
..Lines::NONE
},
),
0x2516 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
right: Style::Light,
..Lines::NONE
},
),
0x2517 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2518 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Light,
..Lines::NONE
},
),
0x2519 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Heavy,
..Lines::NONE
},
),
0x251a => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
left: Style::Light,
..Lines::NONE
},
),
0x251b => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
left: Style::Heavy,
..Lines::NONE
},
),
0x251c => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x251d => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
right: Style::Heavy,
..Lines::NONE
},
),
0x251e => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
right: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x251f => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
right: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2520 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
right: Style::Light,
..Lines::NONE
},
),
0x2521 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
right: Style::Heavy,
up: Style::Heavy,
..Lines::NONE
},
),
0x2522 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
right: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x2523 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2524 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Light,
..Lines::NONE
},
),
0x2525 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Heavy,
..Lines::NONE
},
),
0x2526 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
left: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x2527 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
left: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2528 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
left: Style::Light,
..Lines::NONE
},
),
0x2529 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Heavy,
up: Style::Heavy,
..Lines::NONE
},
),
0x252a => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x252b => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
left: Style::Heavy,
..Lines::NONE
},
),
0x252c => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x252d => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
right: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x252e => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
left: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x252f => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2530 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2531 => lines_char(
metrics,
canvas,
Lines {
right: Style::Light,
left: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x2532 => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
right: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x2533 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2534 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2535 => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
right: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2536 => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
left: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2537 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2538 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2539 => lines_char(
metrics,
canvas,
Lines {
right: Style::Light,
left: Style::Heavy,
up: Style::Heavy,
..Lines::NONE
},
),
0x253a => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
right: Style::Heavy,
up: Style::Heavy,
..Lines::NONE
},
),
0x253b => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x253c => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x253d => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
right: Style::Light,
up: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x253e => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
left: Style::Light,
up: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x253f => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2540 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Light,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2541 => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
up: Style::Light,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2542 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2543 => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
up: Style::Heavy,
right: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x2544 => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
up: Style::Heavy,
left: Style::Light,
down: Style::Light,
..Lines::NONE
},
),
0x2545 => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
down: Style::Heavy,
right: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2546 => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
down: Style::Heavy,
left: Style::Light,
up: Style::Light,
..Lines::NONE
},
),
0x2547 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
up: Style::Heavy,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2548 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Heavy,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2549 => lines_char(
metrics,
canvas,
Lines {
right: Style::Light,
left: Style::Heavy,
up: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x254a => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
right: Style::Heavy,
up: Style::Heavy,
down: Style::Heavy,
..Lines::NONE
},
),
0x254b => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Heavy,
left: Style::Heavy,
right: Style::Heavy,
..Lines::NONE
},
),
0x2550 => lines_char(
metrics,
canvas,
Lines {
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2551 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
..Lines::NONE
},
),
0x2552 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
right: Style::Double,
..Lines::NONE
},
),
0x2553 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
right: Style::Light,
..Lines::NONE
},
),
0x2554 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2555 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Double,
..Lines::NONE
},
),
0x2556 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
left: Style::Light,
..Lines::NONE
},
),
0x2557 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
left: Style::Double,
..Lines::NONE
},
),
0x2558 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
right: Style::Double,
..Lines::NONE
},
),
0x2559 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
right: Style::Light,
..Lines::NONE
},
),
0x255a => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x255b => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Double,
..Lines::NONE
},
),
0x255c => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
left: Style::Light,
..Lines::NONE
},
),
0x255d => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
left: Style::Double,
..Lines::NONE
},
),
0x255e => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
right: Style::Double,
..Lines::NONE
},
),
0x255f => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
right: Style::Light,
..Lines::NONE
},
),
0x2560 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2561 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Double,
..Lines::NONE
},
),
0x2562 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
left: Style::Light,
..Lines::NONE
},
),
0x2563 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
left: Style::Double,
..Lines::NONE
},
),
0x2564 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2565 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2566 => lines_char(
metrics,
canvas,
Lines {
down: Style::Double,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2567 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2568 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x2569 => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x256a => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Light,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x256b => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
),
0x256c => lines_char(
metrics,
canvas,
Lines {
up: Style::Double,
down: Style::Double,
left: Style::Double,
right: Style::Double,
..Lines::NONE
},
),
0x2574 => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
..Lines::NONE
},
),
0x2575 => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
..Lines::NONE
},
),
0x2576 => lines_char(
metrics,
canvas,
Lines {
right: Style::Light,
..Lines::NONE
},
),
0x2577 => lines_char(
metrics,
canvas,
Lines {
down: Style::Light,
..Lines::NONE
},
),
0x2578 => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
..Lines::NONE
},
),
0x2579 => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
..Lines::NONE
},
),
0x257a => lines_char(
metrics,
canvas,
Lines {
right: Style::Heavy,
..Lines::NONE
},
),
0x257b => lines_char(
metrics,
canvas,
Lines {
down: Style::Heavy,
..Lines::NONE
},
),
0x257c => lines_char(
metrics,
canvas,
Lines {
left: Style::Light,
right: Style::Heavy,
..Lines::NONE
},
),
0x257d => lines_char(
metrics,
canvas,
Lines {
up: Style::Light,
down: Style::Heavy,
..Lines::NONE
},
),
0x257e => lines_char(
metrics,
canvas,
Lines {
left: Style::Heavy,
right: Style::Light,
..Lines::NONE
},
),
0x257f => lines_char(
metrics,
canvas,
Lines {
up: Style::Heavy,
down: Style::Light,
..Lines::NONE
},
),
_ => unreachable!(),
}
Ok(())
}
pub fn lines_char(metrics: &Metrics, canvas: &mut Canvas, lines: Lines) {
use Style::{Double, Heavy, Light, None};
let light_px = Thickness::Light.height(metrics.box_thickness);
let heavy_px = Thickness::Heavy.height(metrics.box_thickness);
// Top of light horizontal strokes
let h_light_top = metrics.cell_height.saturating_sub(light_px) / 2;
// Bottom of light horizontal strokes
let h_light_bottom = h_light_top.saturating_add(light_px);
// Top of heavy horizontal strokes
let h_heavy_top = metrics.cell_height.saturating_sub(heavy_px) / 2;
// Bottom of heavy horizontal strokes
let h_heavy_bottom = h_heavy_top.saturating_add(heavy_px);
// Top of the top doubled horizontal stroke (bottom is `h_light_top`)
let h_double_top = h_light_top.saturating_sub(light_px);
// Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`)
let h_double_bottom = h_light_bottom.saturating_add(light_px);
// Left of light vertical strokes
let v_light_left = metrics.cell_width.saturating_sub(light_px) / 2;
// Right of light vertical strokes
let v_light_right = v_light_left.saturating_add(light_px);
// Left of heavy vertical strokes
let v_heavy_left = metrics.cell_width.saturating_sub(heavy_px) / 2;
// Right of heavy vertical strokes
let v_heavy_right = v_heavy_left.saturating_add(heavy_px);
// Left of the left doubled vertical stroke (right is `v_light_left`)
let v_double_left = v_light_left.saturating_sub(light_px);
// Right of the right doubled vertical stroke (left is `v_light_right`)
let v_double_right = v_light_right.saturating_add(light_px);
// The bottom of the up line
let up_bottom = if lines.left == Heavy || lines.right == Heavy {
h_heavy_bottom
} else if lines.left != lines.right || lines.down == lines.up {
if lines.left == Double || lines.right == Double {
h_double_bottom
} else {
h_light_bottom
}
} else if lines.left == None && lines.right == None {
h_light_bottom
} else {
h_light_top
};
// The top of the down line
let down_top = if lines.left == Heavy || lines.right == Heavy {
h_heavy_top
} else if lines.left != lines.right || lines.up == lines.down {
if lines.left == Double || lines.right == Double {
h_double_top
} else {
h_light_top
}
} else if lines.left == None && lines.right == None {
h_light_top
} else {
h_light_bottom
};
// The right of the left line
let left_right = if lines.up == Heavy || lines.down == Heavy {
v_heavy_right
} else if lines.up != lines.down || lines.left == lines.right {
if lines.up == Double || lines.down == Double {
v_double_right
} else {
v_light_right
}
} else if lines.up == None && lines.down == None {
v_light_right
} else {
v_light_left
};
// The left of the right line
let right_left = if lines.up == Heavy || lines.down == Heavy {
v_heavy_left
} else if lines.up != lines.down || lines.right == lines.left {
if lines.up == Double || lines.down == Double {
v_double_left
} else {
v_light_left
}
} else if lines.up == None && lines.down == None {
v_light_left
} else {
v_light_right
};
let ch = metrics.cell_height as i32;
let cw = metrics.cell_width as i32;
match lines.up {
None => {}
Light => canvas.box_(
v_light_left as i32,
0,
v_light_right as i32,
up_bottom as i32,
Color::ON,
),
Heavy => canvas.box_(
v_heavy_left as i32,
0,
v_heavy_right as i32,
up_bottom as i32,
Color::ON,
),
Double => {
let left_bottom = if lines.left == Double {
h_light_top
} else {
up_bottom
};
let right_bottom = if lines.right == Double {
h_light_top
} else {
up_bottom
};
canvas.box_(
v_double_left as i32,
0,
v_light_left as i32,
left_bottom as i32,
Color::ON,
);
canvas.box_(
v_light_right as i32,
0,
v_double_right as i32,
right_bottom as i32,
Color::ON,
);
}
}
match lines.right {
None => {}
Light => canvas.box_(
right_left as i32,
h_light_top as i32,
cw,
h_light_bottom as i32,
Color::ON,
),
Heavy => canvas.box_(
right_left as i32,
h_heavy_top as i32,
cw,
h_heavy_bottom as i32,
Color::ON,
),
Double => {
let top_left = if lines.up == Double {
v_light_right
} else {
right_left
};
let bottom_left = if lines.down == Double {
v_light_right
} else {
right_left
};
canvas.box_(
top_left as i32,
h_double_top as i32,
cw,
h_light_top as i32,
Color::ON,
);
canvas.box_(
bottom_left as i32,
h_light_bottom as i32,
cw,
h_double_bottom as i32,
Color::ON,
);
}
}
match lines.down {
None => {}
Light => canvas.box_(
v_light_left as i32,
down_top as i32,
v_light_right as i32,
ch,
Color::ON,
),
Heavy => canvas.box_(
v_heavy_left as i32,
down_top as i32,
v_heavy_right as i32,
ch,
Color::ON,
),
Double => {
let left_top = if lines.left == Double {
h_light_bottom
} else {
down_top
};
let right_top = if lines.right == Double {
h_light_bottom
} else {
down_top
};
canvas.box_(
v_double_left as i32,
left_top as i32,
v_light_left as i32,
ch,
Color::ON,
);
canvas.box_(
v_light_right as i32,
right_top as i32,
v_double_right as i32,
ch,
Color::ON,
);
}
}
match lines.left {
None => {}
Light => canvas.box_(
0,
h_light_top as i32,
left_right as i32,
h_light_bottom as i32,
Color::ON,
),
Heavy => canvas.box_(
0,
h_heavy_top as i32,
left_right as i32,
h_heavy_bottom as i32,
Color::ON,
),
Double => {
let top_right = if lines.up == Double {
v_light_left
} else {
left_right
};
let bottom_right = if lines.down == Double {
v_light_left
} else {
left_right
};
canvas.box_(
0,
h_double_top as i32,
top_right as i32,
h_light_top as i32,
Color::ON,
);
canvas.box_(
0,
h_light_bottom as i32,
bottom_right as i32,
h_double_bottom as i32,
Color::ON,
);
}
}
}
pub fn light_diagonal_upper_right_to_lower_left(metrics: &Metrics, canvas: &mut Canvas) {
let float_width = f64::from(metrics.cell_width);
let float_height = f64::from(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
let slope_x = (float_width / float_height).min(1.0);
let slope_y = (float_height / float_width).min(1.0);
let _ = canvas.line(
Line {
p0: Point {
x: float_width + 0.5 * slope_x,
y: -0.5 * slope_y,
},
p1: Point {
x: -0.5 * slope_x,
y: float_height + 0.5 * slope_y,
},
},
f64::from(Thickness::Light.height(metrics.box_thickness)),
Color::ON,
);
}
pub fn light_diagonal_upper_left_to_lower_right(metrics: &Metrics, canvas: &mut Canvas) {
let float_width = f64::from(metrics.cell_width);
let float_height = f64::from(metrics.cell_height);
// We overshoot the corners by a tiny bit, but we need to
// maintain the correct slope, so we calculate that here.
let slope_x = (float_width / float_height).min(1.0);
let slope_y = (float_height / float_width).min(1.0);
let _ = canvas.line(
Line {
p0: Point {
x: -0.5 * slope_x,
y: -0.5 * slope_y,
},
p1: Point {
x: float_width + 0.5 * slope_x,
y: float_height + 0.5 * slope_y,
},
},
f64::from(Thickness::Light.height(metrics.box_thickness)),
Color::ON,
);
}
pub fn light_diagonal_cross(metrics: &Metrics, canvas: &mut Canvas) {
light_diagonal_upper_right_to_lower_left(metrics, canvas);
light_diagonal_upper_left_to_lower_right(metrics, canvas);
}
pub fn arc(
metrics: &Metrics,
canvas: &mut Canvas,
corner: Corner,
thickness: Thickness,
) -> Result<(), DrawError> {
let thick_px = thickness.height(metrics.box_thickness);
let float_width = f64::from(metrics.cell_width);
let float_height = f64::from(metrics.cell_height);
let float_thick = f64::from(thick_px);
let center_x = f64::from(metrics.cell_width.saturating_sub(thick_px) / 2) + float_thick / 2.0;
let center_y = f64::from(metrics.cell_height.saturating_sub(thick_px) / 2) + float_thick / 2.0;
let r = float_width.min(float_height) / 2.0;
// Fraction away from the center to place the middle control points,
let s = 0.25;
let mut path = Path::new();
match corner {
Corner::Tl => {
path.move_to(center_x, 0.0);
path.line_to(center_x, center_y - r);
path.curve_to(
center_x,
center_y - s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.line_to(0.0, center_y);
}
Corner::Tr => {
path.move_to(center_x, 0.0);
path.line_to(center_x, center_y - r);
path.curve_to(
center_x,
center_y - s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.line_to(float_width, center_y);
}
Corner::Bl => {
path.move_to(center_x, float_height);
path.line_to(center_x, center_y + r);
path.curve_to(
center_x,
center_y + s * r,
center_x - s * r,
center_y,
center_x - r,
center_y,
);
path.line_to(0.0, center_y);
}
Corner::Br => {
path.move_to(center_x, float_height);
path.line_to(center_x, center_y + r);
path.curve_to(
center_x,
center_y + s * r,
center_x + s * r,
center_y,
center_x + r,
center_y,
);
path.line_to(float_width, center_y);
}
}
canvas.stroke_path(
&path,
StrokeOptions {
line_cap_mode: CapMode::Butt,
line_width: float_thick,
},
Color::ON,
)
}
fn dash_horizontal(
metrics: &Metrics,
canvas: &mut Canvas,
count: u8,
thick_px: u32,
desired_gap: u32,
) {
assert!((2..=4).contains(&count));
let count_u = u32::from(count);
// 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.
let gap_count = count_u;
// 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_u + gap_count {
hline_middle(metrics, canvas, Thickness::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.
let gap_width = desired_gap.min(metrics.cell_width / (2 * count_u)) as i32;
let total_gap_width = gap_count as i32 * gap_width;
let total_dash_width = metrics.cell_width as i32 - total_gap_width;
let dash_width = total_dash_width.div_euclid(i32::from(count));
let remaining = total_dash_width.rem_euclid(i32::from(count));
debug_assert_eq!(
dash_width * i32::from(count) + gap_width * gap_count as i32 + remaining,
metrics.cell_width as i32
);
// Our dashes should be centered vertically.
let y = (metrics.cell_height.saturating_sub(thick_px) / 2) as i32;
// We start at half a gap from the left edge, in order to center
// our dashes properly.
let mut x = gap_width.div_euclid(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.
let mut extra = remaining;
for _ in 0..count {
let mut 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 dash_vertical(
metrics: &Metrics,
canvas: &mut Canvas,
count: u8,
thick_px: u32,
desired_gap: u32,
) {
assert!((2..=4).contains(&count));
let count_u = u32::from(count);
// 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.
let gap_count = count_u;
// 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_u + gap_count {
vline_middle(metrics, canvas, Thickness::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.
let gap_height = desired_gap.min(metrics.cell_height / (2 * count_u)) as i32;
let total_gap_height = gap_count as i32 * gap_height;
let total_dash_height = metrics.cell_height as i32 - total_gap_height;
let dash_height = total_dash_height.div_euclid(i32::from(count));
let remaining = total_dash_height.rem_euclid(i32::from(count));
debug_assert_eq!(
dash_height * i32::from(count) + gap_height * gap_count as i32 + remaining,
metrics.cell_height as i32
);
// Our dashes should be centered horizontally.
let x = (metrics.cell_width.saturating_sub(thick_px) / 2) as i32;
// We start at the top of the cell.
let mut y = 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.
let mut extra = remaining;
for _ in 0..count {
let mut 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;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn metrics(w: u32, h: u32, t: u32) -> Metrics {
Metrics::calc(crate::metrics::FaceMetrics {
px_per_em: 16.0,
cell_width: f64::from(w),
ascent: f64::from(h) * 0.8,
descent: -(f64::from(h) * 0.2),
line_gap: 0.0,
underline_thickness: Some(f64::from(t)),
..Default::default()
})
}
#[test]
fn all_box_codepoints_draw() {
for (w, h, t) in [(9, 17, 1), (12, 24, 3), (3, 5, 1)] {
let m = metrics(w, h, t);
for cp in 0x2500..=0x257f {
let mut c = Canvas::new(
m.cell_width,
m.cell_height,
m.cell_width / 4,
m.cell_height / 4,
);
draw2500_257f(cp, &mut c, m.cell_width, m.cell_height, &m).expect("draw");
assert!(c.sfc.iter().any(|v| *v != 0), "U+{cp:X} empty at {w}x{h}");
}
}
}
#[test]
fn tiny_cells_do_not_panic() {
let m = metrics(1, 1, 1);
for cp in 0x2500..=0x257f {
let mut c = Canvas::new(1, 1, 0, 0);
draw2500_257f(cp, &mut c, 1, 1, &m).expect("draw");
}
}
#[test]
fn light_horizontal_line_is_centered() {
let m = metrics(10, 20, 2);
let mut c = Canvas::new(10, 20, 0, 0);
lines_char(
&m,
&mut c,
Lines {
left: Style::Light,
right: Style::Light,
..Lines::NONE
},
);
let t = m.box_thickness as usize;
let top = (20 - t) / 2;
for y in 0..20usize {
let on = c.sfc[y * 10] == 255;
assert_eq!(on, y >= top && y < top + t, "row {y}");
}
}
}