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/windows.rs
1use super::{
2 font_handle::FontHandle, FontFamily, LoadedSystemFonts, TextLayoutSystem,
3 ValidateFontSupportsEn,
4};
5use crate::fonts::FontId;
6use anyhow::Result;
7use font_kit::loader::Loader as _;
8use font_kit::{
9 family_name::FamilyName as FKFamilyName, properties::Properties as FKProperties,
10 properties::Style as FKStyle, properties::Weight as FKWeight, source::SystemSource as FKSource,
11};
12use itertools::Itertools;
13use owned_ttf_parser::OwnedFace;
14use std::collections::HashMap;
15use std::sync::Arc;
16 
17const EN_US_LOCALE: &str = "en-US";
18 
19/// Windows symbol fonts that are used to render window control icons. We specifically do not do any
20/// validation of these fonts (i.e. to check if the font contains english characters).
21const SYMBOL_ICON_FONTS: &[&str] = &["Segoe Fluent Icons", "Segoe MDL2 Assets"];
22 
23pub(crate) mod loader {
24 use crate::fonts::FontInfo;
25 
26 use super::*;
27 
28 pub fn load_all_system_fonts() -> LoadedSystemFonts {
29 let source = font_kit::source::SystemSource::new();
30 let fonts = match source.all_fonts() {
31 Ok(fonts) => fonts,
32 Err(err) => {
33 log::warn!("unable to retrieve all fonts from DirectWrite source: {err:?}");
34 return LoadedSystemFonts(vec![]);
35 }
36 };
37 
38 let mut family_map = HashMap::new();
39 
40 for font_handle in fonts.into_iter() {
41 if let Ok(font) = font_handle.load() {
42 let family_name = font.family_name();
43 let is_monospace = font.is_monospace();
44 
45 if font.glyph_for_char('m').is_none() {
46 // Only allow the user to select fonts that have an English character set.
47 log::debug!("skipping family {family_name:?} because no 'm' glyph was found");
48 continue;
49 }
50 // Convert font_kit::Handle into UI framework-specific FontHandle.
51 let font_handle = match font_handle {
52 font_kit::handle::Handle::Path { path, font_index } => {
53 FontHandle::new(path, font_index, is_monospace)
54 }
55 font_kit::handle::Handle::Memory { bytes, font_index } => {
56 let owned_face_result = match Arc::try_unwrap(bytes) {
57 // If we can ensure ownership of the bytes, create an OwnedFace without copying.
58 Ok(owned_bytes) => OwnedFace::from_vec(owned_bytes, font_index),
59 // If we can't get sole ownership, create on OwnedFace from a copy the bytes
60 // (created by .to_vec()).
61 Err(shared_bytes) => {
62 OwnedFace::from_vec(shared_bytes.to_vec(), font_index)
63 }
64 };
65 match owned_face_result {
66 Ok(typeface) => FontHandle::from(typeface),
67 Err(err) => {
68 // If we can't parse the typeface, skip it.
69 log::warn!(
70 "unable to parse typeface from family {family_name}: {err:?}"
71 );
72 continue;
73 }
74 }
75 }
76 };
77 
78 let (entry_info, entry_family) = family_map
79 .entry(family_name.clone())
80 .or_insert_with(move || {
81 (
82 FontInfo {
83 family_name: family_name.clone(),
84 is_monospace,
85 },
86 FontFamily {
87 name: family_name,
88 fonts: vec![],
89 },
90 )
91 });
92 entry_info.is_monospace |= is_monospace;
93 entry_family.fonts.push(font_handle);
94 }
95 }
96 LoadedSystemFonts(family_map.into_values().collect_vec())
97 }
98 
99 pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
100 let source = font_kit::source::SystemSource::new();
101 let family = source.select_family_by_name(font_family)?;
102 
103 let validate_supports_en = if SYMBOL_ICON_FONTS.contains(&font_family) {
104 ValidateFontSupportsEn::No
105 } else {
106 ValidateFontSupportsEn::Yes
107 };
108 
109 Ok(FontFamily {
110 name: font_family.to_string(),
111 fonts: family
112 .fonts()
113 .iter()
114 .flat_map(|font_kit_handle| {
115 load_font_from_handle(font_kit_handle, validate_supports_en)
116 })
117 .collect_vec(),
118 })
119 }
120}
121 
122impl TextLayoutSystem {
123 /// Given a specific character and FontID, find alternate system fonts that can
124 /// render that character.
125 pub fn get_fallback_fonts_for_character(
126 &self,
127 character: char,
128 font_id: FontId,
129 ) -> Result<Vec<FontId>> {
130 // Retrieve the font's family name and properties from the font store.
131 // First, find the font's fontdb ID.
132 let &original_font_id =
133 self.font_id_map
134 .read()
135 .get_by_left(&font_id)
136 .ok_or(anyhow::format_err!(
137 "No left entry found for {font_id:?} in font_id_map"
138 ))?;
139 let (style, weight, family_name) = self.get_font_info_from_store(original_font_id)?;
140 let source = FKSource::new();
141 let style = match style {
142 fontdb::Style::Normal => FKStyle::Normal,
143 fontdb::Style::Italic => FKStyle::Italic,
144 fontdb::Style::Oblique => FKStyle::Oblique,
145 };
146 let weight = FKWeight(weight.0 as f32);
147 let properties = FKProperties {
148 style,
149 weight,
150 stretch: Default::default(),
151 };
152 
153 let font_handle = source
154 .select_best_match(
155 &[
156 FKFamilyName::Title(family_name.to_owned()),
157 FKFamilyName::Monospace,
158 ],
159 &properties,
160 )
161 .map_err(|err| anyhow::anyhow!("Didn't find {family_name} in fontdb: {err}"))?;
162 
163 // Load fallback fonts for the requested character.
164 let loaded_font = font_handle.load().map_err(|err| {
165 anyhow::anyhow!("Unable to load typeface from font_kit Handle: {err:?}")
166 })?;
167 
168 let fallback_result =
169 loaded_font.get_fallbacks(character.to_string().as_str(), EN_US_LOCALE);
170 
171 // Convert each font-kit fallback `Font` into a UI framework `FontHandle` and load it into
172 // fontdb. We deliberately avoid `font_kit::Font::handle()` here: its default impl reads
173 // the full font file into an `Arc<Vec<u8>>` and returns a `Handle::Memory` with
174 // `font_index` hard-coded to `0` (see the FIXME at font-kit/src/loader.rs:172), which
175 // bypasses `TextLayoutSystem::insert_font`'s path-based dedup and loses TTC face indices.
176 // Instead we reach through `NativeFont` to the underlying `IDWriteFontFace` and recover
177 // the on-disk file path + real face index, the same way
178 // `DirectWriteSource::create_handle_from_dwrite_font` does for enumerated system fonts.
179 // This lets fontdb mmap the file lazily and lets `insert_font` dedup by `(path, index)`,
180 // so the same fallback family is loaded at most once per process.
181 let fallback_font_vec = fallback_result
182 .fonts
183 .into_iter()
184 .flat_map(|fallback_font| {
185 let loaded_handle =
186 fallback_font_path_handle(&fallback_font.font).or_else(|| {
187 // Last-resort fallback for fonts that aren't backed by a local file (e.g.
188 // custom collection loaders). These don't appear in practice for DirectWrite
189 // system fallbacks, but preserve the original byte-copy behavior so we
190 // degrade gracefully instead of dropping the glyph.
191 let handle = fallback_font.font.handle()?;
192 load_font_from_handle(&handle, ValidateFontSupportsEn::No).ok()
193 })?;
194 self.insert_font(loaded_handle).ok()
195 })
196 .collect_vec();
197 
198 Ok(fallback_font_vec)
199 }
200 
201 /// Critical section for fetching the font style, weight and family name from fontdb.
202 /// This function performs the minimum work required to fetch this information from
203 /// fontdb to minimize the amount of time spent holding a read lock on the font store.
204 fn get_font_info_from_store(
205 &self,
206 font_id: fontdb::ID,
207 ) -> Result<(fontdb::Style, fontdb::Weight, String)> {
208 let store_read_lock = self.font_store.read();
209 let db_read = store_read_lock.db();
210 let face = db_read.face(font_id).ok_or(anyhow::anyhow!(
211 "Unable to retrieve font face from fontdb font_store"
212 ))?;
213 let style = face.style;
214 let weight = face.weight;
215 let Some(en_us_family_info) = face.families.first() else {
216 return Err(anyhow::anyhow!("Font face doesn't have any family names"));
217 };
218 let (family_name, _) = en_us_family_info;
219 // Clone the family name because it's protected by the font store's RWLock.
220 Ok((style, weight, family_name.to_owned()))
221 }
222}
223 
224fn load_font_from_handle(
225 font_handle: &font_kit::handle::Handle,
226 validate_supports_en_charset: ValidateFontSupportsEn,
227) -> Result<FontHandle> {
228 let font = font_handle.load()?;
229 let is_monospace = font.is_monospace();
230 if matches!(validate_supports_en_charset, ValidateFontSupportsEn::Yes) {
231 font.glyph_for_char('m').ok_or(anyhow::format_err!(
232 "No 'm' glyph found for font {}",
233 font.full_name()
234 ))?;
235 }
236 match font_handle {
237 font_kit::handle::Handle::Path { path, font_index } => {
238 Ok(FontHandle::new(path, *font_index, is_monospace))
239 }
240 font_kit::handle::Handle::Memory { bytes, font_index } => {
241 let typeface = OwnedFace::from_vec(bytes.to_vec(), *font_index)?;
242 Ok(FontHandle::from(typeface))
243 }
244 }
245}
246 
247/// Builds a path-backed [`FontHandle`] for a font-kit DirectWrite `Font` by reaching through
248/// [`font_kit::loaders::directwrite::NativeFont`] to the underlying `IDWriteFontFace`.
249///
250/// This mirrors what font-kit itself does for enumerated system fonts in
251/// `DirectWriteSource::create_handle_from_dwrite_font` (font-kit/src/sources/directwrite.rs:103),
252/// and is the reason we carry `dwrote` as a direct dependency: font-kit's generic
253/// `Loader::handle()` default returns a `Handle::Memory` with a byte copy of the full file, which
254/// we specifically need to avoid on the per-character fallback path.
255///
256/// Returns `None` when DirectWrite cannot produce a local file path for the font, i.e. the font
257/// was loaded via a custom collection loader or backed only by an in-memory stream. For system
258/// fallback fonts returned by `IDWriteFontFallback::MapCharacters` against the system font
259/// collection, a path is always available.
260fn fallback_font_path_handle(font: &font_kit::loaders::directwrite::Font) -> Option<FontHandle> {
261 let native = font.native_font();
262 let file = native.dwrite_font_face.files().ok()?.into_iter().next()?;
263 let path = file.font_file_path().ok()?;
264 let font_index = native.dwrite_font_face.get_index();
265 Some(FontHandle::new(path, font_index, font.is_monospace()))
266}
267