1use std::fmt::{self, Debug, Display, Formatter};
2
3use ecow::EcoString;
4use serde::{Deserialize, Serialize};
5
6use crate::foundations::{Cast, IntoValue, Repr, cast};
7use crate::layout::Ratio;
8use crate::text::AxisValue;
9
10#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
12#[derive(Serialize, Deserialize)]
13pub struct FontVariant {
14 pub style: FontStyle,
16 pub weight: FontWeight,
18 pub stretch: FontStretch,
20}
21
22impl FontVariant {
23 pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self {
25 Self { style, weight, stretch }
26 }
27}
28
29impl Debug for FontVariant {
30 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
31 write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch)
32 }
33}
34
35#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
37#[derive(Cast, Serialize, Deserialize)]
38#[serde(rename_all = "kebab-case")]
39pub enum FontStyle {
40 #[default]
42 Normal,
43 Italic,
45 Oblique,
47}
48
49impl FontStyle {
50 pub fn distance(self, other: Self) -> u16 {
52 if self == other {
53 0
55 } else if self != Self::Normal && other != Self::Normal {
56 1
58 } else {
59 2
61 }
62 }
63}
64
65impl From<usvg::FontStyle> for FontStyle {
66 fn from(style: usvg::FontStyle) -> Self {
67 match style {
68 usvg::FontStyle::Normal => Self::Normal,
69 usvg::FontStyle::Italic => Self::Italic,
70 usvg::FontStyle::Oblique => Self::Oblique,
71 }
72 }
73}
74
75#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
77#[derive(Serialize, Deserialize)]
78#[serde(transparent)]
79pub struct FontWeight(pub(super) u16);
80
81impl FontWeight {
84 pub const THIN: Self = Self(100);
86
87 pub const EXTRALIGHT: Self = Self(200);
89
90 pub const LIGHT: Self = Self(300);
92
93 pub const REGULAR: Self = Self(400);
95
96 pub const MEDIUM: Self = Self(500);
98
99 pub const SEMIBOLD: Self = Self(600);
101
102 pub const BOLD: Self = Self(700);
104
105 pub const EXTRABOLD: Self = Self(800);
107
108 pub const BLACK: Self = Self(900);
110
111 pub fn from_number(weight: u16) -> Self {
114 Self(weight.clamp(100, 900))
115 }
116
117 pub fn from_wght(value: AxisValue) -> Self {
119 Self::from_number(value.0 as u16)
120 }
121
122 pub fn to_number(self) -> u16 {
124 self.0
125 }
126
127 pub fn to_wght(self) -> AxisValue {
129 AxisValue(self.to_number() as f32)
130 }
131
132 pub fn thicken(self, delta: i16) -> Self {
134 Self((self.0 as i16).saturating_add(delta).clamp(100, 900) as u16)
135 }
136
137 pub fn distance(self, other: Self) -> u16 {
139 (self.0 as i16 - other.0 as i16).unsigned_abs()
140 }
141}
142
143impl Default for FontWeight {
144 fn default() -> Self {
145 Self::REGULAR
146 }
147}
148
149impl Display for FontWeight {
150 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
151 write!(f, "{}", self.0)
152 }
153}
154
155impl From<fontdb::Weight> for FontWeight {
156 fn from(weight: fontdb::Weight) -> Self {
157 Self::from_number(weight.0)
158 }
159}
160
161cast! {
162 FontWeight,
163 self => IntoValue::into_value(match self {
164 FontWeight::THIN => "thin",
165 FontWeight::EXTRALIGHT => "extralight",
166 FontWeight::LIGHT => "light",
167 FontWeight::REGULAR => "regular",
168 FontWeight::MEDIUM => "medium",
169 FontWeight::SEMIBOLD => "semibold",
170 FontWeight::BOLD => "bold",
171 FontWeight::EXTRABOLD => "extrabold",
172 FontWeight::BLACK => "black",
173 _ => return self.to_number().into_value(),
174 }),
175 v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16),
176 "thin" => Self::THIN,
178 "extralight" => Self::EXTRALIGHT,
180 "light" => Self::LIGHT,
182 "regular" => Self::REGULAR,
184 "medium" => Self::MEDIUM,
186 "semibold" => Self::SEMIBOLD,
188 "bold" => Self::BOLD,
190 "extrabold" => Self::EXTRABOLD,
192 "black" => Self::BLACK,
194}
195
196#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
198#[derive(Serialize, Deserialize)]
199#[serde(transparent)]
200pub struct FontStretch(pub(super) u16);
201
202impl FontStretch {
203 pub const ULTRA_CONDENSED: Self = Self(500);
205
206 pub const EXTRA_CONDENSED: Self = Self(625);
208
209 pub const CONDENSED: Self = Self(750);
211
212 pub const SEMI_CONDENSED: Self = Self(875);
214
215 pub const NORMAL: Self = Self(1000);
217
218 pub const SEMI_EXPANDED: Self = Self(1125);
220
221 pub const EXPANDED: Self = Self(1250);
223
224 pub const EXTRA_EXPANDED: Self = Self(1500);
226
227 pub const ULTRA_EXPANDED: Self = Self(2000);
229
230 pub fn from_ratio(ratio: Ratio) -> Self {
233 Self((ratio.get().clamp(0.5, 2.0) * 1000.0) as u16)
234 }
235
236 pub fn from_number(stretch: u16) -> Self {
239 match stretch {
240 0 | 1 => Self::ULTRA_CONDENSED,
241 2 => Self::EXTRA_CONDENSED,
242 3 => Self::CONDENSED,
243 4 => Self::SEMI_CONDENSED,
244 5 => Self::NORMAL,
245 6 => Self::SEMI_EXPANDED,
246 7 => Self::EXPANDED,
247 8 => Self::EXTRA_EXPANDED,
248 _ => Self::ULTRA_EXPANDED,
249 }
250 }
251
252 pub fn from_wdth(value: AxisValue) -> Self {
254 Self::from_ratio(Ratio::new(value.0 as f64 / 100.0))
255 }
256
257 pub fn to_ratio(self) -> Ratio {
259 Ratio::new(self.0 as f64 / 1000.0)
260 }
261
262 pub fn to_wdth(self) -> AxisValue {
264 AxisValue(self.to_ratio().get() as f32 * 100.0)
265 }
266
267 pub fn round(self) -> Self {
269 match self.0 {
270 ..=562 => Self::ULTRA_CONDENSED,
271 563..=687 => Self::EXTRA_CONDENSED,
272 688..=812 => Self::CONDENSED,
273 813..=937 => Self::SEMI_CONDENSED,
274 938..=1062 => Self::NORMAL,
275 1063..=1187 => Self::SEMI_EXPANDED,
276 1188..=1374 => Self::EXPANDED,
277 1375..=1749 => Self::EXTRA_EXPANDED,
278 1750.. => Self::ULTRA_EXPANDED,
279 }
280 }
281
282 pub fn distance(self, other: Self) -> Ratio {
284 (self.to_ratio() - other.to_ratio()).abs()
285 }
286}
287
288impl Default for FontStretch {
289 fn default() -> Self {
290 Self::NORMAL
291 }
292}
293
294impl Repr for FontStretch {
295 fn repr(&self) -> EcoString {
296 self.to_ratio().repr()
297 }
298}
299
300impl Display for FontStretch {
301 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
302 let int_part = self.0 / 10;
303 let dec_part = self.0 % 10;
304 if dec_part == 0 {
305 write!(f, "{int_part}%")
306 } else {
307 write!(f, "{int_part}.{dec_part}%")
308 }
309 }
310}
311
312impl From<usvg::FontStretch> for FontStretch {
313 fn from(stretch: usvg::FontStretch) -> Self {
314 match stretch {
315 usvg::FontStretch::UltraCondensed => Self::ULTRA_CONDENSED,
316 usvg::FontStretch::ExtraCondensed => Self::EXTRA_CONDENSED,
317 usvg::FontStretch::Condensed => Self::CONDENSED,
318 usvg::FontStretch::SemiCondensed => Self::SEMI_CONDENSED,
319 usvg::FontStretch::Normal => Self::NORMAL,
320 usvg::FontStretch::SemiExpanded => Self::SEMI_EXPANDED,
321 usvg::FontStretch::Expanded => Self::EXPANDED,
322 usvg::FontStretch::ExtraExpanded => Self::EXTRA_EXPANDED,
323 usvg::FontStretch::UltraExpanded => Self::ULTRA_EXPANDED,
324 }
325 }
326}
327
328cast! {
329 FontStretch,
330 self => self.to_ratio().into_value(),
331 v: Ratio => Self::from_ratio(v),
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_font_weight_distance() {
340 let d = |a, b| FontWeight(a).distance(FontWeight(b));
341 assert_eq!(d(500, 200), 300);
342 assert_eq!(d(500, 500), 0);
343 assert_eq!(d(500, 900), 400);
344 assert_eq!(d(10, 100), 90);
345 }
346
347 #[test]
348 fn test_font_stretch_debug() {
349 assert_eq!(FontStretch::EXPANDED.repr(), "125%")
350 }
351
352 #[test]
353 fn text_font_stretch_fmt() {
354 assert_eq!(format!("{}", FontStretch(0)), "0%");
355 assert_eq!(format!("{}", FontStretch(1)), "0.1%");
356 assert_eq!(format!("{}", FontStretch(10)), "1%");
357 assert_eq!(format!("{}", FontStretch(100)), "10%");
358 assert_eq!(format!("{}", FontStretch(666)), "66.6%");
359 assert_eq!(format!("{}", FontStretch(1000)), "100%");
360 assert_eq!(format!("{}", FontStretch(1120)), "112%");
361 assert_eq!(format!("{}", FontStretch(u16::MAX)), "6553.5%");
362 }
363}