feat: bundled font and text layout for the editor
This commit is contained in:
1 parent
7e08fefc1d
commit
07a4858175
4 files changed
+204
No files matched your search
Generated
+32
@@ -2,6 +2,22 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ab_glyph"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
|
||||
dependencies = [
|
||||
"ab_glyph_rasterizer",
|
||||
"owned_ttf_parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ab_glyph_rasterizer"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
|
||||
[[package]]
|
||||
name = "accesskit"
|
||||
version = "0.24.1"
|
||||
@@ -1834,6 +1850,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
|
||||
dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -2700,6 +2725,12 @@ dependencies = [
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
@@ -2755,6 +2786,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
name = "valw"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
|
||||
@@ -6,6 +6,7 @@ description = "macOS-style screenshots for niri"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
ab_glyph = "0.2.32"
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4.6.7", features = ["derive"] }
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
pub mod doc;
|
||||
pub mod export;
|
||||
pub mod shape;
|
||||
// Used from Task 2 of the 5b plan on; that task removes this.
|
||||
#[allow(dead_code)]
|
||||
pub mod text;
|
||||
pub mod view;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Text for the editor: the bundled DejaVu Sans Bold, laid out with
|
||||
//! ab_glyph and turned into tiny-skia paths, so canvas and export draw the
|
||||
//! same glyphs.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use ab_glyph::{Font, FontRef, GlyphId, OutlineCurve, PxScale, ScaleFont};
|
||||
use tiny_skia::{Path, PathBuilder};
|
||||
|
||||
use crate::editor::shape::P;
|
||||
|
||||
static FONT_DATA: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans-Bold.ttf");
|
||||
|
||||
pub static FONT: LazyLock<FontRef<'static>> =
|
||||
LazyLock::new(|| FontRef::try_from_slice(FONT_DATA).expect("the bundled font is valid"));
|
||||
|
||||
pub fn line_height(size: f32) -> f32 {
|
||||
size * 1.2
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Layout {
|
||||
/// Each glyph and its pen position (baseline) relative to the box's top-left.
|
||||
pub glyphs: Vec<(GlyphId, P)>,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
/// The width of every line.
|
||||
pub lines: Vec<f32>,
|
||||
}
|
||||
|
||||
pub fn layout(text: &str, size: f32) -> Layout {
|
||||
if text.is_empty() {
|
||||
return Layout {
|
||||
glyphs: Vec::new(),
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
lines: Vec::new(),
|
||||
};
|
||||
}
|
||||
let font = FONT.as_scaled(PxScale::from(size));
|
||||
let mut glyphs = Vec::new();
|
||||
let mut lines = Vec::new();
|
||||
for (i, line) in text.split('\n').enumerate() {
|
||||
let baseline = i as f32 * line_height(size) + font.ascent();
|
||||
let mut x = 0.0;
|
||||
let mut previous = None;
|
||||
for c in line.chars() {
|
||||
let id = font.glyph_id(c);
|
||||
if let Some(p) = previous {
|
||||
x += font.kern(p, id);
|
||||
}
|
||||
glyphs.push((id, (x, baseline)));
|
||||
x += font.h_advance(id);
|
||||
previous = Some(id);
|
||||
}
|
||||
lines.push(x);
|
||||
}
|
||||
Layout {
|
||||
glyphs,
|
||||
width: lines.iter().copied().fold(0.0, f32::max),
|
||||
height: lines.len() as f32 * line_height(size),
|
||||
lines,
|
||||
}
|
||||
}
|
||||
|
||||
/// The outlines of `text` with its line box's top-left at `origin`.
|
||||
pub fn path(text: &str, size: f32, origin: P) -> Option<Path> {
|
||||
let layout = layout(text, size);
|
||||
let scale = FONT.as_scaled(PxScale::from(size)).scale_factor();
|
||||
let mut pb = PathBuilder::new();
|
||||
for (id, (gx, gy)) in layout.glyphs {
|
||||
let Some(outline) = FONT.outline(id) else {
|
||||
continue;
|
||||
};
|
||||
// Font units, y up; the pen sits on the baseline.
|
||||
let at = |p: ab_glyph::Point| {
|
||||
(
|
||||
origin.0 + gx + p.x * scale.horizontal,
|
||||
origin.1 + gy - p.y * scale.vertical,
|
||||
)
|
||||
};
|
||||
let mut end: Option<ab_glyph::Point> = None;
|
||||
for curve in &outline.curves {
|
||||
let start = match curve {
|
||||
OutlineCurve::Line(a, _)
|
||||
| OutlineCurve::Quad(a, _, _)
|
||||
| OutlineCurve::Cubic(a, _, _, _) => *a,
|
||||
};
|
||||
if end != Some(start) {
|
||||
if end.is_some() {
|
||||
pb.close();
|
||||
}
|
||||
let (x, y) = at(start);
|
||||
pb.move_to(x, y);
|
||||
}
|
||||
match curve {
|
||||
OutlineCurve::Line(_, b) => {
|
||||
let (x, y) = at(*b);
|
||||
pb.line_to(x, y);
|
||||
end = Some(*b);
|
||||
}
|
||||
OutlineCurve::Quad(_, c, b) => {
|
||||
let ((cx, cy), (x, y)) = (at(*c), at(*b));
|
||||
pb.quad_to(cx, cy, x, y);
|
||||
end = Some(*b);
|
||||
}
|
||||
OutlineCurve::Cubic(_, c1, c2, b) => {
|
||||
let ((ax, ay), (bx, by), (x, y)) = (at(*c1), at(*c2), at(*b));
|
||||
pb.cubic_to(ax, ay, bx, by, x, y);
|
||||
end = Some(*b);
|
||||
}
|
||||
}
|
||||
}
|
||||
if end.is_some() {
|
||||
pb.close();
|
||||
}
|
||||
}
|
||||
pb.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_text_has_no_size() {
|
||||
let l = layout("", 24.0);
|
||||
assert_eq!((l.width, l.height), (0.0, 0.0));
|
||||
assert!(path("", 24.0, (0.0, 0.0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_and_widths() {
|
||||
let one = layout("Hello", 24.0);
|
||||
let longer = layout("Hello!", 24.0);
|
||||
assert!(longer.width > one.width && one.width > 0.0);
|
||||
assert_eq!(one.height, line_height(24.0));
|
||||
let two = layout("Hi\nthere", 24.0);
|
||||
assert_eq!(two.height, 2.0 * line_height(24.0));
|
||||
assert_eq!(two.lines.len(), 2);
|
||||
assert!(two.lines[1] > two.lines[0]);
|
||||
assert_eq!(two.width, two.lines[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kerning_is_applied() {
|
||||
let font = FONT.as_scaled(PxScale::from(48.0));
|
||||
let (a, v) = (FONT.glyph_id('A'), FONT.glyph_id('V'));
|
||||
let plain = font.h_advance(a) + font.h_advance(v);
|
||||
assert!(font.kern(a, v) < 0.0, "DejaVu kerns AV");
|
||||
assert!(layout("AV", 48.0).width < plain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turkish_letters_have_glyphs() {
|
||||
for c in "ğüşıöçĞÜŞİÖÇ".chars() {
|
||||
assert_ne!(FONT.glyph_id(c).0, 0, "{c} has no glyph");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_sits_in_the_line_box() {
|
||||
let p = path("Hg", 24.0, (100.0, 50.0)).unwrap();
|
||||
let b = p.bounds();
|
||||
assert!(b.left() >= 100.0 && b.top() >= 50.0, "{b:?}");
|
||||
assert!(b.bottom() <= 50.0 + line_height(24.0), "{b:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user