blob: ced27cadb55fdf048cb7e9cfc26b48f8067a2227 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
super::{
inspect::zero_pad,
typeface::{
Collection as TypefaceCollection, Typeface, TypefaceCollectionBuilder, TypefaceId,
TypefaceInfoAndCharSet, TypefaceInspectData,
},
AssetId,
},
fidl_fuchsia_fonts::GenericFontFamily,
fuchsia_inspect as finspect,
heck::KebabCase,
itertools::Itertools,
manifest::{serde_ext::StyleOptions, v2},
std::{collections::BTreeMap, sync::Arc},
unicase::UniCase,
};
#[derive(Debug, Eq, Hash, PartialEq)]
pub enum FamilyOrAlias {
Family(FontFamily),
/// The definition of a `Family`'s alias. This is a representation of the alias's _target_
/// (value). The alias's name (key) is stored outside of this enum.
///
/// * `UniCase<String>` - The _canonical_ family name.
/// * `Option<Arc<TypefaceQueryOverrides>>` - Optional query overrides applied when a client
/// requests the associated alias.
Alias(UniCase<String>, Option<Arc<TypefaceQueryOverrides>>),
}
/// Builder for [FamilyOrAlias].
#[derive(Debug)]
pub enum FamilyOrAliasBuilder {
Family(FontFamilyBuilder),
Alias(UniCase<String>, Option<Arc<TypefaceQueryOverrides>>),
}
impl FamilyOrAliasBuilder {
/// Finalizes and builds a `FamilyOrAlias`.
pub fn build(self) -> FamilyOrAlias {
match self {
FamilyOrAliasBuilder::Family(family_builder) => {
FamilyOrAlias::Family(family_builder.build())
}
FamilyOrAliasBuilder::Alias(name, overrides) => FamilyOrAlias::Alias(name, overrides),
}
}
/// Generates a full set of aliases from a given manifest v2 `Family`, including alias variants
/// without spaces.
pub fn aliases_from_family(
manifest_family: &v2::Family,
) -> BTreeMap<UniCase<String>, FamilyOrAliasBuilder> {
let mut output: BTreeMap<UniCase<String>, FamilyOrAliasBuilder> = BTreeMap::new();
let family_name = UniCase::new(manifest_family.name.clone());
let family_name_without_spaces = UniCase::new(manifest_family.name.replace(" ", ""));
if family_name_without_spaces != family_name {
output.insert(
family_name_without_spaces,
FamilyOrAliasBuilder::Alias(family_name.clone(), None),
);
}
for alias_set in &manifest_family.aliases {
let overrides = if alias_set.has_overrides() {
Some(Arc::new(TypefaceQueryOverrides {
style: alias_set.style_overrides().clone(),
languages: alias_set.language_overrides().cloned().collect_vec(),
}))
} else {
None
};
for alias_name in alias_set.names() {
let alias_name = UniCase::new(alias_name.clone());
output.insert(
alias_name.clone(),
FamilyOrAliasBuilder::Alias(family_name.clone(), overrides.clone()),
);
let alias_name_without_spaces = UniCase::new((*alias_name).replace(" ", ""));
if alias_name != alias_name_without_spaces {
output.insert(
alias_name_without_spaces,
FamilyOrAliasBuilder::Alias(family_name.clone(), overrides.clone()),
);
}
}
}
output
}
}
/// Optional set of typeface query overrides embedded in a `FontFamilyAlias`.
///
/// For example, the if a client asks for alias "Roboto Condensed", the server knows that means
/// means the canonical family "Roboto" with style `width: condensed`. If the client asks for "Noto
/// Sans CJK JP", that's interpreted as the canonical family "Noto Sans CJK" with
/// `languages: [ "ja" ]`.
///
#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct TypefaceQueryOverrides {
pub style: StyleOptions,
pub languages: Vec<String>,
}
impl TypefaceQueryOverrides {
pub fn has_style_overrides(&self) -> bool {
self.style != StyleOptions::default()
}
pub fn has_language_overrides(&self) -> bool {
!self.languages.is_empty()
}
}
/// In-memory representation of metadata for a font family and its typefaces.
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct FontFamily {
/// The family's canonical name
pub name: String,
/// Collection of typefaces in the family
pub faces: TypefaceCollection,
/// Generic font family that this family belongs to, if applicable
pub generic_family: Option<GenericFontFamily>,
}
impl FontFamily {
/// Get owned copies of the family's typefaces as `TypefaceInfo`
pub fn extract_faces<'a>(&'a self) -> impl Iterator<Item = TypefaceInfoAndCharSet> + 'a {
// Convert Vec<Arc<Typeface>> to Vec<TypefaceInfo>
self.faces
.faces
.iter()
// Copy most fields from `Typeface` and use the canonical family name
.map(move |face| TypefaceInfoAndCharSet::from_typeface(face, self.name.clone()))
}
}
/// Builder for [FontFamily].
#[derive(Debug)]
pub struct FontFamilyBuilder {
/// The family's canonical name
name: String,
/// Generic font family that this family belongs to, if applicable
generic_family: Option<GenericFontFamily>,
/// Collection of typefaces in the family
faces: TypefaceCollectionBuilder,
}
impl FontFamilyBuilder {
/// Creates a new builder without any typefaces defined.
pub fn new(
name: impl Into<String>,
generic_family: Option<GenericFontFamily>,
) -> FontFamilyBuilder {
FontFamilyBuilder {
name: name.into(),
generic_family,
faces: TypefaceCollectionBuilder::new(),
}
}
/// Returns true if the family's typeface collection already has a typeface with the given ID.
pub fn has_typeface_id(&self, typeface_id: &TypefaceId) -> bool {
self.faces.has_typeface_id(typeface_id)
}
/// If there's no existing typeface with the same typeface ID already in the builder, adds the
/// given typeface and and returns `true`. Otherwise, returns `false`.
pub fn add_typeface_once(&mut self, typeface: Arc<Typeface>) -> bool {
self.faces.add_typeface_once(typeface)
}
/// Finalizes and builds a `FontFamily`.
pub fn build(self) -> FontFamily {
FontFamily {
name: self.name,
faces: self.faces.build(),
generic_family: self.generic_family,
}
}
}
/// Inspect data for a `FontFamily`.
#[derive(Debug)]
pub struct FamilyInspectData {
node: finspect::Node,
aliases_node: finspect::Node,
aliases: Vec<AliasInspectData>,
typefaces_node: finspect::Node,
typefaces: Vec<TypefaceInspectData>,
}
impl FamilyInspectData {
/// Creates a new `FamilyInspectData`.
pub fn new(
parent: &finspect::Node,
family: &FontFamily,
asset_location_lookup: &impl Fn(AssetId) -> Option<String>,
) -> Self {
let node = parent.create_child(&family.name);
let aliases_node = node.create_child("aliases");
let aliases: Vec<AliasInspectData> = Vec::new();
let typefaces_node = node.create_child("typefaces");
let typefaces_count = family.faces.faces.len();
let typefaces: Vec<TypefaceInspectData> = family
.faces
.faces
.iter()
.enumerate()
.map(|(idx, typeface)| {
TypefaceInspectData::new(
&typefaces_node,
&zero_pad(idx, typefaces_count),
typeface,
asset_location_lookup,
)
})
.collect();
FamilyInspectData { node, aliases_node, aliases, typefaces_node, typefaces }
}
/// Adds a new font family alias, optionally with query overrides, to the existing Inspect data.
pub fn add_alias(&mut self, alias: &str, overrides: &Option<Arc<TypefaceQueryOverrides>>) {
let alias_data = AliasInspectData::new(&self.aliases_node, alias, overrides);
self.aliases.push(alias_data);
}
}
/// Inspect data for a single font family alias.
#[derive(Debug)]
pub struct AliasInspectData {
node: finspect::Node,
style_overrides: Option<finspect::Node>,
language_overrides: Option<finspect::StringProperty>,
}
impl AliasInspectData {
/// Creates a new `AliasInspectData`.
fn new(
parent_inspect_node: &finspect::Node,
alias: &str,
overrides: &Option<Arc<TypefaceQueryOverrides>>,
) -> Self {
let node = parent_inspect_node.create_child(alias);
let style_overrides = overrides.as_ref().and_then(|overrides| {
if overrides.has_style_overrides() {
let style = node.create_child("style_overrides");
if let Some(slant) = overrides.style.slant {
style.record_string("slant", format!("{:?}", slant).to_kebab_case());
}
if let Some(weight) = overrides.style.weight {
style.record_uint("weight", weight.into());
}
if let Some(width) = overrides.style.width {
style.record_string("width", format!("{:?}", width).to_kebab_case());
}
Some(style)
} else {
None
}
});
let language_overrides = overrides.as_ref().and_then(|overrides| {
if overrides.has_language_overrides() {
Some(
node.create_string("language_overrides", overrides.languages.iter().join(", ")),
)
} else {
None
}
});
AliasInspectData { node, style_overrides, language_overrides }
}
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::Error,
char_collection::char_collect,
fidl_fuchsia_fonts::{Slant, Width},
finspect::assert_inspect_tree,
maplit::{btreemap, btreeset},
pretty_assertions::assert_eq,
};
#[test]
fn test_aliases_from_family() -> Result<(), Error> {
let family = v2::Family {
name: "Family A".to_string(),
aliases: vec![
v2::FontFamilyAliasSet::without_overrides(vec!["A Family", "Family Ay"])?,
v2::FontFamilyAliasSet::new(
vec!["Family A Condensed", "Family A Squished"],
StyleOptions { width: Some(Width::Condensed), ..Default::default() },
std::iter::empty::<String>(),
)?,
],
generic_family: None,
assets: vec![],
};
let expected = btreemap! {
UniCase::new("FamilyA".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
UniCase::new("A Family".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
UniCase::new("AFamily".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
UniCase::new("Family A Condensed".to_string()) =>
FamilyOrAlias::Alias(
UniCase::new("Family A".to_string()),
Some(Arc::new(
TypefaceQueryOverrides {
style: StyleOptions {
width: Some(Width::Condensed),
..Default::default()
},
languages: vec![]
}
))
),
UniCase::new("FamilyACondensed".to_string()) =>
FamilyOrAlias::Alias(
UniCase::new("Family A".to_string()),
Some(Arc::new(
TypefaceQueryOverrides {
style: StyleOptions {
width: Some(Width::Condensed),
..Default::default()
},
languages: vec![]
}
))
),
UniCase::new("Family A Squished".to_string()) =>
FamilyOrAlias::Alias(
UniCase::new("Family A".to_string()),
Some(Arc::new(
TypefaceQueryOverrides {
style: StyleOptions {
width: Some(Width::Condensed),
..Default::default()
},
languages: vec![]
}
))
),
UniCase::new("FamilyASquished".to_string()) =>
FamilyOrAlias::Alias(
UniCase::new("Family A".to_string()),
Some(Arc::new(
TypefaceQueryOverrides {
style: StyleOptions {
width: Some(Width::Condensed),
..Default::default()
},
languages: vec![]
}
))
),
UniCase::new("Family Ay".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
UniCase::new("FamilyAy".to_string()) =>
FamilyOrAlias::Alias(UniCase::new("Family A".to_string()), None),
};
let actual: BTreeMap<UniCase<String>, FamilyOrAlias> =
FamilyOrAliasBuilder::aliases_from_family(&family)
.into_iter()
.map(|(key, val)| (key, val.build()))
.collect();
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn test_alias_inspect_data() {
let alias = "Alif";
let overrides = Some(Arc::new(TypefaceQueryOverrides {
style: StyleOptions {
slant: None,
weight: Some(600),
width: Some(Width::ExtraExpanded),
},
languages: vec!["ar-EG".to_string(), "ar-MA".to_string()],
}));
let inspector = finspect::Inspector::new();
let _inspect_data = AliasInspectData::new(inspector.root(), alias, &overrides);
assert_inspect_tree!(inspector, root: {
Alif: {
style_overrides: {
weight: 600u64,
width: "extra-expanded"
},
language_overrides: "ar-EG, ar-MA"
}
});
}
#[test]
fn test_family_inspect_data() {
let mut family = FontFamilyBuilder::new("Alpha", Some(GenericFontFamily::Cursive));
family.add_typeface_once(Arc::new(Typeface {
asset_id: AssetId(0),
font_index: 0,
slant: Slant::Upright,
weight: 400,
width: Width::Normal,
languages: btreeset!("ar-EG".to_string(), "ar-MA".to_string()),
char_set: char_collect!(0x0..=0xEE).into(),
generic_family: Some(GenericFontFamily::Cursive),
}));
family.add_typeface_once(Arc::new(Typeface {
asset_id: AssetId(1),
font_index: 0,
slant: Slant::Upright,
weight: 400,
width: Width::UltraCondensed,
languages: btreeset!("und-Latn".to_string()),
char_set: char_collect!(0x0..=0xEE).into(),
generic_family: Some(GenericFontFamily::Cursive),
}));
let family = family.build();
let asset_location_lookup = |asset_id| match asset_id {
AssetId(0) => Some("/path/to/asset-0.ttf".to_string()),
AssetId(1) => Some("/path/to/asset-1.ttf".to_string()),
_ => unreachable!(),
};
let inspector = finspect::Inspector::new();
let mut inspect_data =
FamilyInspectData::new(inspector.root(), &family, &asset_location_lookup);
inspect_data.add_alias(
"Alif",
&Some(Arc::new(TypefaceQueryOverrides {
style: StyleOptions::default(),
languages: vec!["ar-EG".to_string(), "ar-JO".to_string()],
})),
);
inspect_data.add_alias("Ay", &None);
// The contents of `AliasInspectData` and `TypefaceInspectData` are tested in their
// respective unit tests. This test just asserts the overall structure.
assert_inspect_tree!(inspector, root: {
Alpha: {
aliases: {
Ay: contains {},
Alif: contains {},
},
typefaces: {
"0": contains {
asset_location: "/path/to/asset-0.ttf",
},
"1": contains {
asset_location: "/path/to/asset-1.ttf",
},
}
}
});
}
}