StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use super::{ |
| 2 | font_handle::FontHandle, FontFamily, LoadedSystemFonts, TextLayoutSystem, |
| 3 | ValidateFontSupportsEn, |
| 4 | }; |
| 5 | use crate::fonts::FontId; |
| 6 | use anyhow::Result; |
| 7 | use font_kit::loader::Loader as _; |
| 8 | use 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 | }; |
| 12 | use itertools::Itertools; |
| 13 | use owned_ttf_parser::OwnedFace; |
| 14 | use std::collections::HashMap; |
| 15 | use std::sync::Arc; |
| 16 | |
| 17 | const 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). |
| 21 | const SYMBOL_ICON_FONTS: &[&str] = &["Segoe Fluent Icons", "Segoe MDL2 Assets"]; |
| 22 | |
| 23 | pub(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 | |
| 122 | impl 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 | |
| 224 | fn 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. |
| 260 | fn 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 |