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#[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 pub fn new(font_family: FontFamily) -> Self {
19 Self::with_style(font_family, FontStyle::Normal, FontWeight::Normal, FontStretch::Normal)
20 }
21
22 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 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 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 pub fn default_typeface() -> Typeface {
62 Typeface::new(FontFamily::default_family())
63 }
64
65 pub fn font_family(&self) -> &FontFamily {
67 &self.font_family
68 }
69
70 pub fn style(&self) -> FontStyle {
72 self.style
73 }
74
75 pub fn weight(&self) -> FontWeight {
77 self.weight
78 }
79
80 pub fn stretch(&self) -> FontStretch {
82 self.stretch
83 }
84
85 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 pub fn normalize(&self) -> (Typeface, String) {
109 let normalized_family_name = self.font_family.family_names().primary_family_name();
110
111 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 let _ = tokenizer.read_span();
127
128 while let Ok(Some(token)) = tokenizer.try_read_span() {
130 if matches!(SpanStringTokenizer::new(token).try_read_int32(), Ok(Some(_))) {
132 continue;
133 }
134
135 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 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 let normalized = normalized_family_name_builder
159 .as_deref()
160 .unwrap_or(normalized_family_name)
161 .trim_end()
162 .to_owned();
163
164 (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}