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-core/src/fonts.rs
1pub mod canvas;
2mod external_fallback;
3mod metrics;
4mod text_layout_system;
5 
6pub use text_layout_system::TextLayoutSystem;
7 
8use std::hash::Hash;
9 
10use crate::{platform, rendering, scene::GlyphKey, SingletonEntity};
11use anyhow::{Error, Result};
12use dashmap::{
13 mapref::{entry::Entry, one::Ref},
14 DashMap,
15};
16 
17use crate::formatted_text::weight::CustomWeight;
18use enum_iterator::Sequence;
19use ordered_float::OrderedFloat;
20use pathfinder_geometry::vector::Vector2I;
21use pathfinder_geometry::{
22 rect::{RectF, RectI},
23 vector::{vec2f, Vector2F},
24};
25use serde::{Deserialize, Serialize};
26 
27#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Sequence, Serialize, Deserialize)]
28#[cfg_attr(feature = "schema_gen", derive(schemars::JsonSchema))]
29#[cfg_attr(
30 feature = "schema_gen",
31 schemars(
32 description = "Font weight for terminal text.",
33 rename_all = "snake_case"
34 )
35)]
36pub enum Weight {
37 Thin,
38 ExtraLight,
39 Light,
40 #[default]
41 Normal,
42 Medium,
43 Semibold,
44 Bold,
45 ExtraBold,
46 Black,
47}
48 
49impl Weight {
50 pub fn is_normal(&self) -> bool {
51 matches!(self, Self::Normal)
52 }
53 
54 pub fn from_custom_weight(custom_weight: Option<CustomWeight>) -> Weight {
55 if let Some(custom_weight) = custom_weight {
56 match custom_weight {
57 CustomWeight::Thin => Weight::Thin,
58 CustomWeight::ExtraLight => Weight::ExtraLight,
59 CustomWeight::Light => Weight::Light,
60 CustomWeight::Medium => Weight::Medium,
61 CustomWeight::Semibold => Weight::Semibold,
62 CustomWeight::Bold => Weight::Bold,
63 CustomWeight::ExtraBold => Weight::ExtraBold,
64 CustomWeight::Black => Weight::Black,
65 }
66 } else {
67 Weight::Normal
68 }
69 }
70 
71 pub fn to_custom_weight(&self) -> Option<CustomWeight> {
72 match self {
73 Weight::Thin => Some(CustomWeight::Thin),
74 Weight::ExtraLight => Some(CustomWeight::ExtraLight),
75 Weight::Light => Some(CustomWeight::Light),
76 Weight::Normal => None,
77 Weight::Medium => Some(CustomWeight::Medium),
78 Weight::Semibold => Some(CustomWeight::Semibold),
79 Weight::Bold => Some(CustomWeight::Bold),
80 Weight::ExtraBold => Some(CustomWeight::ExtraBold),
81 Weight::Black => Some(CustomWeight::Black),
82 }
83 }
84 
85 pub fn matches_custom_weight(&self, custom_weight: Option<CustomWeight>) -> bool {
86 match custom_weight {
87 Some(custom_weight) => custom_weight.is_weight(*self),
88 None => self.is_normal(),
89 }
90 }
91}
92 
93impl std::fmt::Display for Weight {
94 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
95 match self {
96 Weight::Thin => write!(f, "Thin"),
97 Weight::ExtraLight => write!(f, "ExtraLight"),
98 Weight::Light => write!(f, "Light"),
99 Weight::Normal => write!(f, "Normal"),
100 Weight::Medium => write!(f, "Medium"),
101 Weight::Semibold => write!(f, "Semibold"),
102 Weight::Bold => write!(f, "Bold"),
103 Weight::ExtraBold => write!(f, "ExtraBold"),
104 Weight::Black => write!(f, "Black"),
105 }
106 }
107}
108 
109pub trait CustomWeightConversion {
110 fn is_weight(&self, weight: Weight) -> bool;
111}
112 
113impl CustomWeightConversion for CustomWeight {
114 fn is_weight(&self, weight: Weight) -> bool {
115 matches!(
116 (self, weight),
117 (CustomWeight::Thin, Weight::Thin)
118 | (CustomWeight::ExtraLight, Weight::ExtraLight)
119 | (CustomWeight::Light, Weight::Light)
120 | (CustomWeight::Medium, Weight::Medium)
121 | (CustomWeight::Semibold, Weight::Semibold)
122 | (CustomWeight::Bold, Weight::Bold)
123 | (CustomWeight::ExtraBold, Weight::ExtraBold)
124 | (CustomWeight::Black, Weight::Black)
125 )
126 }
127}
128 
129#[cfg(not(target_family = "wasm"))]
130use {futures_util::future::BoxFuture, futures_util::FutureExt};
131 
132pub(crate) use external_fallback::{FontBytes, RequestedFallbackFontSource};
133 
134pub use external_fallback::{ExternalFontFamily, FallbackFontEvent, FallbackFontModel};
135pub use metrics::Metrics;
136 
137pub type GlyphId = u32;
138 
139#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
140pub struct FamilyId(pub usize);
141 
142#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
143pub struct FontId(pub usize);
144 
145type FontFamilyName = &'static str;
146 
147#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
148pub struct Properties {
149 pub style: Style,
150 pub weight: Weight,
151}
152 
153pub struct RasterizedGlyph {
154 pub canvas: canvas::Canvas,
155 pub is_emoji: bool,
156}
157 
158#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
159pub enum Style {
160 #[default]
161 Normal,
162 Italic,
163}
164 
165/// A structure to represent the subpixel alignment of a given glyph.
166///
167/// The reason this only stores a single value - the _horizontal_ subpixel
168/// alignment - is that we vertically snap glyphs to pixel boundaries.
169#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
170pub struct SubpixelAlignment(u8);
171 
172impl SubpixelAlignment {
173 /// The number of subdivisions to slice a pixel into.
174 const STEPS: u8 = 3;
175 
176 /// Quantizes the horizontal component of the provided position to the
177 /// nearest subpixel position, where `Self::STEPS` is the number of possible
178 /// subpixel positions.
179 ///
180 /// This is semantically a modulus operation - a value such as 0.95 will be
181 /// rounded up-and-around to 0.0.
182 pub fn new(glyph_position: Vector2F) -> Self {
183 // Scale the floating point range [0, 1) to [0, Self::STEPS).
184 let scaled_pos = glyph_position.x().fract() * Self::STEPS as f32;
185 // Round the scaled value to the nearest integer in range 0..Self::STEPS
186 // and convert to an integer. We use modulus to make sure we don't
187 // exceed the range.
188 let alignment = scaled_pos.round() as u8 % Self::STEPS;
189 Self(alignment)
190 }
191 
192 /// Converts a `SubpixelAlignment` to a vector representing the horizontal
193 /// offset the given alignment within its pixel.
194 pub fn to_offset(&self) -> Vector2F {
195 vec2f(self.0 as f32 / Self::STEPS as f32, 0.)
196 }
197}
198 
199#[derive(Debug, Clone)]
200pub struct FontInfo {
201 /// The family name of the font, which is displayed to users.
202 pub family_name: String,
203 /// A list of all Apple font names for fonts in this family.
204 #[cfg(target_os = "macos")]
205 pub font_names: Vec<String>,
206 pub is_monospace: bool,
207}
208 
209type RasterBoundsKey = (GlyphKey, (OrderedFloat<f32>, OrderedFloat<f32>));
210 
211pub struct Cache {
212 selections: DashMap<(FamilyId, Properties), FontId>,
213 /// Note that the properties stored in this map might not exactly match the
214 /// font. The font represents a "best match" in the font family given these
215 /// properties.
216 font_properties: DashMap<FontId, Properties>,
217 platform: Box<dyn platform::FontDB>,
218 font_metrics: DashMap<FontId, Metrics>,
219 glyphs_by_char: DashMap<(FontId, char), Option<(GlyphId, FontId)>>, // Also caching font id here for possible fallback fonts.
220 glyph_advances: DashMap<(FontId, GlyphId), Result<Vector2I, Error>>,
221 glyph_typographic_bounds: DashMap<(FontId, GlyphId), Result<RectI, Error>>,
222 raster_bounds: DashMap<RasterBoundsKey, Result<RectI, Error>>,
223 #[cfg_attr(target_family = "wasm", allow(dead_code))]
224 available_system_fonts: Option<Vec<(Option<FamilyId>, FontInfo)>>,
225 font_fallback_cache: FontFallbackCache,
226}
227 
228#[derive(Default)]
229struct FontFallbackCache {
230 loaded_fallback_families: DashMap<FontFamilyName, FamilyId>,
231 requested_fallback_families: DashMap<ExternalFontFamily, Vec<RequestedFallbackFontSource>>,
232 fallback_font_fn: Option<Box<dyn Fn(char) -> Option<ExternalFontFamily> + Send + Sync>>,
233}
234 
235impl Properties {
236 pub fn style(mut self, style: Style) -> Self {
237 self.style = style;
238 self
239 }
240 
241 pub fn weight(mut self, weight: Weight) -> Self {
242 self.weight = weight;
243 self
244 }
245}
246 
247impl Cache {
248 pub fn new(font_db: Box<dyn platform::FontDB>) -> Self {
249 Self {
250 platform: font_db,
251 selections: Default::default(),
252 font_properties: Default::default(),
253 font_metrics: Default::default(),
254 glyphs_by_char: Default::default(),
255 glyph_advances: Default::default(),
256 glyph_typographic_bounds: Default::default(),
257 raster_bounds: Default::default(),
258 available_system_fonts: Default::default(),
259 font_fallback_cache: Default::default(),
260 }
261 }
262 
263 /// Returns the [`TextLayoutSystem`], which can be used to layout text either on the main thread
264 /// or in the background.
265 pub fn text_layout_system(&self) -> TextLayoutSystem<'_> {
266 TextLayoutSystem {
267 platform: self.font_db().text_layout_system(),
268 cache: &self.font_fallback_cache,
269 }
270 }
271 
272 // TODO(alokedesai): Better consolidate the caching logic between the FontCache and the
273 // TextLayoutCache so we don't need to leak the platform-specific implementation of the
274 // FontDB outside of this struct.
275 pub(super) fn font_db(&self) -> &dyn platform::FontDB {
276 self.platform.as_ref()
277 }
278 
279 /// Returns all of the system fonts on the user's current machine. If already cached, the
280 /// current set of system fonts are immediately returned. If not cached, a future is returned
281 /// that returns all of the system fonts when awaited.
282 /// NOTE it is up to the caller to cache the result of the future via a call to
283 /// [`Self::set_system_fonts`].
284 #[cfg(not(target_family = "wasm"))]
285 pub fn all_system_fonts(
286 &self,
287 ctx: &mut crate::ModelContext<Self>,
288 ) -> BoxFuture<'static, Vec<(Option<FamilyId>, FontInfo)>> {
289 if let Some(fonts) = self.available_system_fonts.as_ref() {
290 futures::future::ready(fonts.clone()).boxed()
291 } else {
292 log::info!("Computing available system fonts");
293 let (tx, rx) = futures::channel::oneshot::channel();
294 ctx.spawn(
295 self.platform.load_all_system_fonts(),
296 |me, loaded_system_fonts, _ctx| {
297 let system_fonts = me.platform.process_loaded_system_fonts(loaded_system_fonts);
298 me.available_system_fonts = Some(system_fonts.clone());
299 let _ = tx.send(system_fonts);
300 },
301 );
302 rx.map(Result::unwrap_or_default).boxed()
303 }
304 }
305 
306 pub fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
307 self.platform.load_family_name_from_id(id)
308 }
309 
310 pub fn load_family_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
311 self.platform.load_from_bytes(name, bytes)
312 }
313 
314 #[cfg(not(target_family = "wasm"))]
315 /// Returns the family ID for a given font, loading it into memory if it's
316 /// not already known to the cache.
317 pub fn get_or_load_system_font(&mut self, font_family: &str) -> Result<FamilyId> {
318 match self.family_id_for_name(font_family) {
319 Some(id) => {
320 if let Some(available_system_fonts) = self.available_system_fonts.as_mut() {
321 if let Some(entry) =
322 available_system_fonts.iter_mut().find(|(family_id, data)| {
323 data.family_name == font_family && family_id.is_none()
324 })
325 {
326 entry.0 = Some(id);
327 }
328 }
329 Ok(id)
330 }
331 None => self.load_system_font(font_family),
332 }
333 }
334 
335 #[cfg(not(target_family = "wasm"))]
336 pub fn load_system_font(&mut self, font_family: &str) -> Result<FamilyId> {
337 self.platform.load_from_system(font_family)
338 }
339 
340 pub fn select_font(&self, family: FamilyId, properties: Properties) -> FontId {
341 match self.selections.entry((family, properties)) {
342 Entry::Occupied(entry) => *entry.get(),
343 Entry::Vacant(entry) => {
344 let font = self.platform.select_font(family, properties);
345 self.font_properties.insert(font, properties);
346 *entry.insert(font)
347 }
348 }
349 }
350 
351 pub fn line_height(&self, font_size: f32, line_height_ratio: f32) -> f32 {
352 line_height_ratio * font_size
353 }
354 
355 pub fn ascent(&self, font: FontId, point_size: f32) -> f32 {
356 let metrics = self.metrics(font);
357 metrics.ascent as f32 * metrics.font_scale(point_size)
358 }
359 
360 pub fn descent(&self, font: FontId, point_size: f32) -> f32 {
361 let metrics = self.metrics(font);
362 metrics.descent as f32 * metrics.font_scale(point_size)
363 }
364 
365 /// Returns the "leading" - the gap between two lines - for this font at the
366 /// given size.
367 pub fn leading(&self, font: FontId, point_size: f32) -> f32 {
368 let metrics = self.metrics(font);
369 metrics.line_gap as f32 * metrics.font_scale(point_size)
370 }
371 
372 pub fn glyph_advance(&self, font: FontId, point_size: f32, glyph: GlyphId) -> Result<Vector2F> {
373 let advance = match self.glyph_advances.entry((font, glyph)) {
374 Entry::Occupied(entry) => entry.into_ref(),
375 Entry::Vacant(entry) => entry.insert(self.platform.glyph_advance(font, glyph)),
376 };
377 match advance.value() {
378 Ok(advance) => Ok(advance.to_f32() * self.metrics(font).font_scale(point_size)),
379 Err(error) => Err(Error::msg(error.to_string())),
380 }
381 }
382 
383 pub fn glyph_typographic_bounds(
384 &self,
385 font: FontId,
386 point_size: f32,
387 glyph: GlyphId,
388 ) -> Result<RectF> {
389 let bounds = match self.glyph_typographic_bounds.entry((font, glyph)) {
390 Entry::Occupied(entry) => entry.into_ref(),
391 Entry::Vacant(entry) => {
392 entry.insert(self.platform.glyph_typographic_bounds(font, glyph))
393 }
394 };
395 match bounds.value() {
396 Ok(bounds) => Ok(bounds.to_f32() * self.metrics(font).font_scale(point_size)),
397 Err(error) => Err(Error::msg(error.to_string())),
398 }
399 }
400 
401 pub fn glyph_raster_bounds(
402 &self,
403 glyph_key: GlyphKey,
404 scale: Vector2F,
405 glyph_config: &rendering::GlyphConfig,
406 ) -> Result<RectI> {
407 let entry = self
408 .raster_bounds
409 .entry((glyph_key, (scale.x().into(), scale.y().into())));
410 let bounds = match entry {
411 Entry::Occupied(entry) => entry.into_ref(),
412 Entry::Vacant(entry) => entry.insert(self.platform.glyph_raster_bounds(
413 glyph_key.font_id,
414 glyph_key.font_size.into(),
415 glyph_key.glyph_id,
416 scale,
417 glyph_config,
418 )),
419 };
420 match bounds.value() {
421 Ok(bounds) => Ok(*bounds),
422 Err(error) => Err(Error::msg(error.to_string())),
423 }
424 }
425 
426 pub fn rasterized_glyph(
427 &self,
428 glyph_key: GlyphKey,
429 scale: Vector2F,
430 subpixel_alignment: SubpixelAlignment,
431 glyph_config: &rendering::GlyphConfig,
432 format: canvas::RasterFormat,
433 ) -> Result<RasterizedGlyph> {
434 self.platform.rasterize_glyph(
435 glyph_key.font_id,
436 glyph_key.font_size.into(),
437 glyph_key.glyph_id,
438 scale,
439 subpixel_alignment,
440 glyph_config,
441 format,
442 )
443 }
444 
445 /// Checks for a matching glyph in the system fallback fonts.
446 fn system_font_fallback(&self, ch: char, font: FontId) -> Option<(GlyphId, FontId)> {
447 self.platform
448 .fallback_fonts(ch, font)
449 .into_iter()
450 .find_map(|font| self.glyph_for_char(font, ch, false))
451 }
452 
453 // Returns the `GlyphId` for a given a character and font. Optionally returns
454 // the font ID of the font the character would be rendered with, which could be
455 // a fallback font if the font does not contain the glyph
456 pub fn glyph_for_char(
457 &self,
458 font: FontId,
459 char: char,
460 include_fallback_fonts: bool,
461 ) -> Option<(GlyphId, FontId)> {
462 let mut should_check_fallback_fonts = false;
463 match self.glyphs_by_char.entry((font, char)) {
464 Entry::Occupied(entry) => {
465 return *entry.into_ref();
466 }
467 Entry::Vacant(entry) => {
468 let glyph_id = self.platform.glyph_for_char(font, char);
469 
470 if let Some(glyph_id) = glyph_id {
471 return *entry.insert(Some((glyph_id, font)));
472 }
473 
474 if include_fallback_fonts {
475 // For getting glyph id from fallback fonts, we should drop the entry
476 // first to avoid dashmap from deadlocking.
477 should_check_fallback_fonts = true;
478 }
479 }
480 }
481 
482 if should_check_fallback_fonts {
483 self.font_fallback_cache.request_fallback_font_for_char(
484 char,
485 RequestedFallbackFontSource::GlyphForChar((font, char)),
486 );
487 
488 let fallback_glyph_and_font = self
489 .app_font_fallback(char, font)
490 .or(self.system_font_fallback(char, font));
491 
492 self.glyphs_by_char
493 .insert((font, char), fallback_glyph_and_font);
494 return fallback_glyph_and_font;
495 }
496 
497 self.glyphs_by_char.insert((font, char), None);
498 None
499 }
500 
501 fn metrics(&self, font: FontId) -> Ref<'_, FontId, Metrics> {
502 match self.font_metrics.entry(font) {
503 Entry::Occupied(entry) => entry.into_ref().downgrade(),
504 Entry::Vacant(entry) => entry.insert(self.platform.font_metrics(font)).downgrade(),
505 }
506 }
507 
508 pub fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
509 self.platform.family_id_for_name(name)
510 }
511 
512 pub fn em_width(&self, font_family: FamilyId, font_size: f32) -> f32 {
513 let font_id = self.select_font(font_family, Default::default());
514 let (glyph_id, _) = self
515 .glyph_for_char(font_id, 'm', false)
516 .expect("we verify in Config::new that the font has an 'm' glyph");
517 let bounds = self
518 .glyph_typographic_bounds(font_id, font_size, glyph_id)
519 .expect(
520 "we verify in Config::new that we can measure the typographic bounds of the 'm' glyph",
521 );
522 bounds.width()
523 }
524 
525 pub(crate) fn remove_glyphs_by_char_entry(&mut self, key: (FontId, char)) {
526 self.glyphs_by_char.remove(&key);
527 }
528}
529 
530impl crate::Entity for Cache {
531 type Event = ();
532}
533 
534impl SingletonEntity for Cache {}
535 
536trait MetricsExt {
537 fn font_scale(&self, point_size: f32) -> f32;
538}
539 
540impl MetricsExt for Metrics {
541 fn font_scale(&self, point_size: f32) -> f32 {
542 point_size / self.units_per_em as f32
543 }
544}
545 
546#[cfg(test)]
547#[path = "fonts_test.rs"]
548mod tests;
549