use std::cell::RefCell;
use std::collections::HashMap;
use parley::FontData;
use parley::PositionedLayoutItem;
use skrifa::GlyphId;
use skrifa::MetadataProvider;
use skrifa::instance::{LocationRef, Size};
use smudge_core::geometry::{Color, Point, Rect};
use smudge_core::paint::{FontBlob, GlyphRun, PositionedGlyph};
use crate::layout::Cut;
use crate::layout::Ellipsis;
use crate::layout::TextLayout;
use crate::layout::Truncation;
thread_local! {
static FONT_BLOBS: RefCell<HashMap<(u64, u32), FontBlob>> = RefCell::new(HashMap::new());
}
impl TextLayout {
pub fn glyph_runs_at(&self, origin: Point, color: Color) -> Vec<GlyphRun> {
glyph_runs_with(&self.layout, origin, color, self.truncation.as_ref())
}
}
pub(crate) fn glyph_runs<B: parley::Brush>(
layout: &parley::Layout<B>,
origin: Point,
color: Color,
) -> Vec<GlyphRun> {
glyph_runs_with(layout, origin, color, None)
}
pub(crate) fn glyph_runs_with<B: parley::Brush>(
layout: &parley::Layout<B>,
origin: Point,
color: Color,
truncation: Option<&Truncation>,
) -> Vec<GlyphRun> {
let kept_lines = truncation.map_or(usize::MAX, |truncation| truncation.kept_lines);
let mut runs = Vec::new();
for (line_index, line) in layout.lines().enumerate().take(kept_lines) {
let cut = match truncation {
Some(truncation) if line_index + 1 == truncation.kept_lines => truncation.cut.as_ref(),
_ => None,
};
let mut run_index = 0usize;
for item in line.items() {
let PositionedLayoutItem::GlyphRun(run) = item else {
continue;
};
let kept = match cut {
Some(cut) if run_index > cut.run => break,
Some(cut) if run_index == cut.run => cut.glyphs,
_ => usize::MAX,
};
run_index += 1;
if kept == 0 {
continue;
}
let glyphs = run
.positioned_glyphs()
.take(kept)
.map(|glyph| (glyph.id, origin.x + glyph.x, origin.y + glyph.y));
runs.push(paint_run(
run.run().font(),
run.run().font_size(),
glyphs,
color,
));
}
if let Some(Cut {
x,
baseline,
ellipsis: Some(ellipsis),
..
}) = cut
{
let y = origin.y + baseline;
let dots = (0..ellipsis.dots).map(|dot| {
let dot_x = origin.x + x + dot as f32 * ellipsis.advance;
(ellipsis.glyph, dot_x, y)
});
runs.push(paint_run(&ellipsis.font, ellipsis.size, dots, color));
}
}
runs
}
fn paint_run(
font: &FontData,
size: f32,
glyphs: impl Iterator<Item = (u32, f32, f32)>,
color: Color,
) -> GlyphRun {
let metrics = skrifa::FontRef::from_index(font.data.as_ref(), font.index)
.ok()
.map(|font| font.glyph_metrics(Size::new(size), LocationRef::default()));
let mut painted = Vec::new();
let mut ink = Rect::default();
for (id, x, y) in glyphs {
if let Some(bounds) = metrics
.as_ref()
.and_then(|metrics| metrics.bounds(GlyphId::new(id)))
{
ink = ink.union(Rect::new(
x + bounds.x_min,
y - bounds.y_max,
x + bounds.x_max,
y - bounds.y_min,
));
}
painted.push(PositionedGlyph { id, x, y });
}
GlyphRun {
font: font_blob(font),
size,
color,
glyphs: painted,
ink_bounds: expand(ink, 1.0),
}
}
pub(crate) fn ellipsis_glyph(font: &FontData, font_size: f32) -> Option<Ellipsis> {
let face = skrifa::FontRef::from_index(font.data.as_ref(), font.index).ok()?;
let charmap = face.charmap();
let (glyph, dots) = match charmap.map('\u{2026}') {
Some(id) => (id.to_u32(), 1),
None => (charmap.map('.')?.to_u32(), 3),
};
let metrics = face.glyph_metrics(Size::new(font_size), LocationRef::default());
let advance = metrics.advance_width(GlyphId::new(glyph)).unwrap_or(0.0);
Some(Ellipsis {
font: font.clone(),
size: font_size,
glyph,
dots,
advance,
})
}
fn font_blob(font: &FontData) -> FontBlob {
FONT_BLOBS.with(|cache| {
cache
.borrow_mut()
.entry((font.data.id(), font.index))
.or_insert_with(|| {
let (data, id) = font.data.clone().into_raw_parts();
FontBlob {
id,
data,
index: font.index,
}
})
.clone()
})
}
fn expand(rect: Rect, by: f32) -> Rect {
if rect.is_empty() {
rect
} else {
Rect::new(rect.x0 - by, rect.y0 - by, rect.x1 + by, rect.y1 + by)
}
}
#[cfg(test)]
#[path = "glyphs_tests.rs"]
mod tests;