Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/fonts.rs
1mod font_handle;
2mod str_index_map;
3#[cfg(not(feature = "fontkit-rasterizer"))]
4mod swash_rasterizer;
5mod text_layout;
6 
7#[cfg(target_os = "linux")]
8mod linux;
9 
10#[cfg(target_os = "windows")]
11mod windows;
12use strato_ui_core::fonts::{Style, Weight};
13#[cfg(target_os = "windows")]
14use windows::loader;
15 
16use std::any::Any;
17use std::collections::HashMap;
18use std::ops::{DerefMut, Range};
19use std::path::PathBuf;
20use std::sync::atomic::{AtomicUsize, Ordering};
21use std::sync::Arc;
22 
23use anyhow::{anyhow, bail, Result};
24use bimap::BiMap;
25use pathfinder_geometry::{
26 rect::{RectF, RectI},
27 vector::Vector2F,
28};
29use resvg::usvg::fontdb;
30use resvg::usvg::fontdb::Query;
31use vec1::Vec1;
32 
33use cosmic_text::{
34 Align, Attrs, AttrsList, BidiParagraphs, LayoutGlyph, LayoutLine, ShapeLine, Shaping, Wrap,
35};
36use dashmap::{mapref::entry::Entry, DashMap};
37use fontdb::Source;
38use itertools::Itertools;
39use parking_lot::RwLock;
40use pathfinder_geometry::vector::{vec2f, vec2i, Vector2I};
41 
42use self::font_handle::{FontData, FontHandle};
43use self::str_index_map::StrIndexMap;
44use self::text_layout::{RunBuilder, TextStylesMap};
45use crate::fonts::Metrics;
46use crate::platform::{self};
47use crate::text_layout::{CaretPosition, TextAlignment};
48use crate::{
49 fonts::{
50 canvas::RasterFormat, FamilyId, FontId, GlyphId, Properties, RasterizedGlyph,
51 SubpixelAlignment,
52 },
53 platform::LineStyle,
54 rendering::GlyphConfig,
55 text_layout::{ClipConfig, Line, StyleAndFont, TextFrame},
56};
57 
58struct FontFamily {
59 name: String,
60 fonts: Vec<FontHandle>,
61}
62 
63#[cfg(target_os = "linux")]
64mod loader {
65 use super::*;
66 use crate::windowing::winit::fonts::linux::{Error, FontconfigLoader};
67 use anyhow::Result;
68 
69 pub fn load_all_system_fonts() -> LoadedSystemFonts {
70 let manager = match FontconfigLoader::new() {
71 Ok(x) => x,
72 Err(e) => {
73 log::error!("Failed to load system fonts: {e:?}");
74 return LoadedSystemFonts(vec![]);
75 }
76 };
77 let handles = match manager.get_all_families() {
78 Ok(x) => x,
79 Err(e) => {
80 log::error!("Failed to load system fonts: {e:?}");
81 return LoadedSystemFonts(vec![]);
82 }
83 };
84 
85 let mut families = vec![];
86 for handle in handles.into_iter() {
87 let name = handle.name().to_string();
88 
89 // Note: this call will do a validation check when we load
90 match handle.into_info_and_family() {
91 Ok((info, family)) => families.push((info, family)),
92 Err(e) => {
93 // Making sure this is just a debug message, since this is not necessarily
94 // a bug if this fails to load (ex: could be an unsupported font format)
95 log::debug!("Failed to load system fonts for family {name}: {e:?}")
96 }
97 };
98 }
99 log::info!("Loaded {} font families", families.len());
100 LoadedSystemFonts(families)
101 }
102 
103 pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
104 let manager = FontconfigLoader::new()?;
105 let handle = manager.get_family(font_family)?;
106 Ok(handle.into_family()?)
107 }
108 
109 pub fn fallback_fonts(
110 family_name: &str,
111 properties: Properties,
112 ) -> Result<Vec<FontHandle>, Error> {
113 let loader = FontconfigLoader::new()?;
114 loader.fallback_fonts(family_name, properties)
115 }
116}
117 
118#[cfg(not(any(target_os = "linux", target_os = "windows")))]
119mod loader {
120 use super::*;
121 #[cfg(not(target_family = "wasm"))]
122 use crate::fonts::FontInfo;
123 
124 #[cfg(not(target_family = "wasm"))]
125 pub fn load_system_font(_font_family: &str) -> Result<FontFamily> {
126 anyhow::bail!("have not yet implemented loading system fonts")
127 }
128 
129 #[cfg(not(target_family = "wasm"))]
130 pub fn load_all_system_fonts() -> Vec<(FontInfo, FontFamily)> {
131 vec![]
132 }
133 
134 pub fn fallback_fonts(_family_name: &str, _properties: Properties) -> Result<Vec<FontHandle>> {
135 anyhow::bail!("Fallback fonts are not yet implemented on wasm")
136 }
137}
138 
139// We use font-kit's family handle to load fonts that come with Warp as
140// these binaries are already in memory and won't increase our memory load.
141fn load_font_family_from_bytes(name: &str, font_bytes: Vec<Vec<u8>>) -> Result<FontFamily> {
142 use owned_ttf_parser::OwnedFace;
143 
144 let fonts = font_bytes
145 .into_iter()
146 .map(|font| {
147 // We use index 0 here since the each set of bytes are assumed to be a single font
148 // face.
149 let face = OwnedFace::from_vec(font, 0)?;
150 let handle = FontHandle::from(face);
151 Ok(handle)
152 })
153 .collect::<Result<_>>()?;
154 
155 Ok(FontFamily {
156 fonts,
157 name: name.to_string(),
158 })
159}
160 
161/// Enum indicating whether font validation should enforce that the font supports the english language.
162#[cfg(any(target_os = "linux", target_os = "windows"))]
163#[derive(Copy, Clone)]
164enum ValidateFontSupportsEn {
165 Yes,
166 No,
167}
168 
169struct Family {
170 name: String,
171 font_ids: Vec1<FontId>,
172}
173 
174fn next_family_id() -> FamilyId {
175 static FAMILY_ID: AtomicUsize = AtomicUsize::new(0);
176 let next = FAMILY_ID.fetch_add(1, Ordering::Relaxed);
177 FamilyId(next)
178}
179 
180fn next_font_id() -> FontId {
181 static FONT_ID: AtomicUsize = AtomicUsize::new(0);
182 let next = FONT_ID.fetch_add(1, Ordering::Relaxed);
183 FontId(next)
184}
185 
186/// Identifier of a system font we've loaded into the database.
187#[derive(PartialEq, Eq, Hash, Debug, Clone)]
188struct FontKey {
189 /// The path to the font on the user's filesystem.
190 path: PathBuf,
191 /// The index within the font file that was loaded.
192 index: u32,
193}
194 
195pub struct TextLayoutSystem {
196 families: HashMap<FamilyId, Family>,
197 /// The internal font database that stores all of our loaded fonts. Since internally,
198 /// `cosmic_text` caches font selection, all of its functions to layout text are `&mut`.
199 /// However, the `FontDB` trait for text layout is _immutable_ and cannot be easily changed to
200 /// mutable. Therefore, we use an [`RwLock`] for internal mutability. `FontDB` is Sync and Send,
201 /// so we can't use a `RefCell`.
202 font_store: RwLock<cosmic_text::FontSystem>,
203 font_id_map: RwLock<BiMap<FontId, fontdb::ID>>,
204 font_selections: DashMap<(FamilyId, Properties), FontId>,
205 loaded_fonts: DashMap<FontKey, FontId>,
206 #[cfg(feature = "fontkit-rasterizer")]
207 /// The set of loaded fonts since the last time we tried to rasterize. There's an unfortunate
208 /// dependency where we need to load fonts within the TextLayoutSystem since we load fallback
209 /// fonts while doing text layout. This means we need cache the fonts we loaded so we can read
210 /// it out at raster time and load any necessary fonts.
211 loaded_font_ids_since_last_raster: RwLock<Vec<FontId>>,
212 #[cfg(not(target_os = "windows"))]
213 fallback_fonts: DashMap<FontId, Vec<FontId>>,
214}
215 
216pub struct FontDB {
217 text_layout_system: TextLayoutSystem,
218 #[cfg(feature = "fontkit-rasterizer")]
219 font_kit_rasterizer: crate::fonts::font_kit::Rasterizer,
220 #[cfg(not(feature = "fontkit-rasterizer"))]
221 swash_cache: RwLock<cosmic_text::SwashCache>,
222}
223 
224impl FontDB {
225 pub fn new() -> Self {
226 Self {
227 text_layout_system: TextLayoutSystem::new(),
228 #[cfg(feature = "fontkit-rasterizer")]
229 font_kit_rasterizer: crate::fonts::font_kit::Rasterizer::new(),
230 #[cfg(not(feature = "fontkit-rasterizer"))]
231 swash_cache: RwLock::new(cosmic_text::SwashCache::new()),
232 }
233 }
234 
235 /// Inserts the given font family into the DB, returning a [`FamilyId`]
236 /// for the inserted family.
237 fn insert_font_family(&mut self, font_family: FontFamily) -> Result<FamilyId> {
238 let mut font_ids = Vec::with_capacity(font_family.fonts.len());
239 for font in font_family.fonts {
240 let font_id = self.text_layout_system.insert_font(font)?;
241 
242 #[cfg(feature = "fontkit-rasterizer")]
243 self.load_font_kit_font(font_id)?;
244 
245 font_ids.push(font_id);
246 }
247 
248 let font_ids = Vec1::try_from_vec(font_ids)?;
249 
250 let family_id = next_family_id();
251 self.text_layout_system.families.insert(
252 family_id,
253 Family {
254 name: font_family.name,
255 font_ids,
256 },
257 );
258 Ok(family_id)
259 }
260 
261 #[cfg(feature = "fontkit-rasterizer")]
262 fn load_font_kit_font(&self, font_id: FontId) -> Result<()> {
263 let font_kit_font = self
264 .text_layout_system
265 .try_read_face_source(font_id, |source, index| {
266 match source {
267 Source::Binary(bytes) => {
268 // There's no easy way to convert from an `Arc<dyn AsRef<[u8]>` to a
269 // `Vec<u8>`, which means we need to clone here. This is unfortunate
270 // since internally the font source is actually represented as a `Vec`,
271 // but there's no way of downcasting back to the concrete type.
272 // TODO(alokedesai): Make refactors to fontdb and/or font-kit to make
273 // this conversion more performant.
274 font_kit::loader::Loader::from_bytes(
275 Arc::new(bytes.as_ref().as_ref().to_vec()),
276 index,
277 )
278 }
279 Source::File(path) => font_kit::loader::Loader::from_path(path, index),
280 Source::SharedFile(path, _) => font_kit::loader::Loader::from_path(path, index),
281 }
282 })
283 .ok_or_else(|| anyhow!("Unable to find font with given font ID"))?;
284 
285 // We have to use an `Arc` with a non send / sync type since
286 // the font kit rasterizer is actually send + sync on Mac, which results
287 // in it being wrapped in an Arc.
288 #[allow(clippy::arc_with_non_send_sync)]
289 self.font_kit_rasterizer
290 .insert(font_id, Arc::new(font_kit_font?));
291 Ok(())
292 }
293}
294 
295impl Default for TextLayoutSystem {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300 
301impl TextLayoutSystem {
302 pub fn new() -> Self {
303 Self {
304 families: Default::default(),
305 font_store: RwLock::new(cosmic_text::FontSystem::new_with_locale_and_db(
306 // Locale is needed for font fallback. For now, we hardcode this to "en" to match
307 // our mac implementation https://github.com/warpdotdev/warp-internal/blob/bf33d651a9fcece70df8eac35f89b0393ca5189a/ui/src/platform/mac/fonts.rs#L383.
308 "en".into(),
309 Default::default(),
310 )),
311 font_id_map: Default::default(),
312 font_selections: Default::default(),
313 loaded_fonts: Default::default(),
314 #[cfg(not(target_os = "windows"))]
315 fallback_fonts: Default::default(),
316 #[cfg(feature = "fontkit-rasterizer")]
317 loaded_font_ids_since_last_raster: Default::default(),
318 }
319 }
320 
321 /// Helper function to read a Font Face ([`owned_ttf_parser::Face`]) identified by a [`FontId`].
322 ///
323 /// ## Panics
324 /// This function can panic under a few conditions:
325 /// 1) If there is no font identified by [`FontId`] stored in the [`TextLayoutSystem`].
326 /// 2) The underlying [`fontdb::Database`] does not hold a font identified by [`ID`]. We only
327 /// store an [`ID`] in the `font_id_map` upon inserting it in the [`fontdb::Database`].
328 /// 3) The font can't be parsed by [`ttf_parser`]. We validate fonts can be parsed by
329 /// `owned_ttf_parser` when we initially load a font.
330 fn read_font_face<T, F: FnOnce(owned_ttf_parser::Face) -> T>(
331 &self,
332 font_id: FontId,
333 ttf_font_callback: F,
334 ) -> T {
335 self.read_font_face_data(font_id, |data, index| {
336 // SAFETY: We've already validated that we can parse the font when loading it.
337 let font_face =
338 owned_ttf_parser::Face::parse(data, index).expect("Must be able to parse font");
339 ttf_font_callback(font_face)
340 })
341 }
342 
343 /// Safe version of [`Self::read_font_face`]. Instead of panicking, will return None
344 fn try_read_font_face<T, F: FnOnce(owned_ttf_parser::Face) -> T>(
345 &self,
346 font_id: FontId,
347 ttf_font_callback: F,
348 ) -> Option<T> {
349 let result = self.try_read_font_face_data(font_id, |data, index| {
350 let Ok(font_face) = owned_ttf_parser::Face::parse(data, index) else {
351 return None;
352 };
353 Some(ttf_font_callback(font_face))
354 });
355 match result {
356 // Flatten out the Option<Option<T>> into an Option<T>,
357 // to keep the API in line with try_read_font_face_data
358 None => None,
359 Some(None) => None,
360 Some(Some(x)) => Some(x),
361 }
362 }
363 
364 /// Helper function to read the font data of a font identified by a [`FontId`].
365 ///
366 /// ## Panics
367 /// This function can panic under a few conditions:
368 /// 1) If there is no font identified by [`FontId`] stored in the [`TextLayoutSystem`].
369 /// 2) The underlying [`fontdb::Database`] does not hold a font identified by [`ID`]. We only
370 /// store an [`ID`] in the `font_id_map` upon inserting it in the [`fontdb::Database`].
371 fn read_font_face_data<T, F: FnOnce(&[u8], u32) -> T>(
372 &self,
373 font_id: FontId,
374 font_data_callback: F,
375 ) -> T {
376 match self.try_read_font_face_data(font_id, font_data_callback) {
377 Some(x) => x,
378 None => {
379 match self.font_id_map.read().get_by_left(&font_id) {
380 Some(internal_font_id) => {
381 let internal_font_id = *internal_font_id;
382 let panic_font_data =
383 self.read_font_face_panic_data(font_id, Some(internal_font_id));
384 let panic_message = match self.font_store.read().db().face_source(internal_font_id)
385 {
386 None => {
387 format!("does not have an internal font source for id {internal_font_id}")
388 }
389 Some((source, idx)) => format!(
390 "was unable to load font source ({source:?}, {idx}) for id {internal_font_id}"
391 )
392 };
393 log::warn!(
394 "Tried to load font data {panic_font_data}, but {panic_message}"
395 );
396 }
397 None => {
398 let panic_font_data = self.read_font_face_panic_data(font_id, None);
399 log::warn!("Tried to load font data {panic_font_data}, but no corresponding internal id exists");
400 }
401 };
402 panic!("Tried to load font data. No font source");
403 }
404 }
405 }
406 
407 /// Safe version of [`Self::read_font_face_data`]. Instead of panicking, will return None
408 fn try_read_font_face_data<T, F: FnOnce(&[u8], u32) -> T>(
409 &self,
410 font_id: FontId,
411 font_data_callback: F,
412 ) -> Option<T> {
413 let internal_font_id = *self.font_id_map.read().get_by_left(&font_id)?;
414 self.font_store
415 .read()
416 .db()
417 .with_face_data(internal_font_id, |font_data, index| {
418 font_data_callback(font_data, index)
419 })
420 }
421 
422 /// Returns the [`Source`] and corresponding font index of a font identified by
423 /// [`FontId`].
424 #[allow(dead_code)]
425 fn try_read_face_source<T, F: FnOnce(Source, u32) -> T>(
426 &self,
427 font_id: FontId,
428 face_source_callback: F,
429 ) -> Option<T> {
430 let internal_font_id = *self.font_id_map.read().get_by_left(&font_id)?;
431 let font_store = self.font_store.read();
432 
433 let (source, index) = font_store.db().face_source(internal_font_id)?;
434 Some(face_source_callback(source, index))
435 }
436 
437 fn read_font_face_panic_data(
438 &self,
439 font_id: FontId,
440 internal_id: Option<fontdb::ID>,
441 ) -> String {
442 let internal_id = internal_id
443 .map(|id| format!("{id}"))
444 .unwrap_or("None".to_string());
445 let family_name = self
446 .families
447 .values()
448 .find(|family| family.font_ids.contains(&font_id))
449 .map(|f| f.name.as_str())
450 .unwrap_or("Unavailable");
451 let font_loaded = self.loaded_fonts.iter().any(|z| z.value() == &font_id);
452 format!(
453 "Font(id={font_id:?}, internal={internal_id}, family={family_name}, loaded={font_loaded}"
454 )
455 }
456 
457 #[cfg(feature = "fontkit-rasterizer")]
458 fn take_fonts_loaded_since_last_raster(&self) -> Vec<FontId> {
459 let mut new_vec = vec![];
460 let mut loaded_fonts = self.loaded_font_ids_since_last_raster.write();
461 
462 std::mem::swap(loaded_fonts.as_mut(), &mut new_vec);
463 new_vec
464 }
465 
466 /// Inserts the font identified by the given [`FontHandle`] into the DB, returning the [`FontId`] identified by that
467 /// font.
468 /// If the font has already been loaded, the same [`FontId`] from the initial time the font was loaded is returned.
469 fn insert_font(&self, font_handle: FontHandle) -> Result<FontId> {
470 let (source, index) = match font_handle.into_data() {
471 FontData::Bytes(face) => (fontdb::Source::Binary(Arc::new(face.into_vec())), 0),
472 FontData::Path { path, index, .. } => (fontdb::Source::File(path.clone()), index),
473 };
474 
475 if let Source::File(path) = &source {
476 // The font we're trying to load has already been loaded into the DB.
477 if let Some(id) = self.loaded_fonts.get(&FontKey {
478 path: path.clone(),
479 index,
480 }) {
481 return Ok(*id);
482 }
483 }
484 
485 // TODO(alokedesai): Consider using FontDB's `make_shared_font_data` here. FontDB creates a temporary memory
486 // mapped file every time data is read via a call to `with_face_data`. When rendering text this can
487 // repeatedly cause allocation of a large amount of virtual address space, especially when trying to load
488 // large fonts. See https://github.com/RazrFalcon/fontdb/issues/18 for more details.
489 let fontdb_ids = self.font_store.write().db_mut().load_font_source(source);
490 
491 if fontdb_ids.is_empty() {
492 bail!("Loaded a font source that corresponds to 0 font faces")
493 }
494 
495 let mut font_id_to_return = None;
496 
497 for id in fontdb_ids {
498 let font_store = self.font_store.read();
499 let Some(face_info) = font_store.db().face(id) else {
500 continue;
501 };
502 
503 // Update our FontID map for every font that was loaded into the internal fontDB. cosmic_text may return a
504 // `fontdb::ID` for a glyph based on the state of the fontDB, so we need to make sure we have a matching
505 // FontID or else we will panic.
506 let font_id = next_font_id();
507 self.font_id_map.write().insert(font_id, id);
508 #[cfg(feature = "fontkit-rasterizer")]
509 self.loaded_font_ids_since_last_raster.write().push(font_id);
510 
511 if let Source::File(path) = &face_info.source {
512 self.loaded_fonts.insert(
513 FontKey {
514 path: path.clone(),
515 index: face_info.index,
516 },
517 font_id,
518 );
519 }
520 
521 if face_info.index == index {
522 font_id_to_return = Some(font_id);
523 }
524 }
525 
526 font_id_to_return
527 .ok_or_else(|| anyhow!("Requested index for the font handle was unable to be loaded"))
528 }
529 
530 /// Loads all of the fallback fonts for the `font_id` with a given `family_name` and set of `properties`.
531 /// Noops if fallback fonts for the font are already loaded.
532 #[cfg(not(target_os = "windows"))]
533 fn load_fallback_fonts(&self, font_id: FontId, family_name: &str, properties: Properties) {
534 if self.fallback_fonts.contains_key(&font_id) {
535 return;
536 }
537 
538 if let Ok(fallback_fonts) = loader::fallback_fonts(family_name, properties) {
539 let fallback_fonts = fallback_fonts
540 .into_iter()
541 .filter_map(|font| self.insert_font(font).ok())
542 .collect_vec();
543 
544 self.fallback_fonts.insert(font_id, fallback_fonts);
545 }
546 }
547 
548 /// On Windows, this is a no-op. We retrieve fallback fonts dynamically as needed.
549 #[cfg(target_os = "windows")]
550 fn load_fallback_fonts(&self, _font_id: FontId, _family_name: &str, _properties: Properties) {}
551 
552 #[allow(clippy::too_many_arguments)]
553 fn create_text_frame(
554 &self,
555 layout_lines: impl Iterator<Item = (LayoutLine, bool)>,
556 line_style: LineStyle,
557 text_styles_map: &TextStylesMap,
558 max_height: f32,
559 alignment: TextAlignment,
560 str_index_map: &StrIndexMap,
561 text: &str,
562 ) -> TextFrame {
563 let (_, upper_bound) = layout_lines.size_hint();
564 let mut lines = match upper_bound {
565 None => vec![],
566 Some(size) => Vec::with_capacity(size),
567 };
568 
569 let mut max_line_width: f32 = 0.;
570 let mut total_height = 0.;
571 let mut line_glyph_start_index: usize = 0;
572 
573 let mut layout_lines = layout_lines.peekable();
574 while let Some((line, has_trailing_newline)) = layout_lines.next() {
575 max_line_width = max_line_width.max(line.w);
576 let is_last_line = layout_lines.peek().is_none();
577 let line = self.create_line(
578 line,
579 line_style,
580 text_styles_map,
581 is_last_line.then_some(ClipConfig::default()),
582 str_index_map,
583 text,
584 line_glyph_start_index,
585 has_trailing_newline,
586 );
587 total_height += line.height();
588 
589 // We add 1 to the last caret position here to skip the newline character.
590 // Since we're working with separate lines within a text frame, there is guaranteed
591 // to be a newline to skip at the end of each iteration of the loop.
592 // The only exception is on the last iteration; in that case, we don't use this
593 // value later anyway.
594 line_glyph_start_index = line
595 .caret_positions
596 .last()
597 .map(|position| position.last_offset)
598 .unwrap_or(line_glyph_start_index)
599 + 1;
600 
601 // TODO(alokedesai): Properly clip multi-line text using the same strategy we use on mac.
602 // See https://github.com/warpdotdev/warp-internal/blob/91dfe429074c6129a6b5c1c57c55c1daf6d274a9/ui/src/platform/mac/text_layout.rs#L318-L359.
603 if total_height > max_height {
604 break;
605 }
606 lines.push(line);
607 }
608 
609 match Vec1::try_from_vec(lines) {
610 Ok(lines) => TextFrame::new(lines, max_line_width, alignment),
611 Err(_) => TextFrame::empty(line_style.font_size, line_style.line_height_ratio),
612 }
613 }
614 
615 #[allow(clippy::too_many_arguments)]
616 fn create_line(
617 &self,
618 layout_line: LayoutLine,
619 line_style: LineStyle,
620 text_styles_map: &TextStylesMap,
621 clip_config: Option<ClipConfig>,
622 str_index_map: &StrIndexMap,
623 text: &str,
624 line_glyph_start_index: usize,
625 has_trailing_newline: bool,
626 ) -> Line {
627 let Some(first_glyph) = layout_line.glyphs.first() else {
628 return Line::empty(
629 line_style.font_size,
630 line_style.line_height_ratio,
631 line_glyph_start_index,
632 );
633 };
634 let width = layout_line.w;
635 
636 let initial_font_id = *self
637 .font_id_map
638 .read()
639 .get_by_right(&first_glyph.font_id)
640 .expect("Font must exist in map");
641 
642 let mut run_builder = RunBuilder::new(text_styles_map, initial_font_id, str_index_map);
643 let mut caret_positions = vec![];
644 let mut chars_with_missing_glyphs = vec![];
645 let mut last_glyph_offset: usize = 0;
646 
647 run_builder.reserve_capacity(layout_line.glyphs.len());
648 for glyph in layout_line.glyphs {
649 // TODO(daprahamian): when we have time, we should investigate pulling
650 // caret position data out of font gdef table. Right now, this impl does
651 // not work for certain ligatures
652 if glyph.w > 0.0 {
653 let start_offset = str_index_map
654 .char_index(line_glyph_start_index + glyph.start)
655 .unwrap_or(0);
656 let last_offset = str_index_map
657 .char_index(line_glyph_start_index + glyph.end)
658 .unwrap_or_else(|| str_index_map.num_chars())
659 - 1;
660 caret_positions.push(CaretPosition {
661 position_in_line: glyph.x,
662 start_offset,
663 last_offset,
664 });
665 last_glyph_offset = last_offset;
666 }
667 
668 // A glyph_id of 0 implies that no glyph was found for this character.
669 if glyph.glyph_id == 0 {
670 if let Some(ch) = Self::char_for_glyph(&glyph, text) {
671 chars_with_missing_glyphs.push(ch);
672 }
673 }
674 
675 run_builder.push_glyph(glyph, |id| {
676 *self
677 .font_id_map
678 .read()
679 .get_by_right(id)
680 .expect("Font must exist in map")
681 });
682 }
683 
684 if has_trailing_newline {
685 caret_positions.push(CaretPosition {
686 position_in_line: layout_line.w,
687 start_offset: last_glyph_offset + 1,
688 last_offset: last_glyph_offset + 1,
689 });
690 }
691 
692 Line {
693 width,
694 // TODO(vorporeal): See if we need to compute this (and if so, how to).
695 trailing_whitespace_width: 0.0,
696 runs: run_builder.build(),
697 font_size: line_style.font_size,
698 line_height_ratio: line_style.line_height_ratio,
699 baseline_ratio: line_style.baseline_ratio,
700 ascent: layout_line.max_ascent,
701 descent: layout_line.max_descent,
702 clip_config,
703 caret_positions,
704 chars_with_missing_glyphs,
705 }
706 }
707 
708 fn char_for_glyph(glyph: &LayoutGlyph, text: &str) -> Option<char> {
709 if !text.is_char_boundary(glyph.start) {
710 log::warn!("Expected glyph start to be a char boundary");
711 return None;
712 }
713 
714 text[glyph.start..].chars().next()
715 }
716 
717 /// Produces an [`AttrsList`] to layout text given a list of `style_runs` and the `text` the
718 /// runs correspond to.
719 fn build_attrs_list(
720 &self,
721 text: &str,
722 style_runs: &[(Range<usize>, StyleAndFont)],
723 text_styles_map: &mut TextStylesMap,
724 str_index_map: &StrIndexMap,
725 ) -> AttrsList {
726 let attrs = Attrs::new();
727 let mut attrs_list = AttrsList::new(attrs);
728 
729 for (range, style_and_font) in style_runs {
730 let start_byte_index = str_index_map.byte_index(range.start).unwrap_or(text.len());
731 let end_byte_index = str_index_map.byte_index(range.end).unwrap_or(text.len());
732 
733 // Perform font selection using font-db before passing the font to cosmic text. Since it
734 // does not use the CSS3 font selection algorithm, it will panic if we pass a font
735 // weight or style that isn't available for the font. See https://github.com/pop-os/cosmic-text/issues/58.
736 let selected_font =
737 self.select_font(style_and_font.font_family, style_and_font.properties);
738 let id = *self
739 .font_id_map
740 .read()
741 .get_by_left(&selected_font)
742 .expect("Selected font must exist in font_id_map");
743 
744 let font_store = self.font_store.read();
745 let face = match font_store.db().face(id) {
746 None => continue,
747 Some(face) => face,
748 };
749 
750 let Some((family, _)) = face.families.first() else {
751 continue;
752 };
753 
754 let style_index = text_styles_map.insert(style_and_font.style);
755 
756 attrs_list.add_span(
757 start_byte_index..end_byte_index,
758 Attrs {
759 color_opt: None,
760 family: cosmic_text::Family::Name(family),
761 stretch: Default::default(),
762 style: face.style,
763 weight: face.weight,
764 metadata: style_index,
765 cache_key_flags: cosmic_text::CacheKeyFlags::empty(),
766 metrics_opt: None,
767 },
768 );
769 }
770 attrs_list
771 }
772}
773 
774#[cfg_attr(target_family = "wasm", expect(dead_code))]
775struct LoadedSystemFonts(Vec<(crate::fonts::FontInfo, FontFamily)>);
776 
777impl platform::LoadedSystemFonts for LoadedSystemFonts {
778 fn as_any(self: Box<Self>) -> Box<dyn Any> {
779 self as Box<dyn Any>
780 }
781}
782 
783impl platform::FontDB for FontDB {
784 fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
785 let family = load_font_family_from_bytes(name, bytes)?;
786 self.insert_font_family(family)
787 }
788 
789 #[cfg(not(target_family = "wasm"))]
790 fn load_from_system(&mut self, font_family: &str) -> Result<FamilyId> {
791 let family = loader::load_system_font(font_family)?;
792 self.insert_font_family(family)
793 }
794 
795 #[cfg(not(target_family = "wasm"))]
796 fn load_all_system_fonts(
797 &self,
798 ) -> futures::future::BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
799 self.text_layout_system.load_all_system_fonts()
800 }
801 
802 #[cfg(not(target_family = "wasm"))]
803 fn process_loaded_system_fonts(
804 &mut self,
805 loaded_system_fonts: Box<dyn platform::LoadedSystemFonts>,
806 ) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)> {
807 let loaded_system_fonts: Box<LoadedSystemFonts> = loaded_system_fonts
808 .as_any()
809 .downcast()
810 .expect("should not fail to downcast to concrete type");
811 
812 loaded_system_fonts
813 .0
814 .into_iter()
815 .map(|(font_info, family)| {
816 let family_id = self.insert_font_family(family).ok();
817 (family_id, font_info)
818 })
819 .collect_vec()
820 }
821 
822 fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
823 self.text_layout_system.family_id_for_name(name)
824 }
825 
826 fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
827 self.text_layout_system.load_family_name_from_id(id)
828 }
829 
830 fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
831 self.text_layout_system.select_font(family_id, properties)
832 }
833 
834 fn fallback_fonts(&self, character: char, font_id: FontId) -> Vec<FontId> {
835 self.text_layout_system.fallback_fonts(character, font_id)
836 }
837 
838 fn font_metrics(&self, font_id: FontId) -> Metrics {
839 self.text_layout_system.font_metrics(font_id)
840 }
841 
842 fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I> {
843 self.text_layout_system.glyph_advance(font_id, glyph_id)
844 }
845 
846 fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
847 self.text_layout_system
848 .glyph_typographic_bounds(font_id, glyph_id)
849 }
850 
851 #[cfg(feature = "fontkit-rasterizer")]
852 fn glyph_raster_bounds(
853 &self,
854 font_id: FontId,
855 size: f32,
856 glyph_id: GlyphId,
857 scale: Vector2F,
858 glyph_config: &GlyphConfig,
859 ) -> Result<RectI> {
860 let fonts = self
861 .text_layout_system
862 .take_fonts_loaded_since_last_raster();
863 for font in fonts {
864 self.load_font_kit_font(font)?
865 }
866 self.font_kit_rasterizer
867 .glyph_raster_bounds(font_id, size, glyph_id, scale, glyph_config)
868 }
869 
870 #[cfg(not(feature = "fontkit-rasterizer"))]
871 fn glyph_raster_bounds(
872 &self,
873 font_id: FontId,
874 size: f32,
875 glyph_id: GlyphId,
876 scale: Vector2F,
877 glyph_config: &GlyphConfig,
878 ) -> Result<RectI> {
879 Self::glyph_raster_bounds(self, font_id, size, glyph_id, scale, glyph_config)
880 }
881 
882 #[cfg(feature = "fontkit-rasterizer")]
883 fn rasterize_glyph(
884 &self,
885 font_id: FontId,
886 size: f32,
887 glyph_id: GlyphId,
888 scale: Vector2F,
889 subpixel_alignment: SubpixelAlignment,
890 glyph_config: &GlyphConfig,
891 format: RasterFormat,
892 ) -> Result<RasterizedGlyph> {
893 let fonts = self
894 .text_layout_system
895 .take_fonts_loaded_since_last_raster();
896 for font in fonts {
897 self.load_font_kit_font(font)?
898 }
899 self.font_kit_rasterizer.rasterize_glyph(
900 font_id,
901 size,
902 glyph_id,
903 scale,
904 subpixel_alignment,
905 glyph_config,
906 format,
907 )
908 }
909 
910 #[cfg(not(feature = "fontkit-rasterizer"))]
911 fn rasterize_glyph(
912 &self,
913 font_id: FontId,
914 size: f32,
915 glyph_id: GlyphId,
916 scale: Vector2F,
917 subpixel_alignment: SubpixelAlignment,
918 glyph_config: &GlyphConfig,
919 format: RasterFormat,
920 ) -> Result<RasterizedGlyph> {
921 Self::rasterize_glyph(
922 self,
923 font_id,
924 size,
925 glyph_id,
926 scale,
927 subpixel_alignment,
928 glyph_config,
929 format,
930 )
931 }
932 
933 fn glyph_for_char(&self, font_id: FontId, char: char) -> Option<GlyphId> {
934 self.text_layout_system.glyph_for_char(font_id, char)
935 }
936 
937 fn text_layout_system(&self) -> &dyn platform::TextLayoutSystem {
938 &self.text_layout_system
939 }
940}
941 
942impl platform::TextLayoutSystem for TextLayoutSystem {
943 fn layout_line(
944 &self,
945 text: &str,
946 line_style: LineStyle,
947 style_runs: &[(Range<usize>, StyleAndFont)],
948 max_width: f32,
949 clip_config: ClipConfig,
950 ) -> Line {
951 // cosmic_text panics if we pass text that is multiple paragraphs. Since we are laying out
952 // a line (which is by definition _not_ wrapped), combine all of the paragraphs together
953 // with a zero-width space (U+200B). We do this to ensure that all of the text renders on a
954 // single line, while also ensuring the style runs (which previously included the separator)
955 // are still respected properly.
956 let text = BidiParagraphs::new(text).join("\u{200B}");
957 
958 let mut text_styles_map = TextStylesMap::new();
959 let str_index_map = StrIndexMap::new(&text);
960 let attrs_list = self.build_attrs_list(
961 text.as_str(),
962 style_runs,
963 &mut text_styles_map,
964 &str_index_map,
965 );
966 
967 let tab_width = line_style.fixed_width_tab_size.unwrap_or(4).into();
968 let shape_line = ShapeLine::new(
969 self.font_store.write().deref_mut(),
970 text.as_str(),
971 &attrs_list,
972 Shaping::Advanced,
973 tab_width,
974 );
975 
976 // Layout the line, passing `Wrap::None` here since we want to render all of the text on a
977 // single line.
978 let layout = shape_line.layout(
979 line_style.font_size,
980 Some(max_width),
981 Wrap::None,
982 Some(Align::Left),
983 None,
984 None,
985 );
986 
987 // Since we passed `Wrap::None`, cosmic text will only produce a single laid out line.
988 debug_assert_eq!(
989 layout.len(),
990 1,
991 "Expected a single laid out line but there were {} lines instead",
992 layout.len()
993 );
994 let first_line = match layout.into_iter().next() {
995 Some(line) => line,
996 None => return Line::empty(line_style.font_size, line_style.line_height_ratio, 0),
997 };
998 
999 self.create_line(
1000 first_line,
1001 line_style,
1002 &text_styles_map,
1003 Some(clip_config),
1004 &str_index_map,
1005 &text,
1006 0,
1007 false,
1008 )
1009 }
1010 fn layout_text(
1011 &self,
1012 text: &str,
1013 line_style: LineStyle,
1014 style_runs: &[(Range<usize>, StyleAndFont)],
1015 max_width: f32,
1016 max_height: f32,
1017 alignment: TextAlignment,
1018 first_line_head_indent: Option<f32>,
1019 ) -> TextFrame {
1020 let mut text_styles_map = TextStylesMap::new();
1021 let str_index_map = StrIndexMap::new(text);
1022 let mut attrs_list =
1023 self.build_attrs_list(text, style_runs, &mut text_styles_map, &str_index_map);
1024 let mut font_store = self.font_store.write();
1025 
1026 let tab_width = line_style.fixed_width_tab_size.unwrap_or(4).into();
1027 let mut num_bytes_seen = 0;
1028 let layouts = BidiParagraphs::new(text).flat_map(|paragraph| {
1029 let following_paragraph_text = &text[num_bytes_seen + paragraph.len()..];
1030 let mut char_indices = following_paragraph_text.char_indices();
1031 
1032 // Determine the byte length of the separator. The `BidiParagraphs` iterator does not
1033 // coalesce multiple paragraph separators so we know there is at most one separator.
1034 let (separator_byte_length, has_trailing_separator) = match char_indices.next() {
1035 Some((start_byte_index, _char)) => {
1036 // Determine the end byte index of the character by checking the start character
1037 // index of the following character. If the current character is the last
1038 // character, the end byte index is the total length of the string.
1039 let end_byte_index = char_indices
1040 .next()
1041 .map(|(index, _)| index)
1042 .unwrap_or(following_paragraph_text.len());
1043 
1044 (end_byte_index - start_byte_index, true)
1045 }
1046 None => (0, false),
1047 };
1048 num_bytes_seen += paragraph.len() + separator_byte_length;
1049 
1050 // Remove the attributes from the attrs list that correspond to this specific paragraph.
1051 // We also remove the paragraph's separator--this won't impact styling (since the length
1052 // of the text is smaller than that of the attribute list) but ensures all of the styles
1053 // related to this line of text are correctly removed.
1054 let mut current_attrs_list =
1055 attrs_list.split_off(paragraph.len() + separator_byte_length);
1056 std::mem::swap(&mut current_attrs_list, &mut attrs_list);
1057 
1058 let shape_line = ShapeLine::new(
1059 font_store.deref_mut(),
1060 paragraph,
1061 &current_attrs_list,
1062 Shaping::Advanced,
1063 tab_width,
1064 );
1065 
1066 let layout_lines = shape_line.layout(
1067 line_style.font_size,
1068 Some(max_width),
1069 Wrap::WordOrGlyph,
1070 Some(Align::Left),
1071 first_line_head_indent,
1072 None,
1073 );
1074 let layout_line_count = layout_lines.len();
1075 layout_lines
1076 .into_iter()
1077 .enumerate()
1078 .map(|(line_idx, line)| {
1079 // Each ShapeLine may get wrapped to multiple LayoutLines.
1080 // We should only add the trailing separator to the last LayoutLine.
1081 let is_last = line_idx == layout_line_count.saturating_sub(1);
1082 let include_trailing_separator = is_last && has_trailing_separator;
1083 (line, include_trailing_separator)
1084 })
1085 .collect_vec()
1086 });
1087 
1088 self.create_text_frame(
1089 layouts,
1090 line_style,
1091 &text_styles_map,
1092 max_height,
1093 alignment,
1094 &str_index_map,
1095 text,
1096 )
1097 }
1098}
1099 
1100impl TextLayoutSystem {
1101 #[cfg(not(target_family = "wasm"))]
1102 fn load_all_system_fonts(
1103 &self,
1104 ) -> futures::future::BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
1105 use futures::FutureExt as _;
1106 
1107 async { Box::new(loader::load_all_system_fonts()) as Box<dyn platform::LoadedSystemFonts> }
1108 .boxed()
1109 }
1110 
1111 fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
1112 self.families.get(&id).map(|family| family.name.to_owned())
1113 }
1114 
1115 fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
1116 match self.font_selections.entry((family_id, properties)) {
1117 Entry::Occupied(entry) => *entry.get(),
1118 Entry::Vacant(entry) => {
1119 let family = self
1120 .families
1121 .get(&family_id)
1122 .expect("Font family must exist");
1123 
1124 let weight = match properties.weight {
1125 Weight::Thin => fontdb::Weight::THIN,
1126 Weight::ExtraLight => fontdb::Weight::EXTRA_LIGHT,
1127 Weight::Light => fontdb::Weight::LIGHT,
1128 Weight::Normal => fontdb::Weight::NORMAL,
1129 Weight::Medium => fontdb::Weight::MEDIUM,
1130 Weight::Semibold => fontdb::Weight::SEMIBOLD,
1131 Weight::Bold => fontdb::Weight::BOLD,
1132 Weight::ExtraBold => fontdb::Weight::EXTRA_BOLD,
1133 Weight::Black => fontdb::Weight::BLACK,
1134 };
1135 let style = match properties.style {
1136 Style::Normal => fontdb::Style::Normal,
1137 Style::Italic => fontdb::Style::Italic,
1138 };
1139 let best_match = self.font_store.read().db().query(&Query {
1140 families: &[fontdb::Family::Name(family.name.as_str())],
1141 weight,
1142 stretch: Default::default(),
1143 style,
1144 });
1145 
1146 let best_match =
1147 best_match.and_then(|id| self.font_id_map.read().get_by_right(&id).copied());
1148 let best_match = best_match.unwrap_or(*family.font_ids.first());
1149 
1150 self.load_fallback_fonts(best_match, family.name.as_str(), properties);
1151 
1152 *entry.insert(best_match)
1153 }
1154 }
1155 }
1156 
1157 #[cfg(not(target_os = "windows"))]
1158 fn fallback_fonts(&self, _ch: char, font_id: FontId) -> Vec<FontId> {
1159 self.fallback_fonts
1160 .get(&font_id)
1161 .map(|fallbacks| fallbacks.clone())
1162 .unwrap_or_default()
1163 }
1164 
1165 #[cfg(target_os = "windows")]
1166 fn fallback_fonts(&self, character: char, font_id: FontId) -> Vec<FontId> {
1167 self.get_fallback_fonts_for_character(character, font_id)
1168 .map_err(|err| {
1169 log::warn!("Unable to fetch fallback fonts for character {character:?}: {err:?}");
1170 err
1171 })
1172 .unwrap_or_default()
1173 }
1174 
1175 fn font_metrics(&self, font_id: FontId) -> crate::fonts::Metrics {
1176 self.read_font_face(font_id, |font_face| crate::fonts::Metrics {
1177 units_per_em: font_face.units_per_em().into(),
1178 ascent: font_face.ascender(),
1179 descent: font_face.descender(),
1180 line_gap: font_face.line_gap(),
1181 })
1182 }
1183 
1184 fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I> {
1185 self.read_font_face(font_id, |font_face| {
1186 let glyph_id = owned_ttf_parser::GlyphId(glyph_id as u16);
1187 
1188 let horizontal_advance = font_face.glyph_hor_advance(glyph_id).unwrap_or(0);
1189 let vertical_advance = font_face.glyph_ver_advance(glyph_id).unwrap_or(0);
1190 
1191 Ok(vec2i(horizontal_advance.into(), vertical_advance.into()))
1192 })
1193 }
1194 
1195 fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
1196 self.read_font_face(font_id, |font_face| {
1197 let ttf_parser_glyph_id = owned_ttf_parser::GlyphId::from_glyph_id(glyph_id);
1198 
1199 let raster_image = font_face.glyph_raster_image(ttf_parser_glyph_id, u16::MAX);
1200 if let Some(raster_image) = raster_image {
1201 // The raster image's bounds is in pixels. Determine the scale factor to convert the raster image's
1202 // bounds back to font units.
1203 let scale = if raster_image.pixels_per_em != 0 {
1204 font_face.units_per_em() as f32 / raster_image.pixels_per_em as f32
1205 } else {
1206 1.0
1207 };
1208 
1209 let bounding_box = RectF::new(
1210 vec2f(raster_image.x as f32, raster_image.y as f32),
1211 vec2f(raster_image.width as f32, raster_image.height as f32),
1212 );
1213 
1214 return Ok((bounding_box * scale).to_i32());
1215 }
1216 let bounding_box = font_face
1217 .glyph_bounding_box(ttf_parser_glyph_id)
1218 .ok_or_else(|| anyhow!("No bounding box for glyph id {glyph_id:?}"))?;
1219 
1220 Ok(bounding_box.to_recti())
1221 })
1222 }
1223 
1224 fn glyph_for_char(&self, font_id: FontId, char: char) -> Option<GlyphId> {
1225 self.try_read_font_face(font_id, |font_face| {
1226 font_face.glyph_index(char).map(GlyphIdExt::to_glyph_id)
1227 })?
1228 }
1229 
1230 fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
1231 self.families
1232 .iter()
1233 .find_map(|(family_id, family)| (family.name == name).then_some(*family_id))
1234 }
1235}
1236 
1237/// Helper extension trait to convert to a [`RectI`].
1238trait ToRectI {
1239 fn to_recti(self) -> RectI;
1240}
1241 
1242impl ToRectI for owned_ttf_parser::Rect {
1243 fn to_recti(self) -> RectI {
1244 RectI::new(
1245 vec2i(self.x_min.into(), self.y_min.into()),
1246 vec2i(self.width().into(), self.height().into()),
1247 )
1248 }
1249}
1250 
1251/// Helper extension trait to convert to a [`GlyphId`]
1252trait GlyphIdExt {
1253 fn to_glyph_id(self) -> GlyphId;
1254 
1255 fn from_glyph_id(glyph_id: GlyphId) -> Self;
1256}
1257 
1258impl GlyphIdExt for owned_ttf_parser::GlyphId {
1259 fn to_glyph_id(self) -> GlyphId {
1260 self.0.into()
1261 }
1262 
1263 fn from_glyph_id(glyph_id: GlyphId) -> Self {
1264 Self(glyph_id as u16)
1265 }
1266}
1267 
1268#[cfg(test)]
1269#[path = "text_layout_tests.rs"]
1270mod layout_tests;
1271