1use topcoat_core::fnv1a::Fnv1a;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct FontWeight(u16);
13
14impl FontWeight {
15 pub const THIN: Self = Self(100);
17 pub const EXTRA_LIGHT: Self = Self(200);
19 pub const LIGHT: Self = Self(300);
21 pub const NORMAL: Self = Self(400);
23 pub const MEDIUM: Self = Self(500);
25 pub const SEMI_BOLD: Self = Self(600);
27 pub const BOLD: Self = Self(700);
29 pub const EXTRA_BOLD: Self = Self(800);
31 pub const BLACK: Self = Self(900);
33
34 #[must_use]
41 #[track_caller]
42 pub const fn new(weight: u16) -> Self {
43 assert!(
44 weight >= 100 && weight <= 900,
45 "font weight out of range 100..=900"
46 );
47 Self(weight)
48 }
49
50 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
52 h.write(&self.0.to_le_bytes())
53 }
54}
55
56impl Default for FontWeight {
57 fn default() -> Self {
58 Self::NORMAL
59 }
60}
61
62impl From<FontWeight> for u16 {
63 fn from(value: FontWeight) -> Self {
64 value.0
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct FontWeightOutOfRangeError;
72
73impl std::fmt::Display for FontWeightOutOfRangeError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str("font weight out of range 100..=900")
76 }
77}
78
79impl std::error::Error for FontWeightOutOfRangeError {}
80
81impl TryFrom<u16> for FontWeight {
82 type Error = FontWeightOutOfRangeError;
83
84 fn try_from(value: u16) -> Result<Self, Self::Error> {
85 if !(100..=900).contains(&value) {
86 return Err(FontWeightOutOfRangeError);
87 }
88 Ok(Self(value))
89 }
90}
91
92impl std::fmt::Display for FontWeight {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 self.0.fmt(f)
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub struct FontWeightRange {
104 start: FontWeight,
105 end: FontWeight,
106}
107
108impl FontWeightRange {
109 #[must_use]
115 #[track_caller]
116 pub const fn new(start: FontWeight, end: FontWeight) -> Self {
117 assert!(end.0 >= start.0, "font weight range must not be empty");
118 Self { start, end }
119 }
120
121 #[must_use]
128 #[track_caller]
129 pub const fn from_u16(start: u16, end: u16) -> Self {
130 Self::new(FontWeight::new(start), FontWeight::new(end))
131 }
132
133 #[must_use]
135 pub const fn start(&self) -> FontWeight {
136 self.start
137 }
138
139 #[must_use]
141 pub const fn end(&self) -> FontWeight {
142 self.end
143 }
144
145 pub(crate) const fn hash(self, h: Fnv1a<u64>) -> Fnv1a<u64> {
147 self.end.hash(self.start.hash(h))
148 }
149}
150
151impl std::fmt::Display for FontWeightRange {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 if self.start == self.end {
154 self.start.fmt(f)
155 } else {
156 write!(f, "{} {}", self.start, self.end)
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn w(value: u16) -> FontWeight {
166 FontWeight::new(value)
167 }
168
169 #[test]
170 fn weight_displays_as_its_number() {
171 assert_eq!(w(100).to_string(), "100");
172 assert_eq!(w(900).to_string(), "900");
173 }
174
175 #[test]
176 fn weight_converts_to_u16() {
177 assert_eq!(u16::from(w(700)), 700);
178 }
179
180 #[test]
181 fn default_is_normal() {
182 assert_eq!(FontWeight::default(), FontWeight::NORMAL);
183 }
184
185 #[test]
186 fn named_constants_match_their_values() {
187 assert_eq!(FontWeight::THIN, w(100));
188 assert_eq!(FontWeight::EXTRA_LIGHT, w(200));
189 assert_eq!(FontWeight::LIGHT, w(300));
190 assert_eq!(FontWeight::NORMAL, w(400));
191 assert_eq!(FontWeight::MEDIUM, w(500));
192 assert_eq!(FontWeight::SEMI_BOLD, w(600));
193 assert_eq!(FontWeight::BOLD, w(700));
194 assert_eq!(FontWeight::EXTRA_BOLD, w(800));
195 assert_eq!(FontWeight::BLACK, w(900));
196 }
197
198 #[test]
199 fn try_from_accepts_the_bounds() {
200 assert_eq!(FontWeight::try_from(100), Ok(w(100)));
201 assert_eq!(FontWeight::try_from(900), Ok(w(900)));
202 }
203
204 #[test]
205 fn try_from_rejects_out_of_range() {
206 assert_eq!(FontWeight::try_from(99), Err(FontWeightOutOfRangeError));
207 assert_eq!(FontWeight::try_from(901), Err(FontWeightOutOfRangeError));
208 }
209
210 #[test]
211 #[should_panic = "out of range"]
212 fn new_panics_below_the_minimum() {
213 let _ = FontWeight::new(99);
214 }
215
216 #[test]
217 #[should_panic = "out of range"]
218 fn new_panics_above_the_maximum() {
219 let _ = FontWeight::new(901);
220 }
221
222 #[test]
223 fn single_weight_range_displays_one_number() {
224 assert_eq!(FontWeightRange::new(w(400), w(400)).to_string(), "400");
225 }
226
227 #[test]
228 fn multi_weight_range_displays_both_numbers() {
229 assert_eq!(FontWeightRange::new(w(400), w(700)).to_string(), "400 700");
230 }
231
232 #[test]
233 #[should_panic = "empty"]
234 fn range_panics_when_end_precedes_start() {
235 let _ = FontWeightRange::new(w(700), w(400));
236 }
237
238 #[test]
239 fn range_from_u16_matches_new() {
240 assert_eq!(
241 FontWeightRange::from_u16(400, 700),
242 FontWeightRange::new(w(400), w(700)),
243 );
244 }
245
246 #[test]
247 #[should_panic = "out of range"]
248 fn range_from_u16_panics_on_out_of_range() {
249 let _ = FontWeightRange::from_u16(100, 901);
250 }
251}