use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use cosmic_text::{
Attrs, Buffer, CacheKeyFlags, Family, FontSystem, Metrics as CosmicMetrics, Shaping,
SwashCache, SwashContent,
};
use denise::Size;
use crate::source::{FontMetrics, GlyphId, GlyphMetrics, GlyphSource, Rasterised, ShapedGlyph};
const FACE_SHIFT: u32 = 16;
pub struct ShapedSource {
name: String,
fonts: FontSystem,
cache: SwashCache,
faces: Vec<cosmic_text::fontdb::ID>,
scratch: Vec<u8>,
}
impl ShapedSource {
pub fn from_fonts(
name: &str,
fonts: impl IntoIterator<Item = Vec<u8>>,
) -> Result<Self, String> {
let mut db = cosmic_text::fontdb::Database::new();
for data in fonts {
db.load_font_data(data);
}
if db.is_empty() {
return Err("no usable faces in the fonts provided".to_owned());
}
let embedded = db
.faces()
.next()
.and_then(|face| face.families.first().map(|(name, _)| name.clone()));
if let Some(family) = embedded {
db.set_sans_serif_family(&family);
db.set_serif_family(&family);
db.set_monospace_family(&family);
db.set_cursive_family(&family);
db.set_fantasy_family(&family);
}
let fonts = FontSystem::new_with_locale_and_db("en-US".to_owned(), db);
Ok(Self {
name: name.to_owned(),
fonts,
cache: SwashCache::new(),
faces: Vec::new(),
scratch: Vec::new(),
})
}
fn slot(&mut self, id: cosmic_text::fontdb::ID) -> Option<u32> {
if let Some(index) = self.faces.iter().position(|&f| f == id) {
return Some(index as u32);
}
if self.faces.len() as u32 >= (1 << FACE_SHIFT) {
return None;
}
self.faces.push(id);
Some(self.faces.len() as u32 - 1)
}
fn pack(slot: u32, glyph: u16) -> GlyphId {
GlyphId((slot << FACE_SHIFT) | u32::from(glyph))
}
fn unpack(&self, id: GlyphId) -> Option<(cosmic_text::fontdb::ID, u16)> {
let slot = (id.0 >> FACE_SHIFT) as usize;
let glyph = (id.0 & ((1 << FACE_SHIFT) - 1)) as u16;
self.faces.get(slot).map(|&face| (face, glyph))
}
fn with_run(
&mut self,
text: &str,
size_px: u16,
mut f: impl FnMut(&mut Self, cosmic_text::LayoutGlyph),
) {
let size = f32::from(size_px.max(1));
let metrics = CosmicMetrics::new(size, size * 1.25);
let mut buffer = Buffer::new(&mut self.fonts, metrics);
let mut borrowed = buffer.borrow_with(&mut self.fonts);
borrowed.set_text(
text,
&Attrs::new().family(Family::SansSerif),
Shaping::Advanced,
None,
);
borrowed.shape_until_scroll(false);
let glyphs: Vec<cosmic_text::LayoutGlyph> = buffer
.layout_runs()
.flat_map(|run| run.glyphs.iter().cloned())
.collect();
for glyph in glyphs {
f(self, glyph);
}
}
}
impl GlyphSource for ShapedSource {
fn name(&self) -> &str {
&self.name
}
fn metrics(&self, size_px: u16) -> FontMetrics {
let size = i32::from(size_px);
FontMetrics {
ascent: size * 4 / 5,
descent: size / 5,
line_gap: size / 4,
}
}
fn glyph_id(&self, _ch: char) -> Option<GlyphId> {
None
}
fn glyph_metrics(&mut self, glyph: GlyphId, size_px: u16) -> Option<GlyphMetrics> {
self.rasterise(glyph, size_px).map(|r| r.metrics)
}
fn rasterise(&mut self, glyph: GlyphId, size_px: u16) -> Option<Rasterised<'_>> {
let (face, index) = self.unpack(glyph)?;
let key = cosmic_text::CacheKey {
font_id: face,
glyph_id: index,
font_size_bits: f32::from(size_px.max(1)).to_bits(),
x_bin: cosmic_text::SubpixelBin::Zero,
y_bin: cosmic_text::SubpixelBin::Zero,
font_weight: cosmic_text::Weight::NORMAL,
flags: CacheKeyFlags::empty(),
};
let image = self.cache.get_image_uncached(&mut self.fonts, key)?;
let width = image.placement.width;
let height = image.placement.height;
self.scratch.clear();
match image.content {
SwashContent::Mask => self.scratch.extend_from_slice(&image.data),
SwashContent::Color => self
.scratch
.extend(image.data.chunks_exact(4).map(|px| px[3])),
SwashContent::SubpixelMask => self
.scratch
.extend(image.data.chunks_exact(4).map(|px| px[3])),
}
if self.scratch.len() < (width * height) as usize {
self.scratch.resize((width * height) as usize, 0);
}
Some(Rasterised {
metrics: GlyphMetrics {
advance: 0,
bearing_x: image.placement.left,
bearing_y: image.placement.top,
size: Size::new(width, height),
},
coverage: &self.scratch,
stride: width as usize,
})
}
fn shape(&mut self, text: &str, size_px: u16, out: &mut Vec<ShapedGlyph>) -> i32 {
let mut width = 0;
let mut placed: Vec<(cosmic_text::fontdb::ID, u16, i32, i32, i32)> = Vec::new();
self.with_run(text, size_px, |_, glyph| {
placed.push((
glyph.font_id,
glyph.glyph_id,
glyph.x.round() as i32,
glyph.y.round() as i32,
glyph.w.round() as i32,
));
});
for (face, index, x, y, advance) in placed {
let Some(slot) = self.slot(face) else {
continue;
};
out.push(ShapedGlyph {
id: Self::pack(slot, index),
x,
y,
});
width = width.max(x + advance);
}
width
}
fn can_shape(&self) -> bool {
true
}
fn contains(&self, ch: char) -> bool {
!ch.is_control()
}
}
impl core::fmt::Debug for ShapedSource {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ShapedSource")
.field("name", &self.name)
.field("faces_seen", &self.faces.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_glyph_id_round_trips_through_its_packing() {
let mut source = ShapedSource {
name: "test".to_owned(),
fonts: FontSystem::new_with_fonts(core::iter::empty()),
cache: SwashCache::new(),
faces: Vec::new(),
scratch: Vec::new(),
};
let a = cosmic_text::fontdb::ID::dummy();
let slot = source.slot(a).expect("first slot");
let id = ShapedSource::pack(slot, 42);
assert_eq!(source.unpack(id), Some((a, 42)));
assert_ne!(id, ShapedSource::pack(slot + 1, 42));
}
#[test]
fn no_fonts_is_an_error_rather_than_a_blank_screen() {
let error = ShapedSource::from_fonts("empty", core::iter::empty())
.expect_err("nothing to shape with");
assert!(!error.is_empty());
}
}