Skip to main content

ferroui_base/media/
typeface.rs

1use std::hash::{Hash, Hasher};
2use std::rc::Rc;
3
4use crate::media::{FontFamily, FontManager, FontStretch, FontStyle, FontWeight, GlyphTypeface};
5use crate::utilities::SpanStringTokenizer;
6
7/// Represents a typeface: a font family with a style, weight and stretch.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct Typeface {
10    font_family: FontFamily,
11    style: FontStyle,
12    weight: FontWeight,
13    stretch: FontStretch,
14}
15
16impl Typeface {
17    /// Creates a normal typeface of a font family.
18    pub fn new(font_family: FontFamily) -> Self {
19        Self::with_style(font_family, FontStyle::Normal, FontWeight::Normal, FontStretch::Normal)
20    }
21
22    /// Creates a typeface.
23    ///
24    /// Panics when `weight` is not positive.
25    pub fn with_style(font_family: FontFamily, style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
26        if weight.value() <= 0 {
27            panic!("Font weight must be > 0.");
28        }
29
30        if (stretch as i32) < 1 {
31            panic!("Font stretch must be > 1.");
32        }
33
34        Self { font_family, style, weight, stretch }
35    }
36
37    /// Creates a normal typeface from a font family name; an empty name gives
38    /// the default font family.
39    pub fn from_name(font_family_name: &str) -> Self {
40        Self::from_name_with_style(font_family_name, FontStyle::Normal, FontWeight::Normal, FontStretch::Normal)
41    }
42
43    /// Creates a typeface from a font family name; an empty name gives the
44    /// default font family.
45    pub fn from_name_with_style(
46        font_family_name: &str,
47        style: FontStyle,
48        weight: FontWeight,
49        stretch: FontStretch,
50    ) -> Self {
51        let font_family = if font_family_name.is_empty() {
52            FontFamily::default_family()
53        } else {
54            FontFamily::new(font_family_name)
55        };
56
57        Self::with_style(font_family, style, weight, stretch)
58    }
59
60    /// The default typeface: the default font family, normal style.
61    pub fn default_typeface() -> Typeface {
62        Typeface::new(FontFamily::default_family())
63    }
64
65    /// Gets the font family.
66    pub fn font_family(&self) -> &FontFamily {
67        &self.font_family
68    }
69
70    /// Gets the font style.
71    pub fn style(&self) -> FontStyle {
72        self.style
73    }
74
75    /// Gets the font weight.
76    pub fn weight(&self) -> FontWeight {
77        self.weight
78    }
79
80    /// Gets the font stretch.
81    pub fn stretch(&self) -> FontStretch {
82        self.stretch
83    }
84
85    /// Gets the glyph typeface.
86    ///
87    /// Panics when the font manager cannot create one (C# throws
88    /// `InvalidOperationException`); use
89    /// [`FontManager::try_get_glyph_typeface`] to handle that case.
90    pub fn glyph_typeface(&self) -> Rc<GlyphTypeface> {
91        if let Some(glyph_typeface) = FontManager::current().try_get_glyph_typeface(self) {
92            return glyph_typeface;
93        }
94
95        panic!(
96            "Could not create glyphTypeface. Font family: {} (key: {}). Style: {}. Weight: {}. Stretch: {}",
97            self.font_family.name(),
98            self.font_family.key().map(|key| key.to_string()).unwrap_or_default(),
99            self.style,
100            self.weight,
101            self.stretch
102        );
103    }
104
105    /// Splits style, weight and stretch words (e.g. "Bold", "Italic") out of
106    /// the primary family name. Returns the typeface with the matched values
107    /// applied and the family name without the matched words.
108    pub fn normalize(&self) -> (Typeface, String) {
109        let normalized_family_name = self.font_family.family_names().primary_family_name();
110
111        // Return early if no separator is present.
112        if !normalized_family_name.contains(' ') {
113            return (self.clone(), normalized_family_name.to_owned());
114        }
115
116        let mut style = self.style;
117        let mut weight = self.weight;
118        let mut stretch = self.stretch;
119
120        let mut normalized_family_name_builder: Option<String> = None;
121        let mut total_chars_removed = 0;
122
123        let mut tokenizer = SpanStringTokenizer::with_separator(normalized_family_name, ' ', None);
124
125        // Skip initial family name.
126        let _ = tokenizer.read_span();
127
128        // A malformed name ends the scan (upstream lets the format exception escape).
129        while let Ok(Some(token)) = tokenizer.try_read_span() {
130            // Don't try to match numbers.
131            if matches!(SpanStringTokenizer::new(token).try_read_int32(), Ok(Some(_))) {
132                continue;
133            }
134
135            // Try match with font style, weight or stretch and update accordingly.
136            let mut matched = false;
137            if let Some(new_style) = FontStyle::try_parse_ignore_case(token) {
138                style = new_style;
139                matched = true;
140            } else if let Some(new_weight) = FontWeight::try_parse_ignore_case(token) {
141                weight = new_weight;
142                matched = true;
143            } else if let Some(new_stretch) = FontStretch::try_parse_ignore_case(token) {
144                stretch = new_stretch;
145                matched = true;
146            }
147
148            if matched {
149                // Carve out matched word from the normalized name.
150                let builder = normalized_family_name_builder.get_or_insert_with(|| normalized_family_name.to_owned());
151                let start = tokenizer.current_token_index().unwrap_or(0) - total_chars_removed;
152                builder.replace_range(start..start + token.len(), "");
153                total_chars_removed += token.len();
154            }
155        }
156
157        // Get rid of any trailing spaces.
158        let normalized = normalized_family_name_builder
159            .as_deref()
160            .unwrap_or(normalized_family_name)
161            .trim_end()
162            .to_owned();
163
164        // Preserve old font source
165        (Typeface::with_style(self.font_family.clone(), style, weight, stretch), normalized)
166    }
167}
168
169impl Default for Typeface {
170    fn default() -> Self {
171        Typeface::default_typeface()
172    }
173}
174
175impl Hash for Typeface {
176    fn hash<H: Hasher>(&self, state: &mut H) {
177        self.font_family.hash(state);
178        (self.style as i32).hash(state);
179        self.weight.value().hash(state);
180        (self.stretch as i32).hash(state);
181    }
182}
183
184impl From<FontFamily> for Typeface {
185    fn from(font_family: FontFamily) -> Self {
186        Typeface::new(font_family)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use std::collections::hash_map::DefaultHasher;
193
194    use super::*;
195
196    #[test]
197    #[should_panic(expected = "Font weight must be > 0.")]
198    fn exception_should_be_thrown_if_font_weight_less_than_equal_to_zero() {
199        Typeface::from_name_with_style("foo", FontStyle::Normal, FontWeight(0), FontStretch::Normal);
200    }
201
202    #[test]
203    fn should_be_equal() {
204        assert_eq!(Typeface::from_name("Font A"), Typeface::from_name("Font A"));
205    }
206
207    #[test]
208    fn should_have_equal_hash() {
209        let hash_of = |typeface: &Typeface| {
210            let mut hasher = DefaultHasher::new();
211            typeface.hash(&mut hasher);
212            hasher.finish()
213        };
214
215        assert_eq!(hash_of(&Typeface::from_name("Font A")), hash_of(&Typeface::from_name("Font A")));
216    }
217
218    #[test]
219    fn should_get_implicit_typeface() {
220        for (input, family_name, style, weight) in [
221            ("Hello World 6", "Hello World 6", FontStyle::Normal, FontWeight::Normal),
222            ("Hello World Italic", "Hello World", FontStyle::Italic, FontWeight::Normal),
223            ("Hello World Italic Bold", "Hello World", FontStyle::Italic, FontWeight::Bold),
224            ("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle::Normal, FontWeight::Normal),
225            ("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle::Normal, FontWeight::Solid),
226            ("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle::Normal, FontWeight::Normal),
227        ] {
228            let typeface = Typeface::from_name(input);
229
230            let (normalized_typeface, normalized_family_name) = typeface.normalize();
231
232            assert_eq!(normalized_family_name, family_name, "{input}");
233            assert_eq!(normalized_typeface.style(), style, "{input}");
234            assert_eq!(normalized_typeface.weight(), weight, "{input}");
235            assert_eq!(normalized_typeface.stretch(), FontStretch::Normal, "{input}");
236        }
237    }
238
239    #[test]
240    fn an_empty_name_gives_the_default_font_family() {
241        assert_eq!(Typeface::from_name("").font_family(), &FontFamily::default_family());
242        assert_eq!(Typeface::default(), Typeface::new(FontFamily::default_family()));
243    }
244}