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/external_fallback.rs
StratoSDK / crates / strato-ui-core / src / fonts / external_fallback.rs
1use super::{Cache, FamilyId, FontFallbackCache, FontFamilyName, FontId, GlyphId};
2 
3use crate::assets::asset_cache::Asset;
4use crate::{text_layout, Entity, ModelContext, SingletonEntity};
5 
6use std::hash::{Hash, Hasher};
7use std::mem;
8use std::sync::Arc;
9 
10use anyhow::Result;
11use itertools::Itertools;
12 
13/// Represents a font family that is lazy loaded from the web. i.e. Not a
14/// bundled or system font.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct ExternalFontFamily {
17 pub font_urls: Arc<Vec<String>>,
18 pub name: &'static str,
19}
20 
21impl Hash for ExternalFontFamily {
22 // Font families should have unique names. To avoid doing unnecessary work
23 // hashing all of the URLs of the fonts in the family, the hash function
24 // will only hash the font family name.
25 fn hash<H: Hasher>(&self, state: &mut H) {
26 self.name.hash(state);
27 }
28}
29 
30/// Specifies the different locations that can request a fallback font. Used to
31/// clear the appropriate cache when a fallback font is loaded.
32#[derive(Eq, PartialEq, Hash)]
33pub(crate) enum RequestedFallbackFontSource {
34 GlyphForChar((FontId, char)),
35 Line(text_layout::CacheKeyValue),
36 TextFrame(text_layout::CacheKeyValue),
37}
38 
39pub(crate) struct FontBytes(pub Vec<u8>);
40 
41impl Asset for FontBytes {
42 fn try_from_bytes(data: &[u8]) -> anyhow::Result<Self>
43 where
44 Self: Sized,
45 {
46 Ok(Self(data.to_vec()))
47 }
48 
49 fn size_in_bytes(&self) -> usize {
50 self.0.len()
51 }
52}
53 
54pub enum FallbackFontEvent {
55 Loaded,
56}
57 
58/// Handles notifying listeners when an external fallback font has loaded.
59pub struct FallbackFontModel {}
60 
61impl FallbackFontModel {
62 pub(crate) fn new() -> Self {
63 Self {}
64 }
65 
66 pub(crate) fn loaded_fallback_font(&mut self, ctx: &mut ModelContext<Self>) {
67 ctx.emit(FallbackFontEvent::Loaded);
68 }
69}
70 
71impl Entity for FallbackFontModel {
72 type Event = FallbackFontEvent;
73}
74 
75impl SingletonEntity for FallbackFontModel {}
76 
77impl FontFallbackCache {
78 pub(super) fn app_fallback_family_for_char(&self, ch: char) -> Option<ExternalFontFamily> {
79 self.fallback_font_fn
80 .as_ref()
81 .and_then(|fallback_font_fn| (fallback_font_fn)(ch))
82 }
83 
84 /// Checks if the application specified a fallback font for the given char.
85 /// If yes, the UI framework will lazy load the fallback font and trigger
86 /// a re-render of the window.
87 pub(crate) fn request_fallback_font_for_char(
88 &self,
89 ch: char,
90 source: RequestedFallbackFontSource,
91 ) {
92 let Some(fallback_font_family) = self.app_fallback_family_for_char(ch) else {
93 return;
94 };
95 
96 if self
97 .loaded_fallback_families
98 .contains_key(fallback_font_family.name)
99 {
100 return;
101 }
102 
103 self.requested_fallback_families
104 .entry(fallback_font_family)
105 .or_default()
106 .push(source);
107 }
108}
109 
110impl Cache {
111 pub(crate) fn load_fallback_family_from_bytes(
112 &mut self,
113 external_family: ExternalFontFamily,
114 bytes: Vec<Vec<u8>>,
115 ) -> Result<FamilyId> {
116 let family_id = self.load_family_from_bytes(external_family.name, bytes)?;
117 self.font_fallback_cache
118 .loaded_fallback_families
119 .insert(external_family.name, family_id);
120 Ok(family_id)
121 }
122 
123 pub(crate) fn set_fallback_font_fn(
124 &mut self,
125 fallback_font_fn: Box<dyn Fn(char) -> Option<ExternalFontFamily> + Send + Sync>,
126 ) {
127 self.font_fallback_cache.fallback_font_fn = Some(fallback_font_fn);
128 }
129 
130 fn app_fallback_family_for_char(&self, ch: char) -> Option<ExternalFontFamily> {
131 self.font_fallback_cache.app_fallback_family_for_char(ch)
132 }
133 
134 /// Checks for a matching glyph in the fallback fonts loaded by the application.
135 pub(crate) fn app_font_fallback(&self, ch: char, font: FontId) -> Option<(GlyphId, FontId)> {
136 let external_fallback_family = self.app_fallback_family_for_char(ch)?;
137 let fallback_family = self
138 .font_fallback_cache
139 .loaded_fallback_families
140 .get(external_fallback_family.name)?;
141 // We need to be careful here. `self.font_properties` is implemented
142 // using a DashMap, which can deadlock if we hold a reference while
143 // inserting into the map. We dereference the properties immediately
144 // to avoid holding a reference, since `select_font` will insert into
145 // the DashMap.
146 let properties = *self.font_properties.get(&font)?;
147 let fallback_font = self.select_font(*fallback_family, properties);
148 
149 self.glyph_for_char(fallback_font, ch, false)
150 }
151 
152 pub(crate) fn take_requested_fallback_families(
153 &self,
154 ) -> impl Iterator<Item = (ExternalFontFamily, Vec<RequestedFallbackFontSource>)> {
155 let result = self
156 .font_fallback_cache
157 .requested_fallback_families
158 .iter_mut()
159 .map(|mut entry| (entry.key().clone(), mem::take(entry.value_mut())))
160 .collect_vec();
161 self.font_fallback_cache.requested_fallback_families.clear();
162 result.into_iter()
163 }
164 
165 pub(crate) fn is_fallback_family_loaded(&self, family: FontFamilyName) -> bool {
166 self.font_fallback_cache
167 .loaded_fallback_families
168 .contains_key(family)
169 }
170}
171