1use std::collections::{HashMap, HashSet};
11use std::ffi::CString;
12use std::path::PathBuf;
13use std::rc::Rc;
14
15use cosmic_text::fontdb::{Database, Source};
16use cosmic_text::{
17 Attrs, Buffer, Ellipsize, EllipsizeHeightLimit, Family, FontSystem, LayoutGlyph, LayoutRun,
18 Metrics, Shaping, Weight, Wrap,
19};
20use fontconfig::{CharSet, FC_FAMILY, FC_WEIGHT, Fontconfig, Pattern};
21use taffy::AvailableSpace;
22
23use crate::geometry::{Point, Rect};
24use crate::style::Font;
25
26pub struct TextSystem {
32 fonts: FontSystem,
33 fontconfig: Lookup,
34 families: HashMap<(&'static str, u16), Option<Rc<str>>>,
37 loaded: HashMap<(PathBuf, u32), Option<Rc<str>>>,
39 files: HashSet<PathBuf>,
41 fallback_tried: HashSet<(char, &'static str, u16)>,
44}
45
46enum Lookup {
48 NotStarted,
49 Ready(Fontconfig),
50 Failed,
51}
52
53impl TextSystem {
54 pub fn new() -> TextSystem {
56 let fonts = FontSystem::new_with_locale_and_db("en-US".to_owned(), Database::new());
59 TextSystem {
60 fonts,
61 fontconfig: Lookup::NotStarted,
62 families: HashMap::new(),
63 loaded: HashMap::new(),
64 files: HashSet::new(),
65 fallback_tried: HashSet::new(),
66 }
67 }
68
69 pub fn font_system(&mut self) -> &mut FontSystem {
71 &mut self.fonts
72 }
73
74 fn fontconfig(&mut self) -> Option<&Fontconfig> {
75 if let Lookup::NotStarted = self.fontconfig {
76 self.fontconfig = match Fontconfig::new() {
77 Some(fontconfig) => Lookup::Ready(fontconfig),
78 None => {
79 tracing::error!("fontconfig failed to start, so no fonts can be loaded");
80 Lookup::Failed
81 }
82 };
83 }
84 match &self.fontconfig {
85 Lookup::Ready(fontconfig) => Some(fontconfig),
86 _ => None,
87 }
88 }
89
90 pub(crate) fn family(&mut self, font: &Font) -> Option<Rc<str>> {
94 let key = (font.family, font.weight);
95 if let Some(name) = self.families.get(&key) {
96 return name.clone();
97 }
98 let matched = self
99 .fontconfig()
100 .and_then(|fontconfig| match_font(fontconfig, font.family, font.weight, None));
101 let name = matched.and_then(|file| self.load(file));
102 match &name {
103 Some(name) => tracing::debug!(requested = font.family, family = %name, "font loaded"),
104 None => tracing::warn!(
105 requested = font.family,
106 "no font found for this family, so its text shows nothing"
107 ),
108 }
109 self.families.insert(key, name.clone());
110 name
111 }
112
113 fn load_fallbacks(&mut self, font: &Font, missing: &[char]) -> bool {
117 let faces = self.fonts.db().len();
118 for &ch in missing {
119 if !self.fallback_tried.insert((ch, font.family, font.weight)) {
120 continue;
121 }
122 let Some(file) = self
123 .fontconfig()
124 .and_then(|fontconfig| match_font(fontconfig, font.family, font.weight, Some(ch)))
125 else {
126 continue;
127 };
128 self.load(file);
129 }
130 self.fonts.db().len() > faces
131 }
132
133 pub(crate) fn load(&mut self, (path, index): (PathBuf, u32)) -> Option<Rc<str>> {
136 let key = (path, index);
137 if let Some(name) = self.loaded.get(&key) {
138 return name.clone();
139 }
140 let (path, index) = &key;
141 if !self.files.contains(path) {
142 self.files.insert(path.clone());
143 if let Err(error) = self.fonts.db_mut().load_font_file(path) {
144 tracing::warn!(path = %path.display(), %error, "couldn't read a font file");
145 }
146 }
147 let name = self
148 .fonts
149 .db()
150 .faces()
151 .find(|face| face.index == *index && source_path(&face.source) == Some(path))
152 .and_then(|face| face.families.first())
153 .map(|(name, _language)| Rc::from(name.as_str()));
154 self.loaded.insert(key, name.clone());
155 name
156 }
157}
158
159impl Default for TextSystem {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165fn source_path(source: &Source) -> Option<&PathBuf> {
166 match source {
167 Source::File(path) | Source::SharedFile(path, _) => Some(path),
168 Source::Binary(_) => None,
169 }
170}
171
172fn match_font(
175 fontconfig: &Fontconfig,
176 family: &str,
177 weight: u16,
178 covering: Option<char>,
179) -> Option<(PathBuf, u32)> {
180 let mut pattern = Pattern::new(fontconfig).ok()?;
181 pattern
182 .add_string(FC_FAMILY, &CString::new(family).ok()?)
183 .ok()?;
184 pattern.add_integer(FC_WEIGHT, fc_weight(weight)).ok()?;
185 if let Some(ch) = covering {
186 let mut chars = CharSet::new(fontconfig).ok()?;
187 chars.add_char(ch).ok()?;
188 pattern.add_charset(chars).ok()?;
189 }
190 let matched = pattern.font_match().ok()?;
191 let path = PathBuf::from(matched.filename().ok()?);
192 let index = matched.face_index().ok().map_or(0, face_number);
193 Some((path, index))
194}
195
196pub(crate) fn face_number(index: i32) -> u32 {
201 u32::try_from(index & 0xFFFF).unwrap_or(0)
203}
204
205pub(crate) fn fc_weight(weight: u16) -> i32 {
209 const POINTS: [(f32, f32); 12] = [
210 (100.0, 0.0),
211 (200.0, 40.0),
212 (300.0, 50.0),
213 (350.0, 55.0),
214 (380.0, 75.0),
215 (400.0, 80.0),
216 (500.0, 100.0),
217 (600.0, 180.0),
218 (700.0, 200.0),
219 (800.0, 205.0),
220 (900.0, 210.0),
221 (1000.0, 215.0),
222 ];
223 let weight = f32::from(weight).clamp(100.0, 1000.0);
224 let upper = POINTS
225 .iter()
226 .position(|&(css, _)| css >= weight)
227 .unwrap_or(POINTS.len() - 1)
228 .max(1);
229 let (css_low, fc_low) = POINTS[upper - 1];
230 let (css_high, fc_high) = POINTS[upper];
231 let fc = fc_low + (weight - css_low) / (css_high - css_low) * (fc_high - fc_low);
232 fc.round() as i32
233}
234
235fn metrics(font: &Font) -> Metrics {
238 let size = if font.size.is_finite() && font.size >= 1.0 {
239 font.size
240 } else {
241 Font::DEFAULT.size
242 };
243 Metrics::new(size, (size * 1.2).round())
244}
245
246pub(crate) struct TextNode {
250 pub(crate) content: String,
251 pub(crate) ellipsis: bool,
252 pub(crate) max_lines: u32,
254 pub(crate) buffer: Buffer,
255 font: Option<Font>,
257 stale: bool,
260 pub(crate) origin: Point,
262}
263
264impl TextNode {
265 pub(crate) fn new() -> TextNode {
266 TextNode {
267 content: String::new(),
268 ellipsis: false,
269 max_lines: 1,
270 buffer: Buffer::new_empty(metrics(&Font::DEFAULT)),
271 font: None,
272 stale: true,
273 origin: Point::default(),
274 }
275 }
276
277 pub(crate) fn set_content(&mut self, content: String) -> bool {
280 self.set(|text| &mut text.content, content)
281 }
282
283 pub(crate) fn set_ellipsis(&mut self, ellipsis: bool) -> bool {
284 self.set(|text| &mut text.ellipsis, ellipsis)
285 }
286
287 pub(crate) fn set_max_lines(&mut self, max_lines: u32) -> bool {
288 self.set(|text| &mut text.max_lines, max_lines.max(1))
289 }
290
291 fn set<T: PartialEq>(&mut self, field: impl FnOnce(&mut TextNode) -> &mut T, value: T) -> bool {
292 let slot = field(self);
293 if *slot == value {
294 return false;
295 }
296 *slot = value;
297 self.stale = true;
298 true
299 }
300
301 fn fits_width(&self) -> bool {
304 self.ellipsis || self.max_lines > 1
305 }
306
307 pub(crate) fn prepare(&mut self, font: Font, text: &mut TextSystem) -> bool {
311 if !self.stale && self.font == Some(font) {
312 return false;
313 }
314 let family = text.family(&font);
315 self.buffer.set_metrics(metrics(&font));
316 self.buffer.set_wrap(if self.max_lines > 1 {
318 Wrap::WordOrGlyph
319 } else {
320 Wrap::None
321 });
322 self.buffer.set_ellipsize(if self.ellipsis {
323 Ellipsize::End(EllipsizeHeightLimit::Lines(self.max_lines as usize))
324 } else {
325 Ellipsize::None
326 });
327 self.buffer.set_size(None, None);
328 self.shape(family.as_deref(), &font, text);
329 let missing = self.fallback_chars(family.as_deref(), text.font_system().db());
330 if !missing.is_empty() && text.load_fallbacks(&font, &missing) {
331 self.shape(family.as_deref(), &font, text);
332 }
333 self.font = Some(font);
334 self.stale = false;
335 true
336 }
337
338 fn shape(&mut self, family: Option<&str>, font: &Font, text: &mut TextSystem) {
339 let attrs = Attrs::new().weight(Weight(font.weight));
340 let attrs = match family {
341 Some(family) => attrs.family(Family::Name(family)),
342 None => attrs,
343 };
344 self
345 .buffer
346 .set_text(&self.content, &attrs, Shaping::Advanced, None);
347 self.buffer.shape_until_scroll(text.font_system(), false);
348 }
349
350 fn fallback_chars(&self, family: Option<&str>, db: &Database) -> Vec<char> {
355 let in_family = |id| {
356 db.face(id).is_some_and(|face| {
357 face
358 .families
359 .iter()
360 .any(|(name, _)| Some(name.as_str()) == family)
361 })
362 };
363 let mut missing = Vec::new();
364 for run in self.buffer.layout_runs() {
365 let fallback = |glyph: &&LayoutGlyph| glyph.glyph_id == 0 || !in_family(glyph.font_id);
366 for glyph in run.glyphs.iter().filter(fallback) {
367 for ch in run.text.get(glyph.start..glyph.end).unwrap_or("").chars() {
368 if !missing.contains(&ch) {
369 missing.push(ch);
370 }
371 }
372 }
373 }
374 missing
375 }
376
377 pub(crate) fn measure(
382 &mut self,
383 fonts: &mut FontSystem,
384 known: taffy::Size<Option<f32>>,
385 available: taffy::Size<AvailableSpace>,
386 ) -> taffy::Size<f32> {
387 let width = if self.fits_width() {
388 known.width.or(match available.width {
389 AvailableSpace::Definite(width) => Some(width),
390 AvailableSpace::MinContent => Some(0.0),
391 AvailableSpace::MaxContent => None,
392 })
393 } else {
394 None
395 };
396 self.buffer.set_size(width, None);
397 self.buffer.shape_until_scroll(fonts, false);
398 taffy::Size {
399 width: known.width.unwrap_or_else(|| self.width().ceil()),
400 height: known.height.unwrap_or_else(|| self.height().ceil()),
401 }
402 }
403
404 pub(crate) fn finish(&mut self, content: Rect, fonts: &mut FontSystem) {
407 let width = self.fits_width().then_some(content.width);
408 self.buffer.set_size(width, None);
409 self.buffer.shape_until_scroll(fonts, false);
410 let top = ((content.height - self.height()) / 2.0).round();
411 self.origin = Point::new(content.x, content.y + top);
412 }
413
414 pub(crate) fn ink_box(&self) -> Rect {
420 let em = self.buffer.metrics().font_size;
421 Rect::new(
422 self.origin.x - em,
423 self.origin.y - em / 2.0,
424 self.width().ceil() + 2.0 * em,
425 self.height() + em,
426 )
427 }
428
429 pub(crate) fn runs(&self) -> impl Iterator<Item = LayoutRun<'_>> {
431 self.buffer.layout_runs().take(self.max_lines as usize)
432 }
433
434 fn width(&self) -> f32 {
435 self.runs().map(|run| run.line_w).fold(0.0, f32::max)
436 }
437
438 fn height(&self) -> f32 {
439 self.runs().map(|run| run.line_height).sum()
440 }
441}