Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
1492 lines
53 KiB
Rust
1492 lines
53 KiB
Rust
//! A font collection is a list of faces of different styles. The list is
|
|
//! ordered by priority (per style). All fonts in a collection share the same
|
|
//! size so they can be used interchangeably in cases a glyph is missing in one
|
|
//! and present in another.
|
|
//!
|
|
//! The purpose of a collection is to store a list of fonts by style
|
|
//! and priority order. A collection does not handle searching for font
|
|
//! callbacks, rasterization, etc. For this, see CodepointResolver.
|
|
//!
|
|
//! The collection can contain both loaded and deferred faces. Deferred faces
|
|
//! typically use less memory while still providing some necessary information
|
|
//! such as codepoint support, presentation, etc. This is useful for looking
|
|
//! for fallback fonts as efficiently as possible. For example, when the glyph
|
|
//! "X" is not found, we can quickly search through deferred fonts rather
|
|
//! than loading the font completely.
|
|
|
|
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
|
|
|
|
use crate::{
|
|
DeferredFace,
|
|
Face,
|
|
Library,
|
|
Metrics,
|
|
Presentation,
|
|
Style,
|
|
config_types::{FontSyntheticStyle, FreetypeLoadFlags},
|
|
face::{self, DesiredSize},
|
|
metrics::{FaceMetrics, ModifierSet},
|
|
};
|
|
|
|
/// The available faces we have. This shouldn't be modified manually.
|
|
/// Instead, use the functions available on Collection.
|
|
///
|
|
/// (Zig: `StyleArray`, an enum array of segmented lists. Aliases are
|
|
/// stored as the [`Index`] of the non-alias entry they point to, so there
|
|
/// is no need for pointer stability.)
|
|
type StyleArray = [Vec<EntryOrAlias>; 4];
|
|
|
|
pub struct Collection {
|
|
/// The available faces we have. This shouldn't be modified manually.
|
|
/// Instead, use the functions available on Collection.
|
|
faces: StyleArray,
|
|
|
|
/// The metric modifiers to use for this collection.
|
|
///
|
|
/// Call `Collection::update_metrics` to recompute the
|
|
/// collection's metrics after making changes to these.
|
|
pub metric_modifiers: ModifierSet,
|
|
|
|
/// Metrics for this collection. Call `Collection::update_metrics` to
|
|
/// (re)compute these after adding a primary font or making changes to
|
|
/// `metric_modifiers`.
|
|
pub metrics: Option<Metrics>,
|
|
|
|
/// The face metrics for the primary face in the collection.
|
|
///
|
|
/// We keep this around so we don't need to re-compute it when calculating
|
|
/// the scale factor for additional fonts added to the collection.
|
|
pub primary_face_metrics: Option<FaceMetrics>,
|
|
|
|
/// The load options for deferred faces in the face list. If this
|
|
/// is not set, then deferred faces will not be loaded. Attempting to
|
|
/// add a deferred face will result in an error.
|
|
pub load_options: Option<LoadOptions>,
|
|
}
|
|
|
|
impl Default for Collection {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Options for adding a face to the collection.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct AddOptions {
|
|
/// What style this face is.
|
|
pub style: Style,
|
|
/// What size adjustment to use.
|
|
pub size_adjustment: SizeAdjustment,
|
|
/// Whether this is a fallback face.
|
|
pub fallback: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum AddError {
|
|
/// There's no more room in the collection.
|
|
#[error("collection is full")]
|
|
CollectionFull,
|
|
/// The call to `face.set_size` failed.
|
|
#[error("set size failed")]
|
|
SetSizeFailed,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum AddDeferredError {
|
|
/// There's no more room in the collection.
|
|
#[error("collection is full")]
|
|
CollectionFull,
|
|
/// `load_options` is None, can't do deferred loading.
|
|
#[error("deferred loading unavailable")]
|
|
DeferredLoadingUnavailable,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum EntryError {
|
|
/// Index represents a special font (built-in) and these don't
|
|
/// have an associated face. This should be caught upstream and use
|
|
/// alternate logic.
|
|
#[error("special font has no face")]
|
|
SpecialHasNoFace,
|
|
|
|
/// Invalid index.
|
|
#[error("index out of bounds")]
|
|
IndexOutOfBounds,
|
|
}
|
|
|
|
/// Errors from [`Collection::get_face`].
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum GetFaceError {
|
|
#[error(transparent)]
|
|
Entry(#[from] EntryError),
|
|
#[error("deferred loading unavailable")]
|
|
DeferredLoadingUnavailable,
|
|
#[error("face load failed: {0}")]
|
|
Load(#[from] crate::deferred_face::LoadError),
|
|
#[error("face error: {0}")]
|
|
Face(#[from] face::FaceError),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum CompleteError {
|
|
#[error("default face unavailable")]
|
|
DefaultUnavailable,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum UpdateMetricsError {
|
|
#[error("cannot load primary font")]
|
|
CannotLoadPrimaryFont,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
|
pub enum SetSizeError {
|
|
#[error(transparent)]
|
|
UpdateMetrics(#[from] UpdateMetricsError),
|
|
/// `load_options` is `None`.
|
|
#[error("deferred loading unavailable")]
|
|
DeferredLoadingUnavailable,
|
|
/// The call to `face.set_size` failed.
|
|
#[error("set size failed")]
|
|
SetSizeFailed,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
enum SyntheticError {
|
|
#[error("deferred loading unavailable")]
|
|
DeferredLoadingUnavailable,
|
|
#[error(transparent)]
|
|
GetFace(#[from] GetFaceError),
|
|
#[error(transparent)]
|
|
Face(#[from] face::FaceError),
|
|
}
|
|
|
|
impl Collection {
|
|
/// Initialize an empty collection.
|
|
pub fn new() -> Collection {
|
|
Collection {
|
|
faces: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
|
|
metric_modifiers: ModifierSet::new(),
|
|
metrics: None,
|
|
primary_face_metrics: None,
|
|
load_options: None,
|
|
}
|
|
}
|
|
|
|
/// Number of entries (including aliases) for a style.
|
|
pub fn count(&self, style: Style) -> usize {
|
|
self.faces[style as usize].len()
|
|
}
|
|
|
|
/// Add a face to the collection. This face will be added next in priority
|
|
/// if others exist already, i.e. it'll be the _last_ to be searched for a
|
|
/// glyph in that list.
|
|
///
|
|
/// If no error is encountered then the collection takes ownership of the
|
|
/// face. (On error the face is dropped.)
|
|
///
|
|
/// When added, the size of the face will be adjusted to match
|
|
/// `load_options`.
|
|
///
|
|
/// Returns the index for the added face.
|
|
pub fn add(&mut self, face: Face, opts: AddOptions) -> Result<Index, AddError> {
|
|
// We have some special indexes so we must never pass those.
|
|
let idx = self.faces[opts.style as usize].len();
|
|
if idx >= usize::from(Index::SPECIAL_START) - 1 {
|
|
return Err(AddError::CollectionFull);
|
|
}
|
|
|
|
let mut owned_face = face;
|
|
|
|
// Scale factor to adjust the size of the added face.
|
|
let scale_factor = self.scale_factor(&owned_face.get_metrics(), opts.size_adjustment);
|
|
|
|
// If we have load options, we update the size to ensure
|
|
// it's matches and is normalized to the primary if possible.
|
|
if let Some(load_opts) = &self.load_options {
|
|
let mut new_opts = load_opts.clone();
|
|
new_opts.size.points *= scale_factor as f32;
|
|
owned_face
|
|
.set_size(new_opts.face_options())
|
|
.map_err(|_| AddError::SetSizeFailed)?;
|
|
}
|
|
|
|
self.faces[opts.style as usize].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Loaded(owned_face),
|
|
fallback: opts.fallback,
|
|
scale_factor: ScaleFactor::Scale(scale_factor),
|
|
}));
|
|
|
|
Ok(Index {
|
|
style: opts.style,
|
|
idx: idx as u16,
|
|
})
|
|
}
|
|
|
|
/// Add a deferred face to the collection.
|
|
///
|
|
/// Returns the index for the added face.
|
|
pub fn add_deferred(
|
|
&mut self,
|
|
face: DeferredFace,
|
|
opts: AddOptions,
|
|
) -> Result<Index, AddDeferredError> {
|
|
if self.load_options.is_none() {
|
|
return Err(AddDeferredError::DeferredLoadingUnavailable);
|
|
}
|
|
|
|
// We have some special indexes so we must never pass those.
|
|
let idx = self.faces[opts.style as usize].len();
|
|
if idx >= usize::from(Index::SPECIAL_START) - 1 {
|
|
return Err(AddDeferredError::CollectionFull);
|
|
}
|
|
|
|
self.faces[opts.style as usize].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Deferred(face),
|
|
fallback: opts.fallback,
|
|
scale_factor: ScaleFactor::Adjustment(opts.size_adjustment),
|
|
}));
|
|
|
|
Ok(Index {
|
|
style: opts.style,
|
|
idx: idx as u16,
|
|
})
|
|
}
|
|
|
|
/// Return the Face represented by a given Index. The returned reference
|
|
/// is only valid as long as this collection is not modified.
|
|
///
|
|
/// This will initialize the face if it is deferred and not yet loaded,
|
|
/// which can fail.
|
|
pub fn get_face(&mut self, index: Index) -> Result<&mut Face, GetFaceError> {
|
|
let resolved = self.resolve(index)?;
|
|
self.get_face_from_entry(resolved)
|
|
}
|
|
|
|
/// Return the face for an index only if it is already loaded (never
|
|
/// triggers loading, so it only needs a shared reference). Returns `None`
|
|
/// for special, out-of-range or still-deferred entries.
|
|
pub fn get_loaded_face(&self, index: Index) -> Option<&Face> {
|
|
let resolved = self.resolve(index).ok()?;
|
|
match &self.entry_at(resolved).face {
|
|
AnyFace::Loaded(f) => Some(f),
|
|
AnyFace::Deferred(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Resolve an index (which might point at an alias) to the index of the
|
|
/// non-alias entry.
|
|
fn resolve(&self, index: Index) -> Result<Index, EntryError> {
|
|
if index.special().is_some() {
|
|
return Err(EntryError::SpecialHasNoFace);
|
|
}
|
|
let list = &self.faces[index.style as usize];
|
|
match list.get(usize::from(index.idx)) {
|
|
None => Err(EntryError::IndexOutOfBounds),
|
|
Some(EntryOrAlias::Entry(_)) => Ok(index),
|
|
Some(EntryOrAlias::Alias(target)) => Ok(*target),
|
|
}
|
|
}
|
|
|
|
fn entry_at(&self, resolved: Index) -> &Entry {
|
|
match &self.faces[resolved.style as usize][usize::from(resolved.idx)] {
|
|
EntryOrAlias::Entry(e) => e,
|
|
EntryOrAlias::Alias(_) => unreachable!("alias must point to a non-alias entry"),
|
|
}
|
|
}
|
|
|
|
fn entry_at_mut(&mut self, resolved: Index) -> &mut Entry {
|
|
match &mut self.faces[resolved.style as usize][usize::from(resolved.idx)] {
|
|
EntryOrAlias::Entry(e) => e,
|
|
EntryOrAlias::Alias(_) => unreachable!("alias must point to a non-alias entry"),
|
|
}
|
|
}
|
|
|
|
/// Get the unaliased entry from an index
|
|
pub fn get_entry(&mut self, index: Index) -> Result<&mut Entry, EntryError> {
|
|
let resolved = self.resolve(index)?;
|
|
Ok(self.entry_at_mut(resolved))
|
|
}
|
|
|
|
/// Get the unaliased entry from an index (shared).
|
|
pub fn get_const_entry(&self, index: Index) -> Result<&Entry, EntryError> {
|
|
let resolved = self.resolve(index)?;
|
|
Ok(self.entry_at(resolved))
|
|
}
|
|
|
|
/// Get the face from an entry (loading it if deferred).
|
|
///
|
|
/// This entry must not be an alias.
|
|
fn get_face_from_entry(&mut self, entry: Index) -> Result<&mut Face, GetFaceError> {
|
|
if matches!(self.entry_at(entry).face, AnyFace::Deferred(_)) {
|
|
let mut opts = self
|
|
.load_options
|
|
.clone()
|
|
.ok_or(GetFaceError::DeferredLoadingUnavailable)?;
|
|
|
|
// Load the face.
|
|
let mut face = match &mut self.entry_at_mut(entry).face {
|
|
AnyFace::Deferred(d) => d.load(&opts.library, opts.face_options())?,
|
|
AnyFace::Loaded(_) => unreachable!(),
|
|
};
|
|
|
|
// Calculate the scale factor for this
|
|
// entry now that we have a loaded face.
|
|
let mut scale = self.entry_at(entry).scale_factor;
|
|
if let ScaleFactor::Adjustment(adj) = scale {
|
|
let factor = self.scale_factor(&face.get_metrics(), adj);
|
|
scale = ScaleFactor::Scale(factor);
|
|
self.entry_at_mut(entry).scale_factor = scale;
|
|
}
|
|
|
|
// If our scale factor is something other
|
|
// than 1.0 then we need to resize the face.
|
|
if let ScaleFactor::Scale(s) = scale {
|
|
if s != 1.0 {
|
|
opts.size.points *= s as f32;
|
|
face.set_size(opts.face_options())?;
|
|
}
|
|
}
|
|
|
|
// Set the loaded face on the entry (dropping the deferred face).
|
|
self.entry_at_mut(entry).face = AnyFace::Loaded(face);
|
|
}
|
|
|
|
match &mut self.entry_at_mut(entry).face {
|
|
AnyFace::Loaded(f) => Ok(f),
|
|
AnyFace::Deferred(_) => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Return the index of the font in this collection that contains
|
|
/// the given codepoint, style, and presentation. If no font is found,
|
|
/// None is returned.
|
|
///
|
|
/// This does not trigger font loading; deferred fonts can be
|
|
/// searched for codepoints.
|
|
pub fn get_index(&self, cp: u32, style: Style, p_mode: PresentationMode) -> Option<Index> {
|
|
for (i, entry_or_alias) in self.faces[style as usize].iter().enumerate() {
|
|
if self.const_entry(entry_or_alias).has_codepoint(cp, p_mode) {
|
|
return Some(Index {
|
|
style,
|
|
idx: i as u16,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Not found
|
|
None
|
|
}
|
|
|
|
fn const_entry<'a>(&'a self, e: &'a EntryOrAlias) -> &'a Entry {
|
|
match e {
|
|
EntryOrAlias::Entry(e) => e,
|
|
EntryOrAlias::Alias(i) => self.entry_at(*i),
|
|
}
|
|
}
|
|
|
|
/// Check if a specific font index has a specific codepoint. This does not
|
|
/// necessarily force the font to load. The presentation value "p" will
|
|
/// verify the Emoji representation matches if it is non-null. If "p" is
|
|
/// null then any presentation will be accepted.
|
|
pub fn has_codepoint(&self, index: Index, cp: u32, p_mode: PresentationMode) -> bool {
|
|
let list = &self.faces[index.style as usize];
|
|
match list.get(usize::from(index.idx)) {
|
|
None => false,
|
|
Some(e) => self.const_entry(e).has_codepoint(cp, p_mode),
|
|
}
|
|
}
|
|
|
|
/// Ensure we have an option for all styles in the collection, such
|
|
/// as italic and bold by synthesizing them if necessary from the
|
|
/// first regular face that has text glyphs.
|
|
///
|
|
/// If there is no regular face that has text glyphs, then this
|
|
/// does nothing.
|
|
pub fn complete_styles(
|
|
&mut self,
|
|
synthetic_config: FontSyntheticStyle,
|
|
) -> Result<(), CompleteError> {
|
|
// If every style has at least one entry then we're done!
|
|
// This is the most common case.
|
|
if self.faces.iter().all(|l| !l.is_empty()) {
|
|
return Ok(());
|
|
}
|
|
|
|
// Find the first regular face that has non-colorized text glyphs.
|
|
// This is the font we want to fallback to. This may not be index zero
|
|
// if a user configures something like an Emoji font first.
|
|
let regular_entry: Index = 'entry: {
|
|
let count = self.faces[Style::Regular as usize].len();
|
|
if count == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
// Find our first regular face that has text glyphs.
|
|
for i in 0..count {
|
|
// Load our face. If we fail to load it, we just skip it and
|
|
// continue on to try the next one.
|
|
let index = Index {
|
|
style: Style::Regular,
|
|
idx: i as u16,
|
|
};
|
|
let resolved = self.resolve(index).expect("index in range");
|
|
let face = match self.get_face_from_entry(resolved) {
|
|
Ok(f) => f,
|
|
Err(err) => {
|
|
log::warn!("error loading regular entry={i} err={err}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// We have two conditionals here. The color check is obvious:
|
|
// we want to auto-italicize a normal text font. The second
|
|
// check is less obvious... for mixed color/non-color fonts, we
|
|
// accept the regular font if it has basic ASCII. This may not
|
|
// be strictly correct (especially with international fonts) but
|
|
// it's a reasonable heuristic and the first case will match 99%
|
|
// of the time.
|
|
if !face.has_color() || face.glyph_index('A' as u32).is_some() {
|
|
break 'entry resolved;
|
|
}
|
|
}
|
|
|
|
// No regular text face found. We can't provide any fallback.
|
|
return Err(CompleteError::DefaultUnavailable);
|
|
};
|
|
|
|
// If we don't have italic, attempt to create a synthetic italic face.
|
|
// If we can't create a synthetic italic face, we'll just use the regular
|
|
// face for italic.
|
|
let have_italic = !self.faces[Style::Italic as usize].is_empty();
|
|
if !have_italic {
|
|
if !synthetic_config.italic {
|
|
log::info!("italic style not available and synthetic italic disabled");
|
|
self.faces[Style::Italic as usize].push(EntryOrAlias::Alias(regular_entry));
|
|
} else {
|
|
match self.synthetic_italic(regular_entry) {
|
|
Ok(synthetic) => {
|
|
log::info!("synthetic italic face created");
|
|
self.faces[Style::Italic as usize].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Loaded(synthetic),
|
|
fallback: false,
|
|
scale_factor: ScaleFactor::Scale(1.0),
|
|
}));
|
|
}
|
|
Err(err) => {
|
|
log::warn!(
|
|
"failed to create synthetic italic, italic style will not be available err={err}"
|
|
);
|
|
self.faces[Style::Italic as usize].push(EntryOrAlias::Alias(regular_entry));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we don't have bold, use the regular font.
|
|
let have_bold = !self.faces[Style::Bold as usize].is_empty();
|
|
if !have_bold {
|
|
if !synthetic_config.bold {
|
|
log::info!("bold style not available and synthetic bold disabled");
|
|
self.faces[Style::Bold as usize].push(EntryOrAlias::Alias(regular_entry));
|
|
} else {
|
|
match self.synthetic_bold(regular_entry) {
|
|
Ok(synthetic) => {
|
|
log::info!("synthetic bold face created");
|
|
self.faces[Style::Bold as usize].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Loaded(synthetic),
|
|
fallback: false,
|
|
scale_factor: ScaleFactor::Scale(1.0),
|
|
}));
|
|
}
|
|
Err(err) => {
|
|
log::warn!(
|
|
"failed to create synthetic bold, bold style will not be available err={err}"
|
|
);
|
|
self.faces[Style::Bold as usize].push(EntryOrAlias::Alias(regular_entry));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we don't have bold italic, we attempt to synthesize a bold variant
|
|
// of the italic font. If we can't do that, we'll use the italic font.
|
|
if self.faces[Style::BoldItalic as usize].is_empty() {
|
|
self.complete_bold_italic(synthetic_config, regular_entry, have_bold);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn complete_bold_italic(
|
|
&mut self,
|
|
synthetic_config: FontSyntheticStyle,
|
|
regular_entry: Index,
|
|
have_bold: bool,
|
|
) {
|
|
let bi = Style::BoldItalic as usize;
|
|
if !synthetic_config.bold_italic {
|
|
log::info!("bold italic style not available and synthetic bold italic disabled");
|
|
self.faces[bi].push(EntryOrAlias::Alias(regular_entry));
|
|
return;
|
|
}
|
|
|
|
// Prefer to synthesize on top of the face we already had. If we
|
|
// have bold then we try to synthesize italic on top of bold.
|
|
if have_bold {
|
|
let base_entry = self
|
|
.resolve(Index {
|
|
style: Style::Bold,
|
|
idx: 0,
|
|
})
|
|
.expect("bold entry exists");
|
|
if let Ok(synthetic) = self.synthetic_italic(base_entry) {
|
|
log::info!("synthetic bold italic face created from bold");
|
|
self.faces[bi].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Loaded(synthetic),
|
|
fallback: false,
|
|
scale_factor: ScaleFactor::Scale(1.0),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// If synthesizing italic failed, then we try to synthesize
|
|
// bold on whatever italic font we have.
|
|
}
|
|
|
|
let base_entry = self
|
|
.resolve(Index {
|
|
style: Style::Italic,
|
|
idx: 0,
|
|
})
|
|
.expect("italic entry exists");
|
|
if let Ok(synthetic) = self.synthetic_bold(base_entry) {
|
|
log::info!("synthetic bold italic face created from italic");
|
|
self.faces[bi].push(EntryOrAlias::Entry(Entry {
|
|
face: AnyFace::Loaded(synthetic),
|
|
fallback: false,
|
|
scale_factor: ScaleFactor::Scale(1.0),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
log::warn!("bold italic style not available, using italic font");
|
|
self.faces[bi].push(EntryOrAlias::Alias(base_entry));
|
|
}
|
|
|
|
/// Create a synthetic bold font face from the given entry and return it.
|
|
fn synthetic_bold(&mut self, entry: Index) -> Result<Face, SyntheticError> {
|
|
// We require loading options to create a synthetic bold face.
|
|
let opts = self
|
|
.load_options
|
|
.clone()
|
|
.ok_or(SyntheticError::DeferredLoadingUnavailable)?;
|
|
|
|
// Try to bold it.
|
|
let regular = self.get_face_from_entry(entry)?;
|
|
|
|
// NOTE: upstream computes a `face_opts` with the size inherited from
|
|
// the regular face but then (accidentally) passes `opts.faceOptions()`
|
|
// instead; we mirror that behaviour.
|
|
let face = regular.synthetic_bold(opts.face_options())?;
|
|
|
|
log::info!("font synthetic bold created family={}", face.name());
|
|
|
|
Ok(face)
|
|
}
|
|
|
|
/// Create a synthetic italic font face from the given entry and return it.
|
|
fn synthetic_italic(&mut self, entry: Index) -> Result<Face, SyntheticError> {
|
|
// We require loading options to create a synthetic italic face.
|
|
let opts = self
|
|
.load_options
|
|
.clone()
|
|
.ok_or(SyntheticError::DeferredLoadingUnavailable)?;
|
|
|
|
// Try to italicize it.
|
|
let regular = self.get_face_from_entry(entry)?;
|
|
|
|
// See the NOTE in `synthetic_bold`.
|
|
let face = regular.synthetic_italic(opts.face_options())?;
|
|
|
|
log::info!("font synthetic italic created family={}", face.name());
|
|
|
|
Ok(face)
|
|
}
|
|
|
|
/// Update the size of all faces in the collection. This will
|
|
/// also update the size in the load options for future deferred
|
|
/// face loading.
|
|
///
|
|
/// This requires load options to be set.
|
|
pub fn set_size(&mut self, size: DesiredSize) -> Result<(), SetSizeError> {
|
|
// Get our options so we can modify the size.
|
|
let opts = {
|
|
let opts = self
|
|
.load_options
|
|
.as_mut()
|
|
.ok_or(SetSizeError::DeferredLoadingUnavailable)?;
|
|
opts.size = size;
|
|
opts.clone()
|
|
};
|
|
|
|
let primary_entry = self
|
|
.resolve(Index {
|
|
style: Style::Regular,
|
|
idx: 0,
|
|
})
|
|
.ok();
|
|
|
|
// Resize all our faces that are loaded
|
|
for style in Style::ALL {
|
|
for i in 0..self.faces[style as usize].len() {
|
|
// Resize all faces. We skip entries that are aliases, since
|
|
// the underlying face will have a non-alias entry somewhere.
|
|
if matches!(self.faces[style as usize][i], EntryOrAlias::Alias(_)) {
|
|
continue;
|
|
}
|
|
let this = Index {
|
|
style,
|
|
idx: i as u16,
|
|
};
|
|
|
|
let entry = self.entry_at_mut(this);
|
|
let scale = entry.scale_factor;
|
|
if let AnyFace::Loaded(face) = &mut entry.face {
|
|
// If this isn't our primary face, we scale
|
|
// the size appropriately before setting it.
|
|
//
|
|
// If we don't have a primary face we also don't.
|
|
let mut new_opts = opts.clone();
|
|
if primary_entry.is_some() && Some(this) != primary_entry {
|
|
if let ScaleFactor::Scale(s) = scale {
|
|
new_opts.size.points *= s as f32;
|
|
}
|
|
}
|
|
face.set_size(new_opts.face_options())
|
|
.map_err(|_| SetSizeError::SetSizeFailed)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(self.update_metrics()?)
|
|
}
|
|
|
|
/// Calculate a factor by which to scale the provided face to match
|
|
/// it with the primary face, depending on the specified adjustment.
|
|
///
|
|
/// If this encounters any problems loading the primary face or its
|
|
/// metrics then it just returns `1.0`.
|
|
///
|
|
/// This functions very much like the `font-size-adjust` CSS property.
|
|
/// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
|
|
fn scale_factor(&mut self, face_metrics: &FaceMetrics, adjustment: SizeAdjustment) -> f64 {
|
|
// If there's no adjustment, the scale is 1.0
|
|
if adjustment == SizeAdjustment::None {
|
|
return 1.0;
|
|
}
|
|
|
|
// If we haven't calculated our primary face metrics yet, do so now.
|
|
if self.primary_face_metrics.is_none() {
|
|
// If we can't load the primary face, just use 1.0 as the scale factor.
|
|
let Ok(primary_face) = self.get_face(Index::default()) else {
|
|
return 1.0;
|
|
};
|
|
self.primary_face_metrics = Some(primary_face.get_metrics());
|
|
}
|
|
|
|
let primary_metrics = self.primary_face_metrics.expect("set above");
|
|
|
|
// We normalize the metrics values which are expressed in px to instead
|
|
// be in ems, so that it doesn't matter what size the faces actually are.
|
|
let primary_scale = 1.0 / primary_metrics.px_per_em;
|
|
let face_scale = 1.0 / face_metrics.px_per_em;
|
|
|
|
// We get the target metrics from the primary face and this face depending
|
|
// on the specified `adjustment`. If this face doesn't explicitly define a
|
|
// metric, or if the value it defines is invalid, we fall through to other
|
|
// options in the order below.
|
|
//
|
|
// In order to make sure the value is valid, we compare it with the result
|
|
// of the estimator function, which rules out both null and invalid values.
|
|
let mut adj = adjustment;
|
|
let (primary_metric, face_metric) = loop {
|
|
match adj {
|
|
SizeAdjustment::IcWidth => {
|
|
if face_metrics.ic_width != Some(face_metrics.ic_width()) {
|
|
adj = SizeAdjustment::ExHeight;
|
|
continue;
|
|
}
|
|
break (
|
|
primary_metrics.ic_width() * primary_scale,
|
|
face_metrics.ic_width() * face_scale,
|
|
);
|
|
}
|
|
SizeAdjustment::ExHeight => {
|
|
if face_metrics.ex_height != Some(face_metrics.ex_height()) {
|
|
adj = SizeAdjustment::CapHeight;
|
|
continue;
|
|
}
|
|
break (
|
|
primary_metrics.ex_height() * primary_scale,
|
|
face_metrics.ex_height() * face_scale,
|
|
);
|
|
}
|
|
SizeAdjustment::CapHeight => {
|
|
if face_metrics.cap_height != Some(face_metrics.cap_height()) {
|
|
adj = SizeAdjustment::LineHeight;
|
|
continue;
|
|
}
|
|
break (
|
|
primary_metrics.cap_height() * primary_scale,
|
|
face_metrics.cap_height() * face_scale,
|
|
);
|
|
}
|
|
SizeAdjustment::LineHeight => {
|
|
break (
|
|
primary_metrics.line_height() * primary_scale,
|
|
face_metrics.line_height() * face_scale,
|
|
);
|
|
}
|
|
SizeAdjustment::None => unreachable!(),
|
|
}
|
|
};
|
|
|
|
primary_metric / face_metric
|
|
}
|
|
|
|
/// Update the cell metrics for this collection, based on
|
|
/// the primary font and the modifiers in `metric_modifiers`.
|
|
///
|
|
/// This requires a primary font (index `0`) to be present.
|
|
pub fn update_metrics(&mut self) -> Result<(), UpdateMetricsError> {
|
|
let primary_face = self
|
|
.get_face(Index::default())
|
|
.map_err(|_| UpdateMetricsError::CannotLoadPrimaryFont)?;
|
|
|
|
let fm = primary_face.get_metrics();
|
|
self.primary_face_metrics = Some(fm);
|
|
|
|
let mut metrics = Metrics::calc(fm);
|
|
|
|
metrics.apply(&self.metric_modifiers);
|
|
|
|
self.metrics = Some(metrics);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Options for adjusting the size of a face relative to the primary face.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum SizeAdjustment {
|
|
/// Don't adjust the size for this face, use the original point size.
|
|
None,
|
|
/// Match ideograph character width with the primary face.
|
|
IcWidth,
|
|
/// Match ex height with the primary face.
|
|
ExHeight,
|
|
/// Match cap height with the primary face.
|
|
CapHeight,
|
|
/// Match line height with the primary face.
|
|
LineHeight,
|
|
}
|
|
|
|
/// Load options are used to configure all the details a Collection
|
|
/// needs to load deferred faces.
|
|
#[derive(Clone)]
|
|
pub struct LoadOptions {
|
|
/// The library to use for loading faces. This is not owned by
|
|
/// the collection and can be used by multiple collections.
|
|
pub library: Library,
|
|
|
|
/// The desired font size for all loaded faces.
|
|
pub size: DesiredSize,
|
|
|
|
/// Freetype Load Flags to use when loading glyphs. This is a list of
|
|
/// bitfield constants that controls operations to perform during glyph
|
|
/// loading. Only a subset is exposed for configuration, for the whole set
|
|
/// of flags see `pkg.freetype.face.LoadFlags`.
|
|
pub freetype_load_flags: FreetypeLoadFlags,
|
|
}
|
|
|
|
impl LoadOptions {
|
|
/// Options with the default size (12 points) and load flags.
|
|
pub fn new(library: Library) -> LoadOptions {
|
|
LoadOptions {
|
|
library,
|
|
size: DesiredSize::new(12.0),
|
|
freetype_load_flags: face::FREETYPE_LOAD_FLAGS_DEFAULT,
|
|
}
|
|
}
|
|
|
|
/// The options to use for loading faces.
|
|
pub fn face_options(&self) -> face::Options {
|
|
face::Options {
|
|
size: self.size,
|
|
freetype_load_flags: self.freetype_load_flags,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum AnyFace {
|
|
/// Not yet loaded.
|
|
Deferred(DeferredFace),
|
|
|
|
/// Loaded.
|
|
Loaded(Face),
|
|
}
|
|
|
|
/// Factor to multiply the collection size by for a face, or else the size
|
|
/// adjustment that should be used to calculate it once the face is loaded.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum ScaleFactor {
|
|
Adjustment(SizeAdjustment),
|
|
Scale(f64),
|
|
}
|
|
|
|
/// A entry in a collection can be deferred or loaded. A deferred face
|
|
/// is not yet fully loaded and only represents the font descriptor
|
|
/// and usually uses less resources. A loaded face is fully parsed,
|
|
/// ready to rasterize, and usually uses more resources than a
|
|
/// deferred version.
|
|
///
|
|
/// A face can also be a "fallback" variant that is still either
|
|
/// deferred or loaded. Today, there is only one difference between
|
|
/// fallback and non-fallback (or "explicit") faces: the handling
|
|
/// of emoji presentation.
|
|
///
|
|
/// For explicit faces, when an explicit emoji presentation is
|
|
/// not requested, we will use any glyph for that codepoint found
|
|
/// even if the font presentation does not match the UCD
|
|
/// (Unicode Character Database) value. When an explicit presentation
|
|
/// is requested (via either VS15/V16), that is always honored.
|
|
/// The reason we do this is because we assume that if a user
|
|
/// explicitly chosen a font face (hence it is "explicit" and
|
|
/// not "fallback"), they want to use any glyphs possible within that
|
|
/// font face. Fallback fonts on the other hand are picked as a
|
|
/// last resort, so we should prefer exactness if possible.
|
|
pub struct Entry {
|
|
pub face: AnyFace,
|
|
|
|
/// Whether this face is a fallback face, see
|
|
/// main doc comment on Entry for more info.
|
|
pub fallback: bool,
|
|
|
|
/// Factor to multiply the collection size by for this face, or
|
|
/// else the size adjustment that should be used to calculate
|
|
/// once the face is loaded.
|
|
///
|
|
/// This is computed when the face is loaded, based on a scale
|
|
/// factor computed for an adjustment from the primary face to
|
|
/// this one, which allows fallback fonts to be harmonized with
|
|
/// the primary font by matching one of the metrics between them.
|
|
pub scale_factor: ScaleFactor,
|
|
}
|
|
|
|
impl Entry {
|
|
/// If this face is loaded, then this returns the `Face`,
|
|
/// otherwise returns None.
|
|
pub fn get_loaded(&mut self) -> Option<&mut Face> {
|
|
match &mut self.face {
|
|
AnyFace::Deferred(_) => None,
|
|
AnyFace::Loaded(face) => Some(face),
|
|
}
|
|
}
|
|
|
|
/// True if the entry is deferred.
|
|
pub fn is_deferred(&self) -> bool {
|
|
matches!(self.face, AnyFace::Deferred(_))
|
|
}
|
|
|
|
/// True if this face satisfies the given codepoint and presentation.
|
|
pub fn has_codepoint(&self, cp: u32, p_mode: PresentationMode) -> bool {
|
|
let mut mode = p_mode;
|
|
loop {
|
|
match mode {
|
|
PresentationMode::Default(p) => {
|
|
mode = if self.fallback {
|
|
// Fallback fonts require explicit presentation matching.
|
|
PresentationMode::Explicit(p)
|
|
} else {
|
|
// Non-fallback fonts do not.
|
|
PresentationMode::Any
|
|
};
|
|
}
|
|
PresentationMode::Explicit(p) => {
|
|
return match &self.face {
|
|
AnyFace::Deferred(v) => v.has_codepoint(cp, Some(p)),
|
|
AnyFace::Loaded(face) => {
|
|
let Some(index) = face.glyph_index(cp) else {
|
|
return false;
|
|
};
|
|
match p {
|
|
Presentation::Text => !face.is_color_glyph(index),
|
|
Presentation::Emoji => face.is_color_glyph(index),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
PresentationMode::Any => {
|
|
return match &self.face {
|
|
AnyFace::Deferred(v) => v.has_codepoint(cp, None),
|
|
AnyFace::Loaded(face) => face.glyph_index(cp).is_some(),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum EntryOrAlias {
|
|
Entry(Entry),
|
|
|
|
/// An alias to another entry. This is used to share the same face,
|
|
/// avoid memory duplication. An alias must point to a non-alias entry.
|
|
Alias(Index),
|
|
}
|
|
|
|
/// The requested presentation for a codepoint.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PresentationMode {
|
|
/// The codepoint has an explicit presentation that is required,
|
|
/// i.e. VS15/V16.
|
|
Explicit(Presentation),
|
|
|
|
/// The codepoint has no explicit presentation and we should use
|
|
/// the presentation from the UCD.
|
|
Default(Presentation),
|
|
|
|
/// The codepoint can be any presentation.
|
|
Any,
|
|
}
|
|
|
|
/// The special-case fonts that we support.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[repr(u16)]
|
|
pub enum Special {
|
|
/// Sprite drawing, this is rendered JIT using 2D graphics APIs.
|
|
Sprite = Index::SPECIAL_START,
|
|
}
|
|
|
|
/// This represents a specific font in the collection.
|
|
///
|
|
/// The Zig version is a `packed struct(u16)` with a 3 bit style and 13 bit
|
|
/// index (up to 8192 fonts per style). Here the two parts are separate
|
|
/// fields, with [`Index::int`] producing the packed representation.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Index {
|
|
pub style: Style,
|
|
pub idx: u16,
|
|
}
|
|
|
|
impl Default for Index {
|
|
fn default() -> Self {
|
|
Index {
|
|
style: Style::Regular,
|
|
idx: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Index {
|
|
/// The number of bits we use for the index.
|
|
pub const IDX_BITS: u32 = 13;
|
|
|
|
/// We start all special fonts at this index so they can be detected
|
|
/// (`maxInt(IndexInt)`).
|
|
pub const SPECIAL_START: u16 = (1 << Self::IDX_BITS) - 1;
|
|
|
|
/// Initialize a special font index.
|
|
pub const fn init_special(v: Special) -> Index {
|
|
Index {
|
|
style: Style::Regular,
|
|
idx: v as u16,
|
|
}
|
|
}
|
|
|
|
/// Convert to the packed int (style in the low 3 bits).
|
|
pub fn int(self) -> u16 {
|
|
(self.style as u16) | (self.idx << 3)
|
|
}
|
|
|
|
/// Returns Some if this is a "special" index which doesn't map to
|
|
/// a real font face. We can still render it but there is no face for
|
|
/// this font.
|
|
pub fn special(self) -> Option<Special> {
|
|
if self.idx < Self::SPECIAL_START {
|
|
return None;
|
|
}
|
|
Some(Special::Sprite)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
embedded,
|
|
face::{DesiredSize, Options},
|
|
};
|
|
|
|
fn opts_96(points: f32) -> Options {
|
|
Options::new(DesiredSize {
|
|
points,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
})
|
|
}
|
|
|
|
fn add_opts(style: Style, adj: SizeAdjustment) -> AddOptions {
|
|
AddOptions {
|
|
style,
|
|
fallback: false,
|
|
size_adjustment: adj,
|
|
}
|
|
}
|
|
|
|
fn approx(a: f64, b: f64) -> bool {
|
|
(a - b).abs() < 1e-3
|
|
}
|
|
|
|
fn assert_metrics_approx(expected: &Metrics, actual: &Metrics) {
|
|
assert_eq!(expected.cell_width, actual.cell_width);
|
|
assert_eq!(expected.cell_height, actual.cell_height);
|
|
assert_eq!(expected.cell_baseline, actual.cell_baseline);
|
|
assert_eq!(expected.underline_position, actual.underline_position);
|
|
assert_eq!(expected.underline_thickness, actual.underline_thickness);
|
|
assert_eq!(
|
|
expected.strikethrough_position,
|
|
actual.strikethrough_position
|
|
);
|
|
assert_eq!(
|
|
expected.strikethrough_thickness,
|
|
actual.strikethrough_thickness
|
|
);
|
|
assert_eq!(expected.overline_position, actual.overline_position);
|
|
assert_eq!(expected.overline_thickness, actual.overline_thickness);
|
|
assert_eq!(expected.box_thickness, actual.box_thickness);
|
|
assert_eq!(expected.cursor_height, actual.cursor_height);
|
|
assert!(
|
|
approx(expected.icon_height, actual.icon_height),
|
|
"icon_height"
|
|
);
|
|
assert!(
|
|
approx(expected.icon_height_single, actual.icon_height_single),
|
|
"icon_height_single"
|
|
);
|
|
assert!(approx(expected.face_width, actual.face_width), "face_width");
|
|
assert!(
|
|
approx(expected.face_height, actual.face_height),
|
|
"face_height"
|
|
);
|
|
assert!(approx(expected.face_y, actual.face_y), "face_y");
|
|
}
|
|
|
|
#[test]
|
|
fn index_layout() {
|
|
// We never want to take up more than a u16 since font indexes are
|
|
// everywhere. The maximum number of fonts for a style is 13 bits.
|
|
assert_eq!(2, std::mem::size_of::<u16>());
|
|
assert_eq!(13, Index::IDX_BITS);
|
|
assert_eq!(8191, Index::SPECIAL_START);
|
|
let sprite = Index::init_special(Special::Sprite);
|
|
assert_eq!(Some(Special::Sprite), sprite.special());
|
|
assert_eq!(None, Index::default().special());
|
|
assert_eq!(Index::SPECIAL_START << 3, sprite.int());
|
|
}
|
|
|
|
#[test]
|
|
fn init() {
|
|
let c = Collection::new();
|
|
drop(c);
|
|
}
|
|
|
|
#[test]
|
|
fn add_full() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
|
|
for _ in 0..(usize::from(Index::SPECIAL_START) - 1) {
|
|
c.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let face = Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap();
|
|
assert_eq!(
|
|
Err(AddError::CollectionFull),
|
|
c.add(face, add_opts(Style::Regular, SizeAdjustment::None))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_deferred_without_loading_options() {
|
|
let mut c = Collection::new();
|
|
// The deferred face can be anything since it should never be accessed.
|
|
let deferred = DeferredFace::default();
|
|
assert_eq!(
|
|
Err(AddDeferredError::DeferredLoadingUnavailable),
|
|
c.add_deferred(deferred, add_opts(Style::Regular, SizeAdjustment::None))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn get_face() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
let idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
let p1 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
|
|
let p2 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
|
|
assert_eq!(p1, p2);
|
|
}
|
|
|
|
#[test]
|
|
fn get_index() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
// Should find all visible ASCII
|
|
for i in 32u32..127 {
|
|
assert!(
|
|
c.get_index(i, Style::Regular, PresentationMode::Any)
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
// Should not find emoji
|
|
assert!(
|
|
c.get_index('🥸' as u32, Style::Regular, PresentationMode::Any)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_styles() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
c.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
let a = 'A' as u32;
|
|
assert!(c.get_index(a, Style::Bold, PresentationMode::Any).is_none());
|
|
assert!(
|
|
c.get_index(a, Style::Italic, PresentationMode::Any)
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
c.get_index(a, Style::BoldItalic, PresentationMode::Any)
|
|
.is_none()
|
|
);
|
|
c.complete_styles(FontSyntheticStyle::default()).unwrap();
|
|
assert!(c.get_index(a, Style::Bold, PresentationMode::Any).is_some());
|
|
assert!(
|
|
c.get_index(a, Style::Italic, PresentationMode::Any)
|
|
.is_some()
|
|
);
|
|
assert!(
|
|
c.get_index(a, Style::BoldItalic, PresentationMode::Any)
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_styles_synthetic_disabled_uses_aliases() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
c.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
c.complete_styles(FontSyntheticStyle {
|
|
bold: false,
|
|
italic: false,
|
|
bold_italic: false,
|
|
})
|
|
.unwrap();
|
|
for style in [Style::Bold, Style::Italic, Style::BoldItalic] {
|
|
assert_eq!(1, c.count(style));
|
|
let idx = c
|
|
.get_index('A' as u32, style, PresentationMode::Any)
|
|
.unwrap();
|
|
// The alias resolves to the regular entry's face.
|
|
let p1 = std::ptr::from_ref::<Face>(c.get_face(idx).unwrap());
|
|
let p2 = std::ptr::from_ref::<Face>(c.get_face(Index::default()).unwrap());
|
|
assert_eq!(p1, p2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_size() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
c.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(12.0, c.load_options.as_ref().unwrap().size.points);
|
|
c.set_size(DesiredSize::new(24.0)).unwrap();
|
|
assert_eq!(24.0, c.load_options.as_ref().unwrap().size.points);
|
|
}
|
|
|
|
#[test]
|
|
fn has_codepoint() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
let idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::REGULAR.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(c.has_codepoint(idx, 'A' as u32, PresentationMode::Any));
|
|
assert!(!c.has_codepoint(idx, '🥸' as u32, PresentationMode::Any));
|
|
}
|
|
|
|
#[cfg(not(font_coretext))]
|
|
#[test]
|
|
fn has_codepoint_emoji_default_graphical() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
c.load_options = Some(LoadOptions::new(lib.clone()));
|
|
|
|
let idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::EMOJI.into(), opts_96(12.0)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(!c.has_codepoint(idx, 'A' as u32, PresentationMode::Any));
|
|
assert!(c.has_codepoint(idx, '🥸' as u32, PresentationMode::Any));
|
|
// TODO(fontmem): test explicit/implicit
|
|
}
|
|
|
|
#[test]
|
|
fn metrics() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
let size = DesiredSize {
|
|
points: 12.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
};
|
|
let mut lo = LoadOptions::new(lib.clone());
|
|
lo.size = size;
|
|
c.load_options = Some(lo);
|
|
|
|
c.add(
|
|
Face::init(&lib, embedded::INCONSOLATA.into(), Options::new(size)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
c.update_metrics().unwrap();
|
|
|
|
let base = Metrics {
|
|
cell_width: 8,
|
|
// The cell height is 17 px because the calculation is
|
|
//
|
|
// ascender - descender + gap
|
|
//
|
|
// which, for inconsolata is
|
|
//
|
|
// 859 - -190 + 0
|
|
//
|
|
// font units, at 1000 units per em that works out to 1.049 em,
|
|
// and 1em should be the point size * dpi scale, so 12 * (96/72)
|
|
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
|
|
// to 17.
|
|
//
|
|
// The icon height is (2 * cap_height + face_height) / 3
|
|
// = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24.
|
|
cell_height: 17,
|
|
cell_baseline: 3,
|
|
underline_position: 17,
|
|
underline_thickness: 1,
|
|
strikethrough_position: 10,
|
|
strikethrough_thickness: 1,
|
|
overline_position: 0,
|
|
overline_thickness: 1,
|
|
box_thickness: 1,
|
|
cursor_height: 17,
|
|
icon_height: 16.784,
|
|
icon_height_single: 12.24,
|
|
face_width: 8.0,
|
|
face_height: 16.784,
|
|
face_y: -0.04,
|
|
cursor_thickness: 1,
|
|
};
|
|
assert_metrics_approx(&base, &c.metrics.unwrap());
|
|
|
|
// Resize should change metrics
|
|
c.set_size(DesiredSize {
|
|
points: 24.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
})
|
|
.unwrap();
|
|
let big = Metrics {
|
|
cell_width: 16,
|
|
cell_height: 34,
|
|
cell_baseline: 6,
|
|
underline_position: 34,
|
|
underline_thickness: 2,
|
|
strikethrough_position: 19,
|
|
strikethrough_thickness: 2,
|
|
overline_position: 0,
|
|
overline_thickness: 2,
|
|
box_thickness: 2,
|
|
cursor_height: 34,
|
|
icon_height: 33.568,
|
|
icon_height_single: 24.48,
|
|
face_width: 16.0,
|
|
face_height: 33.568,
|
|
face_y: -0.08,
|
|
cursor_thickness: 1,
|
|
};
|
|
assert_metrics_approx(&big, &c.metrics.unwrap());
|
|
}
|
|
|
|
// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
|
|
#[test]
|
|
fn adjusted_sizes() {
|
|
let lib = Library::init().unwrap();
|
|
let mut c = Collection::new();
|
|
let size = DesiredSize {
|
|
points: 12.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
};
|
|
let mut lo = LoadOptions::new(lib.clone());
|
|
lo.size = size;
|
|
c.load_options = Some(lo);
|
|
|
|
// Add our primary face.
|
|
c.add(
|
|
Face::init(&lib, embedded::INCONSOLATA.into(), Options::new(size)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
c.update_metrics().unwrap();
|
|
|
|
for metric in [SizeAdjustment::ExHeight, SizeAdjustment::CapHeight] {
|
|
let pick = |m: &FaceMetrics| match metric {
|
|
SizeAdjustment::ExHeight => m.ex_height.unwrap(),
|
|
SizeAdjustment::CapHeight => m.cap_height.unwrap(),
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
// Add the fallback face with the chosen adjustment metric.
|
|
let fallback_idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::MONASPACE_NEON.into(), Options::new(size)).unwrap(),
|
|
add_opts(Style::Regular, metric),
|
|
)
|
|
.unwrap();
|
|
|
|
// The chosen metric should match.
|
|
{
|
|
let pm = c.get_face(Index::default()).unwrap().get_metrics();
|
|
let fm = c.get_face(fallback_idx).unwrap().get_metrics();
|
|
// We accept anything within half a pixel.
|
|
assert!((pick(&pm) - pick(&fm)).abs() <= 0.5);
|
|
}
|
|
|
|
// Resize should keep that relationship.
|
|
c.set_size(DesiredSize {
|
|
points: 37.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
})
|
|
.unwrap();
|
|
{
|
|
let pm = c.get_face(Index::default()).unwrap().get_metrics();
|
|
let fm = c.get_face(fallback_idx).unwrap().get_metrics();
|
|
assert!((pick(&pm) - pick(&fm)).abs() <= 0.5);
|
|
}
|
|
|
|
// Reset size for the next iteration
|
|
c.set_size(size).unwrap();
|
|
}
|
|
|
|
{
|
|
// A reference metric of "none" should leave the size unchanged.
|
|
let fallback_idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::MONASPACE_NEON.into(), Options::new(size)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::None),
|
|
)
|
|
.unwrap();
|
|
|
|
let primary_points = c.get_face(Index::default()).unwrap().size.points;
|
|
assert_eq!(
|
|
primary_points,
|
|
c.get_face(fallback_idx).unwrap().size.points
|
|
);
|
|
|
|
// Resize should keep that.
|
|
c.set_size(DesiredSize {
|
|
points: 37.0,
|
|
xdpi: 96,
|
|
ydpi: 96,
|
|
})
|
|
.unwrap();
|
|
|
|
let primary_points = c.get_face(Index::default()).unwrap().size.points;
|
|
assert_eq!(
|
|
primary_points,
|
|
c.get_face(fallback_idx).unwrap().size.points
|
|
);
|
|
|
|
// Reset collection size
|
|
c.set_size(size).unwrap();
|
|
}
|
|
|
|
// Add the symbol face.
|
|
let symbol_idx = c
|
|
.add(
|
|
Face::init(&lib, embedded::SYMBOLS_NERD_FONT.into(), Options::new(size)).unwrap(),
|
|
add_opts(Style::Regular, SizeAdjustment::ExHeight),
|
|
)
|
|
.unwrap();
|
|
|
|
// Test fallback to line_height() (ex_height and cap_height not defined
|
|
// in symbols font).
|
|
{
|
|
let pm = c.get_face(Index::default()).unwrap().get_metrics();
|
|
let sm = c.get_face(symbol_idx).unwrap().get_metrics();
|
|
// We accept anything within half a pixel.
|
|
assert!((pm.line_height() - sm.line_height()).abs() <= 0.5);
|
|
}
|
|
}
|
|
}
|