Skip to main content

kas_text/fonts/
attributes.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5//
6// This file is copied from https://github.com/linebender/parley PR #359.
7// Copyright 2024 the Parley Authors
8
9//! Properties for specifying font weight, width and style.
10
11use core::fmt;
12use easy_cast::Cast;
13
14/// Visual width of a font-- a relative change from the normal aspect
15/// ratio, typically in the 50% - 200% range.
16///
17/// The default value is [`FontWidth::NORMAL`].
18///
19/// In variable fonts, this can be controlled with the `wdth` axis.
20///
21/// In Open Type, the `u16` [`usWidthClass`] field has 9 values, from 1-9,
22/// which doesn't allow for the wide range of values possible with variable
23/// fonts.
24///
25/// See <https://fonts.google.com/knowledge/glossary/width>
26///
27/// In CSS, this corresponds to the [`font-width`] property, formerly known as
28/// [`font-stretch`].
29///
30/// [`usWidthClass`]: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
31/// [`font-width`]: https://www.w3.org/TR/css-fonts-4/#font-width-prop
32/// [`font-stretch`]: https://www.w3.org/TR/css-fonts-4/#font-stretch-prop
33#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
34pub struct FontWidth(u16);
35
36impl FontWidth {
37    /// Width that is 50% of normal.
38    pub const ULTRA_CONDENSED: Self = Self(128);
39
40    /// Width that is 62.5% of normal.
41    pub const EXTRA_CONDENSED: Self = Self(160);
42
43    /// Width that is 75% of normal.
44    pub const CONDENSED: Self = Self(192);
45
46    /// Width that is 87.5% of normal.
47    pub const SEMI_CONDENSED: Self = Self(224);
48
49    /// Width that is 100% of normal. This is the default value.
50    pub const NORMAL: Self = Self(256);
51
52    /// Width that is 112.5% of normal.
53    pub const SEMI_EXPANDED: Self = Self(288);
54
55    /// Width that is 125% of normal.
56    pub const EXPANDED: Self = Self(320);
57
58    /// Width that is 150% of normal.
59    pub const EXTRA_EXPANDED: Self = Self(384);
60
61    /// Width that is 200% of normal.
62    pub const ULTRA_EXPANDED: Self = Self(512);
63}
64
65impl FontWidth {
66    /// Creates a new width attribute with the given ratio.
67    ///
68    /// Panics if the ratio is not between `0` and `255.996`.
69    ///
70    /// This can also be created [from a percentage](Self::from_percentage).
71    ///
72    /// # Example
73    ///
74    /// ```
75    /// # use kas_text::fonts::FontWidth;
76    /// assert_eq!(FontWidth::from_ratio(1.5), FontWidth::EXTRA_EXPANDED);
77    /// ```
78    pub fn from_ratio(ratio: f32) -> Self {
79        let value = (ratio * 256.0).round();
80        assert!(0.0 <= value && value <= (u16::MAX as f32));
81        Self(value as u16)
82    }
83
84    /// Creates a width attribute from a percentage.
85    ///
86    /// Panics if the percentage is not between `0%` and `25599.6%`.
87    ///
88    /// This can also be created [from a ratio](Self::from_ratio).
89    ///
90    /// # Example
91    ///
92    /// ```
93    /// # use kas_text::fonts::FontWidth;
94    /// assert_eq!(FontWidth::from_percentage(87.5), FontWidth::SEMI_CONDENSED);
95    /// ```
96    pub fn from_percentage(percentage: f32) -> Self {
97        Self::from_ratio(percentage / 100.0)
98    }
99
100    /// Returns the width attribute as a ratio.
101    ///
102    /// This is a linear scaling factor with `1.0` being "normal" width.
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// # use kas_text::fonts::FontWidth;
108    /// assert_eq!(FontWidth::NORMAL.ratio(), 1.0);
109    /// ```
110    pub fn ratio(self) -> f32 {
111        (self.0 as f32) / 256.0
112    }
113
114    /// Returns the width attribute as a percentage value.
115    ///
116    /// This is generally the value associated with the `wdth` axis.
117    pub fn percentage(self) -> f32 {
118        self.ratio() * 100.0
119    }
120
121    /// Returns `true` if the width is [normal].
122    ///
123    /// [normal]: FontWidth::NORMAL
124    pub fn is_normal(self) -> bool {
125        self == Self::NORMAL
126    }
127
128    /// Returns `true` if the width is condensed (less than [normal]).
129    ///
130    /// [normal]: FontWidth::NORMAL
131    pub fn is_condensed(self) -> bool {
132        self < Self::NORMAL
133    }
134
135    /// Returns `true` if the width is expanded (greater than [normal]).
136    ///
137    /// [normal]: FontWidth::NORMAL
138    pub fn is_expanded(self) -> bool {
139        self > Self::NORMAL
140    }
141
142    /// Parses the width from a CSS style keyword or a percentage value.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// # use kas_text::fonts::FontWidth;
148    /// assert_eq!(FontWidth::parse("semi-condensed"), Some(FontWidth::SEMI_CONDENSED));
149    /// assert_eq!(FontWidth::parse("80%"), Some(FontWidth::from_percentage(80.0)));
150    /// assert_eq!(FontWidth::parse("wideload"), None);
151    /// ```
152    pub fn parse(s: &str) -> Option<Self> {
153        let s = s.trim();
154        Some(match s {
155            "ultra-condensed" => Self::ULTRA_CONDENSED,
156            "extra-condensed" => Self::EXTRA_CONDENSED,
157            "condensed" => Self::CONDENSED,
158            "semi-condensed" => Self::SEMI_CONDENSED,
159            "normal" => Self::NORMAL,
160            "semi-expanded" => Self::SEMI_EXPANDED,
161            "expanded" => Self::EXPANDED,
162            "extra-expanded" => Self::EXTRA_EXPANDED,
163            "ultra-expanded" => Self::ULTRA_EXPANDED,
164            _ => {
165                if s.ends_with('%') {
166                    let p = s.get(..s.len() - 1)?.parse::<f32>().ok()?;
167                    return Some(Self::from_percentage(p));
168                }
169                return None;
170            }
171        })
172    }
173}
174
175impl fmt::Display for FontWidth {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        let keyword = match *self {
178            v if v == Self::ULTRA_CONDENSED => "ultra-condensed",
179            v if v == Self::EXTRA_CONDENSED => "extra-condensed",
180            v if v == Self::CONDENSED => "condensed",
181            v if v == Self::SEMI_CONDENSED => "semi-condensed",
182            v if v == Self::NORMAL => "normal",
183            v if v == Self::SEMI_EXPANDED => "semi-expanded",
184            v if v == Self::EXPANDED => "expanded",
185            v if v == Self::EXTRA_EXPANDED => "extra-expanded",
186            v if v == Self::ULTRA_EXPANDED => "ultra-expanded",
187            _ => {
188                return write!(f, "{}%", self.percentage());
189            }
190        };
191        write!(f, "{keyword}")
192    }
193}
194
195impl Default for FontWidth {
196    fn default() -> Self {
197        Self::NORMAL
198    }
199}
200
201impl From<FontWidth> for fontique::FontWidth {
202    #[inline]
203    fn from(width: FontWidth) -> Self {
204        fontique::FontWidth::from_ratio(width.ratio())
205    }
206}
207
208/// Visual weight class of a font, typically on a scale from 1 to 1000.
209///
210/// The default value is [`FontWeight::NORMAL`] or `400`.
211///
212/// In variable fonts, this can be controlled with the `wght` axis. This
213/// is a `u16` so that it can represent the same range of values as the
214/// `wght` axis.
215///
216/// See <https://fonts.google.com/knowledge/glossary/weight>
217///
218/// In CSS, this corresponds to the [`font-weight`] property.
219///
220/// [`font-weight`]: https://www.w3.org/TR/css-fonts-4/#font-weight-prop
221#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
222pub struct FontWeight(u16);
223
224impl FontWeight {
225    /// Weight value of 100.
226    pub const THIN: Self = Self(100);
227
228    /// Weight value of 200.
229    pub const EXTRA_LIGHT: Self = Self(200);
230
231    /// Weight value of 300.
232    pub const LIGHT: Self = Self(300);
233
234    /// Weight value of 350.
235    pub const SEMI_LIGHT: Self = Self(350);
236
237    /// Weight value of 400. This is the default value.
238    pub const NORMAL: Self = Self(400);
239
240    /// Weight value of 500.
241    pub const MEDIUM: Self = Self(500);
242
243    /// Weight value of 600.
244    pub const SEMI_BOLD: Self = Self(600);
245
246    /// Weight value of 700.
247    pub const BOLD: Self = Self(700);
248
249    /// Weight value of 800.
250    pub const EXTRA_BOLD: Self = Self(800);
251
252    /// Weight value of 900.
253    pub const BLACK: Self = Self(900);
254
255    /// Weight value of 950.
256    pub const EXTRA_BLACK: Self = Self(950);
257}
258
259impl FontWeight {
260    /// Creates a new weight attribute with the given value.
261    pub fn new(weight: u16) -> Self {
262        Self(weight)
263    }
264
265    /// Returns the underlying weight value.
266    pub fn value(self) -> u16 {
267        self.0
268    }
269
270    /// Parses a CSS style font weight attribute.
271    ///
272    /// This function accepts only `normal`, `bold` and numeric values.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// # use kas_text::fonts::FontWeight;
278    /// assert_eq!(FontWeight::parse("normal"), Some(FontWeight::NORMAL));
279    /// assert_eq!(FontWeight::parse("bold"), Some(FontWeight::BOLD));
280    /// assert_eq!(FontWeight::parse("850"), Some(FontWeight::new(850)));
281    /// assert_eq!(FontWeight::parse("invalid"), None);
282    /// ```
283    pub fn parse(s: &str) -> Option<Self> {
284        let s = s.trim();
285        Some(match s {
286            "normal" => Self::NORMAL,
287            "bold" => Self::BOLD,
288            _ => Self(s.parse::<u16>().ok()?),
289        })
290    }
291}
292
293impl Default for FontWeight {
294    fn default() -> Self {
295        Self::NORMAL
296    }
297}
298
299impl fmt::Display for FontWeight {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        let keyword = match self.0 {
302            400 => "normal",
303            700 => "bold",
304            _ => return write!(f, "{}", self.0),
305        };
306        write!(f, "{keyword}")
307    }
308}
309
310impl From<FontWeight> for fontique::FontWeight {
311    #[inline]
312    fn from(weight: FontWeight) -> Self {
313        fontique::FontWeight::new(weight.value().cast())
314    }
315}
316
317/// Visual style or 'slope' of a font.
318///
319/// The default value is [`FontStyle::Normal`].
320///
321/// In variable fonts, this can be controlled with the `ital`
322/// and `slnt` axes for italic and oblique styles, respectively.
323/// This uses an `f32` for the `Oblique` variant so so that it
324/// can represent the same range of values as the `slnt` axis.
325///
326/// See <https://fonts.google.com/knowledge/glossary/style>
327///
328/// In CSS, this corresponds to the [`font-style`] property.
329///
330/// [`font-style`]: https://www.w3.org/TR/css-fonts-4/#font-style-prop
331#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, Hash)]
332pub enum FontStyle {
333    /// An upright or "roman" style.
334    #[default]
335    Normal,
336    /// Generally a slanted style, originally based on semi-cursive forms.
337    /// This often has a different structure from the normal style.
338    Italic,
339    /// Oblique (or slanted) style with an optional angle in degrees times 256,
340    /// counter-clockwise from the vertical.
341    ///
342    /// To convert `Some(angle)` to degrees, use
343    /// `degrees = (angle as f32) / 256.0` or [`FontStyle::oblique_degrees`].
344    Oblique(Option<i16>),
345}
346
347impl FontStyle {
348    /// Parses a font style from a CSS value.
349    pub fn parse(mut s: &str) -> Option<Self> {
350        s = s.trim();
351        Some(match s {
352            "normal" => Self::Normal,
353            "italic" => Self::Italic,
354            "oblique" => Self::Oblique(None),
355            _ => {
356                if s.starts_with("oblique ") {
357                    s = s.get(8..)?;
358                    if s.ends_with("deg") {
359                        s = s.get(..s.len() - 3)?;
360                        if let Ok(degrees) = s.trim().parse::<f32>() {
361                            return Some(Self::from_degrees(degrees));
362                        }
363                    } else if s.ends_with("grad") {
364                        s = s.get(..s.len() - 4)?;
365                        if let Ok(gradians) = s.trim().parse::<f32>() {
366                            return Some(Self::from_degrees(gradians / 400.0 * 360.0));
367                        }
368                    } else if s.ends_with("rad") {
369                        s = s.get(..s.len() - 3)?;
370                        if let Ok(radians) = s.trim().parse::<f32>() {
371                            return Some(Self::from_degrees(radians.to_degrees()));
372                        }
373                    } else if s.ends_with("turn") {
374                        s = s.get(..s.len() - 4)?;
375                        if let Ok(turns) = s.trim().parse::<f32>() {
376                            return Some(Self::from_degrees(turns * 360.0));
377                        }
378                    }
379                    return Some(Self::Oblique(None));
380                }
381                return None;
382            }
383        })
384    }
385}
386
387impl FontStyle {
388    /// Convert an `Oblique` payload to an angle in degrees.
389    pub const fn oblique_degrees(angle: Option<i16>) -> f32 {
390        if let Some(a) = angle {
391            (a as f32) / 256.0
392        } else {
393            14.0
394        }
395    }
396
397    /// Creates a new oblique style with angle specified in degrees.
398    ///
399    /// Panics if `degrees` is not between `-90` and `90`.
400    pub fn from_degrees(degrees: f32) -> Self {
401        assert!(-90.0 <= degrees && degrees <= 90.0);
402        let a = (degrees * 256.0).round();
403        Self::Oblique(Some(a as i16))
404    }
405}
406
407impl fmt::Display for FontStyle {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        let value = match *self {
410            Self::Normal => "normal",
411            Self::Italic => "italic",
412            Self::Oblique(None) => "oblique",
413            Self::Oblique(Some(angle)) => {
414                let degrees = (angle as f32) / 256.0;
415                return write!(f, "oblique {degrees}deg");
416            }
417        };
418        write!(f, "{value}")
419    }
420}
421
422impl From<FontStyle> for fontique::FontStyle {
423    #[inline]
424    fn from(style: FontStyle) -> Self {
425        match style {
426            FontStyle::Normal => fontique::FontStyle::Normal,
427            FontStyle::Italic => fontique::FontStyle::Italic,
428            FontStyle::Oblique(None) => fontique::FontStyle::Oblique(None),
429            FontStyle::Oblique(slant) => {
430                fontique::FontStyle::Oblique(Some(FontStyle::oblique_degrees(slant)))
431            }
432        }
433    }
434}
435
436#[cfg(feature = "serde")]
437mod serde_impls {
438    use super::*;
439    use serde::{de, ser};
440
441    impl ser::Serialize for FontWidth {
442        fn serialize<S: ser::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
443            ser.serialize_str(&format!("{}", self))
444        }
445    }
446
447    impl<'de> de::Deserialize<'de> for FontWidth {
448        fn deserialize<D: de::Deserializer<'de>>(de: D) -> Result<FontWidth, D::Error> {
449            struct Visitor;
450            impl<'de> de::Visitor<'de> for Visitor {
451                type Value = FontWidth;
452
453                fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
454                    write!(fmt, "a keyword or integer percentage")
455                }
456
457                fn visit_str<E: de::Error>(self, s: &str) -> Result<FontWidth, E> {
458                    FontWidth::parse(s)
459                        .ok_or_else(|| de::Error::invalid_value(de::Unexpected::Str(s), &self))
460                }
461            }
462
463            de.deserialize_str(Visitor)
464        }
465    }
466
467    impl ser::Serialize for FontWeight {
468        fn serialize<S: ser::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
469            ser.serialize_str(&format!("{}", self))
470        }
471    }
472
473    impl<'de> de::Deserialize<'de> for FontWeight {
474        fn deserialize<D: de::Deserializer<'de>>(de: D) -> Result<FontWeight, D::Error> {
475            struct Visitor;
476            impl<'de> de::Visitor<'de> for Visitor {
477                type Value = FontWeight;
478
479                fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
480                    write!(fmt, "a keyword or integer")
481                }
482
483                fn visit_str<E: de::Error>(self, s: &str) -> Result<FontWeight, E> {
484                    FontWeight::parse(s)
485                        .ok_or_else(|| de::Error::invalid_value(de::Unexpected::Str(s), &self))
486                }
487            }
488
489            de.deserialize_str(Visitor)
490        }
491    }
492
493    impl ser::Serialize for FontStyle {
494        fn serialize<S: ser::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
495            ser.serialize_str(&format!("{}", self))
496        }
497    }
498
499    impl<'de> de::Deserialize<'de> for FontStyle {
500        fn deserialize<D: de::Deserializer<'de>>(de: D) -> Result<FontStyle, D::Error> {
501            struct Visitor;
502            impl<'de> de::Visitor<'de> for Visitor {
503                type Value = FontStyle;
504
505                fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
506                    write!(fmt, "a keyword or 'oblique' value")
507                }
508
509                fn visit_str<E: de::Error>(self, s: &str) -> Result<FontStyle, E> {
510                    FontStyle::parse(s)
511                        .ok_or_else(|| de::Error::invalid_value(de::Unexpected::Str(s), &self))
512                }
513            }
514
515            de.deserialize_str(Visitor)
516        }
517    }
518}