use std::collections::{HashMap, HashSet};
use std::ffi::CString;
use std::path::PathBuf;
use std::rc::Rc;
use cosmic_text::fontdb::{Database, Source};
use cosmic_text::{
Attrs, Buffer, Ellipsize, EllipsizeHeightLimit, Family, FontSystem, LayoutGlyph, LayoutRun,
Metrics, Shaping, Weight, Wrap,
};
use fontconfig::{CharSet, FC_FAMILY, FC_WEIGHT, Fontconfig, Pattern};
use taffy::AvailableSpace;
use crate::geometry::{Point, Rect};
use crate::style::Font;
pub struct TextSystem {
fonts: FontSystem,
fontconfig: Lookup,
families: HashMap<(&'static str, u16), Option<Rc<str>>>,
loaded: HashMap<(PathBuf, u32), Option<Rc<str>>>,
files: HashSet<PathBuf>,
fallback_tried: HashSet<(char, &'static str, u16)>,
}
enum Lookup {
NotStarted,
Ready(Fontconfig),
Failed,
}
impl TextSystem {
pub fn new() -> TextSystem {
let fonts = FontSystem::new_with_locale_and_db("en-US".to_owned(), Database::new());
TextSystem {
fonts,
fontconfig: Lookup::NotStarted,
families: HashMap::new(),
loaded: HashMap::new(),
files: HashSet::new(),
fallback_tried: HashSet::new(),
}
}
pub fn font_system(&mut self) -> &mut FontSystem {
&mut self.fonts
}
fn fontconfig(&mut self) -> Option<&Fontconfig> {
if let Lookup::NotStarted = self.fontconfig {
self.fontconfig = match Fontconfig::new() {
Some(fontconfig) => Lookup::Ready(fontconfig),
None => {
tracing::error!("fontconfig failed to start, so no fonts can be loaded");
Lookup::Failed
}
};
}
match &self.fontconfig {
Lookup::Ready(fontconfig) => Some(fontconfig),
_ => None,
}
}
pub(crate) fn family(&mut self, font: &Font) -> Option<Rc<str>> {
let key = (font.family, font.weight);
if let Some(name) = self.families.get(&key) {
return name.clone();
}
let matched = self
.fontconfig()
.and_then(|fontconfig| match_font(fontconfig, font.family, font.weight, None));
let name = matched.and_then(|file| self.load(file));
match &name {
Some(name) => tracing::debug!(requested = font.family, family = %name, "font loaded"),
None => tracing::warn!(
requested = font.family,
"no font found for this family, so its text shows nothing"
),
}
self.families.insert(key, name.clone());
name
}
fn load_fallbacks(&mut self, font: &Font, missing: &[char]) -> bool {
let faces = self.fonts.db().len();
for &ch in missing {
if !self.fallback_tried.insert((ch, font.family, font.weight)) {
continue;
}
let Some(file) = self
.fontconfig()
.and_then(|fontconfig| match_font(fontconfig, font.family, font.weight, Some(ch)))
else {
continue;
};
self.load(file);
}
self.fonts.db().len() > faces
}
pub(crate) fn load(&mut self, (path, index): (PathBuf, u32)) -> Option<Rc<str>> {
let key = (path, index);
if let Some(name) = self.loaded.get(&key) {
return name.clone();
}
let (path, index) = &key;
if !self.files.contains(path) {
self.files.insert(path.clone());
if let Err(error) = self.fonts.db_mut().load_font_file(path) {
tracing::warn!(path = %path.display(), %error, "couldn't read a font file");
}
}
let name = self
.fonts
.db()
.faces()
.find(|face| face.index == *index && source_path(&face.source) == Some(path))
.and_then(|face| face.families.first())
.map(|(name, _language)| Rc::from(name.as_str()));
self.loaded.insert(key, name.clone());
name
}
}
impl Default for TextSystem {
fn default() -> Self {
Self::new()
}
}
fn source_path(source: &Source) -> Option<&PathBuf> {
match source {
Source::File(path) | Source::SharedFile(path, _) => Some(path),
Source::Binary(_) => None,
}
}
fn match_font(
fontconfig: &Fontconfig,
family: &str,
weight: u16,
covering: Option<char>,
) -> Option<(PathBuf, u32)> {
let mut pattern = Pattern::new(fontconfig).ok()?;
pattern
.add_string(FC_FAMILY, &CString::new(family).ok()?)
.ok()?;
pattern.add_integer(FC_WEIGHT, fc_weight(weight)).ok()?;
if let Some(ch) = covering {
let mut chars = CharSet::new(fontconfig).ok()?;
chars.add_char(ch).ok()?;
pattern.add_charset(chars).ok()?;
}
let matched = pattern.font_match().ok()?;
let path = PathBuf::from(matched.filename().ok()?);
let index = matched.face_index().ok().map_or(0, face_number);
Some((path, index))
}
pub(crate) fn face_number(index: i32) -> u32 {
u32::try_from(index & 0xFFFF).unwrap_or(0)
}
pub(crate) fn fc_weight(weight: u16) -> i32 {
const POINTS: [(f32, f32); 12] = [
(100.0, 0.0),
(200.0, 40.0),
(300.0, 50.0),
(350.0, 55.0),
(380.0, 75.0),
(400.0, 80.0),
(500.0, 100.0),
(600.0, 180.0),
(700.0, 200.0),
(800.0, 205.0),
(900.0, 210.0),
(1000.0, 215.0),
];
let weight = f32::from(weight).clamp(100.0, 1000.0);
let upper = POINTS
.iter()
.position(|&(css, _)| css >= weight)
.unwrap_or(POINTS.len() - 1)
.max(1);
let (css_low, fc_low) = POINTS[upper - 1];
let (css_high, fc_high) = POINTS[upper];
let fc = fc_low + (weight - css_low) / (css_high - css_low) * (fc_high - fc_low);
fc.round() as i32
}
fn metrics(font: &Font) -> Metrics {
let size = if font.size.is_finite() && font.size >= 1.0 {
font.size
} else {
Font::DEFAULT.size
};
Metrics::new(size, (size * 1.2).round())
}
pub(crate) struct TextNode {
pub(crate) content: String,
pub(crate) ellipsis: bool,
pub(crate) max_lines: u32,
pub(crate) buffer: Buffer,
font: Option<Font>,
stale: bool,
pub(crate) origin: Point,
}
impl TextNode {
pub(crate) fn new() -> TextNode {
TextNode {
content: String::new(),
ellipsis: false,
max_lines: 1,
buffer: Buffer::new_empty(metrics(&Font::DEFAULT)),
font: None,
stale: true,
origin: Point::default(),
}
}
pub(crate) fn set_content(&mut self, content: String) -> bool {
self.set(|text| &mut text.content, content)
}
pub(crate) fn set_ellipsis(&mut self, ellipsis: bool) -> bool {
self.set(|text| &mut text.ellipsis, ellipsis)
}
pub(crate) fn set_max_lines(&mut self, max_lines: u32) -> bool {
self.set(|text| &mut text.max_lines, max_lines.max(1))
}
fn set<T: PartialEq>(&mut self, field: impl FnOnce(&mut TextNode) -> &mut T, value: T) -> bool {
let slot = field(self);
if *slot == value {
return false;
}
*slot = value;
self.stale = true;
true
}
fn fits_width(&self) -> bool {
self.ellipsis || self.max_lines > 1
}
pub(crate) fn prepare(&mut self, font: Font, text: &mut TextSystem) -> bool {
if !self.stale && self.font == Some(font) {
return false;
}
let family = text.family(&font);
self.buffer.set_metrics(metrics(&font));
self.buffer.set_wrap(if self.max_lines > 1 {
Wrap::WordOrGlyph
} else {
Wrap::None
});
self.buffer.set_ellipsize(if self.ellipsis {
Ellipsize::End(EllipsizeHeightLimit::Lines(self.max_lines as usize))
} else {
Ellipsize::None
});
self.buffer.set_size(None, None);
self.shape(family.as_deref(), &font, text);
let missing = self.fallback_chars(family.as_deref(), text.font_system().db());
if !missing.is_empty() && text.load_fallbacks(&font, &missing) {
self.shape(family.as_deref(), &font, text);
}
self.font = Some(font);
self.stale = false;
true
}
fn shape(&mut self, family: Option<&str>, font: &Font, text: &mut TextSystem) {
let attrs = Attrs::new().weight(Weight(font.weight));
let attrs = match family {
Some(family) => attrs.family(Family::Name(family)),
None => attrs,
};
self
.buffer
.set_text(&self.content, &attrs, Shaping::Advanced, None);
self.buffer.shape_until_scroll(text.font_system(), false);
}
fn fallback_chars(&self, family: Option<&str>, db: &Database) -> Vec<char> {
let in_family = |id| {
db.face(id).is_some_and(|face| {
face
.families
.iter()
.any(|(name, _)| Some(name.as_str()) == family)
})
};
let mut missing = Vec::new();
for run in self.buffer.layout_runs() {
let fallback = |glyph: &&LayoutGlyph| glyph.glyph_id == 0 || !in_family(glyph.font_id);
for glyph in run.glyphs.iter().filter(fallback) {
for ch in run.text.get(glyph.start..glyph.end).unwrap_or("").chars() {
if !missing.contains(&ch) {
missing.push(ch);
}
}
}
}
missing
}
pub(crate) fn measure(
&mut self,
fonts: &mut FontSystem,
known: taffy::Size<Option<f32>>,
available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
let width = if self.fits_width() {
known.width.or(match available.width {
AvailableSpace::Definite(width) => Some(width),
AvailableSpace::MinContent => Some(0.0),
AvailableSpace::MaxContent => None,
})
} else {
None
};
self.buffer.set_size(width, None);
self.buffer.shape_until_scroll(fonts, false);
taffy::Size {
width: known.width.unwrap_or_else(|| self.width().ceil()),
height: known.height.unwrap_or_else(|| self.height().ceil()),
}
}
pub(crate) fn finish(&mut self, content: Rect, fonts: &mut FontSystem) {
let width = self.fits_width().then_some(content.width);
self.buffer.set_size(width, None);
self.buffer.shape_until_scroll(fonts, false);
let top = ((content.height - self.height()) / 2.0).round();
self.origin = Point::new(content.x, content.y + top);
}
pub(crate) fn ink_box(&self) -> Rect {
let em = self.buffer.metrics().font_size;
Rect::new(
self.origin.x - em,
self.origin.y - em / 2.0,
self.width().ceil() + 2.0 * em,
self.height() + em,
)
}
pub(crate) fn runs(&self) -> impl Iterator<Item = LayoutRun<'_>> {
self.buffer.layout_runs().take(self.max_lines as usize)
}
fn width(&self) -> f32 {
self.runs().map(|run| run.line_w).fold(0.0, f32::max)
}
fn height(&self) -> f32 {
self.runs().map(|run| run.line_height).sum()
}
}