Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
use super::{
|
||||
font_handle::FontHandle, FontFamily, LoadedSystemFonts, TextLayoutSystem,
|
||||
ValidateFontSupportsEn,
|
||||
};
|
||||
use crate::fonts::FontId;
|
||||
use anyhow::Result;
|
||||
use font_kit::loader::Loader as _;
|
||||
use font_kit::{
|
||||
family_name::FamilyName as FKFamilyName, properties::Properties as FKProperties,
|
||||
properties::Style as FKStyle, properties::Weight as FKWeight, source::SystemSource as FKSource,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use owned_ttf_parser::OwnedFace;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EN_US_LOCALE: &str = "en-US";
|
||||
|
||||
/// Windows symbol fonts that are used to render window control icons. We specifically do not do any
|
||||
/// validation of these fonts (i.e. to check if the font contains english characters).
|
||||
const SYMBOL_ICON_FONTS: &[&str] = &["Segoe Fluent Icons", "Segoe MDL2 Assets"];
|
||||
|
||||
pub(crate) mod loader {
|
||||
use crate::fonts::FontInfo;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn load_all_system_fonts() -> LoadedSystemFonts {
|
||||
let source = font_kit::source::SystemSource::new();
|
||||
let fonts = match source.all_fonts() {
|
||||
Ok(fonts) => fonts,
|
||||
Err(err) => {
|
||||
log::warn!("unable to retrieve all fonts from DirectWrite source: {err:?}");
|
||||
return LoadedSystemFonts(vec![]);
|
||||
}
|
||||
};
|
||||
|
||||
let mut family_map = HashMap::new();
|
||||
|
||||
for font_handle in fonts.into_iter() {
|
||||
if let Ok(font) = font_handle.load() {
|
||||
let family_name = font.family_name();
|
||||
let is_monospace = font.is_monospace();
|
||||
|
||||
if font.glyph_for_char('m').is_none() {
|
||||
// Only allow the user to select fonts that have an English character set.
|
||||
log::debug!("skipping family {family_name:?} because no 'm' glyph was found");
|
||||
continue;
|
||||
}
|
||||
// Convert font_kit::Handle into UI framework-specific FontHandle.
|
||||
let font_handle = match font_handle {
|
||||
font_kit::handle::Handle::Path { path, font_index } => {
|
||||
FontHandle::new(path, font_index, is_monospace)
|
||||
}
|
||||
font_kit::handle::Handle::Memory { bytes, font_index } => {
|
||||
let owned_face_result = match Arc::try_unwrap(bytes) {
|
||||
// If we can ensure ownership of the bytes, create an OwnedFace without copying.
|
||||
Ok(owned_bytes) => OwnedFace::from_vec(owned_bytes, font_index),
|
||||
// If we can't get sole ownership, create on OwnedFace from a copy the bytes
|
||||
// (created by .to_vec()).
|
||||
Err(shared_bytes) => {
|
||||
OwnedFace::from_vec(shared_bytes.to_vec(), font_index)
|
||||
}
|
||||
};
|
||||
match owned_face_result {
|
||||
Ok(typeface) => FontHandle::from(typeface),
|
||||
Err(err) => {
|
||||
// If we can't parse the typeface, skip it.
|
||||
log::warn!(
|
||||
"unable to parse typeface from family {family_name}: {err:?}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (entry_info, entry_family) = family_map
|
||||
.entry(family_name.clone())
|
||||
.or_insert_with(move || {
|
||||
(
|
||||
FontInfo {
|
||||
family_name: family_name.clone(),
|
||||
is_monospace,
|
||||
},
|
||||
FontFamily {
|
||||
name: family_name,
|
||||
fonts: vec![],
|
||||
},
|
||||
)
|
||||
});
|
||||
entry_info.is_monospace |= is_monospace;
|
||||
entry_family.fonts.push(font_handle);
|
||||
}
|
||||
}
|
||||
LoadedSystemFonts(family_map.into_values().collect_vec())
|
||||
}
|
||||
|
||||
pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
|
||||
let source = font_kit::source::SystemSource::new();
|
||||
let family = source.select_family_by_name(font_family)?;
|
||||
|
||||
let validate_supports_en = if SYMBOL_ICON_FONTS.contains(&font_family) {
|
||||
ValidateFontSupportsEn::No
|
||||
} else {
|
||||
ValidateFontSupportsEn::Yes
|
||||
};
|
||||
|
||||
Ok(FontFamily {
|
||||
name: font_family.to_string(),
|
||||
fonts: family
|
||||
.fonts()
|
||||
.iter()
|
||||
.flat_map(|font_kit_handle| {
|
||||
load_font_from_handle(font_kit_handle, validate_supports_en)
|
||||
})
|
||||
.collect_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayoutSystem {
|
||||
/// Given a specific character and FontID, find alternate system fonts that can
|
||||
/// render that character.
|
||||
pub fn get_fallback_fonts_for_character(
|
||||
&self,
|
||||
character: char,
|
||||
font_id: FontId,
|
||||
) -> Result<Vec<FontId>> {
|
||||
// Retrieve the font's family name and properties from the font store.
|
||||
// First, find the font's fontdb ID.
|
||||
let &original_font_id =
|
||||
self.font_id_map
|
||||
.read()
|
||||
.get_by_left(&font_id)
|
||||
.ok_or(anyhow::format_err!(
|
||||
"No left entry found for {font_id:?} in font_id_map"
|
||||
))?;
|
||||
let (style, weight, family_name) = self.get_font_info_from_store(original_font_id)?;
|
||||
let source = FKSource::new();
|
||||
let style = match style {
|
||||
fontdb::Style::Normal => FKStyle::Normal,
|
||||
fontdb::Style::Italic => FKStyle::Italic,
|
||||
fontdb::Style::Oblique => FKStyle::Oblique,
|
||||
};
|
||||
let weight = FKWeight(weight.0 as f32);
|
||||
let properties = FKProperties {
|
||||
style,
|
||||
weight,
|
||||
stretch: Default::default(),
|
||||
};
|
||||
|
||||
let font_handle = source
|
||||
.select_best_match(
|
||||
&[
|
||||
FKFamilyName::Title(family_name.to_owned()),
|
||||
FKFamilyName::Monospace,
|
||||
],
|
||||
&properties,
|
||||
)
|
||||
.map_err(|err| anyhow::anyhow!("Didn't find {family_name} in fontdb: {err}"))?;
|
||||
|
||||
// Load fallback fonts for the requested character.
|
||||
let loaded_font = font_handle.load().map_err(|err| {
|
||||
anyhow::anyhow!("Unable to load typeface from font_kit Handle: {err:?}")
|
||||
})?;
|
||||
|
||||
let fallback_result =
|
||||
loaded_font.get_fallbacks(character.to_string().as_str(), EN_US_LOCALE);
|
||||
|
||||
// Convert each font-kit fallback `Font` into a UI framework `FontHandle` and load it into
|
||||
// fontdb. We deliberately avoid `font_kit::Font::handle()` here: its default impl reads
|
||||
// the full font file into an `Arc<Vec<u8>>` and returns a `Handle::Memory` with
|
||||
// `font_index` hard-coded to `0` (see the FIXME at font-kit/src/loader.rs:172), which
|
||||
// bypasses `TextLayoutSystem::insert_font`'s path-based dedup and loses TTC face indices.
|
||||
// Instead we reach through `NativeFont` to the underlying `IDWriteFontFace` and recover
|
||||
// the on-disk file path + real face index, the same way
|
||||
// `DirectWriteSource::create_handle_from_dwrite_font` does for enumerated system fonts.
|
||||
// This lets fontdb mmap the file lazily and lets `insert_font` dedup by `(path, index)`,
|
||||
// so the same fallback family is loaded at most once per process.
|
||||
let fallback_font_vec = fallback_result
|
||||
.fonts
|
||||
.into_iter()
|
||||
.flat_map(|fallback_font| {
|
||||
let loaded_handle =
|
||||
fallback_font_path_handle(&fallback_font.font).or_else(|| {
|
||||
// Last-resort fallback for fonts that aren't backed by a local file (e.g.
|
||||
// custom collection loaders). These don't appear in practice for DirectWrite
|
||||
// system fallbacks, but preserve the original byte-copy behavior so we
|
||||
// degrade gracefully instead of dropping the glyph.
|
||||
let handle = fallback_font.font.handle()?;
|
||||
load_font_from_handle(&handle, ValidateFontSupportsEn::No).ok()
|
||||
})?;
|
||||
self.insert_font(loaded_handle).ok()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
Ok(fallback_font_vec)
|
||||
}
|
||||
|
||||
/// Critical section for fetching the font style, weight and family name from fontdb.
|
||||
/// This function performs the minimum work required to fetch this information from
|
||||
/// fontdb to minimize the amount of time spent holding a read lock on the font store.
|
||||
fn get_font_info_from_store(
|
||||
&self,
|
||||
font_id: fontdb::ID,
|
||||
) -> Result<(fontdb::Style, fontdb::Weight, String)> {
|
||||
let store_read_lock = self.font_store.read();
|
||||
let db_read = store_read_lock.db();
|
||||
let face = db_read.face(font_id).ok_or(anyhow::anyhow!(
|
||||
"Unable to retrieve font face from fontdb font_store"
|
||||
))?;
|
||||
let style = face.style;
|
||||
let weight = face.weight;
|
||||
let Some(en_us_family_info) = face.families.first() else {
|
||||
return Err(anyhow::anyhow!("Font face doesn't have any family names"));
|
||||
};
|
||||
let (family_name, _) = en_us_family_info;
|
||||
// Clone the family name because it's protected by the font store's RWLock.
|
||||
Ok((style, weight, family_name.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_font_from_handle(
|
||||
font_handle: &font_kit::handle::Handle,
|
||||
validate_supports_en_charset: ValidateFontSupportsEn,
|
||||
) -> Result<FontHandle> {
|
||||
let font = font_handle.load()?;
|
||||
let is_monospace = font.is_monospace();
|
||||
if matches!(validate_supports_en_charset, ValidateFontSupportsEn::Yes) {
|
||||
font.glyph_for_char('m').ok_or(anyhow::format_err!(
|
||||
"No 'm' glyph found for font {}",
|
||||
font.full_name()
|
||||
))?;
|
||||
}
|
||||
match font_handle {
|
||||
font_kit::handle::Handle::Path { path, font_index } => {
|
||||
Ok(FontHandle::new(path, *font_index, is_monospace))
|
||||
}
|
||||
font_kit::handle::Handle::Memory { bytes, font_index } => {
|
||||
let typeface = OwnedFace::from_vec(bytes.to_vec(), *font_index)?;
|
||||
Ok(FontHandle::from(typeface))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a path-backed [`FontHandle`] for a font-kit DirectWrite `Font` by reaching through
|
||||
/// [`font_kit::loaders::directwrite::NativeFont`] to the underlying `IDWriteFontFace`.
|
||||
///
|
||||
/// This mirrors what font-kit itself does for enumerated system fonts in
|
||||
/// `DirectWriteSource::create_handle_from_dwrite_font` (font-kit/src/sources/directwrite.rs:103),
|
||||
/// and is the reason we carry `dwrote` as a direct dependency: font-kit's generic
|
||||
/// `Loader::handle()` default returns a `Handle::Memory` with a byte copy of the full file, which
|
||||
/// we specifically need to avoid on the per-character fallback path.
|
||||
///
|
||||
/// Returns `None` when DirectWrite cannot produce a local file path for the font, i.e. the font
|
||||
/// was loaded via a custom collection loader or backed only by an in-memory stream. For system
|
||||
/// fallback fonts returned by `IDWriteFontFallback::MapCharacters` against the system font
|
||||
/// collection, a path is always available.
|
||||
fn fallback_font_path_handle(font: &font_kit::loaders::directwrite::Font) -> Option<FontHandle> {
|
||||
let native = font.native_font();
|
||||
let file = native.dwrite_font_face.files().ok()?.into_iter().next()?;
|
||||
let path = file.font_file_path().ok()?;
|
||||
let font_index = native.dwrite_font_face.get_index();
|
||||
Some(FontHandle::new(path, font_index, font.is_monospace()))
|
||||
}
|
||||
Reference in New Issue
Block a user