1#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2pub struct FontFile {
3 pub path: String,
4 pub weight: FontWeight,
5 pub style: FontStyle,
6}
7
8impl FontFile {
9 pub fn new(path: impl Into<String>) -> Self {
10 Self {
11 path: path.into(),
12 weight: FontWeight::NORMAL,
13 style: FontStyle::Normal,
14 }
15 }
16
17 pub fn with_weight(mut self, weight: FontWeight) -> Self {
18 self.weight = weight;
19 self
20 }
21
22 pub fn with_style(mut self, style: FontStyle) -> Self {
23 self.style = style;
24 self
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub struct FileBackedFontFamily {
30 pub fonts: Vec<FontFile>,
31}
32
33impl FileBackedFontFamily {
34 pub fn new(fonts: Vec<FontFile>) -> Self {
35 assert!(
36 !fonts.is_empty(),
37 "FileBackedFontFamily requires at least one font file"
38 );
39 Self { fonts }
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44pub struct LoadedTypefacePath {
45 pub path: String,
46}
47
48impl LoadedTypefacePath {
49 pub fn new(path: impl Into<String>) -> Self {
50 Self { path: path.into() }
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
55pub enum FontFamily {
56 Default,
57 SansSerif,
58 Serif,
59 Monospace,
60 Cursive,
61 Fantasy,
62 Named(String),
63 FileBacked(FileBackedFontFamily),
64 LoadedTypeface(LoadedTypefacePath),
65}
66
67impl FontFamily {
68 pub fn named(name: impl Into<String>) -> Self {
69 Self::from_name(&name.into())
70 }
71
72 pub fn from_name(name: &str) -> Self {
73 match name {
74 "" | "Default" | "default" => Self::Default,
75 "SansSerif" | "sans-serif" => Self::SansSerif,
76 "Serif" | "serif" => Self::Serif,
77 "Monospace" | "monospace" => Self::Monospace,
78 "Cursive" | "cursive" => Self::Cursive,
79 "Fantasy" | "fantasy" => Self::Fantasy,
80 value => Self::Named(value.to_string()),
81 }
82 }
83
84 pub fn file_backed(fonts: Vec<FontFile>) -> Self {
85 Self::FileBacked(FileBackedFontFamily::new(fonts))
86 }
87
88 pub fn loaded_typeface_path(path: impl Into<String>) -> Self {
89 Self::LoadedTypeface(LoadedTypefacePath::new(path))
90 }
91
92 pub fn family_name(&self) -> Option<&str> {
93 match self {
94 Self::Named(name) => Some(name.as_str()),
95 _ => None,
96 }
97 }
98}
99
100impl Default for FontFamily {
101 fn default() -> Self {
102 Self::Default
103 }
104}
105
106impl From<&str> for FontFamily {
107 fn from(value: &str) -> Self {
108 Self::from_name(value)
109 }
110}
111
112impl From<String> for FontFamily {
113 fn from(value: String) -> Self {
114 Self::from_name(value.as_str())
115 }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
119pub struct FontWeight(pub u16);
120
121impl FontWeight {
122 pub fn new(weight: u16) -> Self {
123 match Self::try_new(weight) {
124 Some(value) => value,
125 None => panic!("Font weight must be in range [1, 1000], got {weight}"),
126 }
127 }
128
129 pub const fn try_new(weight: u16) -> Option<Self> {
130 if weight >= 1 && weight <= 1000 {
131 Some(Self(weight))
132 } else {
133 None
134 }
135 }
136
137 pub const fn value(self) -> u16 {
138 self.0
139 }
140
141 pub const THIN: Self = Self(100);
142 pub const EXTRA_LIGHT: Self = Self(200);
143 pub const LIGHT: Self = Self(300);
144 pub const NORMAL: Self = Self(400);
145 pub const MEDIUM: Self = Self(500);
146 pub const SEMI_BOLD: Self = Self(600);
147 pub const BOLD: Self = Self(700);
148 pub const EXTRA_BOLD: Self = Self(800);
149 pub const BLACK: Self = Self(900);
150 pub const W100: Self = Self(100);
151 pub const W200: Self = Self(200);
152 pub const W300: Self = Self(300);
153 pub const W400: Self = Self(400);
154 pub const W500: Self = Self(500);
155 pub const W600: Self = Self(600);
156 pub const W700: Self = Self(700);
157 pub const W800: Self = Self(800);
158 pub const W900: Self = Self(900);
159}
160
161impl Default for FontWeight {
162 fn default() -> Self {
163 Self::NORMAL
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
168pub enum FontStyle {
169 #[default]
170 Normal,
171 Italic,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
175pub enum FontSynthesis {
176 #[default]
177 None,
178 All,
179 Weight,
180 Style,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn font_family_maps_compose_generic_names() {
189 assert_eq!(FontFamily::from_name("Default"), FontFamily::Default);
190 assert_eq!(FontFamily::from_name("sans-serif"), FontFamily::SansSerif);
191 assert_eq!(FontFamily::from_name("serif"), FontFamily::Serif);
192 assert_eq!(FontFamily::from_name("monospace"), FontFamily::Monospace);
193 assert_eq!(FontFamily::from_name("cursive"), FontFamily::Cursive);
194 assert_eq!(FontFamily::from_name("fantasy"), FontFamily::Fantasy);
195 }
196
197 #[test]
198 fn font_family_preserves_custom_names() {
199 let family = FontFamily::named("Fira Sans");
200 assert_eq!(family, FontFamily::Named("Fira Sans".to_string()));
201 assert_eq!(family.family_name(), Some("Fira Sans"));
202 }
203
204 #[test]
205 fn font_family_file_backed_preserves_font_entries() {
206 let family = FontFamily::file_backed(vec![
207 FontFile::new("/tmp/Roboto-Regular.ttf"),
208 FontFile::new("/tmp/Roboto-Bold.ttf").with_weight(FontWeight::BOLD),
209 ]);
210 let FontFamily::FileBacked(file_backed) = family else {
211 panic!("expected file-backed family");
212 };
213 assert_eq!(file_backed.fonts.len(), 2);
214 assert_eq!(file_backed.fonts[0].path, "/tmp/Roboto-Regular.ttf");
215 assert_eq!(file_backed.fonts[0].weight, FontWeight::NORMAL);
216 assert_eq!(file_backed.fonts[1].path, "/tmp/Roboto-Bold.ttf");
217 assert_eq!(file_backed.fonts[1].weight, FontWeight::BOLD);
218 }
219
220 #[test]
221 fn font_file_builder_applies_style_and_weight() {
222 let file = FontFile::new("/tmp/Roboto-Italic.ttf")
223 .with_weight(FontWeight::MEDIUM)
224 .with_style(FontStyle::Italic);
225 assert_eq!(file.path, "/tmp/Roboto-Italic.ttf");
226 assert_eq!(file.weight, FontWeight::MEDIUM);
227 assert_eq!(file.style, FontStyle::Italic);
228 }
229
230 #[test]
231 #[should_panic(expected = "requires at least one font file")]
232 fn file_backed_font_family_rejects_empty_font_list() {
233 let _ = FileBackedFontFamily::new(Vec::new());
234 }
235
236 #[test]
237 fn font_family_loaded_typeface_path_preserves_path() {
238 let family = FontFamily::loaded_typeface_path("/tmp/FiraSans-Regular.ttf");
239 let FontFamily::LoadedTypeface(typeface) = family else {
240 panic!("expected loaded typeface family");
241 };
242 assert_eq!(typeface.path, "/tmp/FiraSans-Regular.ttf");
243 }
244
245 #[test]
246 fn font_weight_default_is_normal() {
247 assert_eq!(FontWeight::default(), FontWeight::NORMAL);
248 }
249
250 #[test]
251 fn font_weight_try_new_validates_range() {
252 assert_eq!(FontWeight::try_new(0), None);
253 assert_eq!(FontWeight::try_new(1), Some(FontWeight(1)));
254 assert_eq!(FontWeight::try_new(1000), Some(FontWeight(1000)));
255 assert_eq!(FontWeight::try_new(1001), None);
256 }
257}