Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -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()))
}