feat(font): port the Linux font engine
Ports the rest of ZIG/src/font for the freetype/fontconfig/harfbuzz backend: face loading and rasterization (synthetic bold/italic, color emoji bitmaps, glyph constraints for nerd font icons), fontconfig discovery, DeferredFace, Collection, CodepointResolver, CodepointMap, SharedGrid, opentype table parsers, the nerd font attribute table, and the harfbuzz shaper with feature parsing and shaping cache. Embedded fonts (JetBrains Mono 2.304 variable, Symbols Nerd Font 3.4.0, Noto emoji) and the test fonts from Ghostty live in res/ with their OFL/ MIT licenses. The crate opts into unsafe_code = "deny"; FFI is confined to ft.rs, fontconfig.rs and hb.rs with SAFETY comments. Glyph resampling uses an stb-style resampler that is not bit-identical to stb. Deferred to the next font pass (needs ghostty-terminal/config): shaper/run.zig, SharedGridSet.zig and the 23 terminal-driven shaper tests. Config-facing types are mirrored locally in config_types.rs. Test Plan: - cargo test -p ghostty-font (139 tests)
This commit is contained in:
1 parent
b9ac54e794
commit
fc7bb5c025
64 files changed
+35316
-23
No files matched your search
@@ -0,0 +1,111 @@
|
||||
//! This structure caches the shaped cells for a given text run.
|
||||
//!
|
||||
//! At one point, shaping was the most expensive part of rendering text
|
||||
//! (accounting for 96% of frame time on my machine). To speed it up, this
|
||||
//! was introduced so that shaping results can be cached depending on the
|
||||
//! run.
|
||||
//!
|
||||
//! The cache key is the text run. The text run builds its own hash value
|
||||
//! based on the font, style, codepoint, etc. This just utilizes the hash that
|
||||
//! the text run provides.
|
||||
|
||||
use ghostty_datastruct::cache_table::{CacheContext, CacheTable};
|
||||
|
||||
use crate::shape::{Cell, TextRun};
|
||||
|
||||
/// Context for cache table.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CellCacheTableContext;
|
||||
|
||||
impl CacheContext<u64, Vec<Cell>> for CellCacheTableContext {
|
||||
fn hash(&mut self, key: &u64) -> u64 {
|
||||
*key
|
||||
}
|
||||
fn eql(&mut self, a: &u64, b: &u64) -> bool {
|
||||
a == b
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache table for run hash -> shaped cells.
|
||||
///
|
||||
/// Capacity is slightly arbitrary. These numbers are guesses: I'd expect an
|
||||
/// average of 256 frequently cached runs is a safe guess for most terminal
|
||||
/// screens, with 8 items per bucket to give decent resiliency to important
|
||||
/// runs.
|
||||
type CellCacheTable = CacheTable<u64, Vec<Cell>, CellCacheTableContext, 256, 8>;
|
||||
|
||||
/// The cache table of shaped cells.
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
map: CellCacheTable,
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new() -> Cache {
|
||||
Cache {
|
||||
map: CellCacheTable::new(CellCacheTableContext),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the shaped cells for the given text run,
|
||||
/// or None if they are not in the cache.
|
||||
pub fn get(&mut self, run: &TextRun) -> Option<&[Cell]> {
|
||||
self.map.get(&run.hash).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Insert the shaped cells for the given text run into the cache.
|
||||
///
|
||||
/// The cells will be duplicated.
|
||||
pub fn put(&mut self, run: &TextRun, cells: &[Cell]) {
|
||||
// An evicted entry is simply dropped.
|
||||
let _evicted = self.map.put(run.hash, cells.to_vec());
|
||||
}
|
||||
|
||||
/// Remove everything from the cache.
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::collection::Index;
|
||||
|
||||
#[test]
|
||||
fn cache() {
|
||||
let mut c = Cache::new();
|
||||
|
||||
let run = TextRun {
|
||||
hash: 1,
|
||||
offset: 0,
|
||||
cells: 0,
|
||||
font_index: Index::default(),
|
||||
};
|
||||
assert!(c.get(&run).is_none());
|
||||
c.put(
|
||||
&run,
|
||||
&[
|
||||
Cell {
|
||||
x: 0,
|
||||
glyph_index: 0,
|
||||
..Cell::default()
|
||||
},
|
||||
Cell {
|
||||
x: 1,
|
||||
glyph_index: 1,
|
||||
..Cell::default()
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let actual = c.get(&run).unwrap();
|
||||
assert_eq!(actual.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
//! OpenType font feature settings. Port of `font/shaper/feature.zig`.
|
||||
|
||||
// The explicit `continue`s mirror the Zig state machine one-to-one.
|
||||
#![allow(clippy::needless_continue, clippy::single_match_else)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A minimal byte reader mirroring the `std.io.fixedBufferStream` reader
|
||||
/// semantics that the Zig parser relies on: reading past the end yields a
|
||||
/// `,` (the parser uses `reader.readByte() catch ','`).
|
||||
pub struct Reader<'a> {
|
||||
buf: &'a [u8],
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn new(buf: &'a [u8]) -> Self {
|
||||
Reader { buf, pos: 0 }
|
||||
}
|
||||
|
||||
/// Read a byte; at end of stream returns `,` without advancing.
|
||||
fn read_byte(&mut self) -> u8 {
|
||||
match self.buf.get(self.pos) {
|
||||
Some(&b) => {
|
||||
self.pos += 1;
|
||||
b
|
||||
}
|
||||
None => b',',
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip until after the next `,` or end of stream.
|
||||
fn skip_until_delimiter_or_eof(&mut self) {
|
||||
while let Some(&b) = self.buf.get(self.pos) {
|
||||
self.pos += 1;
|
||||
if b == b',' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn at_end(&self) -> bool {
|
||||
self.pos >= self.buf.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an OpenType font feature setting, which consists of a tag and
|
||||
/// a numeric parameter >= 0. Most features are boolean, so only parameters
|
||||
/// of 0 and 1 make sense for them, but some (e.g. 'cv01'..'cv99') can take
|
||||
/// parameters to choose between multiple variants of a given character or
|
||||
/// characters.
|
||||
///
|
||||
/// Ref:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#features-and-lookups
|
||||
/// - https://harfbuzz.github.io/shaping-opentype-features.html
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Feature {
|
||||
pub tag: [u8; 4],
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum State {
|
||||
/// Initial state.
|
||||
Start,
|
||||
/// Parsing the tag.
|
||||
Tag,
|
||||
/// In the space between the tag and the value.
|
||||
Space,
|
||||
/// Parsing an integer parameter directly in to `value`.
|
||||
Int,
|
||||
/// Parsing a boolean keyword parameter ("on"/"off").
|
||||
Bool,
|
||||
/// Encountered an unrecoverable syntax error, advancing to boundary.
|
||||
Err,
|
||||
/// Done parsing feature, skip whitespace until end.
|
||||
Done,
|
||||
/// Successfully finished.
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl Feature {
|
||||
pub fn from_string(s: &str) -> Option<Feature> {
|
||||
Self::from_bytes(s.as_bytes())
|
||||
}
|
||||
|
||||
pub fn from_bytes(s: &[u8]) -> Option<Feature> {
|
||||
Self::from_reader(&mut Reader::new(s))
|
||||
}
|
||||
|
||||
/// Parse a single font feature setting from a reader, with a version
|
||||
/// of the syntax of HarfBuzz's font feature strings. Stops at end of stream
|
||||
/// or when a ',' is encountered.
|
||||
///
|
||||
/// This parsing aims to be as error-tolerant as possible while avoiding any
|
||||
/// assumptions in ambiguous scenarios. When invalid syntax is encountered,
|
||||
/// the reader is advanced to the next boundary (end-of-stream or ',') so
|
||||
/// that further features may be read.
|
||||
///
|
||||
/// Ref: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string
|
||||
pub fn from_reader(reader: &mut Reader<'_>) -> Option<Feature> {
|
||||
let mut tag = [0u8; 4];
|
||||
let mut tag_len = 0usize;
|
||||
let mut value: Option<u32> = None;
|
||||
let mut state = State::Start;
|
||||
|
||||
loop {
|
||||
match state {
|
||||
// If we're done then we skip whitespace until we see a ','.
|
||||
State::Finished => break,
|
||||
State::Done => loop {
|
||||
match reader.read_byte() {
|
||||
b' ' | b'\t' => continue,
|
||||
b',' => {
|
||||
state = State::Finished;
|
||||
break;
|
||||
}
|
||||
// If we see something other than whitespace or a ','
|
||||
// then this is an error since the intent is unclear.
|
||||
_ => {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// If we're fast-forwarding from an error we just wanna
|
||||
// stop at the first boundary and ignore all other bytes.
|
||||
State::Err => {
|
||||
reader.skip_until_delimiter_or_eof();
|
||||
return None;
|
||||
}
|
||||
|
||||
State::Start => loop {
|
||||
match reader.read_byte() {
|
||||
// Ignore leading whitespace.
|
||||
b' ' | b'\t' => continue,
|
||||
// Empty feature string.
|
||||
b',' => return None,
|
||||
// '+' prefix to explicitly enable feature.
|
||||
b'+' => {
|
||||
value = Some(1);
|
||||
state = State::Tag;
|
||||
break;
|
||||
}
|
||||
// '-' prefix to explicitly disable feature.
|
||||
b'-' => {
|
||||
value = Some(0);
|
||||
state = State::Tag;
|
||||
break;
|
||||
}
|
||||
// Quote mark introducing a tag.
|
||||
b'"' | b'\'' => {
|
||||
state = State::Tag;
|
||||
break;
|
||||
}
|
||||
// First letter of tag.
|
||||
byte => {
|
||||
tag_len = 1;
|
||||
tag[0] = byte;
|
||||
state = State::Tag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
State::Tag => loop {
|
||||
match reader.read_byte() {
|
||||
// If the tag is interrupted by a comma it's invalid.
|
||||
b',' => return None,
|
||||
// Ignore quote marks. This does technically ignore cases like
|
||||
// "'k'e'r'n' = 0", but it's unambiguous so if someone really
|
||||
// wants to do that in their config then... sure why not.
|
||||
b'"' | b'\'' => continue,
|
||||
// In all other cases we add the byte to our tag.
|
||||
byte => {
|
||||
tag[tag_len] = byte;
|
||||
tag_len += 1;
|
||||
if tag_len == 4 {
|
||||
state = State::Space;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
State::Space => {
|
||||
loop {
|
||||
match reader.read_byte() {
|
||||
b' ' | b'\t' => continue,
|
||||
// Ignore quote marks since we might have a
|
||||
// closing quote from the tag still ahead.
|
||||
b'"' | b'\'' => continue,
|
||||
// Allow an '=' (which we can safely ignore)
|
||||
// only if we don't already have a value due
|
||||
// to a '+' or '-' prefix.
|
||||
b'=' => {
|
||||
if value.is_some() {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
b',' => {
|
||||
// Specifying only a tag turns a feature on.
|
||||
if value.is_none() {
|
||||
value = Some(1);
|
||||
}
|
||||
state = State::Finished;
|
||||
break;
|
||||
}
|
||||
byte @ b'0'..=b'9' => {
|
||||
// If we already have value because of a
|
||||
// '+' or '-' prefix then this is an error.
|
||||
if value.is_some() {
|
||||
state = State::Err;
|
||||
} else {
|
||||
value = Some(u32::from(byte - b'0'));
|
||||
state = State::Int;
|
||||
}
|
||||
break;
|
||||
}
|
||||
b'o' | b'O' => {
|
||||
// If we already have value because of a
|
||||
// '+' or '-' prefix then this is an error.
|
||||
if value.is_some() {
|
||||
state = State::Err;
|
||||
} else {
|
||||
state = State::Bool;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State::Int => {
|
||||
loop {
|
||||
match reader.read_byte() {
|
||||
b',' => {
|
||||
state = State::Finished;
|
||||
break;
|
||||
}
|
||||
byte @ b'0'..=b'9' => {
|
||||
// If our value gets too big while
|
||||
// parsing we consider it an error.
|
||||
let v = value.unwrap_or(0);
|
||||
match v
|
||||
.checked_mul(10)
|
||||
.and_then(|v| v.checked_add(u32::from(byte - b'0')))
|
||||
{
|
||||
Some(n) => value = Some(n),
|
||||
None => {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State::Bool => loop {
|
||||
match reader.read_byte() {
|
||||
b',' => return None,
|
||||
b'n' | b'N' => {
|
||||
// "ofn"
|
||||
if value.is_some() {
|
||||
debug_assert_eq!(value, Some(0));
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
value = Some(1);
|
||||
state = State::Done;
|
||||
break;
|
||||
}
|
||||
b'f' | b'F' => {
|
||||
// To make sure we consume two 'f's.
|
||||
if value.is_none() {
|
||||
value = Some(0);
|
||||
} else {
|
||||
debug_assert_eq!(value, Some(0));
|
||||
state = State::Done;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
state = State::Err;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if state == State::Finished {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(value.is_some());
|
||||
debug_assert_eq!(tag_len, 4);
|
||||
|
||||
Some(Feature { tag, value: value? })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Feature {
|
||||
/// Serialize this feature. The string that this produces should be valid
|
||||
/// to parse.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let tag = String::from_utf8_lossy(&self.tag);
|
||||
if self.value <= 1 {
|
||||
// Format boolean options as "+tag" for on and "-tag" for off.
|
||||
write!(f, "{}{}", if self.value == 1 { '+' } else { '-' }, tag)
|
||||
} else {
|
||||
// Format non-boolean tags as "tag=value".
|
||||
write!(f, "{tag}={}", self.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of font feature settings (see `Feature` for more documentation).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct FeatureList {
|
||||
pub features: Vec<Feature>,
|
||||
}
|
||||
|
||||
impl FeatureList {
|
||||
/// Parse a comma separated list of features.
|
||||
/// See `Feature::from_reader` for more docs.
|
||||
pub fn from_string(s: &str) -> FeatureList {
|
||||
let mut this = FeatureList::default();
|
||||
this.append_from_string(s);
|
||||
this
|
||||
}
|
||||
|
||||
/// Append features to this list from a string with a comma separated list.
|
||||
/// See `Feature::from_reader` for more docs.
|
||||
pub fn append_from_string(&mut self, s: &str) {
|
||||
let mut reader = Reader::new(s.as_bytes());
|
||||
while !reader.at_end() {
|
||||
let i = reader.pos;
|
||||
if let Some(feature) = Feature::from_reader(&mut reader) {
|
||||
self.features.push(feature);
|
||||
} else {
|
||||
log::warn!(
|
||||
"failed to parse font feature setting: \"{}\"",
|
||||
String::from_utf8_lossy(&s.as_bytes()[i..reader.pos])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FeatureList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, feature) in self.features.iter().enumerate() {
|
||||
if i != 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
write!(f, "{feature}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// These features are hardcoded to always be on by default. Users
|
||||
/// can turn them off by setting the features to "-liga" for example.
|
||||
pub const DEFAULT_FEATURES: [Feature; 1] = [Feature {
|
||||
tag: *b"liga",
|
||||
value: 1,
|
||||
}];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn f(tag: [u8; 4], value: u32) -> Feature {
|
||||
Feature { tag, value }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_from_string() {
|
||||
// This is not *complete* coverage of every possible
|
||||
// combination of syntax, but it covers quite a few.
|
||||
|
||||
// Boolean settings (on)
|
||||
let kern_on = Some(f(*b"kern", 1));
|
||||
for s in [
|
||||
"kern",
|
||||
"kern, ",
|
||||
"kern on",
|
||||
"kern on, ",
|
||||
"+kern",
|
||||
"+kern, ",
|
||||
"\"kern\" = 1",
|
||||
"\"kern\" = 1, ",
|
||||
] {
|
||||
assert_eq!(kern_on, Feature::from_string(s), "{s}");
|
||||
}
|
||||
|
||||
// Boolean settings (off)
|
||||
let kern_off = Some(f(*b"kern", 0));
|
||||
for s in [
|
||||
"kern off",
|
||||
"kern off, ",
|
||||
"-'kern'",
|
||||
"-'kern', ",
|
||||
"\"kern\" = 0",
|
||||
"\"kern\" = 0, ",
|
||||
] {
|
||||
assert_eq!(kern_off, Feature::from_string(s), "{s}");
|
||||
}
|
||||
|
||||
// Non-boolean settings
|
||||
let aalt_2 = Some(f(*b"aalt", 2));
|
||||
for s in ["aalt=2", "aalt=2, ", "'aalt' 2", "'aalt' 2, "] {
|
||||
assert_eq!(aalt_2, Feature::from_string(s), "{s}");
|
||||
}
|
||||
|
||||
// Various ambiguous/error cases which should be null
|
||||
for s in [
|
||||
"aalt=2x", // bad number
|
||||
"toolong", // tag too long
|
||||
"sht", // tag too short
|
||||
"-kern 1", // redundant/conflicting
|
||||
"-kern on", // redundant/conflicting
|
||||
"aalt=o,", // bad keyword
|
||||
"aalt=ofn,", // bad keyword
|
||||
] {
|
||||
assert_eq!(None, Feature::from_string(s), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_list_from_string() {
|
||||
let s = concat!(
|
||||
" kern, kern on , +kern, \"kern\" = 1,", // Boolean settings (on)
|
||||
"kern off, -'kern' , \"kern\"=0,", // Boolean settings (off)
|
||||
"aalt=2, 'aalt'\t2,", // Non-boolean settings
|
||||
"aalt=2x, toolong, sht, -kern 1, -kern on, aalt=o, aalt=ofn,", // Invalid cases
|
||||
"last", // To ensure final element is included correctly.
|
||||
);
|
||||
let feats = FeatureList::from_string(s);
|
||||
let mut expected = vec![f(*b"kern", 1); 4];
|
||||
expected.extend(vec![f(*b"kern", 0); 3]);
|
||||
expected.extend(vec![f(*b"aalt", 2); 2]);
|
||||
expected.push(f(*b"last", 1));
|
||||
assert_eq!(expected, feats.features);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_display() {
|
||||
assert_eq!("+kern", f(*b"kern", 1).to_string());
|
||||
assert_eq!("-kern", f(*b"kern", 0).to_string());
|
||||
assert_eq!("aalt=3", f(*b"aalt", 3).to_string());
|
||||
assert_eq!(
|
||||
"+liga, ss01=2",
|
||||
FeatureList::from_string("liga, ss01=2").to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_features_has_liga() {
|
||||
assert_eq!(DEFAULT_FEATURES, [f(*b"liga", 1)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
//! HarfBuzz shaper. Port of `font/shaper/harfbuzz.zig`.
|
||||
//!
|
||||
//! The Zig shaper is driven by `RunIterator` (shaper/run.zig, deferred to F2)
|
||||
//! through the hooks `prepare` / `addCodepoint` / `finalize`; those hooks are
|
||||
//! exposed here as [`Shaper::prepare`], [`Shaper::add_codepoint`] and
|
||||
//! [`Shaper::finalize`]. [`Shaper::shape`] takes the face of the run's font
|
||||
//! (or `None` for the special sprite font, whose codepoint == glyph index)
|
||||
//! instead of a `TextRun` + `SharedGrid`; the caller is responsible for
|
||||
//! holding whatever lock protects the face (FreeType faces are not thread
|
||||
//! safe). [`Shaper::shape_text`] is a convenience that does the whole
|
||||
//! prepare/add/finalize/shape sequence.
|
||||
|
||||
use super::hb;
|
||||
use crate::face::freetype::Face;
|
||||
use crate::shape::{self, Cell, DEFAULT_FEATURES, FeatureList};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ShapeError {
|
||||
#[error("failed to create harfbuzz buffer")]
|
||||
BufferCreate,
|
||||
#[error("harfbuzz failed")]
|
||||
HarfbuzzFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Codepoint {
|
||||
cluster: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct RunOffset {
|
||||
cluster: u32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct CellOffset {
|
||||
cluster: u32,
|
||||
x: i32,
|
||||
}
|
||||
|
||||
/// Shaper that uses Harfbuzz.
|
||||
pub struct Shaper {
|
||||
/// The buffer used for text shaping. We reuse it across multiple shaping
|
||||
/// calls to prevent allocations.
|
||||
hb_buf: hb::Buffer,
|
||||
|
||||
/// The shared memory used for shaping results.
|
||||
cell_buf: Vec<Cell>,
|
||||
|
||||
/// The features to use for shaping.
|
||||
hb_feats: Vec<hb::Feature>,
|
||||
|
||||
/// The codepoints added to the buffer before shaping. We need to keep
|
||||
/// these separately because after shaping, HarfBuzz replaces codepoints
|
||||
/// with glyph indices in the buffer.
|
||||
codepoints: Vec<Codepoint>,
|
||||
}
|
||||
|
||||
impl Shaper {
|
||||
pub fn new(opts: &shape::Options) -> Result<Shaper, ShapeError> {
|
||||
// Parse all the features we want to use.
|
||||
let mut feature_list = FeatureList::default();
|
||||
feature_list.features.extend_from_slice(&DEFAULT_FEATURES);
|
||||
for feature_str in &opts.features {
|
||||
feature_list.append_from_string(feature_str);
|
||||
}
|
||||
|
||||
let hb_feats = feature_list
|
||||
.features
|
||||
.iter()
|
||||
.map(|f| hb::global_feature(f.tag, f.value))
|
||||
.collect();
|
||||
|
||||
Ok(Shaper {
|
||||
hb_buf: hb::Buffer::new().ok_or(ShapeError::BufferCreate)?,
|
||||
cell_buf: Vec::new(),
|
||||
hb_feats,
|
||||
codepoints: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) {}
|
||||
|
||||
/// Number of codepoints currently in the HarfBuzz buffer (before
|
||||
/// shaping) / glyphs (after shaping).
|
||||
pub fn hb_buf_len(&self) -> usize {
|
||||
self.hb_buf.len()
|
||||
}
|
||||
|
||||
// ---- RunIterator hooks ----
|
||||
|
||||
/// Reset the buffer for the current run.
|
||||
pub fn prepare(&mut self) {
|
||||
self.hb_buf.reset();
|
||||
self.hb_buf.set_content_type_unicode();
|
||||
|
||||
// We set the cluster level to `characters` to give us the most
|
||||
// granularity, matching the CoreText shaper, and allowing us
|
||||
// to use our same ligature detection heuristics.
|
||||
self.hb_buf.set_cluster_level_characters();
|
||||
|
||||
self.codepoints.clear();
|
||||
|
||||
// We don't support RTL text because RTL in terminals is messy.
|
||||
// Its something we want to improve. For now, we force LTR because
|
||||
// our renderers assume a strictly increasing X value.
|
||||
self.hb_buf.set_direction_ltr();
|
||||
}
|
||||
|
||||
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
// We pass the index into codepoints as the cluster value to HarfBuzz.
|
||||
// After shaping, we use info.cluster to get back the index, which
|
||||
// lets us look up the original cluster value from codepoints.
|
||||
let index = self.codepoints.len() as u32;
|
||||
self.hb_buf.add(cp, index);
|
||||
self.codepoints.push(Codepoint { cluster });
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self) {
|
||||
self.hb_buf.guess_segment_properties();
|
||||
}
|
||||
|
||||
/// Convenience: shape `text` as a single run where every item is a
|
||||
/// `(codepoint, cluster)` pair (cluster = cell x position).
|
||||
pub fn shape_text(
|
||||
&mut self,
|
||||
face: Option<&Face>,
|
||||
text: &[(u32, u32)],
|
||||
) -> Result<&[Cell], ShapeError> {
|
||||
self.prepare();
|
||||
for &(cp, cluster) in text {
|
||||
self.add_codepoint(cp, cluster);
|
||||
}
|
||||
self.finalize();
|
||||
self.shape(face)
|
||||
}
|
||||
|
||||
/// Shape the current run (as built through the hooks). `face` is the face
|
||||
/// for the run's font index, or `None` if the font is a special-case
|
||||
/// font (sprite), for which codepoint == glyph_index.
|
||||
///
|
||||
/// The return value is only valid until the next shape call is called.
|
||||
pub fn shape(&mut self, face: Option<&Face>) -> Result<&[Cell], ShapeError> {
|
||||
// We only do shaping if the font is not a special-case. For special-case
|
||||
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
|
||||
if let Some(face) = face {
|
||||
let i = if face.quirks_disable_default_font_features {
|
||||
// If we are disabling default font features we just offset
|
||||
// our features by the hardcoded items because always
|
||||
// add those at the beginning.
|
||||
DEFAULT_FEATURES.len()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
hb::shape(face.hb_font(), &mut self.hb_buf, &self.hb_feats[i..]);
|
||||
}
|
||||
|
||||
self.cell_buf.clear();
|
||||
|
||||
// If our buffer is empty, we short-circuit the rest of the work
|
||||
// return nothing.
|
||||
if self.hb_buf.is_empty() {
|
||||
return Ok(&self.cell_buf);
|
||||
}
|
||||
let info = self.hb_buf.glyph_infos();
|
||||
let pos = self
|
||||
.hb_buf
|
||||
.glyph_positions()
|
||||
.ok_or(ShapeError::HarfbuzzFailed)?;
|
||||
|
||||
// This is perhaps not true somewhere, but we currently assume it is true.
|
||||
// If it isn't true, I'd like to catch it and learn more.
|
||||
debug_assert_eq!(info.len(), pos.len());
|
||||
|
||||
// This keeps track of the current x and y offsets (sum of advances)
|
||||
// and the furthest cluster we've seen so far (max).
|
||||
let mut run_offset = RunOffset::default();
|
||||
|
||||
// This keeps track of the cell starting x and cluster.
|
||||
let mut cell_offset = CellOffset::default();
|
||||
|
||||
// Convert all our info/pos to cells and set it.
|
||||
for (info_v, pos_v) in info.iter().zip(pos.iter()) {
|
||||
// info_v.cluster is the index into our codepoints array. We use it
|
||||
// to get the original cluster.
|
||||
let index = info_v.cluster as usize;
|
||||
// Our cluster is also our cell X position. If the cluster changes
|
||||
// then we need to reset our current cell offsets.
|
||||
let cluster = self.codepoints[index].cluster;
|
||||
if cell_offset.cluster != cluster {
|
||||
let is_after_glyph_from_current_or_next_clusters = cluster <= run_offset.cluster;
|
||||
|
||||
let is_first_codepoint_in_cluster = if index == 0 {
|
||||
true
|
||||
} else {
|
||||
self.codepoints[index - 1].cluster != cluster
|
||||
};
|
||||
|
||||
// We need to reset the `cell_offset` at the start of a new
|
||||
// cluster, but we do that conditionally if the codepoint
|
||||
// `is_first_codepoint_in_cluster` and the cluster is not
|
||||
// `is_after_glyph_from_current_or_next_clusters`, which is
|
||||
// a heuristic to detect ligatures and avoid positioning
|
||||
// glyphs that mark ligatures incorrectly. The idea is that
|
||||
// if the first codepoint in a cluster doesn't appear in
|
||||
// the stream, it's very likely that it combined with
|
||||
// codepoints from a previous cluster into a ligature.
|
||||
// Then, the subsequent codepoints are very likely marking
|
||||
// glyphs that are placed relative to that ligature, so if
|
||||
// we were to reset the `cell_offset` to align it with the
|
||||
// grid, the positions would be off. The
|
||||
// `!is_after_glyph_from_current_or_next_clusters` check is
|
||||
// needed in case these marking glyphs come from a later
|
||||
// cluster but are rendered first (see the Chakma and
|
||||
// Bengali tests). In that case when we get to the
|
||||
// codepoint that `is_first_codepoint_in_cluster`, but in a
|
||||
// cluster that
|
||||
// `is_after_glyph_from_current_or_next_clusters`, we don't
|
||||
// want to reset to the grid and cause the positions to be
|
||||
// off. (Note that we could go back and align the cells to
|
||||
// the grid starting from the one from the cluster that
|
||||
// rendered out of order, but that is more complicated so
|
||||
// we don't do that for now. Also, it's TBD if there are
|
||||
// exceptions to this heuristic for detecting ligatures,
|
||||
// but using the logging below seems to show it works
|
||||
// well.)
|
||||
if is_first_codepoint_in_cluster && !is_after_glyph_from_current_or_next_clusters {
|
||||
cell_offset = CellOffset {
|
||||
cluster,
|
||||
x: run_offset.x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Under both FreeType and CoreText the harfbuzz scale is
|
||||
// in 26.6 fixed point units, so we round to the nearest
|
||||
// whole value here.
|
||||
let x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6);
|
||||
let y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6);
|
||||
|
||||
self.cell_buf.push(Cell {
|
||||
x: cell_offset.cluster as u16,
|
||||
x_offset: x_offset as i16,
|
||||
y_offset: y_offset as i16,
|
||||
glyph_index: info_v.codepoint,
|
||||
});
|
||||
|
||||
// Add our advances to keep track of our run offsets.
|
||||
// Advances apply to the NEXT cell.
|
||||
// Under both FreeType and CoreText the harfbuzz scale is
|
||||
// in 26.6 fixed point units, so we round to the nearest
|
||||
// whole value here.
|
||||
run_offset.x += (pos_v.x_advance + 0b100_000) >> 6;
|
||||
run_offset.y += (pos_v.y_advance + 0b100_000) >> 6;
|
||||
run_offset.cluster = run_offset.cluster.max(cluster);
|
||||
}
|
||||
|
||||
Ok(&self.cell_buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::face::{DesiredSize, FontData, Options};
|
||||
use crate::{Library, embedded};
|
||||
|
||||
fn text(s: &str) -> Vec<(u32, u32)> {
|
||||
s.chars()
|
||||
.enumerate()
|
||||
.map(|(i, c)| (c as u32, i as u32))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn face(data: &'static [u8]) -> Face {
|
||||
let lib = Library::init().unwrap();
|
||||
Face::init(
|
||||
&lib,
|
||||
FontData::Static(data),
|
||||
Options::new(DesiredSize::new(12.0)),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_emoji_run_buffer_length() {
|
||||
// "shape": thumbs up, thumbs up, skin tone modifier => 3 codepoints.
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
s.prepare();
|
||||
s.add_codepoint(0x1F44D, 0);
|
||||
s.add_codepoint(0x1F44D, 1);
|
||||
s.add_codepoint(0x1F3FD, 1);
|
||||
s.finalize();
|
||||
assert_eq!(3, s.hb_buf_len());
|
||||
s.shape(Some(&f)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_inconsolata_ligs() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
// Some of our tests rely on dlig being enabled by default.
|
||||
let mut s = Shaper::new(&shape::Options {
|
||||
features: vec!["dlig".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
let cells = s.shape_text(Some(&f), &text(">=")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_monaspace_ligs() {
|
||||
let f = face(embedded::MONASPACE_NEON);
|
||||
// Some of our tests rely on dlig being enabled by default.
|
||||
let mut s = Shaper::new(&shape::Options {
|
||||
features: vec!["dlig".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
|
||||
assert_eq!(1, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_ligatures_disabled_by_feature() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options {
|
||||
features: vec!["-liga".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
let cells = s.shape_text(Some(&f), &text("===")).unwrap();
|
||||
assert_eq!(3, cells.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_arabic_forced_ltr() {
|
||||
let f = face(embedded::ARABIC);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
let arabic = include_str!("../../testdata/arabic.txt");
|
||||
let line = arabic.lines().next().unwrap();
|
||||
let t = text(line);
|
||||
let n = t.len();
|
||||
let cells = s.shape_text(Some(&f), &t).unwrap();
|
||||
assert_eq!(n, cells.len());
|
||||
let mut x = cells[0].x;
|
||||
for cell in &cells[1..] {
|
||||
assert_eq!(x + 1, cell.x);
|
||||
x = cell.x;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_combining_characters() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
// n + two combining diaeresis in cell 0, then 'a' in cell 1.
|
||||
let t = [('n' as u32, 0), (0x0308, 0), (0x0308, 0), ('a' as u32, 1)];
|
||||
let cells = s.shape_text(Some(&f), &t).unwrap();
|
||||
assert_eq!(4, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(0, cells[1].x);
|
||||
assert_eq!(0, cells[2].x);
|
||||
assert_eq!(1, cells[3].x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_special_font_passthrough() {
|
||||
// Special fonts (sprite) are not shaped: glyph index == codepoint.
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
let cells = s.shape_text(None, &[(0x2500, 0), (0x2502, 1)]).unwrap();
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0x2500, cells[0].glyph_index);
|
||||
assert_eq!(1, cells[1].x);
|
||||
assert_eq!(0, cells[1].x_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_empty_run() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
assert_eq!(0, s.shape_text(Some(&f), &[]).unwrap().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_with_empty_cells_in_between_clusters() {
|
||||
let f = face(embedded::INCONSOLATA);
|
||||
let mut s = Shaper::new(&shape::Options::default()).unwrap();
|
||||
// 'A' at cell 0, 'B' at cell 3.
|
||||
let cells = s
|
||||
.shape_text(Some(&f), &[('A' as u32, 0), ('B' as u32, 3)])
|
||||
.unwrap();
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0, cells[0].x);
|
||||
assert_eq!(3, cells[1].x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Minimal safe wrapper over the HarfBuzz buffer/shape API (the bits of
|
||||
//! the Zig `harfbuzz` package that the shaper uses).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use harfbuzz_sys as sys;
|
||||
|
||||
use crate::face::freetype::HbFont;
|
||||
|
||||
/// A HarfBuzz feature (`hb_feature_t`) applied globally.
|
||||
pub type Feature = sys::hb_feature_t;
|
||||
|
||||
pub const FEATURE_GLOBAL_START: u32 = 0;
|
||||
pub const FEATURE_GLOBAL_END: u32 = u32::MAX;
|
||||
|
||||
/// Build a global feature from a 4-byte tag and value.
|
||||
pub fn global_feature(tag: [u8; 4], value: u32) -> Feature {
|
||||
Feature {
|
||||
tag: u32::from_be_bytes(tag),
|
||||
value,
|
||||
start: FEATURE_GLOBAL_START,
|
||||
end: FEATURE_GLOBAL_END,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct GlyphInfo {
|
||||
pub codepoint: u32,
|
||||
pub cluster: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct GlyphPosition {
|
||||
pub x_advance: i32,
|
||||
pub y_advance: i32,
|
||||
pub x_offset: i32,
|
||||
pub y_offset: i32,
|
||||
}
|
||||
|
||||
/// An owned `hb_buffer_t`.
|
||||
pub struct Buffer(NonNull<sys::hb_buffer_t>);
|
||||
|
||||
// SAFETY: the buffer is uniquely owned and HarfBuzz buffers have no thread
|
||||
// affinity; all access goes through `&self`/`&mut self`.
|
||||
unsafe impl Send for Buffer {}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new() -> Option<Buffer> {
|
||||
// SAFETY: hb_buffer_create always returns a valid (possibly the
|
||||
// inert "empty") buffer; we check for null anyway.
|
||||
NonNull::new(unsafe { sys::hb_buffer_create() }).map(Buffer)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe { sys::hb_buffer_reset(self.0.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn set_content_type_unicode(&mut self) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe {
|
||||
sys::hb_buffer_set_content_type(self.0.as_ptr(), sys::HB_BUFFER_CONTENT_TYPE_UNICODE);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cluster_level_characters(&mut self) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe {
|
||||
sys::hb_buffer_set_cluster_level(
|
||||
self.0.as_ptr(),
|
||||
sys::HB_BUFFER_CLUSTER_LEVEL_CHARACTERS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_direction_ltr(&mut self) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe { sys::hb_buffer_set_direction(self.0.as_ptr(), sys::HB_DIRECTION_LTR) }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, cp: u32, cluster: u32) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe { sys::hb_buffer_add(self.0.as_ptr(), cp, cluster) }
|
||||
}
|
||||
|
||||
pub fn guess_segment_properties(&mut self) {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe { sys::hb_buffer_guess_segment_properties(self.0.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
// SAFETY: self.0 is a valid buffer.
|
||||
unsafe { sys::hb_buffer_get_length(self.0.as_ptr()) as usize }
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Glyph infos (valid after shaping).
|
||||
pub fn glyph_infos(&mut self) -> Vec<GlyphInfo> {
|
||||
let mut len: u32 = 0;
|
||||
// SAFETY: self.0 is valid; the returned pointer addresses `len`
|
||||
// initialized infos that live until the buffer is next mutated, and
|
||||
// we copy them out immediately.
|
||||
unsafe {
|
||||
let ptr = sys::hb_buffer_get_glyph_infos(self.0.as_ptr(), &raw mut len);
|
||||
if ptr.is_null() {
|
||||
return Vec::new();
|
||||
}
|
||||
std::slice::from_raw_parts(ptr, len as usize)
|
||||
.iter()
|
||||
.map(|i| GlyphInfo {
|
||||
codepoint: i.codepoint,
|
||||
cluster: i.cluster,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Glyph positions (valid after shaping); None if unavailable.
|
||||
pub fn glyph_positions(&mut self) -> Option<Vec<GlyphPosition>> {
|
||||
let mut len: u32 = 0;
|
||||
// SAFETY: as for `glyph_infos`.
|
||||
unsafe {
|
||||
let ptr = sys::hb_buffer_get_glyph_positions(self.0.as_ptr(), &raw mut len);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
std::slice::from_raw_parts(ptr, len as usize)
|
||||
.iter()
|
||||
.map(|p| GlyphPosition {
|
||||
x_advance: p.x_advance,
|
||||
y_advance: p.y_advance,
|
||||
x_offset: p.x_offset,
|
||||
y_offset: p.y_offset,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Buffer {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own the buffer reference.
|
||||
unsafe { sys::hb_buffer_destroy(self.0.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape the buffer with the given font and features.
|
||||
pub fn shape(font: &HbFont, buf: &mut Buffer, features: &[Feature]) {
|
||||
// SAFETY: font and buffer pointers are valid; the feature slice is valid
|
||||
// for `features.len()` entries.
|
||||
unsafe {
|
||||
sys::hb_shape(
|
||||
font.as_ptr(),
|
||||
buf.0.as_ptr(),
|
||||
features.as_ptr(),
|
||||
features.len() as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Text shapers. Port of `font/shaper/`.
|
||||
//!
|
||||
//! `run.zig` (RunIterator over terminal rows) is deferred (F2).
|
||||
|
||||
pub mod cache;
|
||||
pub mod feature;
|
||||
pub mod harfbuzz;
|
||||
mod hb;
|
||||
pub mod noop;
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Shaper that doesn't do any shaping. Port of `font/shaper/noop.zig`.
|
||||
//! Each individual codepoint is mapped directly to the detected text run
|
||||
//! font's glyph index.
|
||||
|
||||
use crate::face::freetype::Face;
|
||||
use crate::shape::{self, Cell};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Codepoint {
|
||||
codepoint: u32,
|
||||
cluster: u32,
|
||||
}
|
||||
|
||||
/// Shaper that doesn't do any shaping.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Shaper {
|
||||
/// The codepoints of the current run.
|
||||
codepoints: Vec<Codepoint>,
|
||||
|
||||
/// The shared memory used for shaping results.
|
||||
cell_buf: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Shaper {
|
||||
pub fn new(_opts: &shape::Options) -> Shaper {
|
||||
Shaper::default()
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) {}
|
||||
|
||||
// ---- RunIterator hooks ----
|
||||
|
||||
pub fn prepare(&mut self) {
|
||||
self.codepoints.clear();
|
||||
}
|
||||
|
||||
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
|
||||
self.codepoints.push(Codepoint {
|
||||
codepoint: cp,
|
||||
cluster,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self) {}
|
||||
|
||||
/// Shape the current run. `face` is `None` for special fonts (sprite),
|
||||
/// whose codepoint == glyph index.
|
||||
///
|
||||
/// Panics if a codepoint has no glyph in `face` (the run iterator
|
||||
/// guarantees this never happens).
|
||||
pub fn shape(&mut self, face: Option<&Face>) -> &[Cell] {
|
||||
self.cell_buf.clear();
|
||||
for entry in &self.codepoints {
|
||||
let glyph_index = match face {
|
||||
// Special fonts aren't shaped and their codepoint == glyph.
|
||||
None => entry.codepoint,
|
||||
Some(face) => face
|
||||
.glyph_index(entry.codepoint)
|
||||
.expect("run iterator guarantees a glyph for every codepoint"),
|
||||
};
|
||||
self.cell_buf.push(Cell {
|
||||
x: entry.cluster as u16,
|
||||
glyph_index,
|
||||
..Cell::default()
|
||||
});
|
||||
}
|
||||
&self.cell_buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn noop_special_passthrough() {
|
||||
let mut s = Shaper::new(&shape::Options::default());
|
||||
s.prepare();
|
||||
s.add_codepoint(0x41, 0);
|
||||
s.add_codepoint(0x42, 2);
|
||||
s.finalize();
|
||||
let cells = s.shape(None);
|
||||
assert_eq!(2, cells.len());
|
||||
assert_eq!(0x41, cells[0].glyph_index);
|
||||
assert_eq!(2, cells[1].x);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user