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:
1 parent
bf84326c2f
commit
04a6b7c97a
58 files changed
+9331
-6
No files matched your search
@@ -0,0 +1,553 @@
|
||||
//! Implements a texture atlas (<https://en.wikipedia.org/wiki/Texture_atlas>).
|
||||
//! Port of `font/Atlas.zig`.
|
||||
//!
|
||||
//! The implementation is based on "A Thousand Ways to Pack the Bin - A
|
||||
//! Practical Approach to Two-Dimensional Rectangle Bin Packing" by Jukka
|
||||
//! Jylänki. This specific implementation is based heavily on
|
||||
//! Nicolas P. Rougier's freetype-gl project as well as Jukka's C++
|
||||
//! implementation: <https://github.com/juj/RectangleBinPack>
|
||||
//!
|
||||
//! Limitations that are easy to fix, but I didn't need them:
|
||||
//!
|
||||
//! * Written data must be packed, no support for custom strides.
|
||||
//! * Texture is always a square, no ability to set width != height. Note
|
||||
//! that regions written INTO the atlas do not have to be square, only
|
||||
//! the full atlas texture itself.
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// The format of the texture data being written into the Atlas. This must be
|
||||
/// uniform for all textures in the Atlas. If you have some textures with
|
||||
/// different formats, you must use multiple atlases or convert the textures.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum Format {
|
||||
/// 1 byte per pixel grayscale.
|
||||
#[default]
|
||||
Grayscale = 0,
|
||||
/// 3 bytes per pixel BGR.
|
||||
Bgr = 1,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
Bgra = 2,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
pub fn depth(self) -> u8 {
|
||||
match self {
|
||||
Format::Grayscale => 1,
|
||||
Format::Bgr => 3,
|
||||
Format::Bgra => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Node {
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Atlas cannot fit the desired region. You must enlarge the atlas.
|
||||
#[error("atlas cannot fit the desired region")]
|
||||
AtlasFull,
|
||||
}
|
||||
|
||||
/// A region within the texture atlas. These can be acquired using the
|
||||
/// "reserve" function. A region reservation is required to write data.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Region {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// Number of nodes to preallocate in the list on init.
|
||||
const NODE_PREALLOC: usize = 64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Atlas {
|
||||
/// Data is the raw texture data.
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// Width and height of the atlas texture. The current implementation is
|
||||
/// always square so this is both the width and the height.
|
||||
pub size: u32,
|
||||
|
||||
/// The nodes (rectangles) of available space.
|
||||
nodes: Vec<Node>,
|
||||
|
||||
/// The format of the texture data being written into the Atlas.
|
||||
pub format: Format,
|
||||
|
||||
/// This will be incremented every time the atlas is modified. This is useful
|
||||
/// for knowing if the texture data has changed since the last time it was
|
||||
/// sent to the GPU.
|
||||
pub modified: AtomicUsize,
|
||||
|
||||
/// This will be incremented every time the atlas is resized. This is useful
|
||||
/// for knowing if a GPU texture can be updated in-place or if it requires
|
||||
/// a resize operation.
|
||||
pub resized: AtomicUsize,
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
pub fn new(size: u32, format: Format) -> Atlas {
|
||||
let mut result = Atlas {
|
||||
data: vec![0; size as usize * size as usize * format.depth() as usize],
|
||||
size,
|
||||
nodes: Vec::with_capacity(NODE_PREALLOC),
|
||||
format,
|
||||
modified: AtomicUsize::new(0),
|
||||
resized: AtomicUsize::new(0),
|
||||
};
|
||||
|
||||
// This sets up our initial state
|
||||
result.clear();
|
||||
result
|
||||
}
|
||||
|
||||
/// Reserve a region within the atlas with the given width and height.
|
||||
///
|
||||
/// This will not automatically enlarge the texture if it is full.
|
||||
pub fn reserve(&mut self, width: u32, height: u32) -> Result<Region, Error> {
|
||||
// x, y are populated within the search below
|
||||
let mut region = Region {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
// If our width/height are 0, then we return the region as-is. This
|
||||
// may seem like an error case but it simplifies downstream callers who
|
||||
// might be trying to write empty data.
|
||||
if width == 0 && height == 0 {
|
||||
return Ok(region);
|
||||
}
|
||||
|
||||
// Find the location in our nodes list to insert the new node for this region.
|
||||
let best_idx: usize = {
|
||||
let mut best_height = u32::MAX;
|
||||
let mut best_width = best_height;
|
||||
let mut chosen: Option<usize> = None;
|
||||
|
||||
for i in 0..self.nodes.len() {
|
||||
// Check if our region fits within this node.
|
||||
let Some(y) = self.fit(i, width, height) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let node = self.nodes[i];
|
||||
if (y + height) < best_height
|
||||
|| ((y + height) == best_height && (node.width > 0 && node.width < best_width))
|
||||
{
|
||||
chosen = Some(i);
|
||||
best_width = node.width;
|
||||
best_height = y + height;
|
||||
region.x = node.x;
|
||||
region.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
// If we never found a chosen index, the atlas cannot fit our region.
|
||||
chosen.ok_or(Error::AtlasFull)?
|
||||
};
|
||||
|
||||
// Insert our new node for this rectangle at the exact best index
|
||||
self.nodes.insert(
|
||||
best_idx,
|
||||
Node {
|
||||
x: region.x,
|
||||
y: region.y + height,
|
||||
width,
|
||||
},
|
||||
);
|
||||
|
||||
// Optimize our rectangles
|
||||
let i = best_idx + 1;
|
||||
while i < self.nodes.len() {
|
||||
let prev = self.nodes[i - 1];
|
||||
let node = &mut self.nodes[i];
|
||||
if node.x < (prev.x + prev.width) {
|
||||
let shrink = prev.x + prev.width - node.x;
|
||||
node.x += shrink;
|
||||
node.width = node.width.saturating_sub(shrink);
|
||||
if node.width == 0 {
|
||||
self.nodes.remove(i);
|
||||
// Zig does `i -= 1; continue;` followed by `i += 1` in the
|
||||
// while continue-expression, i.e. re-examines index i.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
self.merge();
|
||||
|
||||
Ok(region)
|
||||
}
|
||||
|
||||
/// Attempts to fit a rectangle of width x height into the node at idx.
|
||||
/// The return value is the y within the texture where the rectangle can be
|
||||
/// placed. The x is the same as the node.
|
||||
fn fit(&self, idx: usize, width: u32, height: u32) -> Option<u32> {
|
||||
// If the added width exceeds our texture size, it doesn't fit.
|
||||
let node = self.nodes[idx];
|
||||
if (node.x + width) > (self.size - 1) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Go node by node looking for space that can fit our width.
|
||||
let mut y = node.y;
|
||||
let mut i = idx;
|
||||
let mut width_left = width;
|
||||
while width_left > 0 {
|
||||
let n = self.nodes[i];
|
||||
if n.y > y {
|
||||
y = n.y;
|
||||
}
|
||||
|
||||
// If the added height exceeds our texture size, it doesn't fit.
|
||||
if (y + height) > (self.size - 1) {
|
||||
return None;
|
||||
}
|
||||
|
||||
width_left = width_left.saturating_sub(n.width);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Some(y)
|
||||
}
|
||||
|
||||
/// Merge adjacent nodes with the same y value.
|
||||
fn merge(&mut self) {
|
||||
let mut i = 0;
|
||||
while i + 1 < self.nodes.len() {
|
||||
let next = self.nodes[i + 1];
|
||||
let node = &mut self.nodes[i];
|
||||
if node.y == next.y {
|
||||
node.width += next.width;
|
||||
self.nodes.remove(i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the data associated with a reserved region. The data is expected
|
||||
/// to fit exactly within the region. The data must be formatted with the
|
||||
/// proper bpp configured on init.
|
||||
pub fn set(&mut self, reg: Region, data: &[u8]) {
|
||||
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));
|
||||
|
||||
let depth = self.format.depth() as usize;
|
||||
let size = self.size as usize;
|
||||
let row_len = reg.width as usize * depth;
|
||||
for i in 0..reg.height as usize {
|
||||
let tex_offset = ((reg.y as usize + i) * size + reg.x as usize) * depth;
|
||||
let data_offset = i * row_len;
|
||||
self.data[tex_offset..tex_offset + row_len]
|
||||
.copy_from_slice(&data[data_offset..data_offset + row_len]);
|
||||
}
|
||||
|
||||
self.modified.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// 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 set_from_larger(
|
||||
&mut self,
|
||||
reg: Region,
|
||||
src: &[u8],
|
||||
src_width: u32,
|
||||
src_x: u32,
|
||||
src_y: u32,
|
||||
) {
|
||||
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));
|
||||
|
||||
let depth = self.format.depth() as usize;
|
||||
let size = self.size as usize;
|
||||
let row_len = reg.width as usize * depth;
|
||||
for i in 0..reg.height as usize {
|
||||
let tex_offset = ((reg.y as usize + i) * size + reg.x as usize) * depth;
|
||||
let src_offset = ((src_y as usize + i) * src_width as usize + src_x as usize) * depth;
|
||||
self.data[tex_offset..tex_offset + row_len]
|
||||
.copy_from_slice(&src[src_offset..src_offset + row_len]);
|
||||
}
|
||||
|
||||
self.modified.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Grow the texture to the new size, preserving all previously written data.
|
||||
pub fn grow(&mut self, size_new: u32) {
|
||||
assert!(size_new >= self.size);
|
||||
if size_new == self.size {
|
||||
return;
|
||||
}
|
||||
|
||||
let data_new =
|
||||
vec![0u8; size_new as usize * size_new as usize * self.format.depth() as usize];
|
||||
|
||||
// Keep track of our old data so that we can copy it.
|
||||
let data_old = std::mem::replace(&mut self.data, data_new);
|
||||
let size_old = self.size;
|
||||
|
||||
// Update our size to our new one.
|
||||
self.size = size_new;
|
||||
|
||||
// Copy the old data over (data is already zeroed).
|
||||
self.set(
|
||||
Region {
|
||||
x: 0, // don't bother skipping border so we can avoid strides
|
||||
y: 1, // skip the first border row
|
||||
width: size_old,
|
||||
height: size_old - 2, // skip the last border row
|
||||
},
|
||||
&data_old[size_old as usize * self.format.depth() as usize..],
|
||||
);
|
||||
|
||||
// Add the new rectangle for our added righthand space.
|
||||
self.nodes.push(Node {
|
||||
x: size_old - 1,
|
||||
y: 1,
|
||||
width: size_new - size_old,
|
||||
});
|
||||
|
||||
// We are both modified and resized
|
||||
self.modified.fetch_add(1, Ordering::Relaxed);
|
||||
self.resized.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Empty the atlas. This doesn't reclaim any previously allocated memory.
|
||||
pub fn clear(&mut self) {
|
||||
self.modified.fetch_add(1, Ordering::Relaxed);
|
||||
self.data.fill(0);
|
||||
self.nodes.clear();
|
||||
|
||||
// Add our initial rectangle. This is the size of the full texture
|
||||
// and is the initial rectangle we fit our regions in. We keep a 1px border
|
||||
// to avoid artifacting when sampling the texture.
|
||||
self.nodes.push(Node {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: self.size - 2,
|
||||
});
|
||||
}
|
||||
|
||||
/// Dump the atlas as a PPM to a writer, for debug purposes.
|
||||
/// Only supports grayscale and bgr atlases.
|
||||
///
|
||||
/// NOTE: BGR atlases will have the red and blue channels
|
||||
/// swapped because PPM expects RGB.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics for formats other than grayscale and bgr (as Zig does).
|
||||
pub fn dump<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
|
||||
let c = match self.format {
|
||||
Format::Grayscale => '5',
|
||||
Format::Bgr => '6',
|
||||
f @ Format::Bgra => {
|
||||
log::error!("Unsupported format for dump: {f:?}");
|
||||
panic!("Cannot dump this atlas format.");
|
||||
}
|
||||
};
|
||||
write!(writer, "P{c}\n{} {}\n255\n", self.size, self.size)?;
|
||||
writer.write_all(&self.data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exact_fit() {
|
||||
let mut atlas = Atlas::new(34, Format::Grayscale); // +2 for 1px border
|
||||
|
||||
let modified = atlas.modified.load(Ordering::Relaxed);
|
||||
atlas.reserve(32, 32).unwrap();
|
||||
assert_eq!(modified, atlas.modified.load(Ordering::Relaxed));
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doesnt_fit() {
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
|
||||
// doesn't fit due to border
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(32, 32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_multiple() {
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
|
||||
atlas.reserve(15, 30).unwrap();
|
||||
atlas.reserve(15, 30).unwrap();
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writing_data() {
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
|
||||
let reg = atlas.reserve(2, 2).unwrap();
|
||||
let old = atlas.modified.load(Ordering::Relaxed);
|
||||
atlas.set(reg, &[1, 2, 3, 4]);
|
||||
let new = atlas.modified.load(Ordering::Relaxed);
|
||||
assert!(new > old);
|
||||
|
||||
// 33 because of the 1px border and so on
|
||||
assert_eq!(1, atlas.data[33]);
|
||||
assert_eq!(2, atlas.data[34]);
|
||||
assert_eq!(3, atlas.data[65]);
|
||||
assert_eq!(4, atlas.data[66]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writing_data_from_a_larger_source() {
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
|
||||
let reg = atlas.reserve(2, 2).unwrap();
|
||||
let old = atlas.modified.load(Ordering::Relaxed);
|
||||
#[rustfmt::skip]
|
||||
atlas.set_from_larger(reg, &[
|
||||
8, 8, 8, 8, 8,
|
||||
8, 8, 1, 2, 8,
|
||||
8, 8, 3, 4, 8,
|
||||
8, 8, 8, 8, 8,
|
||||
], 5, 2, 1);
|
||||
let new = atlas.modified.load(Ordering::Relaxed);
|
||||
assert!(new > old);
|
||||
|
||||
// 33 because of the 1px border and so on
|
||||
assert_eq!(1, atlas.data[33]);
|
||||
assert_eq!(2, atlas.data[34]);
|
||||
assert_eq!(3, atlas.data[65]);
|
||||
assert_eq!(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.
|
||||
assert!(!atlas.data.contains(&8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grow() {
|
||||
let mut atlas = Atlas::new(4, Format::Grayscale); // +2 for 1px border
|
||||
|
||||
let reg = atlas.reserve(2, 2).unwrap();
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(1, 1));
|
||||
|
||||
// Write some data so we can verify that growing doesn't mess it up
|
||||
atlas.set(reg, &[1, 2, 3, 4]);
|
||||
assert_eq!(1, atlas.data[5]);
|
||||
assert_eq!(2, atlas.data[6]);
|
||||
assert_eq!(3, atlas.data[9]);
|
||||
assert_eq!(4, atlas.data[10]);
|
||||
|
||||
// Expand by exactly 1 should fit our new 1x1 block.
|
||||
let old_modified = atlas.modified.load(Ordering::Relaxed);
|
||||
let old_resized = atlas.resized.load(Ordering::Relaxed);
|
||||
atlas.grow(atlas.size + 1);
|
||||
let new_modified = atlas.modified.load(Ordering::Relaxed);
|
||||
let new_resized = atlas.resized.load(Ordering::Relaxed);
|
||||
assert!(new_modified > old_modified);
|
||||
assert!(new_resized > old_resized);
|
||||
atlas.reserve(1, 1).unwrap();
|
||||
|
||||
// Ensure our data is still set. Not the offsets change due to size.
|
||||
let s = atlas.size as usize;
|
||||
assert_eq!(1, atlas.data[s + 1]);
|
||||
assert_eq!(2, atlas.data[s + 2]);
|
||||
assert_eq!(3, atlas.data[s * 2 + 1]);
|
||||
assert_eq!(4, atlas.data[s * 2 + 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writing_bgr_data() {
|
||||
let mut atlas = Atlas::new(32, Format::Bgr);
|
||||
|
||||
// This is BGR so its 3 bpp
|
||||
let reg = atlas.reserve(1, 2).unwrap();
|
||||
atlas.set(reg, &[1, 2, 3, 4, 5, 6]);
|
||||
|
||||
// 33 because of the 1px border and so on
|
||||
let depth = atlas.format.depth() as usize;
|
||||
assert_eq!(1, atlas.data[33 * depth]);
|
||||
assert_eq!(2, atlas.data[33 * depth + 1]);
|
||||
assert_eq!(3, atlas.data[33 * depth + 2]);
|
||||
assert_eq!(4, atlas.data[65 * depth]);
|
||||
assert_eq!(5, atlas.data[65 * depth + 1]);
|
||||
assert_eq!(6, atlas.data[65 * depth + 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grow_bgr() {
|
||||
// Atlas is 4x4 so its a 1px border meaning we only have 2x2 available
|
||||
let mut atlas = Atlas::new(4, Format::Bgr);
|
||||
|
||||
// Get our 2x2, which should be ALL our usable space
|
||||
let reg = atlas.reserve(2, 2).unwrap();
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(1, 1));
|
||||
|
||||
// This is BGR so its 3 bpp
|
||||
#[rustfmt::skip]
|
||||
atlas.set(reg, &[
|
||||
10, 11, 12, // (0, 0) (x, y) from top-left
|
||||
13, 14, 15, // (1, 0)
|
||||
20, 21, 22, // (0, 1)
|
||||
23, 24, 25, // (1, 1)
|
||||
]);
|
||||
|
||||
// Our top left skips the first row (size * depth) and the first
|
||||
// column (depth) for the 1px border.
|
||||
let depth = atlas.format.depth() as usize;
|
||||
let check = |atlas: &Atlas| {
|
||||
let mut tl = (atlas.size as usize * depth) + depth;
|
||||
assert_eq!(&[10, 11, 12, 13, 14, 15, 0], &atlas.data[tl..tl + 7]); // 0 = border
|
||||
tl += atlas.size as usize * depth; // next row
|
||||
assert_eq!(&[20, 21, 22, 23, 24, 25, 0], &atlas.data[tl..tl + 7]);
|
||||
};
|
||||
check(&atlas);
|
||||
|
||||
// Expand by exactly 1 should fit our new 1x1 block.
|
||||
atlas.grow(atlas.size + 1);
|
||||
|
||||
// Data should be in same place accounting for the new size
|
||||
check(&atlas);
|
||||
|
||||
// Should fit the new blocks around the edges
|
||||
atlas.reserve(1, 3).unwrap();
|
||||
atlas.reserve(2, 1).unwrap();
|
||||
assert_eq!(Err(Error::AtlasFull), atlas.reserve(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserve_empty_region() {
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
let r = atlas.reserve(0, 0).unwrap();
|
||||
assert_eq!(Region::default(), r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_ppm_header() {
|
||||
let atlas = Atlas::new(4, Format::Grayscale);
|
||||
let mut out = Vec::new();
|
||||
atlas.dump(&mut out).unwrap();
|
||||
assert!(out.starts_with(b"P5\n4 4\n255\n"));
|
||||
assert_eq!(out.len(), 11 + 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Glyph is a single loaded glyph for a face. Port of `font/Glyph.zig`.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Glyph {
|
||||
/// width of glyph in pixels
|
||||
pub width: u32,
|
||||
|
||||
/// height of glyph in pixels
|
||||
pub height: u32,
|
||||
|
||||
/// left bearing
|
||||
pub offset_x: i32,
|
||||
|
||||
/// top bearing
|
||||
pub offset_y: i32,
|
||||
|
||||
/// coordinates in the atlas of the top-left corner. These have to
|
||||
/// be normalized to be between 0 and 1 prior to use in shaders.
|
||||
pub atlas_x: u32,
|
||||
pub atlas_y: u32,
|
||||
}
|
||||
@@ -1 +1,14 @@
|
||||
//! ghostty-font: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//! Font engine. Currently contains the metrics, atlas, glyph and sprite-font
|
||||
//! modules; the rest of the font engine (faces, shaping, collections, ...) is
|
||||
//! ported later.
|
||||
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
pub mod atlas;
|
||||
pub mod glyph;
|
||||
pub mod metrics;
|
||||
pub mod sprite;
|
||||
|
||||
pub use atlas::Atlas;
|
||||
pub use glyph::Glyph;
|
||||
pub use metrics::Metrics;
|
||||
@@ -0,0 +1,908 @@
|
||||
//! Font metrics for the terminal grid. Port of `font/Metrics.zig`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Metrics describing the terminal grid cell for a font.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Metrics {
|
||||
/// Recommended cell width and height for a monospace grid using this font.
|
||||
pub cell_width: u32,
|
||||
pub cell_height: u32,
|
||||
|
||||
/// Distance in pixels from the bottom of the cell to the text baseline.
|
||||
pub cell_baseline: u32,
|
||||
|
||||
/// Distance in pixels from the top of the cell to the top of the underline.
|
||||
pub underline_position: u32,
|
||||
/// Thickness in pixels of the underline.
|
||||
pub underline_thickness: u32,
|
||||
|
||||
/// Distance in pixels from the top of the cell to the top of the strikethrough.
|
||||
pub strikethrough_position: u32,
|
||||
/// Thickness in pixels of the strikethrough.
|
||||
pub strikethrough_thickness: u32,
|
||||
|
||||
/// Distance in pixels from the top of the cell to the top of the overline.
|
||||
/// Can be negative to adjust the position above the top of the cell.
|
||||
pub overline_position: i32,
|
||||
/// Thickness in pixels of the overline.
|
||||
pub overline_thickness: u32,
|
||||
|
||||
/// Thickness in pixels of box drawing characters.
|
||||
pub box_thickness: u32,
|
||||
|
||||
/// The thickness in pixels of the cursor sprite. This has a default value
|
||||
/// because it is not determined by fonts but rather by user configuration.
|
||||
pub cursor_thickness: u32,
|
||||
|
||||
/// The height in pixels of the cursor sprite.
|
||||
pub cursor_height: u32,
|
||||
|
||||
/// The constraint height for nerd fonts icons.
|
||||
pub icon_height: f64,
|
||||
|
||||
/// The constraint height for nerd fonts icons limited to a single cell width.
|
||||
pub icon_height_single: f64,
|
||||
|
||||
/// The unrounded face width, used in scaling calculations.
|
||||
pub face_width: f64,
|
||||
|
||||
/// The unrounded face height, used in scaling calculations.
|
||||
pub face_height: f64,
|
||||
|
||||
/// The offset from the bottom of the cell to the bottom
|
||||
/// of the face's bounding box, based on the rounded and
|
||||
/// potentially adjusted cell height.
|
||||
pub face_y: f64,
|
||||
}
|
||||
|
||||
/// Minimum acceptable values for some fields to prevent modifiers
|
||||
/// from being able to, for example, cause 0-thickness underlines.
|
||||
mod minimums {
|
||||
pub const CELL_WIDTH: u32 = 1;
|
||||
pub const CELL_HEIGHT: u32 = 1;
|
||||
pub const UNDERLINE_THICKNESS: u32 = 1;
|
||||
pub const STRIKETHROUGH_THICKNESS: u32 = 1;
|
||||
pub const OVERLINE_THICKNESS: u32 = 1;
|
||||
pub const BOX_THICKNESS: u32 = 1;
|
||||
pub const CURSOR_THICKNESS: u32 = 1;
|
||||
pub const CURSOR_HEIGHT: u32 = 1;
|
||||
pub const ICON_HEIGHT: f64 = 1.0;
|
||||
pub const ICON_HEIGHT_SINGLE: f64 = 1.0;
|
||||
pub const FACE_HEIGHT: f64 = 1.0;
|
||||
pub const FACE_WIDTH: f64 = 1.0;
|
||||
}
|
||||
|
||||
/// Metrics extracted from a font face, based on
|
||||
/// the metadata tables and glyph measurements.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct FaceMetrics {
|
||||
/// Pixels per em, dividing the other values in this struct by this should
|
||||
/// yield sizes in ems, to allow comparing metrics from faces of different
|
||||
/// sizes.
|
||||
pub px_per_em: f64,
|
||||
|
||||
/// The minimum cell width that can contain any glyph in the ASCII range.
|
||||
///
|
||||
/// Determined by measuring all printable glyphs in the ASCII range.
|
||||
pub cell_width: f64,
|
||||
|
||||
/// The typographic ascent metric from the font.
|
||||
/// This represents the maximum vertical position of the highest ascender.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
pub ascent: f64,
|
||||
|
||||
/// The typographic descent metric from the font.
|
||||
/// This represents the minimum vertical position of the lowest descender.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
///
|
||||
/// Note:
|
||||
/// As this value is generally below the baseline, it is typically negative.
|
||||
pub descent: f64,
|
||||
|
||||
/// The typographic line gap (aka "leading") metric from the font.
|
||||
/// This represents the additional space to be added between lines in
|
||||
/// addition to the space defined by the ascent and descent metrics.
|
||||
///
|
||||
/// Positive value in px
|
||||
pub line_gap: f64,
|
||||
|
||||
/// The TOP of the underline stroke.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
pub underline_position: Option<f64>,
|
||||
|
||||
/// The thickness of the underline stroke in px.
|
||||
pub underline_thickness: Option<f64>,
|
||||
|
||||
/// The TOP of the strikethrough stroke.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
pub strikethrough_position: Option<f64>,
|
||||
|
||||
/// The thickness of the strikethrough stroke in px.
|
||||
pub strikethrough_thickness: Option<f64>,
|
||||
|
||||
/// The height of capital letters in the font, either derived from
|
||||
/// a provided cap height metric or measured from the height of the
|
||||
/// capital H glyph.
|
||||
pub cap_height: Option<f64>,
|
||||
|
||||
/// The height of lowercase letters in the font, either derived from
|
||||
/// a provided ex height metric or measured from the height of the
|
||||
/// lowercase x glyph.
|
||||
pub ex_height: Option<f64>,
|
||||
|
||||
/// The measured height of the bounding box containing all printable
|
||||
/// ASCII characters. This can be different from ascent - descent for
|
||||
/// two reasons: non-letter symbols like @ and $ often exceed the
|
||||
/// the ascender and descender lines; and fonts often bake the line
|
||||
/// gap into the ascent and descent metrics (as per, e.g., the Google
|
||||
/// Fonts guidelines: https://simoncozens.github.io/gf-docs/metrics.html).
|
||||
///
|
||||
/// Positive value in px
|
||||
pub ascii_height: Option<f64>,
|
||||
|
||||
/// The width of the character "水" (CJK water ideograph, U+6C34),
|
||||
/// if present. This is used for font size adjustment, to normalize
|
||||
/// the width of CJK fonts mixed with latin fonts.
|
||||
///
|
||||
/// NOTE: IC = Ideograph Character
|
||||
pub ic_width: Option<f64>,
|
||||
}
|
||||
|
||||
fn positive(v: Option<f64>) -> Option<f64> {
|
||||
v.filter(|v| *v > 0.0)
|
||||
}
|
||||
|
||||
impl FaceMetrics {
|
||||
/// Convenience function for getting the line height
|
||||
/// (ascent - descent + line_gap).
|
||||
#[inline]
|
||||
pub fn line_height(&self) -> f64 {
|
||||
self.ascent - self.descent + self.line_gap
|
||||
}
|
||||
|
||||
/// Convenience function for getting the cap height. If this is not
|
||||
/// defined in the font, we estimate it as 75% of the ascent.
|
||||
#[inline]
|
||||
pub fn cap_height(&self) -> f64 {
|
||||
positive(self.cap_height).unwrap_or(0.75 * self.ascent)
|
||||
}
|
||||
|
||||
/// Convenience function for getting the ex height. If this is not
|
||||
/// defined in the font, we estimate it as 75% of the cap height.
|
||||
#[inline]
|
||||
pub fn ex_height(&self) -> f64 {
|
||||
positive(self.ex_height).unwrap_or_else(|| 0.75 * self.cap_height())
|
||||
}
|
||||
|
||||
/// Convenience function for getting the ASCII height. If we
|
||||
/// couldn't measure this, we use 1.5 * cap_height as our
|
||||
/// estimator, based on measurements across programming fonts.
|
||||
#[inline]
|
||||
pub fn ascii_height(&self) -> f64 {
|
||||
positive(self.ascii_height).unwrap_or_else(|| 1.5 * self.cap_height())
|
||||
}
|
||||
|
||||
/// Convenience function for getting the ideograph width. If this is
|
||||
/// not defined in the font, we estimate it as the minimum of the
|
||||
/// ascii height and two cell widths.
|
||||
#[inline]
|
||||
pub fn ic_width(&self) -> f64 {
|
||||
positive(self.ic_width).unwrap_or_else(|| self.ascii_height().min(2.0 * self.cell_width))
|
||||
}
|
||||
|
||||
/// Convenience function for getting the underline thickness. If
|
||||
/// this is not defined in the font, we estimate it as 15% of the ex
|
||||
/// height.
|
||||
#[inline]
|
||||
pub fn underline_thickness(&self) -> f64 {
|
||||
positive(self.underline_thickness).unwrap_or_else(|| 0.15 * self.ex_height())
|
||||
}
|
||||
|
||||
/// Convenience function for getting the strikethrough thickness. If
|
||||
/// this is not defined in the font, we set it equal to the
|
||||
/// underline thickness.
|
||||
#[inline]
|
||||
pub fn strikethrough_thickness(&self) -> f64 {
|
||||
positive(self.strikethrough_thickness).unwrap_or_else(|| self.underline_thickness())
|
||||
}
|
||||
|
||||
// NOTE: The getters below return positions, not sizes, so both
|
||||
// positive and negative values are valid, hence no sign validation.
|
||||
|
||||
/// Convenience function for getting the underline position. If
|
||||
/// this is not defined in the font, we place it one underline
|
||||
/// thickness below the baseline.
|
||||
#[inline]
|
||||
pub fn underline_position(&self) -> f64 {
|
||||
self.underline_position
|
||||
.unwrap_or_else(|| -self.underline_thickness())
|
||||
}
|
||||
|
||||
/// Convenience function for getting the strikethrough position. If
|
||||
/// this is not defined in the font, we center it at half the ex
|
||||
/// height, so that it's perfectly centered on lower case text.
|
||||
#[inline]
|
||||
pub fn strikethrough_position(&self) -> f64 {
|
||||
self.strikethrough_position
|
||||
.unwrap_or_else(|| f64::midpoint(self.ex_height(), self.strikethrough_thickness()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig's `@round`: round half away from zero (same as `f64::round`).
|
||||
#[inline]
|
||||
fn zround(v: f64) -> f64 {
|
||||
v.round()
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
/// All-zero metrics, except `cursor_thickness` which defaults to 1.
|
||||
/// (Zig's private `init()`; used by tests.)
|
||||
#[cfg(test)]
|
||||
fn init() -> Metrics {
|
||||
Metrics {
|
||||
cell_width: 0,
|
||||
cell_height: 0,
|
||||
cell_baseline: 0,
|
||||
underline_position: 0,
|
||||
underline_thickness: 0,
|
||||
strikethrough_position: 0,
|
||||
strikethrough_thickness: 0,
|
||||
overline_position: 0,
|
||||
overline_thickness: 0,
|
||||
box_thickness: 0,
|
||||
cursor_thickness: 1,
|
||||
cursor_height: 0,
|
||||
icon_height: 0.0,
|
||||
icon_height_single: 0.0,
|
||||
face_width: 0.0,
|
||||
face_height: 0.0,
|
||||
face_y: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate our metrics based on values extracted from a font.
|
||||
///
|
||||
/// Try to pass values with as much precision as possible,
|
||||
/// do not round them before using them for this function.
|
||||
///
|
||||
/// For any nullable options that are not provided, estimates will be used.
|
||||
pub fn calc(face: FaceMetrics) -> Metrics {
|
||||
// These are the unrounded advance width and line height values,
|
||||
// which are retained separately from the rounded cell width and
|
||||
// height values (below), for calculations that need to know how
|
||||
// much error there is between the design dimensions of the font
|
||||
// and the pixel dimensions of our cells.
|
||||
let face_width = face.cell_width;
|
||||
let face_height = face.line_height();
|
||||
|
||||
// The cell width and height values need to be integers since they
|
||||
// represent pixel dimensions of the grid cells in the terminal.
|
||||
//
|
||||
// We use round for the cell width to limit the difference from
|
||||
// the "true" width value to no more than 0.5px. This is a better
|
||||
// approximation of the authorial intent of the font than ceiling
|
||||
// would be, and makes the apparent spacing match better between
|
||||
// low and high DPI displays.
|
||||
//
|
||||
// This does mean that it's possible for a glyph to overflow the
|
||||
// edge of the cell by a pixel if it has no side bearings, but in
|
||||
// reality such glyphs are generally meant to connect to adjacent
|
||||
// glyphs in some way so it's not really an issue.
|
||||
//
|
||||
// The same is true for the height. Some fonts are poorly authored
|
||||
// and have a descender on a normal glyph that extends right up to
|
||||
// the descent value of the face, and this can result in the glyph
|
||||
// overflowing the bottom of the cell by a pixel, which isn't good
|
||||
// but if we try to prevent it by increasing the cell height then
|
||||
// we get line heights that are too large for most users and even
|
||||
// more inconsistent across DPIs.
|
||||
//
|
||||
// Users who experience such cell-height overflows should:
|
||||
//
|
||||
// 1. Nag the font author to either redesign the glyph to not go
|
||||
// so low, or else adjust the descent value in the metadata.
|
||||
//
|
||||
// 2. Add an `adjust-cell-height` entry to their config to give
|
||||
// the cell enough room for the glyph.
|
||||
let cell_width = zround(face_width);
|
||||
let cell_height = zround(face_height);
|
||||
|
||||
// We split our line gap in two parts, and put half of it on the top
|
||||
// of the cell and the other half on the bottom, so that our text never
|
||||
// bumps up against either edge of the cell vertically.
|
||||
let half_line_gap = face.line_gap / 2.0;
|
||||
|
||||
// NOTE: Unlike all our other metrics, `cell_baseline` is
|
||||
// relative to the BOTTOM of the cell rather than the top.
|
||||
let face_baseline = half_line_gap - face.descent;
|
||||
// We calculate the baseline by trying to center the face vertically
|
||||
// in the pixel-rounded cell height, so that before rounding it will
|
||||
// be an even distance from the top and bottom of the cell, meaning
|
||||
// it either sticks out the same amount or is inset the same amount,
|
||||
// depending on whether the cell height was rounded up or down from
|
||||
// the line height. We do this by adding half the difference between
|
||||
// the cell height and the face height.
|
||||
let cell_baseline = zround(face_baseline - (cell_height - face_height) / 2.0);
|
||||
|
||||
// We keep track of the offset from the bottom of the cell
|
||||
// to the bottom of the face's "true" bounding box, which at
|
||||
// this point, since nothing has been scaled yet, is equivalent
|
||||
// to the offset between the baseline we draw at (cell_baseline)
|
||||
// and the one the font wants (face_baseline).
|
||||
let face_y = cell_baseline - face_baseline;
|
||||
|
||||
// We calculate a top_to_baseline to make following calculations simpler.
|
||||
let top_to_baseline = cell_height - cell_baseline;
|
||||
|
||||
// Get the other font metrics or their estimates. See doc comments
|
||||
// in FaceMetrics for explanations of the estimation heuristics.
|
||||
let cap_height = face.cap_height();
|
||||
let underline_thickness = 1.0f64.max(face.underline_thickness().ceil());
|
||||
let strikethrough_thickness = 1.0f64.max(face.strikethrough_thickness().ceil());
|
||||
let underline_position = zround(top_to_baseline - face.underline_position());
|
||||
let strikethrough_position = zround(top_to_baseline - face.strikethrough_position());
|
||||
|
||||
// Same heuristic as the font_patcher script. We store icon_height
|
||||
// separately from face_height such that modifiers can apply to the former
|
||||
// without affecting the latter.
|
||||
let icon_height = face_height;
|
||||
let icon_height_single = (2.0 * cap_height + face_height) / 3.0;
|
||||
|
||||
let mut result = Metrics {
|
||||
cell_width: cell_width as u32,
|
||||
cell_height: cell_height as u32,
|
||||
cell_baseline: cell_baseline as u32,
|
||||
underline_position: underline_position as u32,
|
||||
underline_thickness: underline_thickness as u32,
|
||||
strikethrough_position: strikethrough_position as u32,
|
||||
strikethrough_thickness: strikethrough_thickness as u32,
|
||||
overline_position: 0,
|
||||
overline_thickness: underline_thickness as u32,
|
||||
box_thickness: underline_thickness as u32,
|
||||
cursor_thickness: 1,
|
||||
cursor_height: cell_height as u32,
|
||||
icon_height,
|
||||
icon_height_single,
|
||||
face_width,
|
||||
face_height,
|
||||
face_y,
|
||||
};
|
||||
|
||||
// Ensure all metrics are within their allowable range.
|
||||
result.clamp();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Apply a set of modifiers.
|
||||
pub fn apply(&mut self, mods: &ModifierSet) {
|
||||
// Zig iterates a hash map (unspecified order); we iterate in Key
|
||||
// declaration order for determinism.
|
||||
let mut entries: Vec<(Key, Modifier)> = mods.iter().map(|(k, v)| (*k, *v)).collect();
|
||||
entries.sort_by_key(|(k, _)| *k);
|
||||
|
||||
for (key, modifier) in entries {
|
||||
match key {
|
||||
// We clamp these values to a minimum of 1 to prevent divide-by-zero
|
||||
// in downstream operations.
|
||||
Key::CellWidth | Key::CellHeight => {
|
||||
let original = if key == Key::CellWidth {
|
||||
self.cell_width
|
||||
} else {
|
||||
self.cell_height
|
||||
};
|
||||
// Compute the new value. If it is the same avoid the work.
|
||||
let new = modifier.apply_u32(original).max(1);
|
||||
if new == original {
|
||||
continue;
|
||||
}
|
||||
|
||||
if key == Key::CellWidth {
|
||||
self.cell_width = new;
|
||||
continue;
|
||||
}
|
||||
self.cell_height = new;
|
||||
|
||||
// For cell height, we have to also modify some positions
|
||||
// that are absolute from the top of the cell. The main goal
|
||||
// here is to center the baseline so that text is vertically
|
||||
// centered in the cell.
|
||||
let original_f64 = f64::from(original);
|
||||
let new_f64 = f64::from(new);
|
||||
let diff = new_f64 - original_f64;
|
||||
let half_diff = diff / 2.0;
|
||||
|
||||
// If the diff is even, the number of pixels we add
|
||||
// will be the same for the top and the bottom, but
|
||||
// if the diff is odd then we want to add the extra
|
||||
// pixel to the edge of the cell that needs it most.
|
||||
//
|
||||
// How much the edge "needs it" depends on whether
|
||||
// the face is higher or lower than it should be to
|
||||
// be perfectly centered in the cell.
|
||||
//
|
||||
// If the face were perfectly centered then face_y
|
||||
// would be equal to half of the difference between
|
||||
// the cell height and the face height.
|
||||
let position_with_respect_to_center =
|
||||
self.face_y - (original_f64 - self.face_height) / 2.0;
|
||||
|
||||
let (diff_top, diff_bottom) = if position_with_respect_to_center > 0.0 {
|
||||
// The baseline is higher than it should be, so we
|
||||
// add the extra to the top, or if it's a negative
|
||||
// diff it gets added to the bottom because of how
|
||||
// floor and ceil work.
|
||||
(half_diff.ceil(), half_diff.floor())
|
||||
} else {
|
||||
// The baseline is lower than it should be, so we
|
||||
// add the extra to the bottom, or vice versa for
|
||||
// negative diffs.
|
||||
(half_diff.floor(), half_diff.ceil())
|
||||
};
|
||||
|
||||
// The cell baseline and face_y values are relative to the
|
||||
// bottom of the cell so we add the bottom diff to them.
|
||||
add_float_to_int(&mut self.cell_baseline, diff_bottom);
|
||||
self.face_y += diff_bottom;
|
||||
|
||||
// These are all relative to the top of the cell.
|
||||
add_float_to_int(&mut self.underline_position, diff_top);
|
||||
add_float_to_int(&mut self.strikethrough_position, diff_top);
|
||||
self.overline_position = self.overline_position.saturating_add(diff_top as i32);
|
||||
}
|
||||
Key::IconHeight => {
|
||||
self.icon_height = modifier.apply_f64(self.icon_height);
|
||||
self.icon_height_single = modifier.apply_f64(self.icon_height_single);
|
||||
}
|
||||
Key::CellBaseline => self.cell_baseline = modifier.apply_u32(self.cell_baseline),
|
||||
Key::UnderlinePosition => {
|
||||
self.underline_position = modifier.apply_u32(self.underline_position);
|
||||
}
|
||||
Key::UnderlineThickness => {
|
||||
self.underline_thickness = modifier.apply_u32(self.underline_thickness);
|
||||
}
|
||||
Key::StrikethroughPosition => {
|
||||
self.strikethrough_position = modifier.apply_u32(self.strikethrough_position);
|
||||
}
|
||||
Key::StrikethroughThickness => {
|
||||
self.strikethrough_thickness = modifier.apply_u32(self.strikethrough_thickness);
|
||||
}
|
||||
Key::OverlinePosition => {
|
||||
self.overline_position = modifier.apply_i32(self.overline_position);
|
||||
}
|
||||
Key::OverlineThickness => {
|
||||
self.overline_thickness = modifier.apply_u32(self.overline_thickness);
|
||||
}
|
||||
Key::BoxThickness => self.box_thickness = modifier.apply_u32(self.box_thickness),
|
||||
Key::CursorThickness => {
|
||||
self.cursor_thickness = modifier.apply_u32(self.cursor_thickness);
|
||||
}
|
||||
Key::CursorHeight => self.cursor_height = modifier.apply_u32(self.cursor_height),
|
||||
Key::IconHeightSingle => {
|
||||
self.icon_height_single = modifier.apply_f64(self.icon_height_single);
|
||||
}
|
||||
Key::FaceWidth => self.face_width = modifier.apply_f64(self.face_width),
|
||||
Key::FaceHeight => self.face_height = modifier.apply_f64(self.face_height),
|
||||
Key::FaceY => self.face_y = modifier.apply_f64(self.face_y),
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent modifiers from pushing metrics out of their allowable range.
|
||||
self.clamp();
|
||||
}
|
||||
|
||||
/// Clamp all metrics to their allowable range.
|
||||
fn clamp(&mut self) {
|
||||
self.cell_width = self.cell_width.max(minimums::CELL_WIDTH);
|
||||
self.cell_height = self.cell_height.max(minimums::CELL_HEIGHT);
|
||||
self.underline_thickness = self.underline_thickness.max(minimums::UNDERLINE_THICKNESS);
|
||||
self.strikethrough_thickness = self
|
||||
.strikethrough_thickness
|
||||
.max(minimums::STRIKETHROUGH_THICKNESS);
|
||||
self.overline_thickness = self.overline_thickness.max(minimums::OVERLINE_THICKNESS);
|
||||
self.box_thickness = self.box_thickness.max(minimums::BOX_THICKNESS);
|
||||
self.cursor_thickness = self.cursor_thickness.max(minimums::CURSOR_THICKNESS);
|
||||
self.cursor_height = self.cursor_height.max(minimums::CURSOR_HEIGHT);
|
||||
self.icon_height = self.icon_height.max(minimums::ICON_HEIGHT);
|
||||
self.icon_height_single = self.icon_height_single.max(minimums::ICON_HEIGHT_SINGLE);
|
||||
self.face_height = self.face_height.max(minimums::FACE_HEIGHT);
|
||||
self.face_width = self.face_width.max(minimums::FACE_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function for adding an f64 to a u32.
|
||||
///
|
||||
/// Performs saturating addition or subtraction
|
||||
/// depending on the sign of the provided float.
|
||||
///
|
||||
/// The f64 is asserted to have an integer value.
|
||||
#[inline]
|
||||
fn add_float_to_int(int: &mut u32, float: f64) {
|
||||
debug_assert!((float.floor() - float).abs() < f64::EPSILON);
|
||||
*int = if float >= 0.0 {
|
||||
int.saturating_add(float as u32)
|
||||
} else {
|
||||
int.saturating_sub((-float) as u32)
|
||||
};
|
||||
}
|
||||
|
||||
/// A set of modifiers to apply to metrics. We use a hash map because
|
||||
/// we expect most metrics to be unmodified and want to take up as
|
||||
/// little space as possible.
|
||||
pub type ModifierSet = HashMap<Key, Modifier>;
|
||||
|
||||
/// Error returned by [`Modifier::parse`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ModifierParseError {
|
||||
#[error("invalid format")]
|
||||
InvalidFormat,
|
||||
#[error("value required")]
|
||||
ValueRequired,
|
||||
}
|
||||
|
||||
/// A modifier to apply to a metrics value. The modifier value represents
|
||||
/// a delta, so percent is a percentage to change, not a percentage of.
|
||||
/// For example, "20%" is 20% larger, not 20% of the value. Likewise,
|
||||
/// an absolute value of "20" is 20 larger, not literally 20.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Percent(f64),
|
||||
Absolute(i32),
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
/// Parses the modifier value. If the value ends in "%" it is assumed
|
||||
/// to be a percent, otherwise the value is parsed as an integer.
|
||||
pub fn parse(input: &str) -> Result<Modifier, ModifierParseError> {
|
||||
if input.is_empty() {
|
||||
return Err(ModifierParseError::InvalidFormat);
|
||||
}
|
||||
|
||||
if let Some(num) = input.strip_suffix('%') {
|
||||
let mut percent = parse_float(num).ok_or(ModifierParseError::InvalidFormat)?;
|
||||
percent /= 100.0;
|
||||
|
||||
if percent <= -1.0 {
|
||||
return Ok(Modifier::Percent(0.0));
|
||||
}
|
||||
if percent < 0.0 {
|
||||
return Ok(Modifier::Percent(1.0 + percent));
|
||||
}
|
||||
return Ok(Modifier::Percent(1.0 + percent));
|
||||
}
|
||||
|
||||
parse_int_i32(input)
|
||||
.map(Modifier::Absolute)
|
||||
.ok_or(ModifierParseError::InvalidFormat)
|
||||
}
|
||||
|
||||
/// So it works with the config framework.
|
||||
pub fn parse_cli(input: Option<&str>) -> Result<Modifier, ModifierParseError> {
|
||||
Modifier::parse(input.ok_or(ModifierParseError::ValueRequired)?)
|
||||
}
|
||||
|
||||
/// Used by config formatter. Returns the value text of the config entry.
|
||||
pub fn format_entry(&self) -> String {
|
||||
match self {
|
||||
Modifier::Percent(v) => format!("{}%", (v - 1.0) * 100.0),
|
||||
Modifier::Absolute(v) => format!("{v}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to an unsigned 32-bit value.
|
||||
pub fn apply_u32(&self, v: u32) -> u32 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => {
|
||||
let p_clamped = p.max(0.0);
|
||||
(f64::from(v) * p_clamped).round() as u32
|
||||
}
|
||||
Modifier::Absolute(abs) => {
|
||||
let applied = i64::from(v).saturating_add(i64::from(abs));
|
||||
let clamped = applied.max(0);
|
||||
u32::try_from(clamped).unwrap_or(u32::MAX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to a signed 32-bit value.
|
||||
pub fn apply_i32(&self, v: i32) -> i32 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => {
|
||||
let p_clamped = p.max(0.0);
|
||||
(f64::from(v) * p_clamped).round() as i32
|
||||
}
|
||||
Modifier::Absolute(abs) => {
|
||||
let applied = i64::from(v).saturating_add(i64::from(abs));
|
||||
// Mirrors Zig: out-of-range yields maxInt * sign.
|
||||
i32::try_from(applied).unwrap_or(if applied > 0 { i32::MAX } else { -i32::MAX })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a modifier to a float value.
|
||||
pub fn apply_f64(&self, v: f64) -> f64 {
|
||||
match *self {
|
||||
Modifier::Percent(p) => v * p.max(0.0),
|
||||
Modifier::Absolute(abs) => v + f64::from(abs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Modifier {
|
||||
type Err = ModifierParseError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Modifier::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig `std.fmt.parseInt(i32, s, 10)`: optional sign, digits only
|
||||
/// (underscores are only accepted with base 0, so not here).
|
||||
fn parse_int_i32(s: &str) -> Option<i32> {
|
||||
let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
|
||||
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
s.parse::<i32>().ok()
|
||||
}
|
||||
|
||||
/// Approximation of Zig `std.fmt.parseFloat(f64, s)` for decimal input.
|
||||
fn parse_float(s: &str) -> Option<f64> {
|
||||
if s.is_empty() || s.contains(char::is_whitespace) {
|
||||
return None;
|
||||
}
|
||||
s.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
/// Key is an enum of all the available metrics keys (every u32/i32/f64
|
||||
/// field of [`Metrics`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Key {
|
||||
CellWidth,
|
||||
CellHeight,
|
||||
CellBaseline,
|
||||
UnderlinePosition,
|
||||
UnderlineThickness,
|
||||
StrikethroughPosition,
|
||||
StrikethroughThickness,
|
||||
OverlinePosition,
|
||||
OverlineThickness,
|
||||
BoxThickness,
|
||||
CursorThickness,
|
||||
CursorHeight,
|
||||
IconHeight,
|
||||
IconHeightSingle,
|
||||
FaceWidth,
|
||||
FaceHeight,
|
||||
FaceY,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// The snake_case field name, matching the Zig `@tagName`.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Key::CellWidth => "cell_width",
|
||||
Key::CellHeight => "cell_height",
|
||||
Key::CellBaseline => "cell_baseline",
|
||||
Key::UnderlinePosition => "underline_position",
|
||||
Key::UnderlineThickness => "underline_thickness",
|
||||
Key::StrikethroughPosition => "strikethrough_position",
|
||||
Key::StrikethroughThickness => "strikethrough_thickness",
|
||||
Key::OverlinePosition => "overline_position",
|
||||
Key::OverlineThickness => "overline_thickness",
|
||||
Key::BoxThickness => "box_thickness",
|
||||
Key::CursorThickness => "cursor_thickness",
|
||||
Key::CursorHeight => "cursor_height",
|
||||
Key::IconHeight => "icon_height",
|
||||
Key::IconHeightSingle => "icon_height_single",
|
||||
Key::FaceWidth => "face_width",
|
||||
Key::FaceHeight => "face_height",
|
||||
Key::FaceY => "face_y",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Key {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, ()> {
|
||||
Ok(match s {
|
||||
"cell_width" => Key::CellWidth,
|
||||
"cell_height" => Key::CellHeight,
|
||||
"cell_baseline" => Key::CellBaseline,
|
||||
"underline_position" => Key::UnderlinePosition,
|
||||
"underline_thickness" => Key::UnderlineThickness,
|
||||
"strikethrough_position" => Key::StrikethroughPosition,
|
||||
"strikethrough_thickness" => Key::StrikethroughThickness,
|
||||
"overline_position" => Key::OverlinePosition,
|
||||
"overline_thickness" => Key::OverlineThickness,
|
||||
"box_thickness" => Key::BoxThickness,
|
||||
"cursor_thickness" => Key::CursorThickness,
|
||||
"cursor_height" => Key::CursorHeight,
|
||||
"icon_height" => Key::IconHeight,
|
||||
"icon_height_single" => Key::IconHeightSingle,
|
||||
"face_width" => Key::FaceWidth,
|
||||
"face_height" => Key::FaceHeight,
|
||||
"face_y" => Key::FaceY,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formatconfig_percent() {
|
||||
let p = Modifier::parse_cli(Some("24%")).unwrap();
|
||||
// Zig test checks the config-formatted line "a = 24%\n"; the config
|
||||
// entry formatter lives in ghostty-config, so check the value text.
|
||||
assert_eq!(p.format_entry(), "24%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatconfig_absolute() {
|
||||
let p = Modifier::parse_cli(Some("-30")).unwrap();
|
||||
assert_eq!(p.format_entry(), "-30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_apply_modifiers() {
|
||||
let mut set = ModifierSet::new();
|
||||
set.insert(Key::CellWidth, Modifier::Percent(1.2));
|
||||
|
||||
let mut m = Metrics::init();
|
||||
m.cell_width = 100;
|
||||
m.apply(&set);
|
||||
assert_eq!(120, m.cell_width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_adjust_cell_height_smaller() {
|
||||
let mut set = ModifierSet::new();
|
||||
// We choose numbers such that the subtracted number of pixels is odd,
|
||||
// as that's the case that could most easily have off-by-one errors.
|
||||
// Here we're removing 25 pixels: 13 on the bottom, 12 on top, split
|
||||
// that way because we're simulating a face that's 0.33px higher than
|
||||
// it "should" be (due to rounding).
|
||||
set.insert(Key::CellHeight, Modifier::Percent(0.75));
|
||||
|
||||
let mut m = Metrics::init();
|
||||
m.face_y = 0.33;
|
||||
m.cell_baseline = 50;
|
||||
m.underline_position = 55;
|
||||
m.strikethrough_position = 30;
|
||||
m.overline_position = 0;
|
||||
m.cell_height = 100;
|
||||
m.face_height = 99.67;
|
||||
m.cursor_height = 100;
|
||||
m.apply(&set);
|
||||
assert_eq!(-12.67, m.face_y);
|
||||
assert_eq!(75, m.cell_height);
|
||||
assert_eq!(37, m.cell_baseline);
|
||||
assert_eq!(43, m.underline_position);
|
||||
assert_eq!(18, m.strikethrough_position);
|
||||
assert_eq!(-12, m.overline_position);
|
||||
// Cursor height is separate from cell height and does not follow it.
|
||||
assert_eq!(100, m.cursor_height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_adjust_cell_height_larger() {
|
||||
let mut set = ModifierSet::new();
|
||||
// We choose numbers such that the added number of pixels is odd,
|
||||
// as that's the case that could most easily have off-by-one errors.
|
||||
// Here we're adding 75 pixels: 37 on the bottom, 38 on top, split
|
||||
// that way because we're simulating a face that's 0.33px higher
|
||||
// than it "should" be (due to rounding).
|
||||
set.insert(Key::CellHeight, Modifier::Percent(1.75));
|
||||
|
||||
let mut m = Metrics::init();
|
||||
m.face_y = 0.33;
|
||||
m.cell_baseline = 50;
|
||||
m.underline_position = 55;
|
||||
m.strikethrough_position = 30;
|
||||
m.overline_position = 0;
|
||||
m.cell_height = 100;
|
||||
m.face_height = 99.67;
|
||||
m.cursor_height = 100;
|
||||
m.apply(&set);
|
||||
assert_eq!(37.33, m.face_y);
|
||||
assert_eq!(175, m.cell_height);
|
||||
assert_eq!(87, m.cell_baseline);
|
||||
assert_eq!(93, m.underline_position);
|
||||
assert_eq!(68, m.strikethrough_position);
|
||||
assert_eq!(38, m.overline_position);
|
||||
// Cursor height is separate from cell height and does not follow it.
|
||||
assert_eq!(100, m.cursor_height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_adjust_icon_height_by_percentage() {
|
||||
let mut set = ModifierSet::new();
|
||||
set.insert(Key::IconHeight, Modifier::Percent(0.75));
|
||||
|
||||
let mut m = Metrics::init();
|
||||
m.icon_height = 100.0;
|
||||
m.icon_height_single = 80.0;
|
||||
m.face_height = 100.0;
|
||||
m.face_y = 1.0;
|
||||
m.apply(&set);
|
||||
assert_eq!(75.0, m.icon_height);
|
||||
assert_eq!(60.0, m.icon_height_single);
|
||||
// Face metrics not affected
|
||||
assert_eq!(100.0, m.face_height);
|
||||
assert_eq!(1.0, m.face_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_adjust_icon_height_by_absolute_pixels() {
|
||||
let mut set = ModifierSet::new();
|
||||
set.insert(Key::IconHeight, Modifier::Absolute(-5));
|
||||
|
||||
let mut m = Metrics::init();
|
||||
m.icon_height = 100.0;
|
||||
m.icon_height_single = 80.0;
|
||||
m.face_height = 100.0;
|
||||
m.face_y = 1.0;
|
||||
m.apply(&set);
|
||||
assert_eq!(95.0, m.icon_height);
|
||||
assert_eq!(75.0, m.icon_height_single);
|
||||
// Face metrics not affected
|
||||
assert_eq!(100.0, m.face_height);
|
||||
assert_eq!(1.0, m.face_y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_parse_absolute() {
|
||||
assert_eq!(Modifier::Absolute(100), Modifier::parse("100").unwrap());
|
||||
assert_eq!(Modifier::Absolute(-100), Modifier::parse("-100").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_parse_percent() {
|
||||
assert_eq!(Modifier::Percent(1.2), Modifier::parse("20%").unwrap());
|
||||
assert_eq!(Modifier::Percent(0.8), Modifier::parse("-20%").unwrap());
|
||||
assert_eq!(Modifier::Percent(1.0), Modifier::parse("0%").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_parse_errors() {
|
||||
assert_eq!(Err(ModifierParseError::InvalidFormat), Modifier::parse(""));
|
||||
assert_eq!(
|
||||
Err(ModifierParseError::InvalidFormat),
|
||||
Modifier::parse("abc")
|
||||
);
|
||||
assert_eq!(Err(ModifierParseError::InvalidFormat), Modifier::parse("%"));
|
||||
assert_eq!(Ok(Modifier::Percent(0.0)), Modifier::parse("-150%"));
|
||||
assert_eq!(
|
||||
Err(ModifierParseError::ValueRequired),
|
||||
Modifier::parse_cli(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_percent() {
|
||||
assert_eq!(80, Modifier::Percent(0.8).apply_u32(100));
|
||||
assert_eq!(180, Modifier::Percent(1.8).apply_u32(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_absolute() {
|
||||
assert_eq!(0, Modifier::Absolute(-100).apply_u32(100));
|
||||
assert_eq!(0, Modifier::Absolute(-120).apply_u32(100));
|
||||
assert_eq!(200, Modifier::Absolute(100).apply_u32(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_names_roundtrip() {
|
||||
for k in [Key::CellWidth, Key::FaceY, Key::IconHeightSingle] {
|
||||
assert_eq!(Ok(k), k.name().parse::<Key>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
//! This exposes primitives to draw 2D graphics and export the graphic to
|
||||
//! a font atlas. Port of `font/sprite/canvas.zig`.
|
||||
//!
|
||||
//! Ghostty uses the `z2d` library for antialiased path drawing; this port
|
||||
//! uses `tiny-skia` to rasterize path coverage (see [`Canvas::fill_path`]).
|
||||
|
||||
use std::ops::Sub;
|
||||
|
||||
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
|
||||
|
||||
use crate::atlas::{self, Atlas, Format, Region};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Point<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Line<T> {
|
||||
pub p0: Point<T>,
|
||||
pub p1: Point<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Box<T> {
|
||||
pub p0: Point<T>,
|
||||
pub p1: Point<T>,
|
||||
}
|
||||
|
||||
impl<T: Copy + PartialOrd + Sub<Output = T>> Box<T> {
|
||||
pub fn rect(&self) -> Rect<T> {
|
||||
let min = |a: T, b: T| if b < a { b } else { a };
|
||||
let max = |a: T, b: T| if b > a { b } else { a };
|
||||
let tl_x = min(self.p0.x, self.p1.x);
|
||||
let tl_y = min(self.p0.y, self.p1.y);
|
||||
let br_x = max(self.p0.x, self.p1.x);
|
||||
let br_y = max(self.p0.y, self.p1.y);
|
||||
|
||||
Rect {
|
||||
x: tl_x,
|
||||
y: tl_y,
|
||||
width: br_x - tl_x,
|
||||
height: br_y - tl_y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Rect<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
pub width: T,
|
||||
pub height: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Triangle<T> {
|
||||
pub p0: Point<T>,
|
||||
pub p1: Point<T>,
|
||||
pub p2: Point<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Quad<T> {
|
||||
pub p0: Point<T>,
|
||||
pub p1: Point<T>,
|
||||
pub p2: Point<T>,
|
||||
pub p3: Point<T>,
|
||||
}
|
||||
|
||||
/// We only use alpha-channel so a pixel is an 8-bit alpha value. Zig models
|
||||
/// this as a non-exhaustive `enum(u8)`; `ON` = 255, `OFF` = 0, and any other
|
||||
/// value is a partial alpha (used for shades).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Color(pub u8);
|
||||
|
||||
impl Color {
|
||||
pub const ON: Color = Color(255);
|
||||
pub const OFF: Color = Color(0);
|
||||
}
|
||||
|
||||
/// Errors that drawing functions can produce.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum DrawError {
|
||||
/// Something went wrong while doing math.
|
||||
#[error("math error")]
|
||||
MathError,
|
||||
#[error("atlas error: {0}")]
|
||||
Atlas(#[from] atlas::Error),
|
||||
#[error("path error")]
|
||||
Path,
|
||||
}
|
||||
|
||||
/// A node of a [`Path`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum PathNode {
|
||||
MoveTo(f64, f64),
|
||||
LineTo(f64, f64),
|
||||
CurveTo(f64, f64, f64, f64, f64, f64),
|
||||
ClosePath,
|
||||
}
|
||||
|
||||
/// A path in cell-relative coordinates (the canvas applies the padding
|
||||
/// translation when the path is filled or stroked, like z2d's
|
||||
/// `staticPath` transformation).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Path {
|
||||
nodes: Vec<PathNode>,
|
||||
current: Option<(f64, f64)>,
|
||||
start: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn new() -> Path {
|
||||
Path::default()
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, x: f64, y: f64) {
|
||||
self.nodes.push(PathNode::MoveTo(x, y));
|
||||
self.current = Some((x, y));
|
||||
self.start = Some((x, y));
|
||||
}
|
||||
|
||||
pub fn line_to(&mut self, x: f64, y: f64) {
|
||||
if self.current.is_none() {
|
||||
self.move_to(x, y);
|
||||
return;
|
||||
}
|
||||
self.nodes.push(PathNode::LineTo(x, y));
|
||||
self.current = Some((x, y));
|
||||
}
|
||||
|
||||
pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
|
||||
if self.current.is_none() {
|
||||
self.move_to(x1, y1);
|
||||
}
|
||||
self.nodes.push(PathNode::CurveTo(x1, y1, x2, y2, x3, y3));
|
||||
self.current = Some((x3, y3));
|
||||
}
|
||||
|
||||
/// Close the current subpath. Like z2d, the current point returns to
|
||||
/// the start of the subpath.
|
||||
pub fn close(&mut self) {
|
||||
if self.current.is_none() {
|
||||
return;
|
||||
}
|
||||
self.nodes.push(PathNode::ClosePath);
|
||||
self.current = self.start;
|
||||
}
|
||||
|
||||
/// Add a circular arc centered at `(x, y)` from angle `a0` to `a1`
|
||||
/// (radians, clockwise in canvas space with +y down). If there is a
|
||||
/// current point, a line is drawn to the start of the arc, otherwise
|
||||
/// the arc starts a new subpath.
|
||||
pub fn arc(&mut self, x: f64, y: f64, radius: f64, a0: f64, a1: f64) {
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
let (sx, sy) = (x + radius * a0.cos(), y + radius * a0.sin());
|
||||
if self.current.is_some() {
|
||||
self.line_to(sx, sy);
|
||||
} else {
|
||||
self.move_to(sx, sy);
|
||||
}
|
||||
|
||||
let mut sweep = a1 - a0;
|
||||
// Split into segments of at most a quarter turn.
|
||||
let segs = ((sweep.abs() / FRAC_PI_2).ceil() as usize).max(1);
|
||||
sweep /= segs as f64;
|
||||
let k = 4.0 / 3.0 * (sweep / 4.0).tan();
|
||||
let mut a = a0;
|
||||
for _ in 0..segs {
|
||||
let (s0, c0) = a.sin_cos();
|
||||
let (s1, c1) = (a + sweep).sin_cos();
|
||||
self.curve_to(
|
||||
x + radius * (c0 - k * s0),
|
||||
y + radius * (s0 + k * c0),
|
||||
x + radius * (c1 + k * s1),
|
||||
y + radius * (s1 - k * c1),
|
||||
x + radius * c1,
|
||||
y + radius * s1,
|
||||
);
|
||||
a += sweep;
|
||||
}
|
||||
}
|
||||
|
||||
fn to_skia(&self, dx: f64, dy: f64) -> Option<tiny_skia::Path> {
|
||||
let mut pb = PathBuilder::new();
|
||||
let f = |v: f64, d: f64| (v + d) as f32;
|
||||
for n in &self.nodes {
|
||||
match *n {
|
||||
PathNode::MoveTo(x, y) => pb.move_to(f(x, dx), f(y, dy)),
|
||||
PathNode::LineTo(x, y) => pb.line_to(f(x, dx), f(y, dy)),
|
||||
PathNode::CurveTo(x1, y1, x2, y2, x3, y3) => pb.cubic_to(
|
||||
f(x1, dx),
|
||||
f(y1, dy),
|
||||
f(x2, dx),
|
||||
f(y2, dy),
|
||||
f(x3, dx),
|
||||
f(y3, dy),
|
||||
),
|
||||
PathNode::ClosePath => pb.close(),
|
||||
}
|
||||
}
|
||||
pb.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Line cap modes (subset of z2d's).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum CapMode {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
/// Stroke options (subset of z2d's `StrokeOptions`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct StrokeOptions {
|
||||
pub line_width: f64,
|
||||
pub line_cap_mode: CapMode,
|
||||
}
|
||||
|
||||
impl Default for StrokeOptions {
|
||||
fn default() -> Self {
|
||||
StrokeOptions {
|
||||
line_width: 2.0,
|
||||
line_cap_mode: CapMode::Butt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The canvas is an 8-bit alpha surface with padding on all sides, onto
|
||||
/// which sprite glyphs are drawn.
|
||||
#[derive(Debug)]
|
||||
pub struct Canvas {
|
||||
/// The underlying alpha8 surface, row-major, `sfc_width * sfc_height`.
|
||||
pub sfc: Vec<u8>,
|
||||
pub sfc_width: u32,
|
||||
pub sfc_height: u32,
|
||||
|
||||
pub padding_x: u32,
|
||||
pub padding_y: u32,
|
||||
|
||||
pub clip_top: u32,
|
||||
pub clip_left: u32,
|
||||
pub clip_right: u32,
|
||||
pub clip_bottom: u32,
|
||||
|
||||
/// Scratch pixmap used to rasterize path coverage.
|
||||
scratch: Pixmap,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new(width: u32, height: u32, padding_x: u32, padding_y: u32) -> Canvas {
|
||||
// We add padding to both sides (hence `2 *`)
|
||||
let w = width + 2 * padding_x;
|
||||
let h = height + 2 * padding_y;
|
||||
Canvas {
|
||||
sfc: vec![0; w as usize * h as usize],
|
||||
sfc_width: w,
|
||||
sfc_height: h,
|
||||
padding_x,
|
||||
padding_y,
|
||||
clip_top: 0,
|
||||
clip_left: 0,
|
||||
clip_right: 0,
|
||||
clip_bottom: 0,
|
||||
// tiny-skia can't make zero sized pixmaps; sprites are never empty
|
||||
// but be defensive.
|
||||
scratch: Pixmap::new(w.max(1), h.max(1)).expect("non-zero pixmap size"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the data in this drawing to the atlas.
|
||||
pub fn write_atlas(&mut self, atlas: &mut Atlas) -> Result<Region, atlas::Error> {
|
||||
assert_eq!(atlas.format, Format::Grayscale);
|
||||
|
||||
self.trim();
|
||||
|
||||
let sfc_width = self.sfc_width;
|
||||
let sfc_height = self.sfc_height;
|
||||
|
||||
// Subtract our clip margins from the
|
||||
// width and height to get region size.
|
||||
let region_width = sfc_width
|
||||
.saturating_sub(self.clip_left)
|
||||
.saturating_sub(self.clip_right);
|
||||
let region_height = sfc_height
|
||||
.saturating_sub(self.clip_top)
|
||||
.saturating_sub(self.clip_bottom);
|
||||
|
||||
// Allocate our texture atlas region
|
||||
let region = atlas.reserve(region_width, region_height)?;
|
||||
|
||||
if region.width > 0 && region.height > 0 {
|
||||
// Write the glyph information into the atlas
|
||||
assert_eq!(region.width, region_width);
|
||||
assert_eq!(region.height, region_height);
|
||||
atlas.set_from_larger(region, &self.sfc, sfc_width, self.clip_left, self.clip_top);
|
||||
}
|
||||
|
||||
Ok(region)
|
||||
}
|
||||
|
||||
/// Adjust clip boundaries to trim off any fully transparent rows or columns.
|
||||
fn trim(&mut self) {
|
||||
let width = self.sfc_width;
|
||||
let height = self.sfc_height;
|
||||
let buf = &self.sfc;
|
||||
let px = |x: u32, y: u32| buf[y as usize * width as usize + x as usize];
|
||||
|
||||
while self.clip_top < height - self.clip_bottom {
|
||||
let y = self.clip_top;
|
||||
let x0 = self.clip_left;
|
||||
let x1 = width - self.clip_right;
|
||||
if (x0..x1).any(|x| px(x, y) != 0) {
|
||||
break;
|
||||
}
|
||||
self.clip_top += 1;
|
||||
}
|
||||
|
||||
while self.clip_bottom < height - self.clip_top {
|
||||
let y = (height - self.clip_bottom).saturating_sub(1);
|
||||
let x0 = self.clip_left;
|
||||
let x1 = width - self.clip_right;
|
||||
if (x0..x1).any(|x| px(x, y) != 0) {
|
||||
break;
|
||||
}
|
||||
self.clip_bottom += 1;
|
||||
}
|
||||
|
||||
while self.clip_left < width - self.clip_right {
|
||||
let x = self.clip_left;
|
||||
let y0 = self.clip_top;
|
||||
let y1 = height - self.clip_bottom;
|
||||
if (y0..y1).any(|y| px(x, y) != 0) {
|
||||
break;
|
||||
}
|
||||
self.clip_left += 1;
|
||||
}
|
||||
|
||||
while self.clip_right < width - self.clip_left {
|
||||
let x = (width - self.clip_right).saturating_sub(1);
|
||||
let y0 = self.clip_top;
|
||||
let y1 = height - self.clip_bottom;
|
||||
if (y0..y1).any(|y| px(x, y) != 0) {
|
||||
break;
|
||||
}
|
||||
self.clip_right += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Only really useful for test purposes, since the clipping region is
|
||||
/// automatically excluded when writing to an atlas with `write_atlas`.
|
||||
pub fn clear_clipping_regions(&mut self) {
|
||||
let width = self.sfc_width as usize;
|
||||
let height = self.sfc_height as usize;
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..self.clip_left as usize {
|
||||
self.sfc[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..height {
|
||||
for x in (width - self.clip_right as usize)..width {
|
||||
self.sfc[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..self.clip_top as usize {
|
||||
for x in 0..width {
|
||||
self.sfc[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for y in (height - self.clip_bottom as usize)..height {
|
||||
for x in 0..width {
|
||||
self.sfc[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the canvas (pixels and clip margins) for reuse. Used by tests
|
||||
/// that render many glyphs onto a single canvas.
|
||||
pub fn reset(&mut self) {
|
||||
self.sfc.fill(0);
|
||||
self.clip_top = 0;
|
||||
self.clip_left = 0;
|
||||
self.clip_right = 0;
|
||||
self.clip_bottom = 0;
|
||||
}
|
||||
|
||||
/// Draw and fill a single pixel (out-of-bounds pixels are ignored).
|
||||
pub fn pixel(&mut self, x: i32, y: i32, color: Color) {
|
||||
let x = i64::from(x) + i64::from(self.padding_x);
|
||||
let y = i64::from(y) + i64::from(self.padding_y);
|
||||
if x < 0 || y < 0 || x >= i64::from(self.sfc_width) || y >= i64::from(self.sfc_height) {
|
||||
return;
|
||||
}
|
||||
self.sfc[y as usize * self.sfc_width as usize + x as usize] = color.0;
|
||||
}
|
||||
|
||||
/// Draw and fill a rectangle. This is the main primitive for drawing
|
||||
/// lines as well (which are just generally skinny rectangles...)
|
||||
pub fn rect(&mut self, v: Rect<i32>, color: Color) {
|
||||
let mut y = v.y;
|
||||
while y < v.y + v.height {
|
||||
let mut x = v.x;
|
||||
while x < v.x + v.width {
|
||||
self.pixel(x, y, color);
|
||||
x += 1;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper for `Canvas::rect`
|
||||
pub fn box_(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
self.rect(
|
||||
Box {
|
||||
p0: Point { x: x0, y: y0 },
|
||||
p1: Point { x: x1, y: y1 },
|
||||
}
|
||||
.rect(),
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw and fill a quad.
|
||||
pub fn quad(&mut self, q: Quad<f64>, color: Color) -> Result<(), DrawError> {
|
||||
let mut path = Path::new();
|
||||
path.move_to(q.p0.x, q.p0.y);
|
||||
path.line_to(q.p1.x, q.p1.y);
|
||||
path.line_to(q.p2.x, q.p2.y);
|
||||
path.line_to(q.p3.x, q.p3.y);
|
||||
path.close();
|
||||
self.fill_path(&path, color)
|
||||
}
|
||||
|
||||
/// Draw and fill a triangle.
|
||||
pub fn triangle(&mut self, t: Triangle<f64>, color: Color) -> Result<(), DrawError> {
|
||||
let mut path = Path::new();
|
||||
path.move_to(t.p0.x, t.p0.y);
|
||||
path.line_to(t.p1.x, t.p1.y);
|
||||
path.line_to(t.p2.x, t.p2.y);
|
||||
path.close();
|
||||
self.fill_path(&path, color)
|
||||
}
|
||||
|
||||
/// Stroke a line.
|
||||
pub fn line(&mut self, l: Line<f64>, thickness: f64, color: Color) -> Result<(), DrawError> {
|
||||
let mut path = Path::new();
|
||||
path.move_to(l.p0.x, l.p0.y);
|
||||
path.line_to(l.p1.x, l.p1.y);
|
||||
self.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_cap_mode: CapMode::Butt,
|
||||
line_width: thickness,
|
||||
},
|
||||
color,
|
||||
)
|
||||
}
|
||||
|
||||
/// Rasterize `path` (filled or stroked) into the scratch pixmap and
|
||||
/// return nothing; the coverage is then in `self.scratch`'s alpha.
|
||||
fn rasterize(&mut self, path: &Path, stroke: Option<StrokeOptions>) -> bool {
|
||||
self.scratch.fill(tiny_skia::Color::TRANSPARENT);
|
||||
let Some(p) = path.to_skia(0.0, 0.0) else {
|
||||
return false;
|
||||
};
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color_rgba8(0, 0, 0, 255);
|
||||
paint.anti_alias = true;
|
||||
let ts = Transform::from_translate(self.padding_x as f32, self.padding_y as f32);
|
||||
match stroke {
|
||||
None => {
|
||||
self.scratch
|
||||
.fill_path(&p, &paint, FillRule::Winding, ts, None);
|
||||
}
|
||||
Some(opts) => {
|
||||
let s = Stroke {
|
||||
width: opts.line_width as f32,
|
||||
miter_limit: 10.0,
|
||||
line_cap: match opts.line_cap_mode {
|
||||
CapMode::Butt => LineCap::Butt,
|
||||
CapMode::Round => LineCap::Round,
|
||||
CapMode::Square => LineCap::Square,
|
||||
},
|
||||
line_join: LineJoin::Miter,
|
||||
dash: None,
|
||||
};
|
||||
self.scratch.stroke_path(&p, &paint, &s, ts, None);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Composite the scratch coverage onto the canvas using `src_over`
|
||||
/// with an opaque pattern of alpha `color`.
|
||||
fn composite_scratch(&mut self, color: Color) {
|
||||
let a = u32::from(color.0);
|
||||
for (dst, px) in self
|
||||
.sfc
|
||||
.iter_mut()
|
||||
.zip(self.scratch.data().as_chunks::<4>().0.iter())
|
||||
{
|
||||
let cov = u32::from(px[3]);
|
||||
if cov == 0 {
|
||||
continue;
|
||||
}
|
||||
let src = (cov * a + 127) / 255;
|
||||
let d = u32::from(*dst);
|
||||
*dst = (src + (d * (255 - src) + 127) / 255).min(255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke a path.
|
||||
pub fn stroke_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
opts: StrokeOptions,
|
||||
color: Color,
|
||||
) -> Result<(), DrawError> {
|
||||
if self.rasterize(path, Some(opts)) {
|
||||
self.composite_scratch(color);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Do an inner stroke on a path: the shape is filled to make a mask,
|
||||
/// then stroked with double width, and the two are multiplied so only
|
||||
/// the part of the stroke inside the shape remains.
|
||||
pub fn inner_stroke_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
opts: StrokeOptions,
|
||||
color: Color,
|
||||
) -> Result<(), DrawError> {
|
||||
// Make a closed version of the path for our fill, so
|
||||
// that we can support open paths for inner stroke.
|
||||
let mut closed = path.clone();
|
||||
closed.close();
|
||||
|
||||
let mut fill = vec![0u8; self.sfc.len()];
|
||||
if self.rasterize(&closed, None) {
|
||||
for (d, px) in fill
|
||||
.iter_mut()
|
||||
.zip(self.scratch.data().as_chunks::<4>().0.iter())
|
||||
{
|
||||
*d = px[3];
|
||||
}
|
||||
}
|
||||
|
||||
// Stroke the shape with double the desired width.
|
||||
let mut stroke = vec![0u8; self.sfc.len()];
|
||||
let mut mut_opts = opts;
|
||||
mut_opts.line_width *= 2.0;
|
||||
if self.rasterize(path, Some(mut_opts)) {
|
||||
let a = u32::from(color.0);
|
||||
for (d, px) in stroke
|
||||
.iter_mut()
|
||||
.zip(self.scratch.data().as_chunks::<4>().0.iter())
|
||||
{
|
||||
*d = ((u32::from(px[3]) * a + 127) / 255) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply the stroke onto the fill, then composite over the canvas.
|
||||
for ((dst, f), s) in self.sfc.iter_mut().zip(&fill).zip(&stroke) {
|
||||
let m = (255.0 * (f64::from(*s) / 255.0) * (f64::from(*f) / 255.0)).round() as u32;
|
||||
let d = u32::from(*dst);
|
||||
*dst = (m + (d * (255 - m) + 127) / 255).min(255) as u8;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fill a path (non-zero winding).
|
||||
pub fn fill_path(&mut self, path: &Path, color: Color) -> Result<(), DrawError> {
|
||||
if self.rasterize(path, None) {
|
||||
self.composite_scratch(color);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invert all pixels on the canvas.
|
||||
pub fn invert(&mut self) {
|
||||
for v in &mut self.sfc {
|
||||
*v = 255 - *v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror the canvas horizontally.
|
||||
pub fn flip_horizontal(&mut self) {
|
||||
let width = self.sfc_width as usize;
|
||||
for row in self.sfc.chunks_exact_mut(width) {
|
||||
row.reverse();
|
||||
}
|
||||
std::mem::swap(&mut self.clip_left, &mut self.clip_right);
|
||||
}
|
||||
|
||||
/// Mirror the canvas vertically.
|
||||
pub fn flip_vertical(&mut self) {
|
||||
let width = self.sfc_width as usize;
|
||||
let height = self.sfc_height as usize;
|
||||
for y in 0..height / 2 {
|
||||
let (top, bottom) = self.sfc.split_at_mut((height - y - 1) * width);
|
||||
top[y * width..(y + 1) * width].swap_with_slice(&mut bottom[..width]);
|
||||
}
|
||||
std::mem::swap(&mut self.clip_top, &mut self.clip_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn at(c: &Canvas, x: u32, y: u32) -> u8 {
|
||||
c.sfc[y as usize * c.sfc_width as usize + x as usize]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_rect_normalizes() {
|
||||
let b = Box {
|
||||
p0: Point { x: 5, y: 9 },
|
||||
p1: Point { x: 2, y: 3 },
|
||||
};
|
||||
assert_eq!(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 3,
|
||||
width: 3,
|
||||
height: 6
|
||||
},
|
||||
b.rect()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_and_padding() {
|
||||
let mut c = Canvas::new(4, 4, 1, 1);
|
||||
assert_eq!((6, 6), (c.sfc_width, c.sfc_height));
|
||||
c.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
assert_eq!(255, at(&c, 1, 1));
|
||||
assert_eq!(255, at(&c, 2, 1));
|
||||
assert_eq!(0, at(&c, 3, 1));
|
||||
assert_eq!(0, at(&c, 1, 2));
|
||||
// out of bounds is ignored
|
||||
c.rect(
|
||||
Rect {
|
||||
x: -10,
|
||||
y: -10,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
Color::OFF,
|
||||
);
|
||||
assert!(c.sfc.iter().all(|v| *v == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_and_write_atlas() {
|
||||
let mut c = Canvas::new(8, 8, 2, 2);
|
||||
c.rect(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 3,
|
||||
height: 2,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
let r = c.write_atlas(&mut atlas).unwrap();
|
||||
assert_eq!((3, 2), (r.width, r.height));
|
||||
assert_eq!(2 + 1, c.clip_left);
|
||||
assert_eq!(2 + 2, c.clip_top);
|
||||
assert_eq!(atlas.data[(r.y * 32 + r.x) as usize], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_canvas_writes_empty_region() {
|
||||
let mut c = Canvas::new(8, 8, 2, 2);
|
||||
let mut atlas = Atlas::new(32, Format::Grayscale);
|
||||
let r = c.write_atlas(&mut atlas).unwrap();
|
||||
assert_eq!((0, 0), (r.width, r.height));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flips() {
|
||||
let mut c = Canvas::new(4, 2, 0, 0);
|
||||
c.pixel(0, 0, Color::ON);
|
||||
c.flip_horizontal();
|
||||
assert_eq!(255, at(&c, 3, 0));
|
||||
c.flip_vertical();
|
||||
assert_eq!(255, at(&c, 3, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invert_all() {
|
||||
let mut c = Canvas::new(2, 2, 0, 0);
|
||||
c.pixel(0, 0, Color::ON);
|
||||
c.invert();
|
||||
assert_eq!(0, at(&c, 0, 0));
|
||||
assert_eq!(255, at(&c, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_triangle_covers_interior() {
|
||||
let mut c = Canvas::new(20, 20, 5, 5);
|
||||
c.triangle(
|
||||
Triangle {
|
||||
p0: Point { x: 0.0, y: 0.0 },
|
||||
p1: Point { x: 20.0, y: 0.0 },
|
||||
p2: Point { x: 0.0, y: 20.0 },
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(255, at(&c, 5 + 2, 5 + 2));
|
||||
assert_eq!(0, at(&c, 5 + 18, 5 + 18));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_line_thickness() {
|
||||
let mut c = Canvas::new(20, 20, 5, 5);
|
||||
c.line(
|
||||
Line {
|
||||
p0: Point { x: 0.0, y: 10.0 },
|
||||
p1: Point { x: 20.0, y: 10.0 },
|
||||
},
|
||||
4.0,
|
||||
Color::ON,
|
||||
)
|
||||
.unwrap();
|
||||
// rows 8..12 inclusive of padding offset
|
||||
assert_eq!(255, at(&c, 15, 5 + 8));
|
||||
assert_eq!(255, at(&c, 15, 5 + 11));
|
||||
assert_eq!(0, at(&c, 15, 5 + 7));
|
||||
assert_eq!(0, at(&c, 15, 5 + 12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_full_circle_fills() {
|
||||
let mut c = Canvas::new(20, 20, 0, 0);
|
||||
let mut p = Path::new();
|
||||
p.arc(10.0, 10.0, 8.0, 0.0, std::f64::consts::TAU);
|
||||
p.close();
|
||||
c.fill_path(&p, Color::ON).unwrap();
|
||||
assert_eq!(255, at(&c, 10, 10));
|
||||
assert_eq!(0, at(&c, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_stroke_stays_inside() {
|
||||
let mut c = Canvas::new(20, 20, 0, 0);
|
||||
let mut p = Path::new();
|
||||
p.move_to(2.0, 2.0);
|
||||
p.line_to(18.0, 2.0);
|
||||
p.line_to(18.0, 18.0);
|
||||
p.line_to(2.0, 18.0);
|
||||
p.close();
|
||||
c.inner_stroke_path(
|
||||
&p,
|
||||
StrokeOptions {
|
||||
line_width: 2.0,
|
||||
..Default::default()
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(0, at(&c, 1, 10)); // outside
|
||||
assert_eq!(255, at(&c, 3, 10)); // inside stroke
|
||||
assert_eq!(0, at(&c, 10, 10)); // interior
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! Block Elements | U+2580...U+259F
|
||||
//! <https://en.wikipedia.org/wiki/Block_Elements>
|
||||
//!
|
||||
//! ▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏
|
||||
//! ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟
|
||||
//!
|
||||
//! Port of `sprite/draw/block.zig`.
|
||||
|
||||
use super::common::{
|
||||
Alignment, DrawFn, FIVE_EIGHTHS, Fraction, HALF, HAlign, ONE_EIGHTH, ONE_QUARTER, Quads, Range,
|
||||
SEVEN_EIGHTHS, Shade, THREE_EIGHTHS, THREE_QUARTERS, VAlign, fill,
|
||||
};
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, Color, DrawError, Rect};
|
||||
|
||||
pub const RANGES: &[Range] = &[Range {
|
||||
min: 0x2580,
|
||||
max: 0x259f,
|
||||
draw: draw2580_259f as DrawFn,
|
||||
}];
|
||||
|
||||
pub fn draw2580_259f(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let q = |tl, tr, bl, br| Quads { tl, tr, bl, br };
|
||||
match cp {
|
||||
// '▀' UPPER HALF BLOCK
|
||||
0x2580 => block(metrics, canvas, Alignment::UPPER, 1.0, HALF),
|
||||
// '▁' LOWER ONE EIGHTH BLOCK
|
||||
0x2581 => block(metrics, canvas, Alignment::LOWER, 1.0, ONE_EIGHTH),
|
||||
// '▂' LOWER ONE QUARTER BLOCK
|
||||
0x2582 => block(metrics, canvas, Alignment::LOWER, 1.0, ONE_QUARTER),
|
||||
// '▃' LOWER THREE EIGHTHS BLOCK
|
||||
0x2583 => block(metrics, canvas, Alignment::LOWER, 1.0, THREE_EIGHTHS),
|
||||
// '▄' LOWER HALF BLOCK
|
||||
0x2584 => block(metrics, canvas, Alignment::LOWER, 1.0, HALF),
|
||||
// '▅' LOWER FIVE EIGHTHS BLOCK
|
||||
0x2585 => block(metrics, canvas, Alignment::LOWER, 1.0, FIVE_EIGHTHS),
|
||||
// '▆' LOWER THREE QUARTERS BLOCK
|
||||
0x2586 => block(metrics, canvas, Alignment::LOWER, 1.0, THREE_QUARTERS),
|
||||
// '▇' LOWER SEVEN EIGHTHS BLOCK
|
||||
0x2587 => block(metrics, canvas, Alignment::LOWER, 1.0, SEVEN_EIGHTHS),
|
||||
// '█' FULL BLOCK
|
||||
0x2588 => full_block_shade(metrics, canvas, Shade::ON),
|
||||
// '▉' LEFT SEVEN EIGHTHS BLOCK
|
||||
0x2589 => block(metrics, canvas, Alignment::LEFT, SEVEN_EIGHTHS, 1.0),
|
||||
// '▊' LEFT THREE QUARTERS BLOCK
|
||||
0x258a => block(metrics, canvas, Alignment::LEFT, THREE_QUARTERS, 1.0),
|
||||
// '▋' LEFT FIVE EIGHTHS BLOCK
|
||||
0x258b => block(metrics, canvas, Alignment::LEFT, FIVE_EIGHTHS, 1.0),
|
||||
// '▌' LEFT HALF BLOCK
|
||||
0x258c => block(metrics, canvas, Alignment::LEFT, HALF, 1.0),
|
||||
// '▍' LEFT THREE EIGHTHS BLOCK
|
||||
0x258d => block(metrics, canvas, Alignment::LEFT, THREE_EIGHTHS, 1.0),
|
||||
// '▎' LEFT ONE QUARTER BLOCK
|
||||
0x258e => block(metrics, canvas, Alignment::LEFT, ONE_QUARTER, 1.0),
|
||||
// '▏' LEFT ONE EIGHTH BLOCK
|
||||
0x258f => block(metrics, canvas, Alignment::LEFT, ONE_EIGHTH, 1.0),
|
||||
|
||||
// '▐' RIGHT HALF BLOCK
|
||||
0x2590 => block(metrics, canvas, Alignment::RIGHT, HALF, 1.0),
|
||||
// '░'
|
||||
0x2591 => full_block_shade(metrics, canvas, Shade::LIGHT),
|
||||
// '▒'
|
||||
0x2592 => full_block_shade(metrics, canvas, Shade::MEDIUM),
|
||||
// '▓'
|
||||
0x2593 => full_block_shade(metrics, canvas, Shade::DARK),
|
||||
// '▔' UPPER ONE EIGHTH BLOCK
|
||||
0x2594 => block(metrics, canvas, Alignment::UPPER, 1.0, ONE_EIGHTH),
|
||||
// '▕' RIGHT ONE EIGHTH BLOCK
|
||||
0x2595 => block(metrics, canvas, Alignment::RIGHT, ONE_EIGHTH, 1.0),
|
||||
// '▖'
|
||||
0x2596 => quadrant(metrics, canvas, q(false, false, true, false)),
|
||||
// '▗'
|
||||
0x2597 => quadrant(metrics, canvas, q(false, false, false, true)),
|
||||
// '▘'
|
||||
0x2598 => quadrant(metrics, canvas, q(true, false, false, false)),
|
||||
// '▙'
|
||||
0x2599 => quadrant(metrics, canvas, q(true, false, true, true)),
|
||||
// '▚'
|
||||
0x259a => quadrant(metrics, canvas, q(true, false, false, true)),
|
||||
// '▛'
|
||||
0x259b => quadrant(metrics, canvas, q(true, true, true, false)),
|
||||
// '▜'
|
||||
0x259c => quadrant(metrics, canvas, q(true, true, false, true)),
|
||||
// '▝'
|
||||
0x259d => quadrant(metrics, canvas, q(false, true, false, false)),
|
||||
// '▞'
|
||||
0x259e => quadrant(metrics, canvas, q(false, true, true, false)),
|
||||
// '▟'
|
||||
0x259f => quadrant(metrics, canvas, q(false, true, true, true)),
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn block(
|
||||
metrics: &Metrics,
|
||||
canvas: &mut Canvas,
|
||||
alignment: Alignment,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) {
|
||||
block_shade(metrics, canvas, alignment, width, height, Shade::ON);
|
||||
}
|
||||
|
||||
pub fn block_shade(
|
||||
metrics: &Metrics,
|
||||
canvas: &mut Canvas,
|
||||
alignment: Alignment,
|
||||
width: f64,
|
||||
height: f64,
|
||||
shade: Shade,
|
||||
) {
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
|
||||
let w = (float_width * width).round() as u32;
|
||||
let h = (float_height * height).round() as u32;
|
||||
|
||||
let x = match alignment.horizontal {
|
||||
HAlign::Left => 0,
|
||||
HAlign::Right => metrics.cell_width - w,
|
||||
HAlign::Center => (metrics.cell_width - w) / 2,
|
||||
};
|
||||
let y = match alignment.vertical {
|
||||
VAlign::Top => 0,
|
||||
VAlign::Bottom => metrics.cell_height - h,
|
||||
VAlign::Middle => (metrics.cell_height - h) / 2,
|
||||
};
|
||||
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
width: w as i32,
|
||||
height: h as i32,
|
||||
},
|
||||
Color(shade.0),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn full_block_shade(metrics: &Metrics, canvas: &mut Canvas, shade: Shade) {
|
||||
canvas.box_(
|
||||
0,
|
||||
0,
|
||||
metrics.cell_width as i32,
|
||||
metrics.cell_height as i32,
|
||||
Color(shade.0),
|
||||
);
|
||||
}
|
||||
|
||||
fn quadrant(metrics: &Metrics, canvas: &mut Canvas, quads: Quads) {
|
||||
use Fraction::{Full, Half, Zero};
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Braille Patterns | U+2800...U+28FF
|
||||
//! <https://en.wikipedia.org/wiki/Braille_Patterns>
|
||||
//!
|
||||
//! (6 dot patterns)
|
||||
//! ⠀ ⠁ ⠂ ⠃ ⠄ ⠅ ⠆ ⠇ ⠈ ⠉ ⠊ ⠋ ⠌ ⠍ ⠎ ⠏
|
||||
//! ⠐ ⠑ ⠒ ⠓ ⠔ ⠕ ⠖ ⠗ ⠘ ⠙ ⠚ ⠛ ⠜ ⠝ ⠞ ⠟
|
||||
//! ⠠ ⠡ ⠢ ⠣ ⠤ ⠥ ⠦ ⠧ ⠨ ⠩ ⠪ ⠫ ⠬ ⠭ ⠮ ⠯
|
||||
//! ⠰ ⠱ ⠲ ⠳ ⠴ ⠵ ⠶ ⠷ ⠸ ⠹ ⠺ ⠻ ⠼ ⠽ ⠾ ⠿
|
||||
//!
|
||||
//! (8 dot patterns)
|
||||
//! ⡀ ⡁ ⡂ ⡃ ⡄ ⡅ ⡆ ⡇ ⡈ ⡉ ⡊ ⡋ ⡌ ⡍ ⡎ ⡏
|
||||
//! ⡐ ⡑ ⡒ ⡓ ⡔ ⡕ ⡖ ⡗ ⡘ ⡙ ⡚ ⡛ ⡜ ⡝ ⡞ ⡟
|
||||
//! ⡠ ⡡ ⡢ ⡣ ⡤ ⡥ ⡦ ⡧ ⡨ ⡩ ⡪ ⡫ ⡬ ⡭ ⡮ ⡯
|
||||
//! ⡰ ⡱ ⡲ ⡳ ⡴ ⡵ ⡶ ⡷ ⡸ ⡹ ⡺ ⡻ ⡼ ⡽ ⡾ ⡿
|
||||
//! ⢀ ⢁ ⢂ ⢃ ⢄ ⢅ ⢆ ⢇ ⢈ ⢉ ⢊ ⢋ ⢌ ⢍ ⢎ ⢏
|
||||
//! ⢐ ⢑ ⢒ ⢓ ⢔ ⢕ ⢖ ⢗ ⢘ ⢙ ⢚ ⢛ ⢜ ⢝ ⢞ ⢟
|
||||
//! ⢠ ⢡ ⢢ ⢣ ⢤ ⢥ ⢦ ⢧ ⢨ ⢩ ⢪ ⢫ ⢬ ⢭ ⢮ ⢯
|
||||
//! ⢰ ⢱ ⢲ ⢳ ⢴ ⢵ ⢶ ⢷ ⢸ ⢹ ⢺ ⢻ ⢼ ⢽ ⢾ ⢿
|
||||
//! ⣀ ⣁ ⣂ ⣃ ⣄ ⣅ ⣆ ⣇ ⣈ ⣉ ⣊ ⣋ ⣌ ⣍ ⣎ ⣏
|
||||
//! ⣐ ⣑ ⣒ ⣓ ⣔ ⣕ ⣖ ⣗ ⣘ ⣙ ⣚ ⣛ ⣜ ⣝ ⣞ ⣟
|
||||
//! ⣠ ⣡ ⣢ ⣣ ⣤ ⣥ ⣦ ⣧ ⣨ ⣩ ⣪ ⣫ ⣬ ⣭ ⣮ ⣯
|
||||
//! ⣰ ⣱ ⣲ ⣳ ⣴ ⣵ ⣶ ⣷ ⣸ ⣹ ⣺ ⣻ ⣼ ⣽ ⣾ ⣿
|
||||
//!
|
||||
//! Port of `sprite/draw/braille.zig`.
|
||||
|
||||
use super::common::{DrawFn, Range};
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, Color, DrawError};
|
||||
|
||||
pub const RANGES: &[Range] = &[Range {
|
||||
min: 0x2800,
|
||||
max: 0x28ff,
|
||||
draw: draw2800_28ff as DrawFn,
|
||||
}];
|
||||
|
||||
/// A braille pattern.
|
||||
///
|
||||
/// Mnemonic:
|
||||
/// [t]op - . .
|
||||
/// [u]pper - . .
|
||||
/// [l]ower - . .
|
||||
/// [b]ottom - . .
|
||||
/// | |
|
||||
/// [l]eft, [r]ight
|
||||
///
|
||||
/// Bit layout matches bit patterns of unicode codepoints
|
||||
/// (bit 0 = `tl`, ... bit 7 = `br`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Pattern {
|
||||
tl: bool,
|
||||
ul: bool,
|
||||
ll: bool,
|
||||
tr: bool,
|
||||
ur: bool,
|
||||
lr: bool,
|
||||
bl: bool,
|
||||
br: bool,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
fn from(cp: u32) -> Pattern {
|
||||
let b = cp as u8;
|
||||
Pattern {
|
||||
tl: b & 1 != 0,
|
||||
ul: b & (1 << 1) != 0,
|
||||
ll: b & (1 << 2) != 0,
|
||||
tr: b & (1 << 3) != 0,
|
||||
ur: b & (1 << 4) != 0,
|
||||
lr: b & (1 << 5) != 0,
|
||||
bl: b & (1 << 6) != 0,
|
||||
br: b & (1 << 7) != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw2800_28ff(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let (wi, hi) = (width as i32, height as i32);
|
||||
let mut w: i32 = (width / 4).min(height / 8) as i32;
|
||||
let mut x_spacing: i32 = (width / 4) as i32;
|
||||
let mut y_spacing: i32 = (height / 8) as i32;
|
||||
let mut x_margin: i32 = x_spacing.div_euclid(2);
|
||||
let mut y_margin: i32 = y_spacing.div_euclid(2);
|
||||
|
||||
let mut x_px_left: i32 = wi - 2 * x_margin - x_spacing - 2 * w;
|
||||
|
||||
let mut y_px_left: i32 = hi - 2 * y_margin - 3 * y_spacing - 4 * w;
|
||||
|
||||
// First, try hard to ensure the DOT width is non-zero
|
||||
if x_px_left >= 2 && y_px_left >= 4 && w == 0 {
|
||||
w += 1;
|
||||
x_px_left -= 2;
|
||||
y_px_left -= 4;
|
||||
}
|
||||
|
||||
// Second, prefer a non-zero margin
|
||||
if x_px_left >= 2 && x_margin == 0 {
|
||||
x_margin = 1;
|
||||
x_px_left -= 2;
|
||||
}
|
||||
if y_px_left >= 2 && 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 && y_px_left >= 4 {
|
||||
w += 1;
|
||||
x_px_left -= 2;
|
||||
y_px_left -= 4;
|
||||
}
|
||||
|
||||
debug_assert!(x_px_left <= 1 || y_px_left <= 1);
|
||||
debug_assert!(2 * x_margin + 2 * w + x_spacing <= wi);
|
||||
debug_assert!(2 * y_margin + 4 * w + 3 * y_spacing <= hi);
|
||||
|
||||
let x = [x_margin, x_margin + w + x_spacing];
|
||||
let y0 = y_margin;
|
||||
let y1 = y0 + w + y_spacing;
|
||||
let y2 = y1 + w + y_spacing;
|
||||
let y3 = y2 + w + y_spacing;
|
||||
let y = [y0, y1, y2, y3];
|
||||
|
||||
debug_assert!(cp >= 0x2800);
|
||||
debug_assert!(cp <= 0x28ff);
|
||||
let p = Pattern::from(cp);
|
||||
|
||||
let mut dot = |on: bool, xi: usize, yi: usize| {
|
||||
if on {
|
||||
canvas.box_(x[xi], y[yi], x[xi] + w, y[yi] + w, Color::ON);
|
||||
}
|
||||
};
|
||||
dot(p.tl, 0, 0);
|
||||
dot(p.ul, 0, 1);
|
||||
dot(p.ll, 0, 2);
|
||||
dot(p.bl, 0, 3);
|
||||
dot(p.tr, 1, 0);
|
||||
dot(p.ur, 1, 1);
|
||||
dot(p.lr, 1, 2);
|
||||
dot(p.br, 1, 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pattern_bits_match_codepoint() {
|
||||
let p = Pattern::from(0x2801);
|
||||
assert!(p.tl && !p.ul && !p.br);
|
||||
let p = Pattern::from(0x2880);
|
||||
assert!(p.br && !p.tl);
|
||||
let p = Pattern::from(0x28ff);
|
||||
assert!(p.tl && p.ul && p.ll && p.tr && p.ur && p.lr && p.bl && p.br);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_braille_render_at_common_sizes() {
|
||||
// Exercises the (debug) asserts in the layout algorithm. Like Zig, some
|
||||
// odd sizes (e.g. 14x23) trip the `x_px_left <= 1 || y_px_left <= 1`
|
||||
// assert in debug builds, so only realistic sizes are tested here.
|
||||
let m = Metrics::calc(crate::metrics::FaceMetrics::default());
|
||||
for (w, h) in [
|
||||
(7, 14),
|
||||
(8, 16),
|
||||
(9, 17),
|
||||
(10, 20),
|
||||
(11, 21),
|
||||
(12, 24),
|
||||
(18, 36),
|
||||
(20, 40),
|
||||
] {
|
||||
let mut c = Canvas::new(w, h, w / 4, h / 4);
|
||||
for cp in 0x2800..=0x28ff {
|
||||
draw2800_28ff(cp, &mut c, w, h, &m).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Port of `sprite/draw/branch.zig`.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use super::box_;
|
||||
use super::common::{Corner, DrawFn, Edge, Range, Shade, Thickness, hline_middle, vline_middle};
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, CapMode, Color, DrawError, Path, StrokeOptions};
|
||||
|
||||
pub const RANGES: &[Range] = &[Range {
|
||||
min: 0xf5d0,
|
||||
max: 0xf60d,
|
||||
draw: draw_f5d0_f60d as DrawFn,
|
||||
}];
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct BranchNode {
|
||||
up: bool,
|
||||
right: bool,
|
||||
down: bool,
|
||||
left: bool,
|
||||
filled: bool,
|
||||
}
|
||||
|
||||
pub fn draw_f5d0_f60d(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
0x0f5d0 => {
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5d1 => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5d2 => {
|
||||
fading_line(metrics, canvas, Edge::Right, Thickness::Light);
|
||||
}
|
||||
0x0f5d3 => {
|
||||
fading_line(metrics, canvas, Edge::Left, Thickness::Light);
|
||||
}
|
||||
0x0f5d4 => {
|
||||
fading_line(metrics, canvas, Edge::Bottom, Thickness::Light);
|
||||
}
|
||||
0x0f5d5 => {
|
||||
fading_line(metrics, canvas, Edge::Top, Thickness::Light);
|
||||
}
|
||||
0x0f5d6 => {
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5d7 => {
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5d8 => {
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
}
|
||||
0x0f5d9 => {
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5da => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
}
|
||||
0x0f5db => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5dc => {
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5dd => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5de => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5df => {
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e0 => {
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5e1 => {
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5e2 => {
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e3 => {
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5e4 => {
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
}
|
||||
0x0f5e5 => {
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e6 => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e7 => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e8 => {
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5e9 => {
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5ea => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5eb => {
|
||||
vline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5ec => {
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tl, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Br, Thickness::Light)?;
|
||||
}
|
||||
0x0f5ed => {
|
||||
hline_middle(metrics, canvas, Thickness::Light);
|
||||
box_::arc(metrics, canvas, Corner::Tr, Thickness::Light)?;
|
||||
box_::arc(metrics, canvas, Corner::Bl, Thickness::Light)?;
|
||||
}
|
||||
0x0f5ee => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5ef => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f0 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f1 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f2 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f3 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f4 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f5 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f6 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f7 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f8 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5f9 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5fa => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5fb => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5fc => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5fd => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5fe => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f5ff => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: false,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f600 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f601 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: false,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f602 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f603 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f604 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f605 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: true,
|
||||
left: false,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f606 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f607 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: false,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f608 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f609 => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: false,
|
||||
right: true,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f60a => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f60b => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: false,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f60c => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: true,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
0x0f60d => branch_node(
|
||||
metrics,
|
||||
canvas,
|
||||
BranchNode {
|
||||
up: true,
|
||||
right: true,
|
||||
down: true,
|
||||
left: true,
|
||||
filled: false,
|
||||
},
|
||||
Thickness::Light,
|
||||
),
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn branch_node(metrics: &Metrics, canvas: &mut Canvas, node: BranchNode, thickness: Thickness) {
|
||||
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);
|
||||
|
||||
// Top of horizontal strokes
|
||||
let h_top = metrics.cell_height.saturating_sub(thick_px) / 2;
|
||||
// Bottom of horizontal strokes
|
||||
let h_bottom = h_top.saturating_add(thick_px);
|
||||
// Left of vertical strokes
|
||||
let v_left = metrics.cell_width.saturating_sub(thick_px) / 2;
|
||||
// Right of vertical strokes
|
||||
let v_right = v_left.saturating_add(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.
|
||||
let cx = f64::from(v_left) + float_thick / 2.0;
|
||||
let cy = f64::from(h_top) + float_thick / 2.0;
|
||||
// The radius needs to be the smallest distance from the center to an edge.
|
||||
let r = cx.min(cy).min((float_width - cx).min(float_height - cy));
|
||||
|
||||
// These float->int 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_(
|
||||
v_left as i32,
|
||||
0,
|
||||
v_right as i32,
|
||||
(cy - r + float_thick / 2.0).ceil() as i32,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if node.right {
|
||||
canvas.box_(
|
||||
(cx + r - float_thick / 2.0).floor() as i32,
|
||||
h_top as i32,
|
||||
metrics.cell_width as i32,
|
||||
h_bottom as i32,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if node.down {
|
||||
canvas.box_(
|
||||
v_left as i32,
|
||||
(cy + r - float_thick / 2.0).floor() as i32,
|
||||
v_right as i32,
|
||||
metrics.cell_height as i32,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if node.left {
|
||||
canvas.box_(
|
||||
0,
|
||||
h_top as i32,
|
||||
(cx - r + float_thick / 2.0).ceil() as i32,
|
||||
h_bottom as i32,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
|
||||
let mut path = Path::new();
|
||||
if node.filled {
|
||||
path.arc(cx, cy, r, 0.0, PI * 2.0);
|
||||
path.close();
|
||||
let _ = canvas.fill_path(&path, Shade::ON.color());
|
||||
} else {
|
||||
path.arc(cx, cy, r - float_thick / 2.0, 0.0, PI * 2.0);
|
||||
path.close();
|
||||
let _ = canvas.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_width: float_thick,
|
||||
line_cap_mode: CapMode::Butt,
|
||||
},
|
||||
Shade::ON.color(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn fading_line(metrics: &Metrics, canvas: &mut Canvas, to: Edge, thickness: Thickness) {
|
||||
let thick_px = thickness.height(metrics.box_thickness);
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
|
||||
// Top of horizontal strokes
|
||||
let h_top = metrics.cell_height.saturating_sub(thick_px) / 2;
|
||||
// Bottom of horizontal strokes
|
||||
let h_bottom = h_top.saturating_add(thick_px);
|
||||
// Left of vertical strokes
|
||||
let v_left = metrics.cell_width.saturating_sub(thick_px) / 2;
|
||||
// Right of vertical strokes
|
||||
let v_right = v_left.saturating_add(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).
|
||||
let mut color: f64 = match to {
|
||||
Edge::Top | Edge::Left => 0.0,
|
||||
Edge::Bottom | Edge::Right => 255.0,
|
||||
};
|
||||
let inc: f64 = 255.0
|
||||
/ match to {
|
||||
Edge::Top => float_height,
|
||||
Edge::Bottom => -float_height,
|
||||
Edge::Left => float_width,
|
||||
Edge::Right => -float_width,
|
||||
};
|
||||
|
||||
match to {
|
||||
Edge::Top | Edge::Bottom => {
|
||||
for y in 0..metrics.cell_height {
|
||||
for x in v_left..v_right {
|
||||
canvas.pixel(x as i32, y as i32, Color(color.round() as u8));
|
||||
}
|
||||
color += inc;
|
||||
}
|
||||
}
|
||||
Edge::Left | Edge::Right => {
|
||||
for x in 0..metrics.cell_width {
|
||||
for y in h_top..h_bottom {
|
||||
canvas.pixel(x as i32, y as i32, Color(color.round() as u8));
|
||||
}
|
||||
color += inc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
//! 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. Port of `sprite/draw/common.zig`.
|
||||
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, Color, DrawError};
|
||||
|
||||
/// A function that draws a glyph on the provided canvas.
|
||||
pub type DrawFn = fn(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError>;
|
||||
|
||||
/// A range of codepoints (inclusive) and the function that draws them.
|
||||
/// (Zig discovers these at comptime from `draw<MIN>_<MAX>` function names.)
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Range {
|
||||
pub min: u32,
|
||||
pub max: u32,
|
||||
pub draw: DrawFn,
|
||||
}
|
||||
|
||||
// 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.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Thickness {
|
||||
SuperLight,
|
||||
Light,
|
||||
Heavy,
|
||||
}
|
||||
|
||||
impl Thickness {
|
||||
/// 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, base: u32) -> u32 {
|
||||
match self {
|
||||
Thickness::SuperLight => (base / 2).max(1),
|
||||
Thickness::Light => base,
|
||||
Thickness::Heavy => base * 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shades. Zig's is a non-exhaustive `enum(u8)`, so any alpha value is valid.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Shade(pub u8);
|
||||
|
||||
impl Shade {
|
||||
pub const OFF: Shade = Shade(0x00);
|
||||
pub const LIGHT: Shade = Shade(0x40);
|
||||
pub const MEDIUM: Shade = Shade(0x80);
|
||||
pub const DARK: Shade = Shade(0xc0);
|
||||
pub const ON: Shade = Shade(0xff);
|
||||
|
||||
/// The canvas color with the same alpha value.
|
||||
pub fn color(self) -> Color {
|
||||
Color(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Applicable to any set of glyphs with features
|
||||
/// that may be present or not in each quadrant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Quads {
|
||||
pub tl: bool,
|
||||
pub tr: bool,
|
||||
pub bl: bool,
|
||||
pub br: bool,
|
||||
}
|
||||
|
||||
/// A corner of a cell.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
Tl,
|
||||
Tr,
|
||||
Bl,
|
||||
Br,
|
||||
}
|
||||
|
||||
/// An edge of a cell.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Edge {
|
||||
Top,
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HAlign {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VAlign {
|
||||
Top,
|
||||
Bottom,
|
||||
Middle,
|
||||
}
|
||||
|
||||
/// Alignment of a figure within a cell.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Alignment {
|
||||
pub horizontal: HAlign,
|
||||
pub vertical: VAlign,
|
||||
}
|
||||
|
||||
impl Alignment {
|
||||
pub const CENTER: Alignment = Alignment {
|
||||
horizontal: HAlign::Center,
|
||||
vertical: VAlign::Middle,
|
||||
};
|
||||
pub const UPPER: Alignment = Alignment {
|
||||
horizontal: HAlign::Center,
|
||||
vertical: VAlign::Top,
|
||||
};
|
||||
pub const LOWER: Alignment = Alignment {
|
||||
horizontal: HAlign::Center,
|
||||
vertical: VAlign::Bottom,
|
||||
};
|
||||
pub const LEFT: Alignment = Alignment {
|
||||
horizontal: HAlign::Left,
|
||||
vertical: VAlign::Middle,
|
||||
};
|
||||
pub const RIGHT: Alignment = Alignment {
|
||||
horizontal: HAlign::Right,
|
||||
vertical: VAlign::Middle,
|
||||
};
|
||||
|
||||
pub const UPPER_LEFT: Alignment = Alignment {
|
||||
horizontal: HAlign::Left,
|
||||
vertical: VAlign::Top,
|
||||
};
|
||||
pub const UPPER_RIGHT: Alignment = Alignment {
|
||||
horizontal: HAlign::Right,
|
||||
vertical: VAlign::Top,
|
||||
};
|
||||
pub const LOWER_LEFT: Alignment = Alignment {
|
||||
horizontal: HAlign::Left,
|
||||
vertical: VAlign::Bottom,
|
||||
};
|
||||
pub const LOWER_RIGHT: Alignment = Alignment {
|
||||
horizontal: HAlign::Right,
|
||||
vertical: VAlign::Bottom,
|
||||
};
|
||||
|
||||
pub const UPPER_CENTER: Alignment = Self::UPPER;
|
||||
pub const LOWER_CENTER: Alignment = Self::LOWER;
|
||||
pub const MIDDLE_LEFT: Alignment = Self::LEFT;
|
||||
pub const MIDDLE_RIGHT: Alignment = Self::RIGHT;
|
||||
pub const MIDDLE_CENTER: Alignment = Self::CENTER;
|
||||
|
||||
pub const TOP: Alignment = Self::UPPER;
|
||||
pub const BOTTOM: Alignment = Self::LOWER;
|
||||
pub const CENTER_TOP: Alignment = Self::TOP;
|
||||
pub const CENTER_BOTTOM: Alignment = Self::BOTTOM;
|
||||
|
||||
pub const TOP_LEFT: Alignment = Self::UPPER_LEFT;
|
||||
pub const TOP_RIGHT: Alignment = Self::UPPER_RIGHT;
|
||||
pub const BOTTOM_LEFT: Alignment = Self::LOWER_LEFT;
|
||||
pub const BOTTOM_RIGHT: Alignment = Self::LOWER_RIGHT;
|
||||
}
|
||||
|
||||
impl Default for Alignment {
|
||||
fn default() -> Self {
|
||||
Alignment::CENTER
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Fraction {
|
||||
// Names for the min edge
|
||||
Start,
|
||||
Left,
|
||||
Top,
|
||||
Zero,
|
||||
|
||||
// Names based on eighths
|
||||
Eighth,
|
||||
OneEighth,
|
||||
TwoEighths,
|
||||
ThreeEighths,
|
||||
FourEighths,
|
||||
FiveEighths,
|
||||
SixEighths,
|
||||
SevenEighths,
|
||||
|
||||
// Names based on quarters
|
||||
Quarter,
|
||||
OneQuarter,
|
||||
TwoQuarters,
|
||||
ThreeQuarters,
|
||||
|
||||
// Names based on thirds
|
||||
Third,
|
||||
OneThird,
|
||||
TwoThirds,
|
||||
|
||||
// Names based on halves
|
||||
Half,
|
||||
OneHalf,
|
||||
|
||||
// Alternative names for 1/2
|
||||
Center,
|
||||
Middle,
|
||||
|
||||
// Names for the max edge
|
||||
End,
|
||||
Right,
|
||||
Bottom,
|
||||
One,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl Fraction {
|
||||
/// This can be indexed to get the fraction for `i/8`.
|
||||
pub const EIGHTHS: [Fraction; 9] = [
|
||||
Fraction::Zero,
|
||||
Fraction::OneEighth,
|
||||
Fraction::TwoEighths,
|
||||
Fraction::ThreeEighths,
|
||||
Fraction::FourEighths,
|
||||
Fraction::FiveEighths,
|
||||
Fraction::SixEighths,
|
||||
Fraction::SevenEighths,
|
||||
Fraction::One,
|
||||
];
|
||||
|
||||
/// This can be indexed to get the fraction for `i/4`.
|
||||
pub const QUARTERS: [Fraction; 5] = [
|
||||
Fraction::Zero,
|
||||
Fraction::OneQuarter,
|
||||
Fraction::TwoQuarters,
|
||||
Fraction::ThreeQuarters,
|
||||
Fraction::One,
|
||||
];
|
||||
|
||||
/// This can be indexed to get the fraction for `i/3`.
|
||||
pub const THIRDS: [Fraction; 4] = [
|
||||
Fraction::Zero,
|
||||
Fraction::OneThird,
|
||||
Fraction::TwoThirds,
|
||||
Fraction::One,
|
||||
];
|
||||
|
||||
/// This can be indexed to get the fraction for `i/2`.
|
||||
pub const HALVES: [Fraction; 3] = [Fraction::Zero, Fraction::OneHalf, Fraction::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.
|
||||
pub fn min(self, size: u32) -> i32 {
|
||||
let s = f64::from(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`.
|
||||
(s - ((1.0 - self.fraction()) * s).round()) as i32
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn max(self, size: u32) -> i32 {
|
||||
let s = f64::from(size);
|
||||
// See explanation of why these are different in `min`.
|
||||
(self.fraction() * s).round() as i32
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn float(self, size: u32) -> f64 {
|
||||
self.fraction() * f64::from(size)
|
||||
}
|
||||
|
||||
/// Get a float for the fraction this represents.
|
||||
pub fn fraction(self) -> f64 {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Fraction::*;
|
||||
match self {
|
||||
Start | Left | Top | Zero => 0.0,
|
||||
Eighth | OneEighth => 0.125,
|
||||
Quarter | OneQuarter | TwoEighths => 0.25,
|
||||
Third | OneThird => 1.0 / 3.0,
|
||||
ThreeEighths => 0.375,
|
||||
Half | OneHalf | TwoQuarters | FourEighths | Center | Middle => 0.5,
|
||||
FiveEighths => 0.625,
|
||||
TwoThirds => 2.0 / 3.0,
|
||||
ThreeQuarters | SixEighths => 0.75,
|
||||
SevenEighths => 0.875,
|
||||
End | Right | Bottom | One | Full => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a section of the cell, specified by a
|
||||
/// horizontal and vertical pair of fraction lines.
|
||||
pub fn fill(
|
||||
metrics: &Metrics,
|
||||
canvas: &mut Canvas,
|
||||
x0: Fraction,
|
||||
x1: Fraction,
|
||||
y0: Fraction,
|
||||
y1: Fraction,
|
||||
) {
|
||||
canvas.box_(
|
||||
x0.min(metrics.cell_width),
|
||||
y0.min(metrics.cell_height),
|
||||
x1.max(metrics.cell_width),
|
||||
y1.max(metrics.cell_height),
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
|
||||
/// Centered vertical line of the provided thickness.
|
||||
pub fn vline_middle(metrics: &Metrics, canvas: &mut Canvas, thickness: Thickness) {
|
||||
let thick_px = thickness.height(metrics.box_thickness);
|
||||
vline(
|
||||
canvas,
|
||||
0,
|
||||
metrics.cell_height as i32,
|
||||
(metrics.cell_width.saturating_sub(thick_px) / 2) as i32,
|
||||
thick_px,
|
||||
);
|
||||
}
|
||||
|
||||
/// Centered horizontal line of the provided thickness.
|
||||
pub fn hline_middle(metrics: &Metrics, canvas: &mut Canvas, thickness: Thickness) {
|
||||
let thick_px = thickness.height(metrics.box_thickness);
|
||||
hline(
|
||||
canvas,
|
||||
0,
|
||||
metrics.cell_width as i32,
|
||||
(metrics.cell_height.saturating_sub(thick_px) / 2) as i32,
|
||||
thick_px,
|
||||
);
|
||||
}
|
||||
|
||||
/// Vertical line with the left edge at `x`, between `y1` and `y2`.
|
||||
pub fn vline(canvas: &mut Canvas, y1: i32, y2: i32, x: i32, thickness_px: u32) {
|
||||
canvas.box_(x, y1, x + thickness_px as i32, y2, Color::ON);
|
||||
}
|
||||
|
||||
/// Horizontal line with the top edge at `y`, between `x1` and `x2`.
|
||||
pub fn hline(canvas: &mut Canvas, x1: i32, x2: i32, y: i32, thickness_px: u32) {
|
||||
canvas.box_(x1, y, x2, y + thickness_px as i32, Color::ON);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fraction_min_max_even_out() {
|
||||
// size 7, half: min = 7 - round(3.5) = 3, max = round(3.5) = 4
|
||||
assert_eq!(3, Fraction::Half.min(7));
|
||||
assert_eq!(4, Fraction::Half.max(7));
|
||||
assert_eq!(0, Fraction::Zero.min(7));
|
||||
assert_eq!(7, Fraction::Full.max(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thickness_heights() {
|
||||
assert_eq!(1, Thickness::SuperLight.height(1));
|
||||
assert_eq!(2, Thickness::SuperLight.height(4));
|
||||
assert_eq!(3, Thickness::Light.height(3));
|
||||
assert_eq!(6, Thickness::Heavy.height(3));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Port of `sprite/draw/geometric_shapes.zig`.
|
||||
|
||||
use super::common::{Corner, DrawFn, Range, Shade, Thickness};
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, CapMode, Color, DrawError, Path, StrokeOptions};
|
||||
|
||||
pub const RANGES: &[Range] = &[
|
||||
Range {
|
||||
min: 0x25e2,
|
||||
max: 0x25e5,
|
||||
draw: draw25e2_25e5 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x25f8,
|
||||
max: 0x25fa,
|
||||
draw: draw25f8_25fa as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x25ff,
|
||||
max: 0x25ff,
|
||||
draw: draw25ff as DrawFn,
|
||||
},
|
||||
];
|
||||
|
||||
/// ◢ ◣ ◤ ◥
|
||||
pub fn draw25e2_25e5(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
// ◢
|
||||
0x25e2 => corner_triangle_shade(metrics, canvas, Corner::Br, Shade::ON),
|
||||
// ◣
|
||||
0x25e3 => corner_triangle_shade(metrics, canvas, Corner::Bl, Shade::ON),
|
||||
// ◤
|
||||
0x25e4 => corner_triangle_shade(metrics, canvas, Corner::Tl, Shade::ON),
|
||||
// ◥
|
||||
0x25e5 => corner_triangle_shade(metrics, canvas, Corner::Tr, Shade::ON),
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ◸ ◹ ◺
|
||||
pub fn draw25f8_25fa(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
// ◸
|
||||
0x25f8 => corner_triangle_outline(metrics, canvas, Corner::Tl),
|
||||
// ◹
|
||||
0x25f9 => corner_triangle_outline(metrics, canvas, Corner::Tr),
|
||||
// ◺
|
||||
0x25fa => corner_triangle_outline(metrics, canvas, Corner::Bl),
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ◿
|
||||
pub fn draw25ff(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
corner_triangle_outline(metrics, canvas, Corner::Br)
|
||||
}
|
||||
|
||||
fn corner_path(metrics: &Metrics, corner: Corner) -> Path {
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
|
||||
let (x0, y0, x1, y1, x2, y2) = match corner {
|
||||
Corner::Tl => (0.0, 0.0, 0.0, float_height, float_width, 0.0),
|
||||
Corner::Tr => (0.0, 0.0, float_width, float_height, float_width, 0.0),
|
||||
Corner::Bl => (0.0, 0.0, 0.0, float_height, float_width, float_height),
|
||||
Corner::Br => (
|
||||
0.0,
|
||||
float_height,
|
||||
float_width,
|
||||
float_height,
|
||||
float_width,
|
||||
0.0,
|
||||
),
|
||||
};
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(x0, y0);
|
||||
path.line_to(x1, y1);
|
||||
path.line_to(x2, y2);
|
||||
path.close();
|
||||
path
|
||||
}
|
||||
|
||||
pub fn corner_triangle_shade(
|
||||
metrics: &Metrics,
|
||||
canvas: &mut Canvas,
|
||||
corner: Corner,
|
||||
shade: Shade,
|
||||
) -> Result<(), DrawError> {
|
||||
let path = corner_path(metrics, corner);
|
||||
canvas.fill_path(&path, Color(shade.0))
|
||||
}
|
||||
|
||||
pub fn corner_triangle_outline(
|
||||
metrics: &Metrics,
|
||||
canvas: &mut Canvas,
|
||||
corner: Corner,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_thick = f64::from(Thickness::Light.height(metrics.box_thickness));
|
||||
let path = corner_path(metrics, corner);
|
||||
|
||||
canvas.inner_stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_cap_mode: CapMode::Butt,
|
||||
line_width: float_thick,
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Glyph drawing functions for the sprite font. Port of `font/sprite/draw/*.zig`.
|
||||
//!
|
||||
//! Each submodule exposes `RANGES`, a table of the codepoint ranges (inclusive)
|
||||
//! it can draw. In Zig these are discovered at comptime from function names of
|
||||
//! the form `draw<CP>` / `draw<MIN>_<MAX>`.
|
||||
|
||||
pub mod block;
|
||||
pub mod box_;
|
||||
pub mod braille;
|
||||
pub mod branch;
|
||||
pub mod common;
|
||||
pub mod geometric_shapes;
|
||||
pub mod powerline;
|
||||
pub mod special;
|
||||
pub mod symbols_for_legacy_computing;
|
||||
pub mod symbols_for_legacy_computing_supplement;
|
||||
@@ -0,0 +1,234 @@
|
||||
# This is the list of all the octants for the Symbols for Legacy
|
||||
# Computing block. It is used at comptime to generate the lookup
|
||||
# table for drawing them since we weren't able to discern a
|
||||
# mathematical pattern for them.
|
||||
BLOCK OCTANT-3
|
||||
BLOCK OCTANT-23
|
||||
BLOCK OCTANT-123
|
||||
BLOCK OCTANT-4
|
||||
BLOCK OCTANT-14
|
||||
BLOCK OCTANT-124
|
||||
BLOCK OCTANT-34
|
||||
BLOCK OCTANT-134
|
||||
BLOCK OCTANT-234
|
||||
BLOCK OCTANT-5
|
||||
BLOCK OCTANT-15
|
||||
BLOCK OCTANT-25
|
||||
BLOCK OCTANT-125
|
||||
BLOCK OCTANT-135
|
||||
BLOCK OCTANT-235
|
||||
BLOCK OCTANT-1235
|
||||
BLOCK OCTANT-45
|
||||
BLOCK OCTANT-145
|
||||
BLOCK OCTANT-245
|
||||
BLOCK OCTANT-1245
|
||||
BLOCK OCTANT-345
|
||||
BLOCK OCTANT-1345
|
||||
BLOCK OCTANT-2345
|
||||
BLOCK OCTANT-12345
|
||||
BLOCK OCTANT-6
|
||||
BLOCK OCTANT-16
|
||||
BLOCK OCTANT-26
|
||||
BLOCK OCTANT-126
|
||||
BLOCK OCTANT-36
|
||||
BLOCK OCTANT-136
|
||||
BLOCK OCTANT-236
|
||||
BLOCK OCTANT-1236
|
||||
BLOCK OCTANT-146
|
||||
BLOCK OCTANT-246
|
||||
BLOCK OCTANT-1246
|
||||
BLOCK OCTANT-346
|
||||
BLOCK OCTANT-1346
|
||||
BLOCK OCTANT-2346
|
||||
BLOCK OCTANT-12346
|
||||
BLOCK OCTANT-56
|
||||
BLOCK OCTANT-156
|
||||
BLOCK OCTANT-256
|
||||
BLOCK OCTANT-1256
|
||||
BLOCK OCTANT-356
|
||||
BLOCK OCTANT-1356
|
||||
BLOCK OCTANT-2356
|
||||
BLOCK OCTANT-12356
|
||||
BLOCK OCTANT-456
|
||||
BLOCK OCTANT-1456
|
||||
BLOCK OCTANT-2456
|
||||
BLOCK OCTANT-12456
|
||||
BLOCK OCTANT-3456
|
||||
BLOCK OCTANT-13456
|
||||
BLOCK OCTANT-23456
|
||||
BLOCK OCTANT-17
|
||||
BLOCK OCTANT-27
|
||||
BLOCK OCTANT-127
|
||||
BLOCK OCTANT-37
|
||||
BLOCK OCTANT-137
|
||||
BLOCK OCTANT-237
|
||||
BLOCK OCTANT-1237
|
||||
BLOCK OCTANT-47
|
||||
BLOCK OCTANT-147
|
||||
BLOCK OCTANT-247
|
||||
BLOCK OCTANT-1247
|
||||
BLOCK OCTANT-347
|
||||
BLOCK OCTANT-1347
|
||||
BLOCK OCTANT-2347
|
||||
BLOCK OCTANT-12347
|
||||
BLOCK OCTANT-157
|
||||
BLOCK OCTANT-257
|
||||
BLOCK OCTANT-1257
|
||||
BLOCK OCTANT-357
|
||||
BLOCK OCTANT-2357
|
||||
BLOCK OCTANT-12357
|
||||
BLOCK OCTANT-457
|
||||
BLOCK OCTANT-1457
|
||||
BLOCK OCTANT-12457
|
||||
BLOCK OCTANT-3457
|
||||
BLOCK OCTANT-13457
|
||||
BLOCK OCTANT-23457
|
||||
BLOCK OCTANT-67
|
||||
BLOCK OCTANT-167
|
||||
BLOCK OCTANT-267
|
||||
BLOCK OCTANT-1267
|
||||
BLOCK OCTANT-367
|
||||
BLOCK OCTANT-1367
|
||||
BLOCK OCTANT-2367
|
||||
BLOCK OCTANT-12367
|
||||
BLOCK OCTANT-467
|
||||
BLOCK OCTANT-1467
|
||||
BLOCK OCTANT-2467
|
||||
BLOCK OCTANT-12467
|
||||
BLOCK OCTANT-3467
|
||||
BLOCK OCTANT-13467
|
||||
BLOCK OCTANT-23467
|
||||
BLOCK OCTANT-123467
|
||||
BLOCK OCTANT-567
|
||||
BLOCK OCTANT-1567
|
||||
BLOCK OCTANT-2567
|
||||
BLOCK OCTANT-12567
|
||||
BLOCK OCTANT-3567
|
||||
BLOCK OCTANT-13567
|
||||
BLOCK OCTANT-23567
|
||||
BLOCK OCTANT-123567
|
||||
BLOCK OCTANT-4567
|
||||
BLOCK OCTANT-14567
|
||||
BLOCK OCTANT-24567
|
||||
BLOCK OCTANT-124567
|
||||
BLOCK OCTANT-34567
|
||||
BLOCK OCTANT-134567
|
||||
BLOCK OCTANT-234567
|
||||
BLOCK OCTANT-1234567
|
||||
BLOCK OCTANT-18
|
||||
BLOCK OCTANT-28
|
||||
BLOCK OCTANT-128
|
||||
BLOCK OCTANT-38
|
||||
BLOCK OCTANT-138
|
||||
BLOCK OCTANT-238
|
||||
BLOCK OCTANT-1238
|
||||
BLOCK OCTANT-48
|
||||
BLOCK OCTANT-148
|
||||
BLOCK OCTANT-248
|
||||
BLOCK OCTANT-1248
|
||||
BLOCK OCTANT-348
|
||||
BLOCK OCTANT-1348
|
||||
BLOCK OCTANT-2348
|
||||
BLOCK OCTANT-12348
|
||||
BLOCK OCTANT-58
|
||||
BLOCK OCTANT-158
|
||||
BLOCK OCTANT-258
|
||||
BLOCK OCTANT-1258
|
||||
BLOCK OCTANT-358
|
||||
BLOCK OCTANT-1358
|
||||
BLOCK OCTANT-2358
|
||||
BLOCK OCTANT-12358
|
||||
BLOCK OCTANT-458
|
||||
BLOCK OCTANT-1458
|
||||
BLOCK OCTANT-2458
|
||||
BLOCK OCTANT-12458
|
||||
BLOCK OCTANT-3458
|
||||
BLOCK OCTANT-13458
|
||||
BLOCK OCTANT-23458
|
||||
BLOCK OCTANT-123458
|
||||
BLOCK OCTANT-168
|
||||
BLOCK OCTANT-268
|
||||
BLOCK OCTANT-1268
|
||||
BLOCK OCTANT-368
|
||||
BLOCK OCTANT-2368
|
||||
BLOCK OCTANT-12368
|
||||
BLOCK OCTANT-468
|
||||
BLOCK OCTANT-1468
|
||||
BLOCK OCTANT-12468
|
||||
BLOCK OCTANT-3468
|
||||
BLOCK OCTANT-13468
|
||||
BLOCK OCTANT-23468
|
||||
BLOCK OCTANT-568
|
||||
BLOCK OCTANT-1568
|
||||
BLOCK OCTANT-2568
|
||||
BLOCK OCTANT-12568
|
||||
BLOCK OCTANT-3568
|
||||
BLOCK OCTANT-13568
|
||||
BLOCK OCTANT-23568
|
||||
BLOCK OCTANT-123568
|
||||
BLOCK OCTANT-4568
|
||||
BLOCK OCTANT-14568
|
||||
BLOCK OCTANT-24568
|
||||
BLOCK OCTANT-124568
|
||||
BLOCK OCTANT-34568
|
||||
BLOCK OCTANT-134568
|
||||
BLOCK OCTANT-234568
|
||||
BLOCK OCTANT-1234568
|
||||
BLOCK OCTANT-178
|
||||
BLOCK OCTANT-278
|
||||
BLOCK OCTANT-1278
|
||||
BLOCK OCTANT-378
|
||||
BLOCK OCTANT-1378
|
||||
BLOCK OCTANT-2378
|
||||
BLOCK OCTANT-12378
|
||||
BLOCK OCTANT-478
|
||||
BLOCK OCTANT-1478
|
||||
BLOCK OCTANT-2478
|
||||
BLOCK OCTANT-12478
|
||||
BLOCK OCTANT-3478
|
||||
BLOCK OCTANT-13478
|
||||
BLOCK OCTANT-23478
|
||||
BLOCK OCTANT-123478
|
||||
BLOCK OCTANT-578
|
||||
BLOCK OCTANT-1578
|
||||
BLOCK OCTANT-2578
|
||||
BLOCK OCTANT-12578
|
||||
BLOCK OCTANT-3578
|
||||
BLOCK OCTANT-13578
|
||||
BLOCK OCTANT-23578
|
||||
BLOCK OCTANT-123578
|
||||
BLOCK OCTANT-4578
|
||||
BLOCK OCTANT-14578
|
||||
BLOCK OCTANT-24578
|
||||
BLOCK OCTANT-124578
|
||||
BLOCK OCTANT-34578
|
||||
BLOCK OCTANT-134578
|
||||
BLOCK OCTANT-234578
|
||||
BLOCK OCTANT-1234578
|
||||
BLOCK OCTANT-678
|
||||
BLOCK OCTANT-1678
|
||||
BLOCK OCTANT-2678
|
||||
BLOCK OCTANT-12678
|
||||
BLOCK OCTANT-3678
|
||||
BLOCK OCTANT-13678
|
||||
BLOCK OCTANT-23678
|
||||
BLOCK OCTANT-123678
|
||||
BLOCK OCTANT-4678
|
||||
BLOCK OCTANT-14678
|
||||
BLOCK OCTANT-24678
|
||||
BLOCK OCTANT-124678
|
||||
BLOCK OCTANT-34678
|
||||
BLOCK OCTANT-134678
|
||||
BLOCK OCTANT-234678
|
||||
BLOCK OCTANT-1234678
|
||||
BLOCK OCTANT-15678
|
||||
BLOCK OCTANT-25678
|
||||
BLOCK OCTANT-125678
|
||||
BLOCK OCTANT-35678
|
||||
BLOCK OCTANT-235678
|
||||
BLOCK OCTANT-1235678
|
||||
BLOCK OCTANT-45678
|
||||
BLOCK OCTANT-145678
|
||||
BLOCK OCTANT-1245678
|
||||
BLOCK OCTANT-1345678
|
||||
BLOCK OCTANT-2345678
|
||||
@@ -0,0 +1,496 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Port of `sprite/draw/powerline.zig`.
|
||||
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use super::box_;
|
||||
use super::common::{DrawFn, Range, Thickness};
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{
|
||||
Canvas, CapMode, Color, DrawError, Path, Point, StrokeOptions, Triangle,
|
||||
};
|
||||
|
||||
pub const RANGES: &[Range] = &[
|
||||
Range {
|
||||
min: 0xe0b0,
|
||||
max: 0xe0b0,
|
||||
draw: drawe0b0 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b1,
|
||||
max: 0xe0b1,
|
||||
draw: drawe0b1 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b2,
|
||||
max: 0xe0b2,
|
||||
draw: drawe0b2 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b3,
|
||||
max: 0xe0b3,
|
||||
draw: drawe0b3 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b4,
|
||||
max: 0xe0b4,
|
||||
draw: drawe0b4 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b5,
|
||||
max: 0xe0b5,
|
||||
draw: drawe0b5 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b6,
|
||||
max: 0xe0b6,
|
||||
draw: drawe0b6 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b7,
|
||||
max: 0xe0b7,
|
||||
draw: drawe0b7 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b8,
|
||||
max: 0xe0b8,
|
||||
draw: drawe0b8 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0b9,
|
||||
max: 0xe0b9,
|
||||
draw: drawe0b9 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0ba,
|
||||
max: 0xe0ba,
|
||||
draw: drawe0ba as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0bb,
|
||||
max: 0xe0bb,
|
||||
draw: drawe0bb as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0bc,
|
||||
max: 0xe0bc,
|
||||
draw: drawe0bc as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0bd,
|
||||
max: 0xe0bd,
|
||||
draw: drawe0bd as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0be,
|
||||
max: 0xe0be,
|
||||
draw: drawe0be as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0bf,
|
||||
max: 0xe0bf,
|
||||
draw: drawe0bf as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0d2,
|
||||
max: 0xe0d2,
|
||||
draw: drawe0d2 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0xe0d4,
|
||||
max: 0xe0d4,
|
||||
draw: drawe0d4 as DrawFn,
|
||||
},
|
||||
];
|
||||
|
||||
/// E0B0
|
||||
pub fn drawe0b0(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point { x: 0.0, y: 0.0 },
|
||||
p1: Point {
|
||||
x: float_width,
|
||||
y: float_height / 2.0,
|
||||
},
|
||||
p2: Point {
|
||||
x: 0.0,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0B2
|
||||
pub fn drawe0b2(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point {
|
||||
x: float_width,
|
||||
y: 0.0,
|
||||
},
|
||||
p1: Point {
|
||||
x: 0.0,
|
||||
y: float_height / 2.0,
|
||||
},
|
||||
p2: Point {
|
||||
x: float_width,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0B8
|
||||
pub fn drawe0b8(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point { x: 0.0, y: 0.0 },
|
||||
p1: Point {
|
||||
x: float_width,
|
||||
y: float_height,
|
||||
},
|
||||
p2: Point {
|
||||
x: 0.0,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0BA
|
||||
pub fn drawe0ba(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point {
|
||||
x: float_width,
|
||||
y: 0.0,
|
||||
},
|
||||
p1: Point {
|
||||
x: float_width,
|
||||
y: float_height,
|
||||
},
|
||||
p2: Point {
|
||||
x: 0.0,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0BC
|
||||
pub fn drawe0bc(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point { x: 0.0, y: 0.0 },
|
||||
p1: Point {
|
||||
x: float_width,
|
||||
y: 0.0,
|
||||
},
|
||||
p2: Point {
|
||||
x: 0.0,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0BE
|
||||
pub fn drawe0be(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
canvas.triangle(
|
||||
Triangle {
|
||||
p0: Point { x: 0.0, y: 0.0 },
|
||||
p1: Point {
|
||||
x: float_width,
|
||||
y: 0.0,
|
||||
},
|
||||
p2: Point {
|
||||
x: float_width,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0B9
|
||||
pub fn drawe0b9(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::light_diagonal_upper_left_to_lower_right(metrics, canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0BB
|
||||
pub fn drawe0bb(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::light_diagonal_upper_right_to_lower_left(metrics, canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0BD
|
||||
pub fn drawe0bd(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::light_diagonal_upper_right_to_lower_left(metrics, canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0BF
|
||||
pub fn drawe0bf(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::light_diagonal_upper_left_to_lower_right(metrics, canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0B1
|
||||
pub fn drawe0b1(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, 0.0);
|
||||
path.line_to(float_width, float_height / 2.0);
|
||||
path.line_to(0.0, float_height);
|
||||
|
||||
canvas.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_cap_mode: CapMode::Butt,
|
||||
line_width: f64::from(Thickness::Light.height(metrics.box_thickness)),
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0B3
|
||||
pub fn drawe0b3(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
drawe0b1(cp, canvas, width, height, metrics)?;
|
||||
canvas.flip_horizontal();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Path for the rounded "half circle" shapes E0B4 / E0B5 (open path; E0B4
|
||||
/// closes it itself).
|
||||
fn rounded_path(width: u32, height: u32) -> Path {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
|
||||
// Coefficient for approximating a circular arc.
|
||||
let c: f64 = (SQRT_2 - 1.0) * 4.0 / 3.0;
|
||||
|
||||
let radius: f64 = float_width.min(float_height / 2.0);
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, 0.0);
|
||||
path.curve_to(radius * c, 0.0, radius, radius - radius * c, radius, radius);
|
||||
path.line_to(radius, float_height - radius);
|
||||
path.curve_to(
|
||||
radius,
|
||||
float_height - radius + radius * c,
|
||||
radius * c,
|
||||
float_height,
|
||||
0.0,
|
||||
float_height,
|
||||
);
|
||||
path
|
||||
}
|
||||
|
||||
/// E0B4
|
||||
pub fn drawe0b4(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let mut path = rounded_path(width, height);
|
||||
path.close();
|
||||
canvas.fill_path(&path, Color::ON)
|
||||
}
|
||||
|
||||
/// E0B5
|
||||
pub fn drawe0b5(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let path = rounded_path(width, height);
|
||||
canvas.inner_stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_width: f64::from(metrics.box_thickness),
|
||||
line_cap_mode: CapMode::Butt,
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
/// E0B6
|
||||
pub fn drawe0b6(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
drawe0b4(cp, canvas, width, height, metrics)?;
|
||||
canvas.flip_horizontal();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0B7
|
||||
pub fn drawe0b7(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
drawe0b5(cp, canvas, width, height, metrics)?;
|
||||
canvas.flip_horizontal();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0D2
|
||||
pub fn drawe0d2(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
let float_thick = f64::from(metrics.box_thickness);
|
||||
|
||||
// Top piece
|
||||
{
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, 0.0);
|
||||
path.line_to(float_width, 0.0);
|
||||
path.line_to(float_width / 2.0, float_height / 2.0 - float_thick / 2.0);
|
||||
path.line_to(0.0, float_height / 2.0 - float_thick / 2.0);
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, Color::ON)?;
|
||||
}
|
||||
|
||||
// Bottom piece
|
||||
{
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, float_height);
|
||||
path.line_to(float_width, float_height);
|
||||
path.line_to(float_width / 2.0, float_height / 2.0 + float_thick / 2.0);
|
||||
path.line_to(0.0, float_height / 2.0 + float_thick / 2.0);
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, Color::ON)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// E0D4
|
||||
pub fn drawe0d4(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
drawe0d2(cp, canvas, width, height, metrics)?;
|
||||
canvas.flip_horizontal();
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
//! This file contains glyph drawing functions for all of the
|
||||
//! non-Unicode sprite glyphs, such as cursors and underlines.
|
||||
//! Port of `sprite/draw/special.zig`.
|
||||
//!
|
||||
//! The function names match the variants of [`Sprite`](crate::sprite::Sprite).
|
||||
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, PI, TAU};
|
||||
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, CapMode, Color, DrawError, Path, Rect, StrokeOptions};
|
||||
|
||||
pub fn underline(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// We can go beyond the height of the cell a bit, but
|
||||
// we want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell below the cell
|
||||
// height.
|
||||
let y = metrics.underline_position.min(
|
||||
height
|
||||
.saturating_add(canvas.padding_y)
|
||||
.saturating_sub(metrics.underline_thickness),
|
||||
);
|
||||
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: y as i32,
|
||||
width: width as i32,
|
||||
height: metrics.underline_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn underline_double(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// We can go beyond the height of the cell a bit, but
|
||||
// we want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell below the cell
|
||||
// height.
|
||||
let y = metrics.underline_position.min(
|
||||
height
|
||||
.saturating_add(canvas.padding_y)
|
||||
.saturating_sub(2 * metrics.underline_thickness),
|
||||
);
|
||||
|
||||
// We place one underline above the underline position, and one below
|
||||
// by one thickness, creating a "negative" underline where the single
|
||||
// underline would be placed.
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: y.saturating_sub(metrics.underline_thickness) as i32,
|
||||
width: width as i32,
|
||||
height: metrics.underline_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: y.saturating_add(metrics.underline_thickness) as i32,
|
||||
width: width as i32,
|
||||
height: metrics.underline_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn underline_dotted(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
let float_pos = f64::from(metrics.underline_position);
|
||||
let float_thick = f64::from(metrics.underline_thickness);
|
||||
|
||||
// The diameter will be sqrt2 * the usual underline thickness
|
||||
// since otherwise dotted underlines look somewhat anemic.
|
||||
let radius = FRAC_1_SQRT_2 * float_thick;
|
||||
|
||||
// We can go beyond the height of the cell a bit, but
|
||||
// we want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell below the cell
|
||||
// height.
|
||||
let padding = f64::from(canvas.padding_y);
|
||||
let y = (
|
||||
// The center of the underline stem.
|
||||
float_pos + 0.5 * float_thick
|
||||
)
|
||||
.min(
|
||||
// The lowest we can go on the canvas and not get clipped.
|
||||
float_height + padding - radius.ceil(),
|
||||
);
|
||||
|
||||
let dot_count = (
|
||||
// We should try to have enough dots that the
|
||||
// space between them matches their diameter.
|
||||
(float_width / (4.0 * radius))
|
||||
.ceil()
|
||||
// And not enough that the space between
|
||||
// each dot is less than their radius.
|
||||
.min((float_width / (3.0 * radius)).floor())
|
||||
// And definitely not enough that the space
|
||||
// between them is less than a single pixel.
|
||||
.min((float_width / (2.0 * radius + 1.0)).floor())
|
||||
)
|
||||
// And we must have at least one dot per cell.
|
||||
.max(1.0);
|
||||
|
||||
// What we essentially do is divide the cell in to
|
||||
// dot_count areas with a dot centered in each one.
|
||||
let mut path = Path::new();
|
||||
let mut x = (float_width / dot_count) / 2.0;
|
||||
for _ in 0..(dot_count as usize) {
|
||||
path.arc(x, y, radius, 0.0, TAU);
|
||||
path.close();
|
||||
x += float_width / dot_count;
|
||||
}
|
||||
|
||||
canvas.fill_path(&path, Color::ON)
|
||||
}
|
||||
|
||||
pub fn underline_dashed(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// We can go beyond the height of the cell a bit, but
|
||||
// we want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell below the cell
|
||||
// height.
|
||||
let y = metrics.underline_position.min(
|
||||
height
|
||||
.saturating_add(canvas.padding_y)
|
||||
.saturating_sub(metrics.underline_thickness),
|
||||
);
|
||||
|
||||
let dash_width = width / 3 + 1;
|
||||
let dash_count = (width / dash_width) + 1;
|
||||
let mut i = 0;
|
||||
while i < dash_count {
|
||||
let x = i * dash_width;
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
width: dash_width as i32,
|
||||
height: metrics.underline_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
i += 2;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn underline_curly(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let float_width = f64::from(width);
|
||||
let float_height = f64::from(height);
|
||||
let float_pos = f64::from(metrics.underline_position);
|
||||
|
||||
// Because of we way we draw the undercurl, we end up making it around 1px
|
||||
// thicker than it should be, to fix this we just reduce the thickness by 1.
|
||||
//
|
||||
// We use a minimum thickness of 0.414 because this empirically produces
|
||||
// the nicest undercurls at 1px underline thickness; thinner tends to look
|
||||
// too thin compared to straight underlines and has artefacting.
|
||||
let line_width = f64::from(metrics.underline_thickness);
|
||||
|
||||
// Empirically this looks good.
|
||||
let amplitude = float_width / PI;
|
||||
|
||||
// Make sure we don't exceed the drawable area. This can still be outside
|
||||
// of the cell by some amount (one quarter of the height), but we don't
|
||||
// want underlines to disappear for fonts with bad metadata or when users
|
||||
// set their underline position way too low.
|
||||
let padding = f64::from(canvas.padding_y);
|
||||
let top = float_pos.min(
|
||||
// The lowest we can draw this and not get clipped.
|
||||
float_height + padding - amplitude - line_width,
|
||||
);
|
||||
let bottom = top + amplitude;
|
||||
|
||||
// Curvature multiplier.
|
||||
// To my eye, 0.4 creates a nice smooth wiggle.
|
||||
let r = 0.4;
|
||||
|
||||
let center = 0.5 * float_width;
|
||||
|
||||
// We create a single cycle of a wave that peaks at the center of the cell.
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, bottom);
|
||||
path.curve_to(center * r, bottom, center - center * r, top, center, top);
|
||||
path.curve_to(
|
||||
center + center * r,
|
||||
top,
|
||||
float_width - center * r,
|
||||
bottom,
|
||||
float_width,
|
||||
bottom,
|
||||
);
|
||||
canvas.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_width,
|
||||
// Rounded caps, adjacent underlines will have these overlap and so not be
|
||||
// visible, but it makes the ends look cleaner.
|
||||
line_cap_mode: CapMode::Round,
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn strikethrough(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: metrics.strikethrough_position as i32,
|
||||
width: width as i32,
|
||||
height: metrics.strikethrough_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn overline(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// We can go beyond the top of the cell a bit, but we
|
||||
// want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell above the top
|
||||
// of the cell.
|
||||
let y = metrics.overline_position.max(-(canvas.padding_y as i32));
|
||||
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y,
|
||||
width: width as i32,
|
||||
height: metrics.overline_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_rect(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_hollow_rect(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// 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(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: metrics.cursor_thickness as i32,
|
||||
y: metrics.cursor_thickness as i32,
|
||||
width: width.saturating_sub(metrics.cursor_thickness * 2) as i32,
|
||||
height: height.saturating_sub(metrics.cursor_thickness * 2) as i32,
|
||||
},
|
||||
Color::OFF,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_bar(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// 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(
|
||||
Rect {
|
||||
x: -(metrics.cursor_thickness.div_ceil(2) as i32),
|
||||
y: 0,
|
||||
width: metrics.cursor_thickness as i32,
|
||||
height: height as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_underline(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// We can go beyond the height of the cell a bit, but
|
||||
// we want to be sure never to exceed the height of the
|
||||
// canvas, which extends a quarter cell below the cell
|
||||
// height.
|
||||
let y = metrics.underline_position.min(
|
||||
height
|
||||
.saturating_add(canvas.padding_y)
|
||||
.saturating_sub(metrics.underline_thickness),
|
||||
);
|
||||
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: y as i32,
|
||||
width: width as i32,
|
||||
height: metrics.cursor_thickness as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,1074 @@
|
||||
//! Symbols for Legacy Computing | U+1FB00...U+1FBFF
|
||||
//! <https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing>
|
||||
//!
|
||||
//! Port of `sprite/draw/symbols_for_legacy_computing.zig`.
|
||||
|
||||
use super::block;
|
||||
use super::box_;
|
||||
use super::common::{
|
||||
Alignment, Corner, DrawFn, Edge, FIVE_EIGHTHS, Fraction, HALF, HAlign, ONE_EIGHTH, ONE_QUARTER,
|
||||
ONE_THIRD, Quads, Range, SEVEN_EIGHTHS, Shade, THREE_EIGHTHS, THREE_QUARTERS, TWO_THIRDS,
|
||||
Thickness, VAlign, fill,
|
||||
};
|
||||
use super::geometric_shapes as geo;
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, Color, DrawError, Line, Path, Point, Rect, StrokeOptions};
|
||||
|
||||
const _: (f64, f64) = (ONE_THIRD, TWO_THIRDS);
|
||||
|
||||
pub const RANGES: &[Range] = &[
|
||||
Range {
|
||||
min: 0x1FB00,
|
||||
max: 0x1FB3B,
|
||||
draw: draw1fb00_1fb3b as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB3C,
|
||||
max: 0x1FB67,
|
||||
draw: draw1fb3c_1fb67 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB68,
|
||||
max: 0x1FB6F,
|
||||
draw: draw1fb68_1fb6f as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB70,
|
||||
max: 0x1FB75,
|
||||
draw: draw1fb70_1fb75 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB76,
|
||||
max: 0x1FB7B,
|
||||
draw: draw1fb76_1fb7b as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB7C,
|
||||
max: 0x1FB97,
|
||||
draw: draw1fb7c_1fb97 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB98,
|
||||
max: 0x1FB98,
|
||||
draw: draw1fb98 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB99,
|
||||
max: 0x1FB99,
|
||||
draw: draw1fb99 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FB9A,
|
||||
max: 0x1FB9F,
|
||||
draw: draw1fb9a_1fb9f as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBA0,
|
||||
max: 0x1FBAE,
|
||||
draw: draw1fba0_1fbae as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBAF,
|
||||
max: 0x1FBAF,
|
||||
draw: draw1fbaf as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBBD,
|
||||
max: 0x1FBBD,
|
||||
draw: draw1fbbd as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBBE,
|
||||
max: 0x1FBBE,
|
||||
draw: draw1fbbe as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBBF,
|
||||
max: 0x1FBBF,
|
||||
draw: draw1fbbf as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBCE,
|
||||
max: 0x1FBCE,
|
||||
draw: draw1fbce as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBCF,
|
||||
max: 0x1FBCF,
|
||||
draw: draw1fbcf as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBD0,
|
||||
max: 0x1FBDF,
|
||||
draw: draw1fbd0_1fbdf as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1FBE0,
|
||||
max: 0x1FBEF,
|
||||
draw: draw1fbe0_1fbef as DrawFn,
|
||||
},
|
||||
];
|
||||
|
||||
/// Set the clip so we don't include anything outside of the cell.
|
||||
fn clip_to_cell(canvas: &mut Canvas) {
|
||||
canvas.clip_left = canvas.padding_x;
|
||||
canvas.clip_right = canvas.padding_x;
|
||||
canvas.clip_top = canvas.padding_y;
|
||||
canvas.clip_bottom = canvas.padding_y;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct SmoothMosaic {
|
||||
tl: bool,
|
||||
ul: bool,
|
||||
ll: bool,
|
||||
bl: bool,
|
||||
bc: bool,
|
||||
br: bool,
|
||||
lr: bool,
|
||||
ur: bool,
|
||||
tr: bool,
|
||||
tc: bool,
|
||||
}
|
||||
|
||||
impl SmoothMosaic {
|
||||
/// `rows` are the four 3-character rows of the pattern; this mirrors
|
||||
/// indexing the Zig 15-byte `"...\n...\n...\n..."` pattern.
|
||||
fn from(rows: (&str, &str, &str, &str)) -> SmoothMosaic {
|
||||
let mut p = [b' '; 15];
|
||||
for (i, r) in [rows.0, rows.1, rows.2, rows.3].iter().enumerate() {
|
||||
p[i * 4..i * 4 + 3].copy_from_slice(r.as_bytes());
|
||||
if i < 3 {
|
||||
p[i * 4 + 3] = b'\n';
|
||||
}
|
||||
}
|
||||
let h = |i: usize| p[i] == b'#';
|
||||
SmoothMosaic {
|
||||
tl: h(0),
|
||||
ul: h(4) && (!h(0) || !h(8)),
|
||||
ll: h(8) && (!h(4) || !h(12)),
|
||||
bl: h(12),
|
||||
bc: h(13) && (!h(12) || !h(14)),
|
||||
br: h(14),
|
||||
lr: h(10) && (!h(14) || !h(6)),
|
||||
ur: h(6) && (!h(10) || !h(2)),
|
||||
tr: h(2),
|
||||
tc: h(1) && (!h(2) || !h(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sextants
|
||||
pub fn draw1fb00_1fb3b(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
use Fraction::{End, Full, Half, OneThird, TwoThirds, Zero};
|
||||
assert!((0x1fb00..=0x1fb3b).contains(&cp));
|
||||
let idx = cp - 0x1fb00;
|
||||
// Packed struct(u6): tl, tr, ml, mr, bl, br from the low bit up.
|
||||
let sex = idx + (idx / 0x14) + 1;
|
||||
let bit = |n: u32| sex & (1 << n) != 0;
|
||||
if bit(0) {
|
||||
fill(metrics, canvas, Zero, Half, Zero, OneThird);
|
||||
}
|
||||
if bit(1) {
|
||||
fill(metrics, canvas, Half, Full, Zero, OneThird);
|
||||
}
|
||||
if bit(2) {
|
||||
fill(metrics, canvas, Zero, Half, OneThird, TwoThirds);
|
||||
}
|
||||
if bit(3) {
|
||||
fill(metrics, canvas, Half, Full, OneThird, TwoThirds);
|
||||
}
|
||||
if bit(4) {
|
||||
fill(metrics, canvas, Zero, Half, TwoThirds, End);
|
||||
}
|
||||
if bit(5) {
|
||||
fill(metrics, canvas, Half, Full, TwoThirds, End);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Smooth Mosaics
|
||||
pub fn draw1fb3c_1fb67(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// Hand written lookup table for these shapes since I couldn't
|
||||
// determine any sort of mathematical pattern in the codepoints.
|
||||
let pattern = match cp {
|
||||
0x1fb3c => ("...", "...", "#..", "##."),
|
||||
0x1fb3d => ("...", "...", "#\\.", "###"),
|
||||
0x1fb3e => ("...", "#..", "#\\.", "##."),
|
||||
0x1fb3f => ("...", "#..", "##.", "###"),
|
||||
0x1fb40 => ("#..", "#..", "##.", "##."),
|
||||
0x1fb41 => ("/##", "###", "###", "###"),
|
||||
0x1fb42 => ("./#", "###", "###", "###"),
|
||||
0x1fb43 => (".##", ".##", "###", "###"),
|
||||
0x1fb44 => ("..#", ".##", "###", "###"),
|
||||
0x1fb45 => (".##", ".##", ".##", "###"),
|
||||
0x1fb46 => ("...", "./#", "###", "###"),
|
||||
0x1fb47 => ("...", "...", "..#", ".##"),
|
||||
0x1fb48 => ("...", "...", "./#", "###"),
|
||||
0x1fb49 => ("...", "..#", "./#", ".##"),
|
||||
0x1fb4a => ("...", "..#", ".##", "###"),
|
||||
0x1fb4b => ("..#", "..#", ".##", ".##"),
|
||||
0x1fb4c => ("##\\", "###", "###", "###"),
|
||||
0x1fb4d => ("#\\.", "###", "###", "###"),
|
||||
0x1fb4e => ("##.", "##.", "###", "###"),
|
||||
0x1fb4f => ("#..", "##.", "###", "###"),
|
||||
0x1fb50 => ("##.", "##.", "##.", "###"),
|
||||
0x1fb51 => ("...", "#\\.", "###", "###"),
|
||||
0x1fb52 => ("###", "###", "###", "\\##"),
|
||||
0x1fb53 => ("###", "###", "###", ".\\#"),
|
||||
0x1fb54 => ("###", "###", ".##", ".##"),
|
||||
0x1fb55 => ("###", "###", ".##", "..#"),
|
||||
0x1fb56 => ("###", ".##", ".##", ".##"),
|
||||
0x1fb57 => ("##.", "#..", "...", "..."),
|
||||
0x1fb58 => ("###", "#/.", "...", "..."),
|
||||
0x1fb59 => ("##.", "#/.", "#..", "..."),
|
||||
0x1fb5a => ("###", "##.", "#..", "..."),
|
||||
0x1fb5b => ("##.", "##.", "#..", "#.."),
|
||||
0x1fb5c => ("###", "###", "#/.", "..."),
|
||||
0x1fb5d => ("###", "###", "###", "##/"),
|
||||
0x1fb5e => ("###", "###", "###", "#/."),
|
||||
0x1fb5f => ("###", "###", "##.", "##."),
|
||||
0x1fb60 => ("###", "###", "##.", "#.."),
|
||||
0x1fb61 => ("###", "##.", "##.", "##."),
|
||||
0x1fb62 => (".##", "..#", "...", "..."),
|
||||
0x1fb63 => ("###", ".\\#", "...", "..."),
|
||||
0x1fb64 => (".##", ".\\#", "..#", "..."),
|
||||
0x1fb65 => ("###", ".##", "..#", "..."),
|
||||
0x1fb66 => (".##", ".##", "..#", "..#"),
|
||||
0x1fb67 => ("###", "###", ".\\#", "..."),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mosaic = SmoothMosaic::from(pattern);
|
||||
|
||||
let top = 0.0;
|
||||
let upper = Fraction::OneThird.float(metrics.cell_height);
|
||||
let lower = Fraction::TwoThirds.float(metrics.cell_height);
|
||||
let bottom = f64::from(metrics.cell_height);
|
||||
let left = 0.0;
|
||||
let center = Fraction::Half.float(metrics.cell_width);
|
||||
let right = f64::from(metrics.cell_width);
|
||||
|
||||
let mut path = Path::new();
|
||||
if mosaic.tl {
|
||||
path.line_to(left, top);
|
||||
}
|
||||
if mosaic.ul {
|
||||
path.line_to(left, upper);
|
||||
}
|
||||
if mosaic.ll {
|
||||
path.line_to(left, lower);
|
||||
}
|
||||
if mosaic.bl {
|
||||
path.line_to(left, bottom);
|
||||
}
|
||||
if mosaic.bc {
|
||||
path.line_to(center, bottom);
|
||||
}
|
||||
if mosaic.br {
|
||||
path.line_to(right, bottom);
|
||||
}
|
||||
if mosaic.lr {
|
||||
path.line_to(right, lower);
|
||||
}
|
||||
if mosaic.ur {
|
||||
path.line_to(right, upper);
|
||||
}
|
||||
if mosaic.tr {
|
||||
path.line_to(right, top);
|
||||
}
|
||||
if mosaic.tc {
|
||||
path.line_to(center, top);
|
||||
}
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, Color::ON)
|
||||
}
|
||||
|
||||
pub fn draw1fb68_1fb6f(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
// '🭨'
|
||||
0x1fb68 => {
|
||||
edge_triangle(metrics, canvas, Edge::Left)?;
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
}
|
||||
// '🭩'
|
||||
0x1fb69 => {
|
||||
edge_triangle(metrics, canvas, Edge::Top)?;
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
}
|
||||
// '🭪'
|
||||
0x1fb6a => {
|
||||
edge_triangle(metrics, canvas, Edge::Right)?;
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
}
|
||||
// '🭫'
|
||||
0x1fb6b => {
|
||||
edge_triangle(metrics, canvas, Edge::Bottom)?;
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
}
|
||||
// '🭬'
|
||||
0x1fb6c => edge_triangle(metrics, canvas, Edge::Left)?,
|
||||
// '🭭'
|
||||
0x1fb6d => edge_triangle(metrics, canvas, Edge::Top)?,
|
||||
// '🭮'
|
||||
0x1fb6e => edge_triangle(metrics, canvas, Edge::Right)?,
|
||||
// '🭯'
|
||||
0x1fb6f => edge_triangle(metrics, canvas, Edge::Bottom)?,
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Vertical one eighth blocks
|
||||
pub fn draw1fb70_1fb75(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let n = (cp + 1 - 0x1fb70) as usize;
|
||||
|
||||
fill(
|
||||
metrics,
|
||||
canvas,
|
||||
Fraction::EIGHTHS[n],
|
||||
Fraction::EIGHTHS[n + 1],
|
||||
Fraction::Top,
|
||||
Fraction::Bottom,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Horizontal one eighth blocks
|
||||
pub fn draw1fb76_1fb7b(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let n = (cp + 1 - 0x1fb76) as usize;
|
||||
|
||||
fill(
|
||||
metrics,
|
||||
canvas,
|
||||
Fraction::Left,
|
||||
Fraction::Right,
|
||||
Fraction::EIGHTHS[n],
|
||||
Fraction::EIGHTHS[n + 1],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw1fb7c_1fb97(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let blk = block::block;
|
||||
match cp {
|
||||
// '🭼' LEFT AND LOWER ONE EIGHTH BLOCK
|
||||
0x1fb7c => {
|
||||
blk(metrics, canvas, Alignment::LEFT, ONE_EIGHTH, 1.0);
|
||||
blk(metrics, canvas, Alignment::LOWER, 1.0, ONE_EIGHTH);
|
||||
}
|
||||
// '🭽' LEFT AND UPPER ONE EIGHTH BLOCK
|
||||
0x1fb7d => {
|
||||
blk(metrics, canvas, Alignment::LEFT, ONE_EIGHTH, 1.0);
|
||||
blk(metrics, canvas, Alignment::UPPER, 1.0, ONE_EIGHTH);
|
||||
}
|
||||
// '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK
|
||||
0x1fb7e => {
|
||||
blk(metrics, canvas, Alignment::RIGHT, ONE_EIGHTH, 1.0);
|
||||
blk(metrics, canvas, Alignment::UPPER, 1.0, ONE_EIGHTH);
|
||||
}
|
||||
// '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK
|
||||
0x1fb7f => {
|
||||
blk(metrics, canvas, Alignment::RIGHT, ONE_EIGHTH, 1.0);
|
||||
blk(metrics, canvas, Alignment::LOWER, 1.0, ONE_EIGHTH);
|
||||
}
|
||||
// '🮀' UPPER AND LOWER ONE EIGHTH BLOCK
|
||||
0x1fb80 => {
|
||||
blk(metrics, canvas, Alignment::UPPER, 1.0, ONE_EIGHTH);
|
||||
blk(metrics, canvas, Alignment::LOWER, 1.0, 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.
|
||||
draw1fb76_1fb7b(0x1fb74 + 1, canvas, width, height, metrics)?;
|
||||
draw1fb76_1fb7b(0x1fb74 + 3, canvas, width, height, metrics)?;
|
||||
draw1fb76_1fb7b(0x1fb74 + 5, canvas, width, height, metrics)?;
|
||||
draw1fb76_1fb7b(0x1fb74 + 8, canvas, width, height, metrics)?;
|
||||
}
|
||||
|
||||
// '🮂' UPPER ONE QUARTER BLOCK
|
||||
0x1fb82 => blk(metrics, canvas, Alignment::UPPER, 1.0, ONE_QUARTER),
|
||||
// '🮃' UPPER THREE EIGHTHS BLOCK
|
||||
0x1fb83 => blk(metrics, canvas, Alignment::UPPER, 1.0, THREE_EIGHTHS),
|
||||
// '🮄' UPPER FIVE EIGHTHS BLOCK
|
||||
0x1fb84 => blk(metrics, canvas, Alignment::UPPER, 1.0, FIVE_EIGHTHS),
|
||||
// '🮅' UPPER THREE QUARTERS BLOCK
|
||||
0x1fb85 => blk(metrics, canvas, Alignment::UPPER, 1.0, THREE_QUARTERS),
|
||||
// '🮆' UPPER SEVEN EIGHTHS BLOCK
|
||||
0x1fb86 => blk(metrics, canvas, Alignment::UPPER, 1.0, SEVEN_EIGHTHS),
|
||||
|
||||
// '🮇' RIGHT ONE QUARTER BLOCK
|
||||
0x1fb87 => blk(metrics, canvas, Alignment::RIGHT, ONE_QUARTER, 1.0),
|
||||
// '🮈' RIGHT THREE EIGHTHS BLOCK
|
||||
0x1fb88 => blk(metrics, canvas, Alignment::RIGHT, THREE_EIGHTHS, 1.0),
|
||||
// '🮉' RIGHT FIVE EIGHTHS BLOCK
|
||||
0x1fb89 => blk(metrics, canvas, Alignment::RIGHT, FIVE_EIGHTHS, 1.0),
|
||||
// '🮊' RIGHT THREE QUARTERS BLOCK
|
||||
0x1fb8a => blk(metrics, canvas, Alignment::RIGHT, THREE_QUARTERS, 1.0),
|
||||
// '🮋' RIGHT SEVEN EIGHTHS BLOCK/
|
||||
0x1fb8b => blk(metrics, canvas, Alignment::RIGHT, SEVEN_EIGHTHS, 1.0),
|
||||
|
||||
// '🮌'
|
||||
0x1fb8c => block::block_shade(metrics, canvas, Alignment::LEFT, HALF, 1.0, Shade::MEDIUM),
|
||||
// '🮍'
|
||||
0x1fb8d => block::block_shade(metrics, canvas, Alignment::RIGHT, HALF, 1.0, Shade::MEDIUM),
|
||||
// '🮎'
|
||||
0x1fb8e => block::block_shade(metrics, canvas, Alignment::UPPER, 1.0, HALF, Shade::MEDIUM),
|
||||
// '🮏'
|
||||
0x1fb8f => block::block_shade(metrics, canvas, Alignment::LOWER, 1.0, HALF, Shade::MEDIUM),
|
||||
|
||||
// '🮐'
|
||||
0x1fb90 => block::full_block_shade(metrics, canvas, Shade::MEDIUM),
|
||||
// '🮑'
|
||||
0x1fb91 => {
|
||||
block::full_block_shade(metrics, canvas, Shade::MEDIUM);
|
||||
blk(metrics, canvas, Alignment::UPPER, 1.0, HALF);
|
||||
}
|
||||
// '🮒'
|
||||
0x1fb92 => {
|
||||
block::full_block_shade(metrics, canvas, Shade::MEDIUM);
|
||||
blk(metrics, canvas, Alignment::LOWER, 1.0, 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::full_block_shade(metrics, canvas, Shade::MEDIUM);
|
||||
blk(metrics, canvas, Alignment::RIGHT, HALF, 1.0);
|
||||
}
|
||||
// '🮕'
|
||||
0x1fb95 => checkerboard_fill(metrics, canvas, 0),
|
||||
// '🮖'
|
||||
0x1fb96 => checkerboard_fill(metrics, canvas, 1),
|
||||
// '🮗'
|
||||
0x1fb97 => {
|
||||
canvas.box_(
|
||||
0,
|
||||
(height / 4) as i32,
|
||||
width as i32,
|
||||
(2 * height / 4) as i32,
|
||||
Color::ON,
|
||||
);
|
||||
canvas.box_(
|
||||
0,
|
||||
(3 * height / 4) as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draws the diagonal hatch lines shared by U+1FB98 and U+1FB99.
|
||||
/// `upper_left_to_lower_right` selects the direction.
|
||||
fn diagonal_fill(metrics: &Metrics, canvas: &mut Canvas, upper_left_to_lower_right: bool) {
|
||||
// Set the clip so we don't include anything outside of the cell.
|
||||
clip_to_cell(canvas);
|
||||
|
||||
// TODO: This doesn't align properly for most cell sizes, fix that.
|
||||
|
||||
let thick_px = Thickness::Light.height(metrics.box_thickness);
|
||||
let line_count = metrics.cell_width / (2 * thick_px);
|
||||
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
let float_thick = f64::from(thick_px);
|
||||
let stride = (float_width / f64::from(line_count)).round();
|
||||
|
||||
for i in 0..=(line_count * 2) {
|
||||
let i = i as i32 - line_count as i32;
|
||||
let (top_x, bottom_x) = if upper_left_to_lower_right {
|
||||
let top_x = f64::from(i) * stride;
|
||||
(top_x, float_width + top_x)
|
||||
} else {
|
||||
let bottom_x = f64::from(i) * stride;
|
||||
(float_width + bottom_x, bottom_x)
|
||||
};
|
||||
let _ = canvas.line(
|
||||
Line {
|
||||
p0: Point { x: top_x, y: 0.0 },
|
||||
p1: Point {
|
||||
x: bottom_x,
|
||||
y: float_height,
|
||||
},
|
||||
},
|
||||
float_thick,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Upper Left to Lower Right Fill
|
||||
/// 🮘
|
||||
pub fn draw1fb98(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
diagonal_fill(metrics, canvas, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upper Right to Lower Left Fill
|
||||
/// 🮙
|
||||
pub fn draw1fb99(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
diagonal_fill(metrics, canvas, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw1fb9a_1fb9f(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
// '🮚'
|
||||
0x1fb9a => {
|
||||
edge_triangle(metrics, canvas, Edge::Top)?;
|
||||
edge_triangle(metrics, canvas, Edge::Bottom)?;
|
||||
}
|
||||
// '🮛'
|
||||
0x1fb9b => {
|
||||
edge_triangle(metrics, canvas, Edge::Left)?;
|
||||
edge_triangle(metrics, canvas, Edge::Right)?;
|
||||
}
|
||||
// '🮜'
|
||||
0x1fb9c => geo::corner_triangle_shade(metrics, canvas, Corner::Tl, Shade::MEDIUM)?,
|
||||
// '🮝'
|
||||
0x1fb9d => geo::corner_triangle_shade(metrics, canvas, Corner::Tr, Shade::MEDIUM)?,
|
||||
// '🮞'
|
||||
0x1fb9e => geo::corner_triangle_shade(metrics, canvas, Corner::Br, Shade::MEDIUM)?,
|
||||
// '🮟'
|
||||
0x1fb9f => geo::corner_triangle_shade(metrics, canvas, Corner::Bl, Shade::MEDIUM)?,
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw1fba0_1fbae(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let q = |tl, tr, bl, br| Quads { tl, tr, bl, br };
|
||||
let c = match cp {
|
||||
// '🮠'
|
||||
0x1fba0 => q(true, false, false, false),
|
||||
// '🮡'
|
||||
0x1fba1 => q(false, true, false, false),
|
||||
// '🮢'
|
||||
0x1fba2 => q(false, false, true, false),
|
||||
// '🮣'
|
||||
0x1fba3 => q(false, false, false, true),
|
||||
// '🮤'
|
||||
0x1fba4 => q(true, false, true, false),
|
||||
// '🮥'
|
||||
0x1fba5 => q(false, true, false, true),
|
||||
// '🮦'
|
||||
0x1fba6 => q(false, false, true, true),
|
||||
// '🮧'
|
||||
0x1fba7 => q(true, true, false, false),
|
||||
// '🮨'
|
||||
0x1fba8 => q(true, false, false, true),
|
||||
// '🮩'
|
||||
0x1fba9 => q(false, true, true, false),
|
||||
// '🮪'
|
||||
0x1fbaa => q(false, true, true, true),
|
||||
// '🮫'
|
||||
0x1fbab => q(true, false, true, true),
|
||||
// '🮬'
|
||||
0x1fbac => q(true, true, false, true),
|
||||
// '🮭'
|
||||
0x1fbad => q(true, true, true, false),
|
||||
// '🮮'
|
||||
0x1fbae => q(true, true, true, true),
|
||||
|
||||
_ => unreachable!(),
|
||||
};
|
||||
corner_diagonal_lines(metrics, canvas, c);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 🮯
|
||||
pub fn draw1fbaf(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::lines_char(
|
||||
metrics,
|
||||
canvas,
|
||||
box_::Lines {
|
||||
up: box_::Style::Heavy,
|
||||
down: box_::Style::Heavy,
|
||||
left: box_::Style::Light,
|
||||
right: box_::Style::Light,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 🮽
|
||||
pub fn draw1fbbd(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
box_::light_diagonal_cross(metrics, canvas);
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 🮾
|
||||
pub fn draw1fbbe(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
corner_diagonal_lines(
|
||||
metrics,
|
||||
canvas,
|
||||
Quads {
|
||||
br: true,
|
||||
..Quads::default()
|
||||
},
|
||||
);
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 🮿
|
||||
pub fn draw1fbbf(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
corner_diagonal_lines(
|
||||
metrics,
|
||||
canvas,
|
||||
Quads {
|
||||
tl: true,
|
||||
tr: true,
|
||||
bl: true,
|
||||
br: true,
|
||||
},
|
||||
);
|
||||
canvas.invert();
|
||||
clip_to_cell(canvas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn draw1fbce(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
block::block(metrics, canvas, Alignment::LEFT, TWO_THIRDS, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn draw1fbcf(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
block::block(metrics, canvas, Alignment::LEFT, ONE_THIRD, 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cell diagonals.
|
||||
pub fn draw1fbd0_1fbdf(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
use Alignment as A;
|
||||
let mut d = |from: Alignment, to: Alignment| cell_diagonal(metrics, canvas, from, to);
|
||||
match cp {
|
||||
// ''
|
||||
0x1fbd0 => d(A::MIDDLE_RIGHT, A::LOWER_LEFT),
|
||||
// ''
|
||||
0x1fbd1 => d(A::UPPER_RIGHT, A::MIDDLE_LEFT),
|
||||
// ''
|
||||
0x1fbd2 => d(A::UPPER_LEFT, A::MIDDLE_RIGHT),
|
||||
// ''
|
||||
0x1fbd3 => d(A::MIDDLE_LEFT, A::LOWER_RIGHT),
|
||||
// ''
|
||||
0x1fbd4 => d(A::UPPER_LEFT, A::LOWER_CENTER),
|
||||
// ''
|
||||
0x1fbd5 => d(A::UPPER_CENTER, A::LOWER_RIGHT),
|
||||
// ''
|
||||
0x1fbd6 => d(A::UPPER_RIGHT, A::LOWER_CENTER),
|
||||
// ''
|
||||
0x1fbd7 => d(A::UPPER_CENTER, A::LOWER_LEFT),
|
||||
// ''
|
||||
0x1fbd8 => {
|
||||
d(A::UPPER_LEFT, A::MIDDLE_CENTER);
|
||||
d(A::MIDDLE_CENTER, A::UPPER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbd9 => {
|
||||
d(A::UPPER_RIGHT, A::MIDDLE_CENTER);
|
||||
d(A::MIDDLE_CENTER, A::LOWER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbda => {
|
||||
d(A::LOWER_LEFT, A::MIDDLE_CENTER);
|
||||
d(A::MIDDLE_CENTER, A::LOWER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbdb => {
|
||||
d(A::UPPER_LEFT, A::MIDDLE_CENTER);
|
||||
d(A::MIDDLE_CENTER, A::LOWER_LEFT);
|
||||
}
|
||||
// ''
|
||||
0x1fbdc => {
|
||||
d(A::UPPER_LEFT, A::LOWER_CENTER);
|
||||
d(A::LOWER_CENTER, A::UPPER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbdd => {
|
||||
d(A::UPPER_RIGHT, A::MIDDLE_LEFT);
|
||||
d(A::MIDDLE_LEFT, A::LOWER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbde => {
|
||||
d(A::LOWER_LEFT, A::UPPER_CENTER);
|
||||
d(A::UPPER_CENTER, A::LOWER_RIGHT);
|
||||
}
|
||||
// ''
|
||||
0x1fbdf => {
|
||||
d(A::UPPER_LEFT, A::MIDDLE_RIGHT);
|
||||
d(A::MIDDLE_RIGHT, A::LOWER_LEFT);
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw1fbe0_1fbef(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
use Alignment as A;
|
||||
match cp {
|
||||
// ''
|
||||
0x1fbe0 => circle(metrics, canvas, A::TOP, false),
|
||||
// ''
|
||||
0x1fbe1 => circle(metrics, canvas, A::RIGHT, false),
|
||||
// ''
|
||||
0x1fbe2 => circle(metrics, canvas, A::BOTTOM, false),
|
||||
// ''
|
||||
0x1fbe3 => circle(metrics, canvas, A::LEFT, false),
|
||||
// ''
|
||||
0x1fbe4 => block::block(metrics, canvas, A::UPPER_CENTER, 0.5, 0.5),
|
||||
// ''
|
||||
0x1fbe5 => block::block(metrics, canvas, A::LOWER_CENTER, 0.5, 0.5),
|
||||
// ''
|
||||
0x1fbe6 => block::block(metrics, canvas, A::MIDDLE_LEFT, 0.5, 0.5),
|
||||
// ''
|
||||
0x1fbe7 => block::block(metrics, canvas, A::MIDDLE_RIGHT, 0.5, 0.5),
|
||||
// ''
|
||||
0x1fbe8 => circle(metrics, canvas, A::TOP, true),
|
||||
// ''
|
||||
0x1fbe9 => circle(metrics, canvas, A::RIGHT, true),
|
||||
// ''
|
||||
0x1fbea => circle(metrics, canvas, A::BOTTOM, true),
|
||||
// ''
|
||||
0x1fbeb => circle(metrics, canvas, A::LEFT, true),
|
||||
// ''
|
||||
0x1fbec => circle(metrics, canvas, A::TOP_RIGHT, true),
|
||||
// ''
|
||||
0x1fbed => circle(metrics, canvas, A::BOTTOM_LEFT, true),
|
||||
// ''
|
||||
0x1fbee => circle(metrics, canvas, A::BOTTOM_RIGHT, true),
|
||||
// ''
|
||||
0x1fbef => circle(metrics, canvas, A::TOP_LEFT, true),
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edge_triangle(metrics: &Metrics, canvas: &mut Canvas, edge: Edge) -> Result<(), DrawError> {
|
||||
let upper = 0.0;
|
||||
let middle = (f64::from(metrics.cell_height) / 2.0).round();
|
||||
let lower = f64::from(metrics.cell_height);
|
||||
let left = 0.0;
|
||||
let center = (f64::from(metrics.cell_width) / 2.0).round();
|
||||
let right = f64::from(metrics.cell_width);
|
||||
|
||||
let (x0, y0, x1, y1) = match edge {
|
||||
Edge::Top => (right, upper, left, upper),
|
||||
Edge::Left => (left, upper, left, lower),
|
||||
Edge::Bottom => (left, lower, right, lower),
|
||||
Edge::Right => (right, lower, right, upper),
|
||||
};
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(center, middle);
|
||||
path.line_to(x0, y0);
|
||||
path.line_to(x1, y1);
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, Color::ON)
|
||||
}
|
||||
|
||||
fn corner_diagonal_lines(metrics: &Metrics, canvas: &mut Canvas, corners: Quads) {
|
||||
let thick_px = Thickness::Light.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 / 2 + metrics.cell_width % 2);
|
||||
let center_y = f64::from(metrics.cell_height / 2 + metrics.cell_height % 2);
|
||||
|
||||
let mut line = |p0: (f64, f64), p1: (f64, f64)| {
|
||||
let _ = canvas.line(
|
||||
Line {
|
||||
p0: Point { x: p0.0, y: p0.1 },
|
||||
p1: Point { x: p1.0, y: p1.1 },
|
||||
},
|
||||
float_thick,
|
||||
Color::ON,
|
||||
);
|
||||
};
|
||||
|
||||
if corners.tl {
|
||||
line((center_x, 0.0), (0.0, center_y));
|
||||
}
|
||||
if corners.tr {
|
||||
line((center_x, 0.0), (float_width, center_y));
|
||||
}
|
||||
if corners.bl {
|
||||
line((center_x, float_height), (0.0, center_y));
|
||||
}
|
||||
if corners.br {
|
||||
line((center_x, float_height), (float_width, center_y));
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_diagonal(metrics: &Metrics, canvas: &mut Canvas, from: Alignment, to: Alignment) {
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
|
||||
let hx = |a: HAlign| match a {
|
||||
HAlign::Left => 0.0,
|
||||
HAlign::Right => float_width,
|
||||
HAlign::Center => float_width / 2.0,
|
||||
};
|
||||
let vy = |a: VAlign| match a {
|
||||
VAlign::Top => 0.0,
|
||||
VAlign::Bottom => float_height,
|
||||
VAlign::Middle => float_height / 2.0,
|
||||
};
|
||||
|
||||
let _ = canvas.line(
|
||||
Line {
|
||||
p0: Point {
|
||||
x: hx(from.horizontal),
|
||||
y: vy(from.vertical),
|
||||
},
|
||||
p1: Point {
|
||||
x: hx(to.horizontal),
|
||||
y: vy(to.vertical),
|
||||
},
|
||||
},
|
||||
f64::from(Thickness::Light.height(metrics.box_thickness)),
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
|
||||
fn checkerboard_fill(metrics: &Metrics, canvas: &mut Canvas, parity: usize) {
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
let cell_width = metrics.cell_width as usize;
|
||||
let cell_height = metrics.cell_height as usize;
|
||||
let x_size: usize = 4;
|
||||
let y_size: usize = (4.0 * (float_height / float_width)).round() as usize;
|
||||
for x in 0..x_size {
|
||||
let x0 = (cell_width * x) / x_size;
|
||||
let x1 = (cell_width * (x + 1)) / x_size;
|
||||
for y in 0..y_size {
|
||||
let y0 = (cell_height * y) / y_size;
|
||||
let y1 = (cell_height * (y + 1)) / y_size;
|
||||
if (x + y) % 2 == parity {
|
||||
canvas.rect(
|
||||
Rect {
|
||||
x: x0 as i32,
|
||||
y: y0 as i32,
|
||||
width: x1.saturating_sub(x0) as i32,
|
||||
height: y1.saturating_sub(y0) as i32,
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn circle(metrics: &Metrics, canvas: &mut Canvas, position: Alignment, filled: bool) {
|
||||
// Set the clip so we don't include anything outside of the cell.
|
||||
clip_to_cell(canvas);
|
||||
|
||||
let float_width = f64::from(metrics.cell_width);
|
||||
let float_height = f64::from(metrics.cell_height);
|
||||
|
||||
let x = match position.horizontal {
|
||||
HAlign::Left => 0.0,
|
||||
HAlign::Right => float_width,
|
||||
HAlign::Center => float_width / 2.0,
|
||||
};
|
||||
let y = match position.vertical {
|
||||
VAlign::Top => 0.0,
|
||||
VAlign::Bottom => float_height,
|
||||
VAlign::Middle => float_height / 2.0,
|
||||
};
|
||||
let r = 0.5 * float_width.min(float_height);
|
||||
|
||||
let line_width = f64::from(Thickness::Light.height(metrics.box_thickness));
|
||||
|
||||
let mut path = Path::new();
|
||||
if filled {
|
||||
path.arc(x, y, r, 0.0, std::f64::consts::PI * 2.0);
|
||||
path.close();
|
||||
let _ = canvas.fill_path(&path, Color::ON);
|
||||
} else {
|
||||
path.arc(x, y, r - line_width / 2.0, 0.0, std::f64::consts::PI * 2.0);
|
||||
path.close();
|
||||
let _ = canvas.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_width,
|
||||
..StrokeOptions::default()
|
||||
},
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn metrics() -> Metrics {
|
||||
let mut m = Metrics::calc(crate::metrics::FaceMetrics {
|
||||
px_per_em: 16.0,
|
||||
cell_width: 12.0,
|
||||
ascent: 20.0,
|
||||
descent: -4.0,
|
||||
line_gap: 0.0,
|
||||
underline_thickness: Some(2.0),
|
||||
strikethrough_thickness: Some(2.0),
|
||||
..Default::default()
|
||||
});
|
||||
m.box_thickness = 2;
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mosaic_table_complete() {
|
||||
let m = metrics();
|
||||
for cp in 0x1fb3c..=0x1fb67 {
|
||||
let mut c = Canvas::new(m.cell_width, m.cell_height, 3, 6);
|
||||
draw1fb3c_1fb67(cp, &mut c, m.cell_width, m.cell_height, &m).expect("draw");
|
||||
assert!(c.sfc.iter().any(|v| *v != 0), "cp {cp:x} drew nothing");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sextants_draw() {
|
||||
let m = metrics();
|
||||
for cp in 0x1fb00..=0x1fb3b {
|
||||
let mut c = Canvas::new(m.cell_width, m.cell_height, 3, 6);
|
||||
draw1fb00_1fb3b(cp, &mut c, m.cell_width, m.cell_height, &m).expect("draw");
|
||||
assert!(c.sfc.iter().any(|v| *v != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,800 @@
|
||||
//! Symbols for Legacy Computing Supplement | U+1CC00...U+1CEBF
|
||||
//! <https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement>
|
||||
//!
|
||||
//! Port of `sprite/draw/symbols_for_legacy_computing_supplement.zig`.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::box_::{self, Lines, Style};
|
||||
use super::common::{Alignment, Corner, DrawFn, Fraction, Range, fill};
|
||||
use super::symbols_for_legacy_computing as sflc;
|
||||
use crate::metrics::Metrics;
|
||||
use crate::sprite::canvas::{Canvas, CapMode, Color, DrawError, Path, StrokeOptions};
|
||||
|
||||
pub const RANGES: &[Range] = &[
|
||||
Range {
|
||||
min: 0x1cc1b,
|
||||
max: 0x1cc1e,
|
||||
draw: draw1cc1b_1cc1e as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1cc21,
|
||||
max: 0x1cc2f,
|
||||
draw: draw1cc21_1cc2f as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1cc30,
|
||||
max: 0x1cc3f,
|
||||
draw: draw1cc30_1cc3f as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1cd00,
|
||||
max: 0x1cde5,
|
||||
draw: draw1cd00_1cde5 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce00,
|
||||
max: 0x1ce00,
|
||||
draw: draw1ce00 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce01,
|
||||
max: 0x1ce01,
|
||||
draw: draw1ce01 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce0b,
|
||||
max: 0x1ce0b,
|
||||
draw: draw1ce0b as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce0c,
|
||||
max: 0x1ce0c,
|
||||
draw: draw1ce0c as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce16,
|
||||
max: 0x1ce19,
|
||||
draw: draw1ce16_1ce19 as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce51,
|
||||
max: 0x1ce8f,
|
||||
draw: draw1ce51_1ce8f as DrawFn,
|
||||
},
|
||||
Range {
|
||||
min: 0x1ce90,
|
||||
max: 0x1ceaf,
|
||||
draw: draw1ce90_1ceaf as DrawFn,
|
||||
},
|
||||
];
|
||||
|
||||
const OCTANT_MIN: u32 = 0x1cd00;
|
||||
const OCTANT_MAX: u32 = 0x1cde5;
|
||||
|
||||
/// Parse the octant data. Each entry is a bitmask where bit `n-1` is set for
|
||||
/// octant `n` (1..=8). Lines are in the format "BLOCK OCTANT-1235".
|
||||
fn octants() -> &'static [u8] {
|
||||
static OCTANTS: OnceLock<Vec<u8>> = OnceLock::new();
|
||||
OCTANTS.get_or_init(|| {
|
||||
let mut result = Vec::new();
|
||||
for line in include_str!("octants.txt").split('\n') {
|
||||
// Skip comments
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let idx = line.find('-').expect("octant line has '-'");
|
||||
let mut bits = 0u8;
|
||||
for c in line[idx + 1..].bytes() {
|
||||
bits |= 1 << (c - b'1');
|
||||
}
|
||||
result.push(bits);
|
||||
}
|
||||
assert_eq!(result.len(), (OCTANT_MAX - OCTANT_MIN + 1) as usize);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Octants
|
||||
pub fn draw1cd00_1cde5(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
use Fraction::{End, Full, Half, OneQuarter, ThreeQuarters, TwoQuarters, Zero};
|
||||
|
||||
let oct = octants()[(cp - OCTANT_MIN) as usize];
|
||||
let has = |n: u8| oct & (1 << (n - 1)) != 0;
|
||||
if has(1) {
|
||||
fill(metrics, canvas, Zero, Half, Zero, OneQuarter);
|
||||
}
|
||||
if has(2) {
|
||||
fill(metrics, canvas, Half, Full, Zero, OneQuarter);
|
||||
}
|
||||
if has(3) {
|
||||
fill(metrics, canvas, Zero, Half, OneQuarter, TwoQuarters);
|
||||
}
|
||||
if has(4) {
|
||||
fill(metrics, canvas, Half, Full, OneQuarter, TwoQuarters);
|
||||
}
|
||||
if has(5) {
|
||||
fill(metrics, canvas, Zero, Half, TwoQuarters, ThreeQuarters);
|
||||
}
|
||||
if has(6) {
|
||||
fill(metrics, canvas, Half, Full, TwoQuarters, ThreeQuarters);
|
||||
}
|
||||
if has(7) {
|
||||
fill(metrics, canvas, Zero, Half, ThreeQuarters, End);
|
||||
}
|
||||
if has(8) {
|
||||
fill(metrics, canvas, Half, Full, ThreeQuarters, End);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Separated Block Quadrants
|
||||
///
|
||||
pub fn draw1cc21_1cc2f(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// Bits laid out to match the codepoint order.
|
||||
let bits = (cp - 0x1CC20) & 0xF;
|
||||
let (tl, tr, bl, br) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0);
|
||||
|
||||
let gap = (width / 12).max(1) as i32;
|
||||
|
||||
let mid_gap_x = gap * 2 + (width % 2) as i32;
|
||||
let mid_gap_y = gap * 2 + (height % 2) as i32;
|
||||
|
||||
let w = (width as i32 - gap * 2 - mid_gap_x) / 2;
|
||||
let h = (height as i32 - gap * 2 - mid_gap_y) / 2;
|
||||
|
||||
if tl {
|
||||
canvas.box_(gap, gap, gap + w, gap + h, Color::ON);
|
||||
}
|
||||
if tr {
|
||||
canvas.box_(
|
||||
gap + w + mid_gap_x,
|
||||
gap,
|
||||
gap + w + mid_gap_x + w,
|
||||
gap + h,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if bl {
|
||||
canvas.box_(
|
||||
gap,
|
||||
gap + h + mid_gap_y,
|
||||
gap + w,
|
||||
gap + h + mid_gap_y + h,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if 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,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
match cp {
|
||||
0x1CC30 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
0.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tl,
|
||||
)?,
|
||||
0x1CC31 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
0.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tl,
|
||||
)?,
|
||||
0x1CC32 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
2.0,
|
||||
0.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tr,
|
||||
)?,
|
||||
0x1CC33 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
3.0,
|
||||
0.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tr,
|
||||
)?,
|
||||
0x1CC34 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
1.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tl,
|
||||
)?,
|
||||
0x1CC35 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
Corner::Tl,
|
||||
)?,
|
||||
0x1CC36 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
Corner::Tr,
|
||||
)?,
|
||||
0x1CC37 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
3.0,
|
||||
1.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Tr,
|
||||
)?,
|
||||
0x1CC38 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
2.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Bl,
|
||||
)?,
|
||||
0x1CC39 => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
Corner::Bl,
|
||||
)?,
|
||||
0x1CC3A => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
Corner::Br,
|
||||
)?,
|
||||
0x1CC3B => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
3.0,
|
||||
2.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Br,
|
||||
)?,
|
||||
0x1CC3C => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
3.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Bl,
|
||||
)?,
|
||||
0x1CC3D => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
3.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Bl,
|
||||
)?,
|
||||
0x1CC3E => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
2.0,
|
||||
3.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Br,
|
||||
)?,
|
||||
0x1CC3F => circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
3.0,
|
||||
3.0,
|
||||
2.0,
|
||||
2.0,
|
||||
Corner::Br,
|
||||
)?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let w = width as i32;
|
||||
let h = height as i32;
|
||||
let t = metrics.box_thickness as i32;
|
||||
let horiz = Lines {
|
||||
left: Style::Light,
|
||||
right: Style::Light,
|
||||
..Lines::NONE
|
||||
};
|
||||
match cp {
|
||||
// BOX DRAWINGS LIGHT HORIZONTAL AND UPPER RIGHT
|
||||
0x1CC1B => {
|
||||
box_::lines_char(metrics, canvas, horiz);
|
||||
canvas.box_(w - t, 0, w, h.div_euclid(2), Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT HORIZONTAL AND LOWER RIGHT
|
||||
0x1CC1C => {
|
||||
box_::lines_char(metrics, canvas, horiz);
|
||||
canvas.box_(w - t, h.div_euclid(2), w, h, Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT TOP AND UPPER LEFT
|
||||
0x1CC1D => {
|
||||
canvas.box_(0, 0, w, t, Color::ON);
|
||||
canvas.box_(0, 0, t, h.div_euclid(2), Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT BOTTOM AND LOWER LEFT
|
||||
0x1CC1E => {
|
||||
canvas.box_(0, h - t, w, h, Color::ON);
|
||||
canvas.box_(0, h.div_euclid(2), t, h, Color::ON);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// RIGHT HALF AND LEFT HALF WHITE CIRCLE
|
||||
pub fn draw1ce00(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
sflc::circle(metrics, canvas, Alignment::LEFT, false);
|
||||
sflc::circle(metrics, canvas, Alignment::RIGHT, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// LOWER HALF AND UPPER HALF WHITE CIRCLE
|
||||
pub fn draw1ce01(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
sflc::circle(metrics, canvas, Alignment::TOP, false);
|
||||
sflc::circle(metrics, canvas, Alignment::BOTTOM, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// LEFT HALF WHITE ELLIPSE
|
||||
pub fn draw1ce0b(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.5,
|
||||
Corner::Tl,
|
||||
)?;
|
||||
circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.5,
|
||||
Corner::Bl,
|
||||
)
|
||||
}
|
||||
|
||||
/// RIGHT HALF WHITE ELLIPSE
|
||||
pub fn draw1ce0c(
|
||||
_cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.5,
|
||||
Corner::Tr,
|
||||
)?;
|
||||
circle_piece(
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.5,
|
||||
Corner::Br,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn draw1ce16_1ce19(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let w = width as i32;
|
||||
let h = height as i32;
|
||||
let t = metrics.box_thickness as i32;
|
||||
let vert = Lines {
|
||||
up: Style::Light,
|
||||
down: Style::Light,
|
||||
..Lines::NONE
|
||||
};
|
||||
match cp {
|
||||
// BOX DRAWINGS LIGHT VERTICAL AND TOP RIGHT
|
||||
0x1CE16 => {
|
||||
box_::lines_char(metrics, canvas, vert);
|
||||
canvas.box_(w.div_euclid(2), 0, w, t, Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT VERTICAL AND BOTTOM RIGHT
|
||||
0x1CE17 => {
|
||||
box_::lines_char(metrics, canvas, vert);
|
||||
canvas.box_(w.div_euclid(2), h - t, w, h, Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT VERTICAL AND TOP LEFT
|
||||
0x1CE18 => {
|
||||
box_::lines_char(metrics, canvas, vert);
|
||||
canvas.box_(0, 0, w.div_euclid(2), t, Color::ON);
|
||||
}
|
||||
// BOX DRAWINGS LIGHT VERTICAL AND BOTTOM LEFT
|
||||
0x1CE19 => {
|
||||
box_::lines_char(metrics, canvas, vert);
|
||||
canvas.box_(0, h - t, w.div_euclid(2), h, Color::ON);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Separated Block Sextants
|
||||
pub fn draw1ce51_1ce8f(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
// Bits laid out to match the codepoint order: tl, tr, ml, mr, bl, br.
|
||||
let bits = (cp - 0x1CE50) & 0x3F;
|
||||
let (tl, tr, ml, mr, bl, br) = (
|
||||
bits & 1 != 0,
|
||||
bits & 2 != 0,
|
||||
bits & 4 != 0,
|
||||
bits & 8 != 0,
|
||||
bits & 16 != 0,
|
||||
bits & 32 != 0,
|
||||
);
|
||||
|
||||
let gap = (width / 12).max(1) as i32;
|
||||
|
||||
let mid_gap_x = gap * 2 + (width % 2) as i32;
|
||||
let y_extra = (height % 3) as i32;
|
||||
let mid_gap_y = gap * 2 + y_extra.div_euclid(2);
|
||||
|
||||
let w = (width as i32 - gap * 2 - mid_gap_x) / 2;
|
||||
let h = (height as i32 - gap * 2 - mid_gap_y * 2).div_euclid(3);
|
||||
// Distribute any leftover height in to the middle row of blocks.
|
||||
let h_m = height as i32 - gap * 2 - mid_gap_y * 2 - h * 2;
|
||||
|
||||
if tl {
|
||||
canvas.box_(gap, gap, gap + w, gap + h, Color::ON);
|
||||
}
|
||||
if tr {
|
||||
canvas.box_(
|
||||
gap + w + mid_gap_x,
|
||||
gap,
|
||||
gap + w + mid_gap_x + w,
|
||||
gap + h,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if ml {
|
||||
canvas.box_(
|
||||
gap,
|
||||
gap + h + mid_gap_y,
|
||||
gap + w,
|
||||
gap + h + mid_gap_y + h_m,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if 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,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if 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,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
if 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,
|
||||
Color::ON,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sixteenth Blocks
|
||||
pub fn draw1ce90_1ceaf(
|
||||
cp: u32,
|
||||
canvas: &mut Canvas,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
metrics: &Metrics,
|
||||
) -> Result<(), DrawError> {
|
||||
let q = Fraction::QUARTERS;
|
||||
match cp {
|
||||
0x1CE90 => fill(metrics, canvas, q[0], q[1], q[0], q[1]),
|
||||
0x1CE91 => fill(metrics, canvas, q[1], q[2], q[0], q[1]),
|
||||
0x1CE92 => fill(metrics, canvas, q[2], q[3], q[0], q[1]),
|
||||
0x1CE93 => fill(metrics, canvas, q[3], q[4], q[0], q[1]),
|
||||
0x1CE94 => fill(metrics, canvas, q[0], q[1], q[1], q[2]),
|
||||
0x1CE95 => fill(metrics, canvas, q[1], q[2], q[1], q[2]),
|
||||
0x1CE96 => fill(metrics, canvas, q[2], q[3], q[1], q[2]),
|
||||
0x1CE97 => fill(metrics, canvas, q[3], q[4], q[1], q[2]),
|
||||
0x1CE98 => fill(metrics, canvas, q[0], q[1], q[2], q[3]),
|
||||
0x1CE99 => fill(metrics, canvas, q[1], q[2], q[2], q[3]),
|
||||
0x1CE9A => fill(metrics, canvas, q[2], q[3], q[2], q[3]),
|
||||
0x1CE9B => fill(metrics, canvas, q[3], q[4], q[2], q[3]),
|
||||
0x1CE9C => fill(metrics, canvas, q[0], q[1], q[3], q[4]),
|
||||
0x1CE9D => fill(metrics, canvas, q[1], q[2], q[3], q[4]),
|
||||
0x1CE9E => fill(metrics, canvas, q[2], q[3], q[3], q[4]),
|
||||
0x1CE9F => fill(metrics, canvas, q[3], q[4], q[3], q[4]),
|
||||
0x1CEA0 => fill(metrics, canvas, q[2], q[4], q[3], q[4]),
|
||||
0x1CEA1 => fill(metrics, canvas, q[1], q[4], q[3], q[4]),
|
||||
0x1CEA2 => fill(metrics, canvas, q[0], q[3], q[3], q[4]),
|
||||
0x1CEA3 => fill(metrics, canvas, q[0], q[2], q[3], q[4]),
|
||||
0x1CEA4 => fill(metrics, canvas, q[0], q[1], q[2], q[4]),
|
||||
0x1CEA5 => fill(metrics, canvas, q[0], q[1], q[1], q[4]),
|
||||
0x1CEA6 => fill(metrics, canvas, q[0], q[1], q[0], q[3]),
|
||||
0x1CEA7 => fill(metrics, canvas, q[0], q[1], q[0], q[2]),
|
||||
0x1CEA8 => fill(metrics, canvas, q[0], q[2], q[0], q[1]),
|
||||
0x1CEA9 => fill(metrics, canvas, q[0], q[3], q[0], q[1]),
|
||||
0x1CEAA => fill(metrics, canvas, q[1], q[4], q[0], q[1]),
|
||||
0x1CEAB => fill(metrics, canvas, q[2], q[4], q[0], q[1]),
|
||||
0x1CEAC => fill(metrics, canvas, q[3], q[4], q[0], q[2]),
|
||||
0x1CEAD => fill(metrics, canvas, q[3], q[4], q[0], q[3]),
|
||||
0x1CEAE => fill(metrics, canvas, q[3], q[4], q[1], q[4]),
|
||||
0x1CEAF => fill(metrics, canvas, q[3], q[4], q[2], q[4]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn circle_piece(
|
||||
canvas: &mut Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: &Metrics,
|
||||
x: f64,
|
||||
y: f64,
|
||||
w: f64,
|
||||
h: f64,
|
||||
corner: Corner,
|
||||
) -> Result<(), DrawError> {
|
||||
// Radius in pixels of the arc we need to draw.
|
||||
let wdth = f64::from(width) * w;
|
||||
let hght = f64::from(height) * h;
|
||||
|
||||
// Position in pixels (rather than cells) for x/y
|
||||
let xp = f64::from(width) * x;
|
||||
let yp = f64::from(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.
|
||||
let c = (std::f64::consts::SQRT_2 - 1.0) * 4.0 / 3.0;
|
||||
let cw = c * wdth;
|
||||
let ch = c * hght;
|
||||
|
||||
let thick = f64::from(metrics.box_thickness);
|
||||
let ht = thick * 0.5;
|
||||
|
||||
let mut path = Path::new();
|
||||
|
||||
match corner {
|
||||
Corner::Tl => {
|
||||
path.move_to(wdth - xp, ht - yp);
|
||||
path.curve_to(
|
||||
wdth - cw - xp,
|
||||
ht - yp,
|
||||
ht - xp,
|
||||
hght - ch - yp,
|
||||
ht - xp,
|
||||
hght - yp,
|
||||
);
|
||||
}
|
||||
Corner::Tr => {
|
||||
path.move_to(wdth - xp, ht - yp);
|
||||
path.curve_to(
|
||||
wdth + cw - xp,
|
||||
ht - yp,
|
||||
wdth * 2.0 - ht - xp,
|
||||
hght - ch - yp,
|
||||
wdth * 2.0 - ht - xp,
|
||||
hght - yp,
|
||||
);
|
||||
}
|
||||
Corner::Bl => {
|
||||
path.move_to(ht - xp, hght - yp);
|
||||
path.curve_to(
|
||||
ht - xp,
|
||||
hght + ch - yp,
|
||||
wdth - cw - xp,
|
||||
hght * 2.0 - ht - yp,
|
||||
wdth - xp,
|
||||
hght * 2.0 - ht - yp,
|
||||
);
|
||||
}
|
||||
Corner::Br => {
|
||||
path.move_to(wdth * 2.0 - ht - xp, hght - yp);
|
||||
path.curve_to(
|
||||
wdth * 2.0 - ht - xp,
|
||||
hght + ch - yp,
|
||||
wdth + cw - xp,
|
||||
hght * 2.0 - ht - yp,
|
||||
wdth - xp,
|
||||
hght * 2.0 - ht - yp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.stroke_path(
|
||||
&path,
|
||||
StrokeOptions {
|
||||
line_cap_mode: CapMode::Butt,
|
||||
line_width: f64::from(metrics.box_thickness),
|
||||
},
|
||||
Color::ON,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn octant_table_parses() {
|
||||
assert_eq!(octants().len(), 230);
|
||||
// BLOCK OCTANT-3
|
||||
assert_eq!(0b100, octants()[0]);
|
||||
// BLOCK OCTANT-23
|
||||
assert_eq!(0b110, octants()[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
//! This implements the built-in "sprite face". This font renders
|
||||
//! the built-in glyphs for the terminal, such as box drawing fonts, as well
|
||||
//! as specific sprites that are part of our rendering model such as
|
||||
//! text decorations (underlines).
|
||||
//!
|
||||
//! This isn't really a "font face" so much as it is quacks like a font
|
||||
//! face with regards to how it works with font.Group. We don't use any
|
||||
//! dynamic dispatch so it isn't truly an interface but the functions
|
||||
//! and behaviors are close enough to a system face that it makes it easy
|
||||
//! to integrate with font.Group. This is desirable so that higher level
|
||||
//! processes such as GroupCache, Shaper, etc. don't need to be aware of
|
||||
//! special sprite handling and just treat it like a normal font face.
|
||||
//!
|
||||
//! Port of `font/sprite/Face.zig`.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::Sprite;
|
||||
use super::canvas::{Canvas, DrawError};
|
||||
use super::draw::common::{DrawFn, Range};
|
||||
use super::draw::{
|
||||
block, box_, braille, branch, geometric_shapes, powerline, special,
|
||||
symbols_for_legacy_computing, symbols_for_legacy_computing_supplement,
|
||||
};
|
||||
use crate::atlas::{self, Atlas};
|
||||
use crate::glyph::Glyph;
|
||||
use crate::metrics::Metrics;
|
||||
|
||||
/// Text/emoji presentation.
|
||||
// TODO(integration): replace with the real `font::Presentation` once the
|
||||
// rest of the font engine (face.zig / main.zig) is ported.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Presentation {
|
||||
Text,
|
||||
Emoji,
|
||||
}
|
||||
|
||||
/// The subset of `font.face.RenderOptions` that the sprite face uses.
|
||||
// TODO(integration): replace with the real `font::face::RenderOptions`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct RenderOptions {
|
||||
/// The number of grid cells this glyph will take up (0..=3, a `u2` in Zig).
|
||||
pub cell_width: Option<u8>,
|
||||
}
|
||||
|
||||
/// Errors from [`Face::render_glyph`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum RenderError {
|
||||
#[error(transparent)]
|
||||
Draw(#[from] DrawError),
|
||||
#[error(transparent)]
|
||||
Atlas(#[from] atlas::Error),
|
||||
}
|
||||
|
||||
/// All codepoint ranges drawn by the draw modules, sorted ascending by `min`.
|
||||
/// Panics (at first use) if any ranges overlap, mirroring Zig's
|
||||
/// compile error.
|
||||
pub fn ranges() -> &'static [Range] {
|
||||
static RANGES: OnceLock<Vec<Range>> = OnceLock::new();
|
||||
RANGES.get_or_init(|| {
|
||||
let mut r: Vec<Range> = [
|
||||
block::RANGES,
|
||||
box_::RANGES,
|
||||
braille::RANGES,
|
||||
branch::RANGES,
|
||||
geometric_shapes::RANGES,
|
||||
powerline::RANGES,
|
||||
symbols_for_legacy_computing::RANGES,
|
||||
symbols_for_legacy_computing_supplement::RANGES,
|
||||
]
|
||||
.concat();
|
||||
|
||||
// Sort ranges in ascending order
|
||||
r.sort_by_key(|r| r.min);
|
||||
|
||||
// Ensure there's no overlapping ranges
|
||||
let mut i = 0;
|
||||
for (k, n) in r.iter().enumerate() {
|
||||
assert!(
|
||||
n.min > i || k == 0,
|
||||
"Codepoint range {:X}..{:X} overlaps previous range ending at {:X}",
|
||||
n.min,
|
||||
n.max,
|
||||
i
|
||||
);
|
||||
i = n.max;
|
||||
}
|
||||
|
||||
r
|
||||
})
|
||||
}
|
||||
|
||||
fn get_draw_fn(cp: u32) -> Option<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 {
|
||||
return Sprite::from_u32(cp).map(|s| match s {
|
||||
Sprite::Underline => special::underline as DrawFn,
|
||||
Sprite::UnderlineDouble => special::underline_double,
|
||||
Sprite::UnderlineDotted => special::underline_dotted,
|
||||
Sprite::UnderlineDashed => special::underline_dashed,
|
||||
Sprite::UnderlineCurly => special::underline_curly,
|
||||
Sprite::Strikethrough => special::strikethrough,
|
||||
Sprite::Overline => special::overline,
|
||||
Sprite::CursorRect => special::cursor_rect,
|
||||
Sprite::CursorHollowRect => special::cursor_hollow_rect,
|
||||
Sprite::CursorBar => special::cursor_bar,
|
||||
Sprite::CursorUnderline => special::cursor_underline,
|
||||
});
|
||||
}
|
||||
|
||||
let r = ranges();
|
||||
// Ranges are sorted and non-overlapping: binary search.
|
||||
let idx = r.partition_point(|range| range.max < cp);
|
||||
r.get(idx)
|
||||
.filter(|range| cp >= range.min && cp <= range.max)
|
||||
.map(|range| range.draw)
|
||||
}
|
||||
|
||||
/// The sprite face.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Face {
|
||||
/// Grid metrics for rendering sprites.
|
||||
pub metrics: Metrics,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
pub fn new(metrics: Metrics) -> Face {
|
||||
Face { metrics }
|
||||
}
|
||||
|
||||
/// Returns true if the codepoint exists in our sprite font.
|
||||
pub fn has_codepoint(&self, cp: u32, _p: Option<Presentation>) -> bool {
|
||||
// We ignore presentation. No matter what presentation is
|
||||
// requested we always provide glyphs for our codepoints.
|
||||
get_draw_fn(cp).is_some()
|
||||
}
|
||||
|
||||
/// Render the glyph.
|
||||
pub fn render_glyph(
|
||||
&self,
|
||||
atlas: &mut Atlas,
|
||||
cp: u32,
|
||||
opts: RenderOptions,
|
||||
) -> Result<Glyph, RenderError> {
|
||||
if cfg!(debug_assertions) && !self.has_codepoint(cp, None) {
|
||||
log::error!("invalid codepoint cp={cp:x}");
|
||||
unreachable!("invalid sprite codepoint");
|
||||
}
|
||||
|
||||
// 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.
|
||||
let Some(draw) = get_draw_fn(cp) else {
|
||||
return Ok(Glyph::default());
|
||||
};
|
||||
|
||||
let metrics = &self.metrics;
|
||||
|
||||
// We adjust our sprite width based on the cell width.
|
||||
let width = match opts.cell_width.unwrap_or(1) {
|
||||
0 | 1 => metrics.cell_width,
|
||||
w => metrics.cell_width * u32::from(w),
|
||||
};
|
||||
|
||||
let height = metrics.cell_height;
|
||||
|
||||
let padding_x = width / 4;
|
||||
let padding_y = height / 4;
|
||||
|
||||
// Make a canvas of the desired size
|
||||
let mut canvas = Canvas::new(width, height, padding_x, padding_y);
|
||||
|
||||
draw(cp, &mut canvas, width, height, metrics)?;
|
||||
|
||||
// Write the drawing to the atlas
|
||||
let region = canvas.write_atlas(atlas)?;
|
||||
|
||||
Ok(Glyph {
|
||||
width: region.width,
|
||||
height: region.height,
|
||||
offset_x: canvas.clip_left as i32 - padding_x as i32,
|
||||
offset_y: region.height.saturating_add(canvas.clip_bottom) as i32 - padding_y as i32,
|
||||
atlas_x: region.x,
|
||||
atlas_y: region.y,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::metrics::FaceMetrics;
|
||||
|
||||
fn manifest_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
/// Directory where review sheets / diffs are written (`<workspace>/target/sprite_sheets`).
|
||||
fn out_dir() -> PathBuf {
|
||||
let d = manifest_dir().join("../../target/sprite_sheets");
|
||||
std::fs::create_dir_all(&d).expect("create sprite_sheets dir");
|
||||
d
|
||||
}
|
||||
|
||||
fn write_png(path: &Path, w: u32, h: u32, data: &[u8]) {
|
||||
let file = std::fs::File::create(path).expect("create png");
|
||||
let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h);
|
||||
enc.set_color(png::ColorType::Grayscale);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = enc.write_header().expect("png header");
|
||||
writer.write_image_data(data).expect("png data");
|
||||
}
|
||||
|
||||
fn read_png(path: &Path) -> (u32, u32, Vec<u8>) {
|
||||
let file = std::fs::File::open(path).expect("open reference png");
|
||||
let mut reader = png::Decoder::new(std::io::BufReader::new(file))
|
||||
.read_info()
|
||||
.expect("png info");
|
||||
let mut buf = vec![0; reader.output_buffer_size().expect("png size")];
|
||||
let info = reader.next_frame(&mut buf).expect("png frame");
|
||||
buf.truncate(info.buffer_size());
|
||||
// The reference files are 8-bit grayscale; if not, take the first channel.
|
||||
let ch = info.color_type.samples();
|
||||
let gray = if ch == 1 {
|
||||
buf
|
||||
} else {
|
||||
buf.chunks_exact(ch).map(|c| c[0]).collect()
|
||||
};
|
||||
(info.width, info.height, gray)
|
||||
}
|
||||
|
||||
fn test_metrics(width: u32, ascent: u32, descent: u32, thickness: u32) -> Metrics {
|
||||
Metrics::calc(FaceMetrics {
|
||||
// Fudged number, not used in anything we care about here.
|
||||
px_per_em: 16.0,
|
||||
cell_width: f64::from(width),
|
||||
ascent: f64::from(ascent),
|
||||
descent: -f64::from(descent),
|
||||
line_gap: 0.0,
|
||||
underline_thickness: Some(f64::from(thickness)),
|
||||
strikethrough_thickness: Some(f64::from(thickness)),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Stats of the comparison of one atlas sheet against its reference.
|
||||
#[derive(Debug, Default)]
|
||||
struct Diff {
|
||||
/// number of pixels that differ at all
|
||||
differing: usize,
|
||||
/// number of pixels differing by more than `BIG`
|
||||
big: usize,
|
||||
/// number of pixels that are non-zero in either image
|
||||
ink: usize,
|
||||
max: u8,
|
||||
}
|
||||
|
||||
/// Per-pixel difference above which a pixel counts as a "big" difference.
|
||||
/// z2d and tiny-skia antialias differently, so small differences are expected
|
||||
/// along diagonals and curves.
|
||||
const BIG: u8 = 96;
|
||||
|
||||
fn diff_sheet(test: &[u8], reference: &[u8]) -> Diff {
|
||||
let mut d = Diff::default();
|
||||
for (t, r) in test.iter().zip(reference) {
|
||||
if *t != 0 || *r != 0 {
|
||||
d.ink += 1;
|
||||
}
|
||||
let delta = t.abs_diff(*r);
|
||||
if delta != 0 {
|
||||
d.differing += 1;
|
||||
d.max = d.max.max(delta);
|
||||
}
|
||||
if delta > BIG {
|
||||
d.big += 1;
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// Draws all ranges in to a set of 16x16 glyph sheets and compares them
|
||||
/// with the reference files (with tolerance). Returns the failure
|
||||
/// messages (empty if all ok). Mirrors `testDrawRanges` in Face.zig.
|
||||
fn test_draw_ranges(width: u32, ascent: u32, descent: u32, thickness: u32) -> Vec<String> {
|
||||
let metrics = test_metrics(width, ascent, descent, thickness);
|
||||
let height = ascent + descent;
|
||||
let padding_x = width / 4;
|
||||
let padding_y = height / 4;
|
||||
|
||||
let mut canvas = Canvas::new(width, height, padding_x, padding_y);
|
||||
|
||||
// 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.
|
||||
let stride_x = width + 2 * padding_x;
|
||||
let stride_y = height + 2 * padding_y;
|
||||
let (aw, ah) = (stride_x * 16, stride_y * 16);
|
||||
let mut atlas = vec![0u8; (aw * ah) as usize];
|
||||
|
||||
let rs = ranges();
|
||||
let mut i = rs[0].min & !0xff;
|
||||
let mut failures = Vec::new();
|
||||
|
||||
let flush = |i: u32, atlas: &[u8], failures: &mut Vec<String>| {
|
||||
let name = format!(
|
||||
"U+{i:X}...U+{:X}-{width}x{height}+{thickness}.png",
|
||||
i + 0xff
|
||||
);
|
||||
write_png(&out_dir().join(format!("test-{name}")), aw, ah, atlas);
|
||||
|
||||
let ref_path = manifest_dir().join("testdata").join(&name);
|
||||
if !ref_path.exists() {
|
||||
failures.push(format!("missing reference file {name}"));
|
||||
return;
|
||||
}
|
||||
let (rw, rh, reference) = read_png(&ref_path);
|
||||
if (rw, rh) != (aw, ah) {
|
||||
failures.push(format!("{name}: size {aw}x{ah} != reference {rw}x{rh}"));
|
||||
return;
|
||||
}
|
||||
let d = diff_sheet(atlas, &reference);
|
||||
|
||||
// Tolerances: antialiasing differences between z2d and tiny-skia
|
||||
// are fine, but the geometry must match: very few pixels may be
|
||||
// off by a lot, relative to the amount of ink on the sheet.
|
||||
let big_ratio = d.big as f64 / d.ink.max(1) as f64;
|
||||
let ok = big_ratio <= 0.04;
|
||||
if !ok || d.differing > 0 {
|
||||
// Write a visual diff (red = reference, green = ours).
|
||||
let mut rgb = Vec::with_capacity(atlas.len() * 3);
|
||||
for (t, r) in atlas.iter().zip(&reference) {
|
||||
if t == r {
|
||||
rgb.extend_from_slice(&[t / 3, t / 3, t / 3]);
|
||||
} else {
|
||||
rgb.extend_from_slice(&[*r, *t, 0]);
|
||||
}
|
||||
}
|
||||
let path = out_dir().join(format!("diff-{name}"));
|
||||
let file = std::fs::File::create(&path).expect("create diff png");
|
||||
let mut enc = png::Encoder::new(std::io::BufWriter::new(file), aw, ah);
|
||||
enc.set_color(png::ColorType::Rgb);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
enc.write_header()
|
||||
.and_then(|mut w| w.write_image_data(&rgb))
|
||||
.expect("write diff");
|
||||
}
|
||||
if !ok {
|
||||
failures.push(format!(
|
||||
"{name}: {} big diffs out of {} ink pixels ({:.2}%), {} differing, max delta {}",
|
||||
d.big,
|
||||
d.ink,
|
||||
big_ratio * 100.0,
|
||||
d.differing,
|
||||
d.max
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for range in rs {
|
||||
for cp in range.min..=range.max {
|
||||
// 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 {
|
||||
flush(i, &atlas, &mut failures);
|
||||
i = cp & !0xff;
|
||||
atlas.fill(0);
|
||||
}
|
||||
|
||||
(range.draw)(cp, &mut canvas, width, height, &metrics).expect("draw");
|
||||
canvas.clear_clipping_regions();
|
||||
|
||||
// Composite (src) the canvas into its slot of the sheet.
|
||||
let ox = stride_x * ((cp - i) % 16);
|
||||
let oy = stride_y * ((cp - i) / 16);
|
||||
for y in 0..canvas.sfc_height {
|
||||
let src =
|
||||
&canvas.sfc[(y * canvas.sfc_width) as usize..][..canvas.sfc_width as usize];
|
||||
let dst = ((oy + y) * aw + ox) as usize;
|
||||
atlas[dst..dst + src.len()].copy_from_slice(src);
|
||||
}
|
||||
canvas.reset();
|
||||
}
|
||||
}
|
||||
flush(i, &atlas, &mut failures);
|
||||
|
||||
failures
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_face_render_all_sprites() {
|
||||
// Renders all sprites to sheets and compares them to the reference
|
||||
// images from upstream (rendered with z2d), with a tolerance for
|
||||
// antialiasing differences.
|
||||
//
|
||||
// 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.)
|
||||
let mut failures = Vec::new();
|
||||
failures.extend(test_draw_ranges(18, 30, 6, 4));
|
||||
failures.extend(test_draw_ranges(12, 20, 4, 3));
|
||||
failures.extend(test_draw_ranges(11, 19, 2, 2));
|
||||
failures.extend(test_draw_ranges(9, 15, 2, 1));
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"sprite sheets differ from reference (see {}):\n{}",
|
||||
out_dir().display(),
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranges_sorted_and_non_overlapping() {
|
||||
let rs = ranges();
|
||||
assert!(!rs.is_empty());
|
||||
for w in rs.windows(2) {
|
||||
assert!(w[0].min <= w[0].max);
|
||||
assert!(
|
||||
w[0].max < w[1].min,
|
||||
"{:X?} overlaps {:X?}",
|
||||
w[0].max,
|
||||
w[1].min
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_codepoint() {
|
||||
let face = Face::new(test_metrics(9, 15, 2, 1));
|
||||
assert!(face.has_codepoint(0x2500, None));
|
||||
assert!(face.has_codepoint(0x2588, Some(Presentation::Emoji)));
|
||||
assert!(face.has_codepoint(0x28ff, None));
|
||||
assert!(face.has_codepoint(0xe0b0, None));
|
||||
assert!(face.has_codepoint(0x1fb00, None));
|
||||
assert!(face.has_codepoint(Sprite::Underline as u32, None));
|
||||
assert!(face.has_codepoint(Sprite::CursorUnderline as u32, None));
|
||||
assert!(!face.has_codepoint(u32::from('a'), None));
|
||||
assert!(!face.has_codepoint(Sprite::START + 11, None));
|
||||
}
|
||||
|
||||
/// Render every supported codepoint (and special sprite) through the
|
||||
/// full `render_glyph` path at several cell sizes / cell widths and
|
||||
/// check basic invariants. Also writes a review sheet to
|
||||
/// `target/sprite_sheets/review-<w>x<h>.png`.
|
||||
#[test]
|
||||
fn render_every_sprite_at_several_sizes() {
|
||||
for (w, asc, desc, th) in [
|
||||
(9, 15, 2, 1),
|
||||
(12, 20, 4, 3),
|
||||
(18, 30, 6, 4),
|
||||
(7, 11, 2, 1),
|
||||
(25, 40, 8, 5),
|
||||
] {
|
||||
let metrics = test_metrics(w, asc, desc, th);
|
||||
let face = Face::new(metrics);
|
||||
let mut atlas = Atlas::new(4096, crate::atlas::Format::Grayscale);
|
||||
|
||||
let mut cps: Vec<u32> = ranges().iter().flat_map(|r| r.min..=r.max).collect();
|
||||
cps.extend(Sprite::ALL.iter().map(|s| *s as u32));
|
||||
|
||||
let mut inked = 0usize;
|
||||
let mut count = 0usize;
|
||||
for cp in cps {
|
||||
for cell_width in [None, Some(1), Some(2)] {
|
||||
let g = face
|
||||
.render_glyph(&mut atlas, cp, RenderOptions { cell_width })
|
||||
.unwrap_or_else(|e| panic!("cp {cp:X} {w}x{}: {e}", asc + desc));
|
||||
// The glyph must stay within the padded canvas.
|
||||
let cw = metrics.cell_width * u32::from(cell_width.unwrap_or(1).max(1));
|
||||
assert!(g.width <= cw + 2 * (cw / 4), "cp {cp:X} too wide");
|
||||
assert!(
|
||||
g.height <= metrics.cell_height + 2 * (metrics.cell_height / 4),
|
||||
"cp {cp:X} too tall"
|
||||
);
|
||||
if g.width > 0 && g.height > 0 {
|
||||
// Trimmed regions must contain ink in every edge row/col
|
||||
// (that is the definition of trim).
|
||||
let row = |y: u32| {
|
||||
(0..g.width).any(|x| {
|
||||
atlas.data[((g.atlas_y + y) * atlas.size + g.atlas_x + x) as usize]
|
||||
!= 0
|
||||
})
|
||||
};
|
||||
assert!(row(0) && row(g.height - 1), "cp {cp:X} not trimmed");
|
||||
inked += 1;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
// Most sprites have ink; only blank braille/space-like ones don't.
|
||||
assert!(inked * 10 > count * 9, "{inked} of {count} have ink");
|
||||
}
|
||||
|
||||
// A review sheet of the most common blocks.
|
||||
for (w, asc, desc, th) in [(9, 15, 2, 1), (18, 30, 6, 4)] {
|
||||
let metrics = test_metrics(w, asc, desc, th);
|
||||
let height = asc + desc;
|
||||
let (px, py) = (w / 4, height / 4);
|
||||
let (sx, sy) = (w + 2 * px, height + 2 * py);
|
||||
let cps: Vec<u32> = ranges().iter().flat_map(|r| r.min..=r.max).collect();
|
||||
let cols = 64u32;
|
||||
let rows = (cps.len() as u32).div_ceil(cols);
|
||||
let (aw, ah) = (sx * cols, sy * rows);
|
||||
let mut sheet = vec![0u8; (aw * ah) as usize];
|
||||
let mut canvas = Canvas::new(w, height, px, py);
|
||||
for (n, cp) in cps.iter().enumerate() {
|
||||
let draw = get_draw_fn(*cp).expect("draw fn");
|
||||
draw(*cp, &mut canvas, w, height, &metrics).expect("draw");
|
||||
let (ox, oy) = (sx * (n as u32 % cols), sy * (n as u32 / cols));
|
||||
for y in 0..canvas.sfc_height {
|
||||
let src =
|
||||
&canvas.sfc[(y * canvas.sfc_width) as usize..][..canvas.sfc_width as usize];
|
||||
let dst = ((oy + y) * aw + ox) as usize;
|
||||
sheet[dst..dst + src.len()].copy_from_slice(src);
|
||||
}
|
||||
canvas.reset();
|
||||
}
|
||||
write_png(
|
||||
&out_dir().join(format!("review-all-{w}x{height}.png")),
|
||||
aw,
|
||||
ah,
|
||||
&sheet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_glyph_offsets_for_cursor_bar() {
|
||||
let face = Face::new(test_metrics(9, 15, 2, 1));
|
||||
let mut atlas = Atlas::new(256, crate::atlas::Format::Grayscale);
|
||||
let g = face
|
||||
.render_glyph(
|
||||
&mut atlas,
|
||||
Sprite::CursorBar as u32,
|
||||
RenderOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
// 1px bar placed at x = -1 (half its thickness rounded up).
|
||||
assert_eq!(1, g.width);
|
||||
assert_eq!(17, g.height);
|
||||
assert_eq!(-1, g.offset_x);
|
||||
// offset_y = region.height + clip_bottom - padding_y = 17 + 4 - 4
|
||||
assert_eq!(17, g.offset_y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! The built-in "sprite font": box drawing, block elements, braille, powerline
|
||||
//! and other glyphs rasterized by code, plus special sprites (underlines,
|
||||
//! cursors). Port of `font/sprite.zig`.
|
||||
|
||||
pub mod canvas;
|
||||
pub mod draw;
|
||||
pub mod face;
|
||||
|
||||
pub use canvas::{Box, Canvas, Color, Point};
|
||||
pub use face::Face;
|
||||
|
||||
/// Sprites are represented as special codepoints outside of the Unicode
|
||||
/// codepoint range. Unicode maxes out at U+10FFFF (21 bits), and we use the
|
||||
/// high 11 bits to hide our special characters.
|
||||
///
|
||||
/// These characters are ONLY used for rendering and NEVER used written to
|
||||
/// text files or any other exported format, so we don't use the Private Use
|
||||
/// Area of Unicode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u32)]
|
||||
pub enum Sprite {
|
||||
Underline = Sprite::START,
|
||||
UnderlineDouble,
|
||||
UnderlineDotted,
|
||||
UnderlineDashed,
|
||||
UnderlineCurly,
|
||||
|
||||
Strikethrough,
|
||||
|
||||
Overline,
|
||||
|
||||
CursorRect,
|
||||
CursorHollowRect,
|
||||
CursorBar,
|
||||
CursorUnderline,
|
||||
}
|
||||
|
||||
impl Sprite {
|
||||
/// Start 1 above the maximum Unicode codepoint.
|
||||
pub const START: u32 = 0x1F_FFFF + 1;
|
||||
pub const END: u32 = u32::MAX;
|
||||
|
||||
pub const ALL: [Sprite; 11] = [
|
||||
Sprite::Underline,
|
||||
Sprite::UnderlineDouble,
|
||||
Sprite::UnderlineDotted,
|
||||
Sprite::UnderlineDashed,
|
||||
Sprite::UnderlineCurly,
|
||||
Sprite::Strikethrough,
|
||||
Sprite::Overline,
|
||||
Sprite::CursorRect,
|
||||
Sprite::CursorHollowRect,
|
||||
Sprite::CursorBar,
|
||||
Sprite::CursorUnderline,
|
||||
];
|
||||
|
||||
/// Convert a codepoint in the sprite range to a `Sprite`.
|
||||
pub fn from_u32(cp: u32) -> Option<Sprite> {
|
||||
Sprite::ALL.iter().copied().find(|s| *s as u32 == cp)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sprite_start() {
|
||||
assert_eq!(Sprite::START, Sprite::Underline as u32);
|
||||
assert_eq!(Sprite::START + 10, Sprite::CursorUnderline as u32);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user