Skip to main content

topcoat_font/
face.rs

1//! Font faces for building CSS `@font-face` rules.
2
3use std::{fmt::Write, ops::Deref};
4
5use topcoat_core::{context::Cx, fnv1a::Fnv1a};
6
7use crate::{CssString, FontDisplay, FontSources, FontStyle, FontWeightRange, UnicodeRanges};
8
9/// A single CSS `@font-face` rule: a font family backed by one set of sources,
10/// scoped to an optional weight range, style, display strategy, and unicode
11/// range.
12///
13/// Renders as a complete `@font-face { ... }` block, with the optional
14/// descriptors omitted when unset.
15#[derive(Debug, Clone, PartialEq)]
16pub struct FontFace {
17    family: String,
18    src: FontSources,
19    weight: Option<FontWeightRange>,
20    style: Option<FontStyle>,
21    display: Option<FontDisplay>,
22    unicode_range: Option<UnicodeRanges>,
23}
24
25impl FontFace {
26    /// Creates a face for `family`, served from `src`.
27    ///
28    /// The weight, style, display strategy, and unicode range start unset; add
29    /// them with [`with_weight`](Self::with_weight),
30    /// [`with_style`](Self::with_style), [`with_display`](Self::with_display),
31    /// and [`with_unicode_range`](Self::with_unicode_range).
32    ///
33    /// # Panics
34    ///
35    /// Panics if the [`TryInto`] conversion of `src` fails.
36    #[must_use]
37    #[track_caller]
38    pub fn new(family: impl Into<String>, src: impl TryInto<FontSources>) -> Self {
39        Self {
40            family: family.into(),
41            src: src
42                .try_into()
43                .unwrap_or_else(|_| panic!("font sources must not be empty")),
44            weight: None,
45            style: None,
46            display: None,
47            unicode_range: None,
48        }
49    }
50
51    /// Sets the `font-weight` descriptor.
52    #[must_use]
53    pub fn with_weight(mut self, weight: FontWeightRange) -> Self {
54        self.weight = Some(weight);
55        self
56    }
57
58    /// Sets the `font-style` descriptor.
59    #[must_use]
60    pub fn with_style(mut self, style: FontStyle) -> Self {
61        self.style = Some(style);
62        self
63    }
64
65    /// Sets the `font-display` descriptor.
66    #[must_use]
67    pub fn with_display(mut self, display: FontDisplay) -> Self {
68        self.display = Some(display);
69        self
70    }
71
72    /// Sets the `unicode-range` descriptor.
73    #[must_use]
74    pub fn with_unicode_range(mut self, unicode_range: UnicodeRanges) -> Self {
75        self.unicode_range = Some(unicode_range);
76        self
77    }
78
79    /// Writes this face as a complete CSS `@font-face` rule.
80    ///
81    /// # Errors
82    ///
83    /// Returns any error produced while writing to `f`.
84    #[track_caller]
85    pub fn fmt(&self, cx: &Cx, f: &mut dyn Write) -> std::fmt::Result {
86        f.write_str("@font-face { font-family: \"")?;
87        CssString(&mut *f).write_str(&self.family)?;
88        f.write_str("\"; src: ")?;
89        self.src.fmt(cx, &mut *f)?;
90        if let Some(weight) = self.weight {
91            write!(f, "; font-weight: {weight}")?;
92        }
93        if let Some(style) = self.style {
94            write!(f, "; font-style: {style}")?;
95        }
96        if let Some(display) = self.display {
97            write!(f, "; font-display: {display}")?;
98        }
99        if let Some(unicode_range) = self.unicode_range {
100            write!(f, "; unicode-range: {unicode_range}")?;
101        }
102        f.write_str(" }")?;
103        Ok(())
104    }
105
106    /// Folds this face into a running content hash.
107    pub(crate) fn hash(&self, h: Fnv1a<u64>) -> Fnv1a<u64> {
108        let h = h.write(self.family.as_bytes());
109        let h = self.src.hash(h);
110        let h = match self.weight {
111            Some(weight) => weight.hash(h.write(&[1])),
112            None => h.write(&[0]),
113        };
114        let h = match self.style {
115            Some(style) => style.hash(h.write(&[1])),
116            None => h.write(&[0]),
117        };
118        let h = match self.display {
119            Some(display) => display.hash(h.write(&[1])),
120            None => h.write(&[0]),
121        };
122        match self.unicode_range {
123            Some(unicode_range) => unicode_range.hash(h.write(&[1])),
124            None => h.write(&[0]),
125        }
126    }
127
128    /// The `font-family` this face defines.
129    #[must_use]
130    pub fn family(&self) -> &str {
131        &self.family
132    }
133
134    /// The sources backing this face's `src` descriptor.
135    #[must_use]
136    pub fn src(&self) -> &FontSources {
137        &self.src
138    }
139
140    /// The `font-weight` descriptor, if set.
141    #[must_use]
142    pub fn weight(&self) -> Option<FontWeightRange> {
143        self.weight
144    }
145
146    /// The `font-style` descriptor, if set.
147    #[must_use]
148    pub fn style(&self) -> Option<FontStyle> {
149        self.style
150    }
151
152    /// The `font-display` descriptor, if set.
153    #[must_use]
154    pub fn display(&self) -> Option<FontDisplay> {
155        self.display
156    }
157
158    /// The `unicode-range` descriptor, if set.
159    #[must_use]
160    pub fn unicode_range(&self) -> Option<UnicodeRanges> {
161        self.unicode_range
162    }
163}
164
165/// An ordered, non-empty list of [`FontFace`]s.
166///
167/// Renders as the faces' `@font-face` rules, separated by a space.
168#[derive(Debug, Clone, PartialEq)]
169pub struct FontFaces(Vec<FontFace>);
170
171impl FontFaces {
172    /// Creates a list of `faces`.
173    ///
174    /// # Panics
175    ///
176    /// Panics if `faces` is empty.
177    #[must_use]
178    #[track_caller]
179    pub fn new(faces: impl Into<Vec<FontFace>>) -> Self {
180        let faces = faces.into();
181        assert!(!faces.is_empty(), "font faces must not be empty");
182        Self(faces)
183    }
184
185    /// Folds these faces into a running content hash.
186    pub(crate) fn hash(&self, mut h: Fnv1a<u64>) -> Fnv1a<u64> {
187        for face in &self.0 {
188            h = face.hash(h);
189        }
190        h
191    }
192
193    /// Writes the faces as space-separated CSS `@font-face` rules.
194    ///
195    /// # Errors
196    ///
197    /// Returns any error produced while writing to `f`.
198    #[track_caller]
199    pub fn fmt(&self, cx: &Cx, f: &mut dyn Write) -> std::fmt::Result {
200        for (index, face) in self.0.iter().enumerate() {
201            if index > 0 {
202                f.write_str(" ")?;
203            }
204            face.fmt(cx, &mut *f)?;
205        }
206        Ok(())
207    }
208
209    /// Returns the faces as a slice.
210    ///
211    /// The slice is never empty, mirroring the non-empty invariant of
212    /// [`FontFaces`].
213    #[must_use]
214    pub fn as_slice(&self) -> &[FontFace] {
215        &self.0
216    }
217
218    /// Builds a [`FontFaces`] from `faces`, validating the non-empty invariant.
219    fn try_from_vec(faces: Vec<FontFace>) -> Result<Self, EmptyFontFacesError> {
220        if faces.is_empty() {
221            return Err(EmptyFontFacesError);
222        }
223        Ok(Self(faces))
224    }
225}
226
227/// Error returned when converting an empty collection into [`FontFaces`].
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct EmptyFontFacesError;
230
231impl std::fmt::Display for EmptyFontFacesError {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        f.write_str("font faces must not be empty")
234    }
235}
236
237impl std::error::Error for EmptyFontFacesError {}
238
239impl TryFrom<Vec<FontFace>> for FontFaces {
240    type Error = EmptyFontFacesError;
241
242    fn try_from(faces: Vec<FontFace>) -> Result<Self, Self::Error> {
243        Self::try_from_vec(faces)
244    }
245}
246
247impl TryFrom<&[FontFace]> for FontFaces {
248    type Error = EmptyFontFacesError;
249
250    fn try_from(faces: &[FontFace]) -> Result<Self, Self::Error> {
251        Self::try_from_vec(faces.to_vec())
252    }
253}
254
255impl Deref for FontFaces {
256    type Target = [FontFace];
257
258    fn deref(&self) -> &Self::Target {
259        self.as_slice()
260    }
261}