Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// Neither macOS nor wasm make use of the "load font from path" functionality,
|
||||
// and so there's a lot of unused code in here. Instead of marking each of the
|
||||
// relevant functions with allow(dead_code), we'll do it at the module level
|
||||
// instead for simplicity.
|
||||
#![cfg_attr(
|
||||
any(target_os = "macos", target_os = "windows", target_family = "wasm"),
|
||||
allow(dead_code)
|
||||
)]
|
||||
|
||||
use owned_ttf_parser::{AsFaceRef, Face, FaceParsingError, OwnedFace};
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A handle that wraps around a font face.
|
||||
pub struct FontHandle {
|
||||
data: FontData,
|
||||
}
|
||||
|
||||
/// Source data for a font to be loaded within the winit font system.
|
||||
pub enum FontData {
|
||||
/// The font is to be loaded via bytes. This should be used sparingly since it requires loading the font into
|
||||
/// memory.
|
||||
Bytes(OwnedFace),
|
||||
/// The font identified at the given `path` and `index` will be loaded.
|
||||
/// NOTE the font will never be loaded into memory. Instead, data from the font will be read via a memory-mapped
|
||||
/// file.
|
||||
Path {
|
||||
path: PathBuf,
|
||||
index: u32,
|
||||
is_monospace: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl FontData {
|
||||
/// Returns an [`Error`] if the [`FontData`] does not map to a valid font.
|
||||
///
|
||||
/// A font is considered valid iff:
|
||||
/// * The file referenced by [`FontData::Path`] exists and can be read.
|
||||
/// * The data can be parsed into a valid [`ttf_parser::Face`].
|
||||
/// * The font face contains a glyph for the 'm' character.
|
||||
fn validate(&self) -> Result<(), Error> {
|
||||
match self {
|
||||
FontData::Bytes(_) => Ok(()),
|
||||
FontData::Path { path, index, .. } => {
|
||||
let file = File::open(path).map_err(|e| Error::Load {
|
||||
path: path.clone(),
|
||||
io_error: e,
|
||||
})?;
|
||||
let mmap = unsafe {
|
||||
memmap2::Mmap::map(&file).map_err(|e| Error::Load {
|
||||
path: path.clone(),
|
||||
io_error: e,
|
||||
})?
|
||||
};
|
||||
let face = Face::parse(&mmap, *index).map_err(|e| Error::Parse {
|
||||
path: path.clone(),
|
||||
parse_error: e,
|
||||
})?;
|
||||
|
||||
if face.as_face_ref().glyph_index('m').is_none() {
|
||||
Err(Error::Validate { path: path.clone() })
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FontHandle {
|
||||
pub fn new(path: impl Into<PathBuf>, index: u32, is_monospace: bool) -> Self {
|
||||
Self {
|
||||
data: FontData::Path {
|
||||
path: path.into(),
|
||||
index,
|
||||
is_monospace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_monospace(&self) -> bool {
|
||||
match &self.data {
|
||||
FontData::Path { is_monospace, .. } => *is_monospace,
|
||||
FontData::Bytes(face) => face.as_face_ref().is_monospaced(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the the [`FontHandle`] is a parseable font.
|
||||
pub fn validate_font_data(&self) -> Result<(), Error> {
|
||||
self.data.validate()
|
||||
}
|
||||
|
||||
pub(super) fn into_data(self) -> FontData {
|
||||
self.data
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn data(&self) -> &FontData {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OwnedFace> for FontHandle {
|
||||
fn from(value: OwnedFace) -> Self {
|
||||
Self {
|
||||
data: FontData::Bytes(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors associated with loading fonts
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Failed to load font data due to an underlying std::io::Error
|
||||
#[error("Error loading font data for font {path}")]
|
||||
Load {
|
||||
path: PathBuf,
|
||||
io_error: std::io::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse the underlying data into a valid font
|
||||
#[error("Error parsing font data for font {path}")]
|
||||
Parse {
|
||||
path: PathBuf,
|
||||
parse_error: FaceParsingError,
|
||||
},
|
||||
|
||||
/// A font was properly loaded, but did not have a codepoint
|
||||
/// for the letter m, indicating it would not work within Warp.
|
||||
#[error("Font {path} does not have a valid codepoint for the letter m")]
|
||||
Validate { path: PathBuf },
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
//! Loads fonts on linux.
|
||||
//!
|
||||
//! Handles discovering and loading fonts on linux systems.
|
||||
//! Leverages the fontconfig crate to detect all fonts
|
||||
//! available on the user's device, creating handles for the fonts.
|
||||
//! Handles can be converted to owned_ttf_parser::OwnedFace objects
|
||||
//! by loading the fonts into memory.
|
||||
|
||||
use std::ffi::c_int;
|
||||
use std::{collections::HashMap, ffi::CString};
|
||||
|
||||
use super::{
|
||||
font_handle::{Error as FontDataError, FontHandle},
|
||||
FontFamily, ValidateFontSupportsEn,
|
||||
};
|
||||
use crate::fonts::{FontInfo, Properties, Style, Weight};
|
||||
|
||||
use fontconfig::{
|
||||
list_fonts, sort_fonts, FontSet, Fontconfig, ObjectSet, Pattern, FC_FAMILY, FC_FILE,
|
||||
FC_FONTFORMAT, FC_FULLNAME, FC_INDEX, FC_LANG, FC_MONO, FC_SLANT, FC_SLANT_ITALIC,
|
||||
FC_SLANT_ROMAN, FC_SPACING, FC_WEIGHT, FC_WEIGHT_BLACK, FC_WEIGHT_BOLD, FC_WEIGHT_EXTRABOLD,
|
||||
FC_WEIGHT_EXTRALIGHT, FC_WEIGHT_LIGHT, FC_WEIGHT_MEDIUM, FC_WEIGHT_NORMAL, FC_WEIGHT_SEMIBOLD,
|
||||
FC_WEIGHT_THIN,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Manages font detection and handle generation.
|
||||
///
|
||||
/// Contains our font loading object, wrapping around fontconfig::FontConfig
|
||||
/// to query the available fonts on the system and return handles grouped into
|
||||
/// families
|
||||
pub struct FontconfigLoader {
|
||||
fc: Fontconfig,
|
||||
}
|
||||
|
||||
impl FontconfigLoader {
|
||||
/// Creates a new FontLoader instance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return an Error::Init if the underlying FFI wrapper
|
||||
/// for Fontconfig fails to initialize
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
if let Some(fc) = Fontconfig::new() {
|
||||
Ok(Self { fc })
|
||||
} else {
|
||||
Err(Error::Init)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a handle for a single font family.
|
||||
///
|
||||
/// Looks up all fonts in the font family specified by `family_name`.
|
||||
/// Returns a FamilyHandle for those fonts
|
||||
///
|
||||
/// # Errors
|
||||
/// If there are zero valid fonts within the family, this will error with
|
||||
/// Error::FamilyHasNoFonts
|
||||
///
|
||||
/// Additionally, passing a malformed CString name (ex: a string w/ a null terminator)
|
||||
/// can trigger an Error::InvalidFontName.
|
||||
pub(super) fn get_family(&self, family_name: &str) -> Result<FamilyHandle, Error> {
|
||||
let fonts = self.query_fonts(Some(family_name))?;
|
||||
let mut family = FamilyHandle::new(family_name);
|
||||
let mut errors = Vec::<Error>::new();
|
||||
for pattern in fonts.iter() {
|
||||
match Self::parse_font(pattern, ValidateFontSupportsEn::Yes) {
|
||||
Ok(font) => family.add_font(font),
|
||||
Err(err) => errors.push(err),
|
||||
}
|
||||
}
|
||||
if !family.fonts.is_empty() {
|
||||
Ok(family)
|
||||
} else {
|
||||
Err(Error::FamilyHasNoFonts(family_name.to_string(), errors))
|
||||
}
|
||||
}
|
||||
|
||||
// Gets handles for all font families present on the device.
|
||||
//
|
||||
// Searches for all available fonts on the device, and returns
|
||||
// font families for all valid results. A font is considered valid if:
|
||||
//
|
||||
// * It has a valid family_name, filename, and face_index
|
||||
// * It supports the language 'en'.
|
||||
// * It has a TTF or CFF format
|
||||
//
|
||||
// Any invalid fonts are skipped over, with logging explaining why it was skipped
|
||||
pub(super) fn get_all_families(&self) -> Result<Vec<FamilyHandle>, Error> {
|
||||
let fonts = self.query_fonts(None)?;
|
||||
|
||||
let mut family_map = HashMap::new();
|
||||
for pattern in fonts.iter() {
|
||||
let font_name = pattern.name().unwrap_or("unknown");
|
||||
let Some(family_name) = pattern.get_string(FC_FAMILY).map(|name| name.to_string())
|
||||
else {
|
||||
log::warn!("could not parse font_family for font {font_name}",);
|
||||
continue;
|
||||
};
|
||||
|
||||
let font_handle = match Self::parse_font(pattern, ValidateFontSupportsEn::Yes) {
|
||||
Ok(handle) => handle,
|
||||
Err(_) => continue,
|
||||
};
|
||||
family_map
|
||||
.entry(family_name.to_string())
|
||||
.or_insert_with(|| FamilyHandle::new(&family_name))
|
||||
.add_font(font_handle);
|
||||
}
|
||||
let mut results = family_map.into_values().collect::<Vec<_>>();
|
||||
results.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Convenience function to parse a font from a pattern, and log appropriately
|
||||
/// if the parsing fails.
|
||||
/// If `validate` is set to [`ValidateFontSupportsEn::Yes`] an error is returned if the font does not support
|
||||
/// english.
|
||||
fn parse_font(
|
||||
pattern: Pattern<'_>,
|
||||
validate: ValidateFontSupportsEn,
|
||||
) -> Result<FontHandle, Error> {
|
||||
FontHandle::try_from_pattern(&pattern, validate).map_err(|err| {
|
||||
let font_name = pattern.name().unwrap_or("unknown");
|
||||
match &err {
|
||||
Error::InvalidFontFormat(_) | Error::DoesNotSupportEn => {
|
||||
log::debug!("skipping font {font_name} because of error: {err:#}")
|
||||
}
|
||||
_ => {
|
||||
log::warn!("could not parse font {font_name}: {err:#}");
|
||||
}
|
||||
};
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a list of fallback fonts that match the `family_name` and given `properties`, in order of closeness.
|
||||
pub fn fallback_fonts(
|
||||
&self,
|
||||
family_name: &str,
|
||||
properties: Properties,
|
||||
) -> Result<Vec<FontHandle>, Error> {
|
||||
let mut pattern = Pattern::new(&self.fc);
|
||||
// Though unlikely, return an `Error` if the requested family name has a null character in it.
|
||||
let name = CString::new(family_name)
|
||||
.map_err(|_| Error::InvalidFontName(family_name.to_string()))?;
|
||||
pattern.add_string(FC_FAMILY, &name);
|
||||
pattern.add_integer(FC_WEIGHT, to_fontconfig_weight(properties.weight));
|
||||
pattern.add_integer(FC_SLANT, to_fontconfig_style(properties.style));
|
||||
|
||||
let mut object_set = ObjectSet::new(&self.fc);
|
||||
object_set.add(FC_FAMILY);
|
||||
object_set.add(FC_FULLNAME);
|
||||
object_set.add(FC_FILE);
|
||||
object_set.add(FC_INDEX);
|
||||
|
||||
// By setting trim to true, we omit fonts that have a unicode range covered by prior fonts in chain. Doing this
|
||||
// reduces the overall set of fallback fonts we need to load.
|
||||
let sort_fonts = sort_fonts(&pattern, true /* trim */);
|
||||
|
||||
// Skip the first font, since this is considered the primary "font" we're trying to match.
|
||||
let fallback_fonts = sort_fonts
|
||||
.iter()
|
||||
.skip(1)
|
||||
.filter_map(|pattern| {
|
||||
// Fallback fonts we load aren't guaranteed to support english.
|
||||
// Also, parse_font already has logging for parsing, so we log there.
|
||||
Self::parse_font(pattern, ValidateFontSupportsEn::No).ok()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
Ok(fallback_fonts)
|
||||
}
|
||||
|
||||
fn query_fonts(&self, family_name: Option<&str>) -> Result<FontSet<'_>, Error> {
|
||||
let mut pattern = Pattern::new(&self.fc);
|
||||
if let Some(name) = family_name {
|
||||
// Very unlikely that someone is going to pass a font name with a \0 in,
|
||||
// but covering just in case w/ an error
|
||||
let name = CString::new(name).map_err(|_| Error::InvalidFontName(name.to_string()))?;
|
||||
pattern.add_string(FC_FAMILY, &name)
|
||||
}
|
||||
|
||||
let mut object_set = ObjectSet::new(&self.fc);
|
||||
object_set.add(FC_FAMILY);
|
||||
object_set.add(FC_FULLNAME);
|
||||
object_set.add(FC_FILE);
|
||||
object_set.add(FC_INDEX);
|
||||
object_set.add(FC_SPACING);
|
||||
object_set.add(FC_LANG);
|
||||
object_set.add(FC_FONTFORMAT);
|
||||
|
||||
Ok(list_fonts(&pattern, Some(&object_set)))
|
||||
}
|
||||
}
|
||||
|
||||
impl FontHandle {
|
||||
/// Attempts to generate a FontHandle from a Fontconfig Pattern.
|
||||
///
|
||||
/// In order to properly parse out a FontHandle, the pattern needs to have
|
||||
///
|
||||
/// * A filename
|
||||
/// * a face_index
|
||||
///
|
||||
/// If either of these fields are missing, an Error::MissingMetadataField will
|
||||
/// be returned
|
||||
///
|
||||
/// Additionally, will return the following errors:
|
||||
///
|
||||
/// * Error::DoesNotSupportEn: if the pattern is missing en as a supported language and `validate_fonts_support_en`
|
||||
/// is set to [`ValidateFontSupportsEn::Yes`].
|
||||
/// * Error::InvalidFontFormat: if the pattern's font format is not TTF or CFF.
|
||||
fn try_from_pattern(
|
||||
value: &Pattern<'_>,
|
||||
validate_font_supports_en: ValidateFontSupportsEn,
|
||||
) -> Result<Self, Error> {
|
||||
let file_path = value
|
||||
.filename()
|
||||
.ok_or_else(|| Error::MissingMetadataField("filename".to_owned()))?;
|
||||
|
||||
let index = value
|
||||
.face_index()
|
||||
.ok_or_else(|| Error::MissingMetadataField("face_index".to_owned()))?
|
||||
as u32;
|
||||
|
||||
if matches!(validate_font_supports_en, ValidateFontSupportsEn::Yes)
|
||||
&& !value
|
||||
.lang_set()
|
||||
.is_some_and(|lang_set| lang_set.into_iter().any(|lang| lang == "en"))
|
||||
{
|
||||
return Err(Error::DoesNotSupportEn);
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
value.format(),
|
||||
Ok(fontconfig::FontFormat::TrueType) | Ok(fontconfig::FontFormat::CFF)
|
||||
) {
|
||||
// NOTE: fontconfig::FontFormat does not impl Debug or any mapping to strings,
|
||||
// so for debugging purposes we pull the underlying string field the
|
||||
// enum is computed from.
|
||||
let font_format_str = value.get_string(FC_FONTFORMAT).unwrap_or_default();
|
||||
return Err(Error::InvalidFontFormat(font_format_str.to_string()));
|
||||
}
|
||||
|
||||
let spacing = value.get_int(FC_SPACING);
|
||||
|
||||
Ok(FontHandle::new(
|
||||
file_path,
|
||||
index,
|
||||
match spacing {
|
||||
None => false,
|
||||
Some(v) => v == FC_MONO,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle containing information necessary to load all font faces in a family.
|
||||
pub(super) struct FamilyHandle {
|
||||
name: String,
|
||||
fonts: Vec<FontHandle>,
|
||||
}
|
||||
|
||||
impl FamilyHandle {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
fonts: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn add_font(&mut self, font: FontHandle) {
|
||||
self.fonts.push(font);
|
||||
}
|
||||
|
||||
/// Consumes the Family Handle into a FontFamily object.
|
||||
pub fn into_family(self) -> Result<FontFamily, Error> {
|
||||
self.into_info_and_family().map(|(_, family)| family)
|
||||
}
|
||||
|
||||
/// Converts the [`FamilyHandle`] into a [`FontInfo`], [`FontFamily`] pair.
|
||||
pub fn into_info_and_family(self) -> Result<(FontInfo, FontFamily), Error> {
|
||||
let mut fonts = Vec::<FontHandle>::with_capacity(self.fonts.len());
|
||||
let mut errors = Vec::<Error>::new();
|
||||
let mut is_monospace = false;
|
||||
let name = self.name;
|
||||
|
||||
for handle in self.fonts {
|
||||
match handle.validate_font_data() {
|
||||
Ok(_) => {
|
||||
is_monospace |= handle.is_monospace();
|
||||
fonts.push(handle);
|
||||
}
|
||||
Err(err) => errors.push(Error::FontData(err)),
|
||||
}
|
||||
}
|
||||
if !fonts.is_empty() {
|
||||
Ok((
|
||||
FontInfo {
|
||||
family_name: name.clone(),
|
||||
is_monospace,
|
||||
},
|
||||
FontFamily { fonts, name },
|
||||
))
|
||||
} else {
|
||||
Err(Error::FamilyHasNoFonts(name, errors))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors associated with loading fonts.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The FontLoader cannot be initialized b/c the underlying
|
||||
/// Fontconfig ffi handle failed to init.
|
||||
#[error("Failed to initialize Fontconfig ffi handle")]
|
||||
Init,
|
||||
|
||||
/// The user has passed a malformed CString font name.
|
||||
#[error("Invalid Font Name {0}")]
|
||||
InvalidFontName(String),
|
||||
|
||||
/// A font could not be parsed into a handle b/c it is missing
|
||||
/// an important metadata field.
|
||||
#[error("Could not parse font, missing metadata field {0}")]
|
||||
MissingMetadataField(String),
|
||||
|
||||
/// A font does not have a valid font format (either TTF or CFF)
|
||||
#[error("Invalid font format '{0}'")]
|
||||
InvalidFontFormat(String),
|
||||
|
||||
/// A font does not support the language en
|
||||
#[error("Font does not support language en")]
|
||||
DoesNotSupportEn,
|
||||
|
||||
/// A font family has been requested, but there are no valid
|
||||
/// fonts for that family
|
||||
#[error("Font family {0} does not contain any valid fonts")]
|
||||
FamilyHasNoFonts(String, Vec<Error>),
|
||||
|
||||
// When the underlying font handle has trouble loading data.
|
||||
#[error("Failed to load font data")]
|
||||
FontData(#[from] FontDataError),
|
||||
}
|
||||
|
||||
fn to_fontconfig_weight(weight: Weight) -> c_int {
|
||||
match weight {
|
||||
Weight::Thin => FC_WEIGHT_THIN,
|
||||
Weight::ExtraLight => FC_WEIGHT_EXTRALIGHT,
|
||||
Weight::Light => FC_WEIGHT_LIGHT,
|
||||
Weight::Normal => FC_WEIGHT_NORMAL,
|
||||
Weight::Medium => FC_WEIGHT_MEDIUM,
|
||||
Weight::Semibold => FC_WEIGHT_SEMIBOLD,
|
||||
Weight::Bold => FC_WEIGHT_BOLD,
|
||||
Weight::ExtraBold => FC_WEIGHT_EXTRABOLD,
|
||||
Weight::Black => FC_WEIGHT_BLACK,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_fontconfig_style(style: Style) -> c_int {
|
||||
match style {
|
||||
Style::Normal => FC_SLANT_ROMAN,
|
||||
Style::Italic => FC_SLANT_ITALIC,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Module containing the definition of [`StrIndexMap`], allowing for repeated, efficient conversion
|
||||
//! between a byte and/or char index from a backing `str`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Map that provides efficient conversion from byte <-> char index from a backing `str`.
|
||||
/// See [`StrIndexMap::byte_index`] and [`StrIndexMap::char_index`] for conversion functions to
|
||||
/// convert from/to a byte index to a char index.
|
||||
pub(super) struct StrIndexMap {
|
||||
byte_to_char_index: HashMap<usize, usize>,
|
||||
char_to_byte_index: Vec<usize>,
|
||||
}
|
||||
|
||||
impl StrIndexMap {
|
||||
/// Constructs a new [`StrIndexMap`] with byte <-> char indices based on the input `str`.
|
||||
/// NOTE this runs in O(n) time as it requires walking through each char index in the `str`.
|
||||
pub(super) fn new(str: impl AsRef<str>) -> Self {
|
||||
let char_indices = str.as_ref().char_indices();
|
||||
let (_, upper_bound) = char_indices.size_hint();
|
||||
|
||||
let (mut byte_to_char_index, mut char_to_byte_index) = match upper_bound {
|
||||
None => (HashMap::new(), Vec::new()),
|
||||
Some(size) => (HashMap::with_capacity(size), Vec::with_capacity(size)),
|
||||
};
|
||||
|
||||
for (char_index, (byte_index, _)) in char_indices.enumerate() {
|
||||
byte_to_char_index.insert(byte_index, char_index);
|
||||
char_to_byte_index.push(byte_index);
|
||||
}
|
||||
|
||||
Self {
|
||||
byte_to_char_index,
|
||||
char_to_byte_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the _byte_ index of the string at the given `char_index`. If the `char_index` does
|
||||
/// not exist in the string, `None` is returned.
|
||||
pub(super) fn byte_index(&self, char_index: usize) -> Option<usize> {
|
||||
self.char_to_byte_index.get(char_index).copied()
|
||||
}
|
||||
|
||||
/// Returns the _char_ index of the string at the given byte index. If the `byte_index` does not
|
||||
/// exist in the string or if it does not lie at a char boundary, `None` is returned.
|
||||
pub(super) fn char_index(&self, byte_index: usize) -> Option<usize> {
|
||||
self.byte_to_char_index.get(&byte_index).copied()
|
||||
}
|
||||
|
||||
/// Returns the total number of characters in the string.
|
||||
pub(super) fn num_chars(&self) -> usize {
|
||||
self.char_to_byte_index.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "str_index_map_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,46 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_str_index_map_get_byte_index() {
|
||||
let text = "ab😈■d";
|
||||
let str_index_map = StrIndexMap::new(text);
|
||||
|
||||
assert_eq!(str_index_map.byte_index(0), Some(0));
|
||||
assert_eq!(str_index_map.byte_index(1), Some(1));
|
||||
assert_eq!(str_index_map.byte_index(2), Some(2));
|
||||
|
||||
// The character at index 2 (😈) is 4 bytes, which means the character at index 3 (■) starts at
|
||||
// byte index 6.
|
||||
assert_eq!(str_index_map.byte_index(3), Some(6));
|
||||
// The character at index 3 (■) is 3 bytes, which means the character at index 4 (d) starts at
|
||||
// byte index 9.
|
||||
assert_eq!(str_index_map.byte_index(4), Some(9));
|
||||
|
||||
// The backing string only has 5 characters. Ensure we return None in the case a character index
|
||||
// that isn't included in the string is passed.
|
||||
assert_eq!(str_index_map.byte_index(5), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_str_index_map_get_char_index() {
|
||||
let text = "ab😈■d";
|
||||
let str_index_map = StrIndexMap::new(text);
|
||||
|
||||
assert_eq!(str_index_map.char_index(0), Some(0));
|
||||
assert_eq!(str_index_map.char_index(1), Some(1));
|
||||
assert_eq!(str_index_map.char_index(2), Some(2));
|
||||
|
||||
// The character at index 2 (😈) is 4 bytes, which means the character at index 3 (■) starts at
|
||||
// byte index 6.
|
||||
assert_eq!(str_index_map.char_index(6), Some(3));
|
||||
// The character at index 3 (■) is 3 bytes, which means the character at index 4 (d) starts at
|
||||
// byte index 9.
|
||||
assert_eq!(str_index_map.char_index(9), Some(4));
|
||||
|
||||
// The backing string only has 10 bytes. Ensure we return None in the case a byte index
|
||||
// that isn't included in the string is passed.
|
||||
assert_eq!(str_index_map.char_index(10), None);
|
||||
|
||||
// Byte index 3 is not a char boundary, so we should return None.
|
||||
assert_eq!(str_index_map.char_index(3), None);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Module that rasterizes text using `swash`.
|
||||
|
||||
use crate::fonts::canvas::{Canvas, RasterFormat};
|
||||
use crate::fonts::{FontId, GlyphId, RasterizedGlyph, SubpixelAlignment};
|
||||
use crate::platform::FontDB as _;
|
||||
use crate::rendering::GlyphConfig;
|
||||
use crate::windowing::winit::fonts::FontDB;
|
||||
use anyhow::{anyhow, Result};
|
||||
use cosmic_text::{CacheKey, CacheKeyFlags};
|
||||
use pathfinder_geometry::rect::RectI;
|
||||
use pathfinder_geometry::vector::{vec2i, Vector2F, Vector2I};
|
||||
|
||||
impl FontDB {
|
||||
pub(super) fn glyph_raster_bounds(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
_glyph_config: &GlyphConfig,
|
||||
) -> Result<RectI> {
|
||||
let Ok(_typographic_bounds) = self
|
||||
.glyph_typographic_bounds(font_id, glyph_id)
|
||||
.map(|bounds| bounds.to_f32())
|
||||
else {
|
||||
// We can't render this glyph using this font, return an empty rect to indicate we
|
||||
// don't need to rasterize this glyph. This can happen if the font doesn't contain
|
||||
// a glyph _or_ if the glyph isn't renderable (some fonts contain a glyph for the
|
||||
// space character, but don't provide outlines for it).
|
||||
return Ok(RectI::new(Vector2I::zero(), Vector2I::zero()));
|
||||
};
|
||||
|
||||
let id = *self
|
||||
.text_layout_system
|
||||
.font_id_map
|
||||
.read()
|
||||
.get_by_left(&font_id)
|
||||
.unwrap();
|
||||
let image = self
|
||||
.swash_cache
|
||||
.write()
|
||||
.get_image_uncached(
|
||||
&mut self.text_layout_system.font_store.write(),
|
||||
CacheKey::new(
|
||||
id,
|
||||
glyph_id as u16,
|
||||
size * scale.x(),
|
||||
(0., 0.),
|
||||
CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Failed to get raster image"))?;
|
||||
|
||||
let origin = vec2i(image.placement.left, -image.placement.top);
|
||||
let size = vec2i(image.placement.width as i32, image.placement.height as i32);
|
||||
Ok(RectI::new(origin, size))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn rasterize_glyph(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
size: f32,
|
||||
glyph_id: GlyphId,
|
||||
scale: Vector2F,
|
||||
subpixel_alignment: SubpixelAlignment,
|
||||
glyph_config: &GlyphConfig,
|
||||
requested_format: RasterFormat,
|
||||
) -> Result<RasterizedGlyph> {
|
||||
let raster_bounds =
|
||||
self.glyph_raster_bounds(font_id, size, glyph_id, scale, glyph_config)?;
|
||||
|
||||
let id = *self
|
||||
.text_layout_system
|
||||
.font_id_map
|
||||
.read()
|
||||
.get_by_left(&font_id)
|
||||
.unwrap();
|
||||
// Get the raster image without caching--the parent FontDB handles all caching for us.
|
||||
let image = self
|
||||
.swash_cache
|
||||
.write()
|
||||
.get_image_uncached(
|
||||
&mut self.text_layout_system.font_store.write(),
|
||||
CacheKey::new(
|
||||
id,
|
||||
glyph_id as u16,
|
||||
size * scale.x(),
|
||||
(subpixel_alignment.to_offset().x(), 0.),
|
||||
CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.unwrap();
|
||||
|
||||
let (original_format, is_color) = match image.content {
|
||||
cosmic_text::SwashContent::Mask => (RasterFormat::A8, false),
|
||||
cosmic_text::SwashContent::SubpixelMask => (RasterFormat::Rgba32, false),
|
||||
cosmic_text::SwashContent::Color => (RasterFormat::Rgba32, true),
|
||||
};
|
||||
|
||||
// Ensure the pixmap is in the correct requested format (in practice this converts A8 to
|
||||
// RGBA32).
|
||||
// TODO(alokedesai): Ensure our font rasterization code is robust to returned formats that
|
||||
// are different than incoming formats. Right now, we create text bounds based on the
|
||||
// _incoming_ format.
|
||||
let pixmap = if original_format == RasterFormat::A8 {
|
||||
let bytes_per_pixel = requested_format.bytes_per_pixel() as usize;
|
||||
let mut pixmap = Vec::with_capacity(image.data.len() * bytes_per_pixel);
|
||||
for byte in image.data {
|
||||
for _ in 0..bytes_per_pixel {
|
||||
pixmap.push(byte);
|
||||
}
|
||||
}
|
||||
pixmap
|
||||
} else {
|
||||
image.data
|
||||
};
|
||||
|
||||
let canvas = Canvas {
|
||||
pixels: pixmap,
|
||||
size: raster_bounds.size(),
|
||||
row_stride: image.placement.width as usize * original_format.bytes_per_pixel() as usize,
|
||||
format: RasterFormat::Rgba32,
|
||||
};
|
||||
|
||||
anyhow::Ok(RasterizedGlyph {
|
||||
canvas,
|
||||
is_emoji: is_color,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use super::str_index_map::StrIndexMap;
|
||||
use crate::fonts::FontId;
|
||||
use crate::text_layout::{Glyph, Run, TextStyle};
|
||||
use cosmic_text::LayoutGlyph;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
|
||||
/// Helper struct to construct [`Run`]s from a series of shaped glyphs.
|
||||
pub(super) struct RunBuilder<'a> {
|
||||
runs: Vec<Run>,
|
||||
font_in_current_run: FontId,
|
||||
current_run_style: TextStyle,
|
||||
current_run_width: f32,
|
||||
glyphs_in_current_run: Vec<Glyph>,
|
||||
styles_map: &'a TextStylesMap,
|
||||
str_index_map: &'a StrIndexMap,
|
||||
}
|
||||
|
||||
impl<'a> RunBuilder<'a> {
|
||||
pub(super) fn new(
|
||||
styles_map: &'a TextStylesMap,
|
||||
initial_font_id: FontId,
|
||||
str_index_map: &'a StrIndexMap,
|
||||
) -> Self {
|
||||
Self {
|
||||
runs: vec![],
|
||||
font_in_current_run: initial_font_id,
|
||||
current_run_style: TextStyle::default(),
|
||||
current_run_width: 0.0,
|
||||
glyphs_in_current_run: vec![],
|
||||
styles_map,
|
||||
str_index_map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserves space for the provided number of glyphs in the current run.
|
||||
pub fn reserve_capacity(&mut self, total: usize) {
|
||||
self.glyphs_in_current_run
|
||||
.reserve_exact(total.saturating_sub(self.glyphs_in_current_run.capacity()))
|
||||
}
|
||||
|
||||
/// Flushes the current style run by appending the current run into the runs list.
|
||||
/// NOTE: if there are no glyphs in the run, it is not appended.
|
||||
fn flush_current_style_run(&mut self) {
|
||||
if !self.glyphs_in_current_run.is_empty() {
|
||||
let excess_capacity =
|
||||
self.glyphs_in_current_run.capacity() - self.glyphs_in_current_run.len();
|
||||
let mut new_glyphs = Vec::with_capacity(excess_capacity);
|
||||
std::mem::swap(&mut new_glyphs, &mut self.glyphs_in_current_run);
|
||||
self.runs.push(Run {
|
||||
font_id: self.font_in_current_run,
|
||||
glyphs: new_glyphs,
|
||||
styles: self.current_run_style,
|
||||
width: self.current_run_width,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a new laid out glyph into the `RunBuilder`. Internally, `font_id_fn` will be called
|
||||
/// to get the `FontId` for the `glyph`.
|
||||
pub(super) fn push_glyph<F: FnOnce(&fontdb::ID) -> FontId>(
|
||||
&mut self,
|
||||
glyph: LayoutGlyph,
|
||||
font_id_fn: F,
|
||||
) {
|
||||
let font_id = font_id_fn(&glyph.font_id);
|
||||
let text_style = self.styles_map.get(glyph.metadata);
|
||||
|
||||
// A run is a series of continuous glyphs that have the same style. We use the combination
|
||||
// of font id (which is a proxy of the font properties such as bold or italic) and the
|
||||
// `TextStyle` to determine when a new run should be created.
|
||||
if font_id != self.font_in_current_run || text_style != self.current_run_style {
|
||||
self.flush_current_style_run();
|
||||
|
||||
self.current_run_width = 0.;
|
||||
self.current_run_style = text_style;
|
||||
self.font_in_current_run = font_id;
|
||||
}
|
||||
|
||||
let glyph_char_index = self
|
||||
.str_index_map
|
||||
.char_index(glyph.start)
|
||||
.unwrap_or_else(|| self.str_index_map.num_chars());
|
||||
|
||||
self.glyphs_in_current_run.push(Glyph {
|
||||
id: glyph.glyph_id as u32,
|
||||
position_along_baseline: vec2f(glyph.x, glyph.y),
|
||||
index: glyph_char_index,
|
||||
width: glyph.w,
|
||||
});
|
||||
|
||||
self.current_run_width += glyph.w;
|
||||
}
|
||||
|
||||
/// Returns the final list of [`Run`]s that were computed.
|
||||
pub(super) fn build(mut self) -> Vec<Run> {
|
||||
self.flush_current_style_run();
|
||||
self.runs
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple map that maps an index to a [`TextStyle`].
|
||||
/// [`cosmic_text`] only supports setting a `usize` as metadata, so this struct is used to generate
|
||||
/// a mapping of an index to its corresponding `TextStyle`.
|
||||
///
|
||||
/// Though this is modeled internally as a `Vec`, use a new type to limit the API since some
|
||||
/// functions on a `Vec` (such as reordering) would break the mapping of index to text style.
|
||||
pub(super) struct TextStylesMap {
|
||||
styles: Vec<TextStyle>,
|
||||
}
|
||||
|
||||
impl TextStylesMap {
|
||||
pub(super) fn insert(&mut self, text_style: TextStyle) -> usize {
|
||||
let size = self.styles.len();
|
||||
self.styles.push(text_style);
|
||||
size
|
||||
}
|
||||
|
||||
/// Gets the [`TextStyle`] at the given index. If no style is at the index, a default
|
||||
/// `TextStyle` is returned.
|
||||
pub(super) fn get(&self, index: usize) -> TextStyle {
|
||||
self.styles.get(index).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
styles: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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