use anyrender::Glyph;
use peniko::FontData;
use std::collections::HashMap;
use std::ops::Range;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RunKey {
face: (u64, u32),
size: u32,
glyphs: Vec<(u32, u32, u32)>,
}
impl RunKey {
pub fn new(font: &FontData, font_size: f32, glyphs: impl IntoIterator<Item = Glyph>) -> Self {
RunKey {
face: (font.data.id(), font.index),
size: font_size.to_bits(),
glyphs: glyphs
.into_iter()
.map(|glyph| (glyph.id, glyph.x.to_bits(), glyph.y.to_bits()))
.collect(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlyphSource<'a> {
pub cluster: usize,
pub text: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RunText {
text: String,
clusters: Vec<Range<usize>>,
}
impl RunText {
pub fn from_glyphs<'a>(glyphs: impl IntoIterator<Item = GlyphSource<'a>>) -> Self {
let mut run = RunText::default();
let mut last: Option<usize> = None;
for glyph in glyphs {
match (last, run.clusters.last().cloned()) {
(Some(cluster), Some(range)) if cluster == glyph.cluster => {
run.clusters.push(range);
}
_ => {
let start = run.text.len();
run.text.push_str(glyph.text);
run.clusters.push(start..run.text.len());
}
}
last = Some(glyph.cluster);
}
run
}
pub fn text(&self) -> &str {
&self.text
}
pub fn clusters(&self) -> &[Range<usize>] {
&self.clusters
}
pub fn len(&self) -> usize {
self.clusters.len()
}
pub fn is_empty(&self) -> bool {
self.clusters.is_empty()
}
pub(crate) fn select(&self, kept: &[usize]) -> RunText {
let mut run = RunText::default();
let mut last: Option<&Range<usize>> = None;
for &index in kept {
let Some(range) = self.clusters.get(index) else {
continue;
};
match (last, run.clusters.last().cloned()) {
(Some(previous), Some(held)) if previous == range => run.clusters.push(held),
_ => {
let start = run.text.len();
run.text
.push_str(self.text.get(range.clone()).unwrap_or(""));
run.clusters.push(start..run.text.len());
}
}
last = Some(range);
}
run
}
}
#[derive(Debug, Default)]
pub struct RunTexts {
runs: HashMap<RunKey, RunText>,
}
impl RunTexts {
pub fn insert(&mut self, key: RunKey, text: RunText) {
self.runs.entry(key).or_insert(text);
}
pub fn get(&self, key: &RunKey) -> Option<&RunText> {
self.runs.get(key)
}
pub fn len(&self) -> usize {
self.runs.len()
}
pub fn is_empty(&self) -> bool {
self.runs.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::{GlyphSource, RunText};
fn glyph(cluster: usize, text: &str) -> GlyphSource<'_> {
GlyphSource { cluster, text }
}
#[test]
fn a_ligature_glyph_covers_its_characters() {
let run = RunText::from_glyphs([glyph(0, "fi"), glyph(2, "n"), glyph(3, "d")]);
assert_eq!(run.text(), "find");
assert_eq!(run.clusters(), &[0..2, 2..3, 3..4]);
}
#[test]
fn glyphs_of_one_cluster_share_its_range() {
let run = RunText::from_glyphs([glyph(0, "é"), glyph(0, "é"), glyph(3, "t")]);
assert_eq!(run.text(), "ét");
assert_eq!(run.clusters(), &[0..2, 0..2, 2..3]);
}
#[test]
fn a_glyph_without_text_does_not_swallow_the_next() {
let run = RunText::from_glyphs([glyph(0, ""), glyph(1, "c")]);
let kept = run.select(&[0, 1]);
assert_eq!(kept.text(), "c");
assert_eq!(kept.clusters(), &[0..0, 0..1]);
}
#[test]
fn selecting_keeps_whole_clusters_in_order() {
let run = RunText::from_glyphs([glyph(0, "a"), glyph(1, "fi"), glyph(3, "b")]);
let kept = run.select(&[1, 2]);
assert_eq!(kept.text(), "fib");
assert_eq!(kept.clusters(), &[0..2, 2..3]);
}
}