use std::borrow::Cow;
use crate::blocks::Block;
use crate::text::{BoundsSource, TextBlock, TextSpan};
pub(crate) const fn letters_behind(ch: char) -> Option<&'static str> {
match ch {
'\u{FB00}' => Some("ff"),
'\u{FB01}' => Some("fi"),
'\u{FB02}' => Some("fl"),
'\u{FB03}' => Some("ffi"),
'\u{FB04}' => Some("ffl"),
'\u{FB05}' => Some("st"),
'\u{FB06}' => Some("st"),
'\u{FB13}' => Some("\u{574}\u{576}"),
'\u{FB14}' => Some("\u{574}\u{565}"),
'\u{FB15}' => Some("\u{574}\u{56B}"),
'\u{FB16}' => Some("\u{57E}\u{576}"),
'\u{FB17}' => Some("\u{574}\u{56D}"),
_ => None,
}
}
pub(crate) fn fold(text: &str) -> Cow<'_, str> {
if !text.chars().any(|ch| letters_behind(ch).is_some()) {
return Cow::Borrowed(text);
}
let mut folded = String::with_capacity(text.len());
for ch in text.chars() {
match letters_behind(ch) {
Some(letters) => folded.push_str(letters),
None => folded.push(ch),
}
}
Cow::Owned(folded)
}
pub(crate) fn fold_span(span: &mut TextSpan) {
let chars: Vec<char> = span.text.chars().collect();
if !chars.iter().any(|ch| letters_behind(*ch).is_some()) {
return;
}
let span_box = [span.x, span.y, span.x + span.width, span.y + span.height];
let mut text = String::with_capacity(span.text.len() + 8);
let mut bounds: Vec<[f64; 4]> = Vec::new();
let mut tight: Vec<[f64; 4]> = Vec::new();
let mut advances: Vec<f64> = Vec::new();
let mut sources: Vec<BoundsSource> = Vec::new();
let fold_boxes = !span.char_bounds.is_empty();
let fold_tight = !span.tight_char_bounds.is_empty();
let fold_advances = !span.glyph_advances.is_empty();
let fold_sources = !span.glyph_bounds_sources.is_empty();
let box_of = align_chars_to_glyphs(&chars, span.char_bounds.len());
let tight_of = align_chars_to_glyphs(&chars, span.tight_char_bounds.len());
let advance_of = align_chars_to_glyphs(&chars, span.glyph_advances.len());
let source_of = align_chars_to_glyphs(&chars, span.glyph_bounds_sources.len());
for (i, ch) in chars.iter().enumerate() {
let letters = letters_behind(*ch);
let count = letters.map_or(1, |s| s.chars().count());
match letters {
Some(letters) => text.push_str(letters),
None => text.push(*ch),
}
if fold_boxes {
push_char_boxes(&mut bounds, &span.char_bounds, &box_of, i, count, span_box);
}
if fold_tight {
push_char_boxes(
&mut tight,
&span.tight_char_bounds,
&tight_of,
i,
count,
span_box,
);
}
if fold_advances {
match advance_of[i] {
None => advances.push(0.0),
Some(g) => {
let each = span.glyph_advances[g] / count as f64;
advances.extend(std::iter::repeat_n(each, count));
}
}
}
if fold_sources {
let source = source_of[i]
.map(|g| span.glyph_bounds_sources[g])
.unwrap_or(BoundsSource::Estimate);
sources.extend(std::iter::repeat_n(source, count));
}
}
span.text = text;
if fold_boxes {
span.char_bounds = bounds;
}
if fold_tight {
span.tight_char_bounds = tight;
}
if fold_advances {
span.glyph_advances = advances;
}
if fold_sources {
span.glyph_bounds_sources = sources;
span.bounds_source = crate::text::aggregate_bounds_source(&span.glyph_bounds_sources);
}
}
fn align_chars_to_glyphs(chars: &[char], glyphs: usize) -> Vec<Option<usize>> {
if glyphs == 0 {
return vec![None; chars.len()];
}
let n = chars.len();
let mut at = 0usize;
chars
.iter()
.enumerate()
.map(|(i, ch)| {
let chars_left = n - i;
let glyphs_left = glyphs.saturating_sub(at);
if ch.is_whitespace() && chars_left > glyphs_left {
None
} else if glyphs_left == 0 {
Some(glyphs - 1)
} else {
at += 1;
Some(at - 1)
}
})
.collect()
}
fn push_char_boxes(
out: &mut Vec<[f64; 4]>,
glyphs: &[[f64; 4]],
aligned: &[Option<usize>],
i: usize,
count: usize,
span_box: [f64; 4],
) {
match aligned[i] {
Some(g) => push_split(out, glyphs[g], count),
None => out.push(gap_box(glyphs, aligned, i, span_box)),
}
}
fn push_split(out: &mut Vec<[f64; 4]>, glyph: [f64; 4], count: usize) {
if count == 1 {
out.push(glyph);
return;
}
let (x0, y0, x1, y1) = (glyph[0], glyph[1], glyph[2], glyph[3]);
for t in 0..count {
let left = x0 + (x1 - x0) * t as f64 / count as f64;
let right = if t + 1 == count {
x1
} else {
x0 + (x1 - x0) * (t + 1) as f64 / count as f64
};
out.push([left, y0, right, y1]);
}
}
fn gap_box(
glyphs: &[[f64; 4]],
aligned: &[Option<usize>],
i: usize,
span_box: [f64; 4],
) -> [f64; 4] {
let prev = aligned[..i].iter().rev().find_map(|a| a.map(|g| glyphs[g]));
let next = aligned[i..].iter().find_map(|a| a.map(|g| glyphs[g]));
let x0 = prev.map_or(span_box[0], |b| b[2]);
let x1 = next.map_or(x0, |b| b[0]);
let (y0, y1) = prev
.or(next)
.map_or((span_box[1], span_box[3]), |b| (b[1], b[3]));
[x0, y0, x1.max(x0), y1]
}
pub(crate) fn fold_text_block(block: &mut TextBlock) {
for span in &mut block.spans {
fold_span(span);
}
}
pub(crate) fn fold_block(block: &mut Block) {
if let Cow::Owned(folded) = fold(&block.text) {
block.text = folded;
}
for span in &mut block.spans {
fold_span(span);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_latin_ligature_folds() {
assert_eq!(
fold("\u{FB00}\u{FB01}\u{FB02}\u{FB03}\u{FB04}\u{FB05}\u{FB06}"),
"fffiflffifflstst"
);
}
#[test]
fn every_armenian_ligature_folds() {
assert_eq!(
fold("\u{FB13}\u{FB14}\u{FB15}\u{FB16}\u{FB17}"),
"\u{574}\u{576}\u{574}\u{565}\u{574}\u{56B}\u{57E}\u{576}\u{574}\u{56D}"
);
}
#[test]
fn a_ligature_folds_where_it_sits() {
assert_eq!(fold("o\u{FB03}ce hours"), "office hours");
}
#[test]
fn text_without_a_ligature_is_not_copied() {
assert!(matches!(fold("office hours"), Cow::Borrowed(_)));
}
#[test]
fn nothing_outside_the_two_ranges_folds() {
for ch in ['\u{FB07}', '\u{FB12}', '\u{FB4F}', '\u{FEFF}', '\u{17F}'] {
assert_eq!(letters_behind(ch), None, "{ch:?} should not fold");
}
}
#[test]
fn no_other_compatibility_folding() {
let untouched = "\u{B3}\u{BD}\u{FF21}\u{FDF2}"; assert_eq!(fold(untouched), untouched);
}
#[test]
fn a_ligature_splits_its_glyph_box_evenly() {
let mut span = TextSpan {
text: "o\u{FB03}ce".into(),
char_bounds: vec![
[0.0, 0.0, 10.0, 10.0],
[10.0, 0.0, 40.0, 10.0],
[40.0, 0.0, 50.0, 10.0],
[50.0, 0.0, 60.0, 10.0],
],
..Default::default()
};
fold_span(&mut span);
assert_eq!(span.text, "office");
assert_eq!(
span.char_bounds,
vec![
[0.0, 0.0, 10.0, 10.0],
[10.0, 0.0, 20.0, 10.0],
[20.0, 0.0, 30.0, 10.0],
[30.0, 0.0, 40.0, 10.0],
[40.0, 0.0, 50.0, 10.0],
[50.0, 0.0, 60.0, 10.0],
]
);
}
#[test]
fn an_inserted_space_does_not_steal_the_ligature_box() {
let mut span = TextSpan {
text: "a\u{FB01} b".into(),
x: 0.0,
y: 0.0,
width: 60.0,
height: 10.0,
char_bounds: vec![
[0.0, 0.0, 10.0, 10.0],
[12.0, 0.0, 32.0, 10.0],
[40.0, 0.0, 50.0, 10.0],
],
..Default::default()
};
fold_span(&mut span);
assert_eq!(span.text, "afi b");
assert_eq!(span.char_bounds.len(), 5);
assert_eq!(span.char_bounds[0], [0.0, 0.0, 10.0, 10.0]);
assert_eq!(span.char_bounds[1], [12.0, 0.0, 22.0, 10.0]);
assert_eq!(span.char_bounds[2], [22.0, 0.0, 32.0, 10.0]);
assert_eq!(span.char_bounds[3], [32.0, 0.0, 40.0, 10.0]);
assert_eq!(span.char_bounds[4], [40.0, 0.0, 50.0, 10.0]);
}
#[test]
fn the_rich_geometry_vectors_fold_with_the_boxes() {
let mut span = TextSpan {
text: "\u{FB00}x".into(),
char_bounds: vec![[0.0, 0.0, 20.0, 10.0], [20.0, 0.0, 30.0, 10.0]],
tight_char_bounds: vec![[1.0, 1.0, 19.0, 9.0], [21.0, 1.0, 29.0, 9.0]],
glyph_advances: vec![20.0, 10.0],
glyph_bounds_sources: vec![BoundsSource::Tight, BoundsSource::Tight],
bounds_source: BoundsSource::Tight,
..Default::default()
};
fold_span(&mut span);
assert_eq!(span.text, "ffx");
assert_eq!(
span.tight_char_bounds,
vec![
[1.0, 1.0, 10.0, 9.0],
[10.0, 1.0, 19.0, 9.0],
[21.0, 1.0, 29.0, 9.0],
]
);
assert_eq!(span.glyph_advances, vec![10.0, 10.0, 10.0]);
assert_eq!(
span.glyph_bounds_sources,
vec![
BoundsSource::Tight,
BoundsSource::Tight,
BoundsSource::Tight
]
);
assert_eq!(span.bounds_source, BoundsSource::Tight);
}
#[test]
fn a_span_without_a_ligature_is_untouched() {
let mut span = TextSpan {
text: "office hours".into(),
char_bounds: vec![[0.0, 0.0, 10.0, 10.0]],
tight_char_bounds: vec![[0.0, 0.0, 9.0, 9.0]],
glyph_advances: vec![10.0],
glyph_bounds_sources: vec![BoundsSource::Advance],
..Default::default()
};
let before = span.clone();
fold_span(&mut span);
assert_eq!(span.text, before.text);
assert_eq!(span.char_bounds, before.char_bounds);
assert_eq!(span.tight_char_bounds, before.tight_char_bounds);
assert_eq!(span.glyph_advances, before.glyph_advances);
assert_eq!(span.glyph_bounds_sources, before.glyph_bounds_sources);
}
#[test]
fn text_without_boxes_still_folds() {
let mut span = TextSpan {
text: "o\u{FB03}ce".into(),
..Default::default()
};
fold_span(&mut span);
assert_eq!(span.text, "office");
assert!(span.char_bounds.is_empty());
}
#[test]
fn a_block_folds_its_text_and_its_spans() {
let mut block = Block::new(
crate::blocks::BlockKind::Paragraph,
0,
[0.0, 0.0, 60.0, 10.0],
"o\u{FB03}ce".into(),
0,
);
block.spans = vec![TextSpan {
text: "o\u{FB03}ce".into(),
char_bounds: vec![
[0.0, 0.0, 10.0, 10.0],
[10.0, 0.0, 40.0, 10.0],
[40.0, 0.0, 50.0, 10.0],
[50.0, 0.0, 60.0, 10.0],
],
..Default::default()
}];
fold_block(&mut block);
assert_eq!(block.text, "office");
assert_eq!(block.spans[0].text, "office");
assert_eq!(block.spans[0].char_bounds.len(), 6);
}
}