Skip to main content

topcoat_font/
source.rs

1//! Font sources for building the `src` descriptor of a CSS `@font-face` rule.
2
3use std::{fmt::Write, ops::Deref};
4
5use topcoat_core::{context::Cx, fnv1a::Fnv1a};
6
7use crate::{CssString, FontFormat, FontTech};
8
9/// The location of a font file, the URL of a `url()` entry in a CSS
10/// `@font-face` `src` descriptor.
11///
12/// A [`Str`](Self::Str) is written verbatim; an [`Asset`](Self::Asset) is
13/// resolved to its hosted URL when formatted. Either is escaped as a CSS
14/// `<string>` so it is safe between the quotes of `url("...")`.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum FontSourceUrl {
17    /// A URL written as-is, such as an absolute URL or an external host.
18    Str(String),
19    /// A bundled [`Asset`](topcoat_asset::Asset), resolved to its hosted URL.
20    #[cfg(feature = "asset")]
21    Asset(topcoat_asset::Asset),
22}
23
24impl FontSourceUrl {
25    /// Writes the URL, escaped as the body of a CSS `<string>` (without the
26    /// surrounding quotes).
27    ///
28    /// # Errors
29    ///
30    /// Returns any error produced while writing to `f`.
31    #[cfg_attr(not(feature = "asset"), expect(unused_variables))]
32    #[track_caller]
33    pub fn fmt(&self, cx: &Cx, f: &mut dyn Write) -> std::fmt::Result {
34        let mut f = CssString(f);
35        match self {
36            Self::Str(inner) => f.write_str(inner),
37            #[cfg(feature = "asset")]
38            Self::Asset(inner) => topcoat_asset::asset_config(cx).fmt_url(*inner, &mut f),
39        }
40    }
41
42    /// Returns `true` if the font source url is [`Str`].
43    ///
44    /// [`Str`]: FontSourceUrl::Str
45    #[must_use]
46    pub fn is_str(&self) -> bool {
47        matches!(self, Self::Str(..))
48    }
49
50    /// Returns the inner URL if this is a [`Str`], otherwise `None`.
51    ///
52    /// [`Str`]: FontSourceUrl::Str
53    #[must_use]
54    pub fn as_str(&self) -> Option<&str> {
55        match self {
56            Self::Str(v) => Some(v),
57            #[cfg(feature = "asset")]
58            Self::Asset(_) => None,
59        }
60    }
61
62    /// Returns `true` if the font source url is [`Asset`].
63    ///
64    /// [`Asset`]: FontSourceUrl::Asset
65    #[must_use]
66    #[cfg(feature = "asset")]
67    pub fn is_asset(&self) -> bool {
68        matches!(self, Self::Asset(..))
69    }
70
71    /// Returns the inner asset if this is an [`Asset`], otherwise `None`.
72    ///
73    /// [`Asset`]: FontSourceUrl::Asset
74    #[must_use]
75    #[cfg(feature = "asset")]
76    pub fn as_asset(&self) -> Option<&topcoat_asset::Asset> {
77        match self {
78            Self::Asset(v) => Some(v),
79            Self::Str(_) => None,
80        }
81    }
82
83    /// Folds this URL into a running content hash.
84    pub(crate) fn hash(&self, h: Fnv1a<u64>) -> Fnv1a<u64> {
85        match self {
86            Self::Str(inner) => h.write(b"s").write(inner.as_bytes()),
87            #[cfg(feature = "asset")]
88            Self::Asset(inner) => h.write(b"a").write(&inner.id().as_u64().to_le_bytes()),
89        }
90    }
91}
92
93impl From<&str> for FontSourceUrl {
94    fn from(v: &str) -> Self {
95        Self::Str(v.to_owned())
96    }
97}
98
99impl From<String> for FontSourceUrl {
100    fn from(v: String) -> Self {
101        Self::Str(v)
102    }
103}
104
105#[cfg(feature = "asset")]
106impl From<topcoat_asset::Asset> for FontSourceUrl {
107    fn from(v: topcoat_asset::Asset) -> Self {
108        Self::Asset(v)
109    }
110}
111
112#[cfg(feature = "view")]
113impl topcoat_view::AttributeValueViewParts for FontSourceUrl {
114    fn attribute_present(&self) -> bool {
115        true
116    }
117
118    fn into_view_parts(
119        self,
120        cx: &topcoat_core::context::Cx,
121        parts: &mut topcoat_view::PartsWriter<'_>,
122    ) {
123        match self {
124            Self::Str(inner) => inner.into_view_parts(cx, parts),
125            #[cfg(feature = "asset")]
126            Self::Asset(inner) => inner.into_view_parts(cx, parts),
127        }
128    }
129}
130
131/// A single entry of a CSS `@font-face` `src` descriptor.
132///
133/// A [`Url`](Self::Url) points at a font file to download, with optional
134/// `format()` and `tech()` hints the browser uses to skip files it cannot use.
135/// A [`Local`](Self::Local) names a font already installed on the system.
136///
137/// Renders as the corresponding CSS, e.g. `url("/font.woff2") format(woff2)` or
138/// `local("Helvetica Neue")`.
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub enum FontSource {
141    /// A downloadable font file, with optional format and technology hints.
142    Url {
143        /// Where the font file is located.
144        url: FontSourceUrl,
145        /// The font file format, written as a `format()` hint.
146        format: Option<FontFormat>,
147        /// The font technology, written as a `tech()` hint.
148        tech: Option<FontTech>,
149    },
150    /// A locally installed font, named by a `local()` entry.
151    Local {
152        /// The family name of the installed font.
153        name: String,
154    },
155}
156
157impl FontSource {
158    /// A downloadable source from a URL, with optional `format()` and `tech()`
159    /// hints.
160    #[must_use]
161    pub fn url(
162        url: impl Into<FontSourceUrl>,
163        format: Option<FontFormat>,
164        tech: Option<FontTech>,
165    ) -> Self {
166        Self::Url {
167            url: url.into(),
168            format,
169            tech,
170        }
171    }
172
173    /// A source naming a font already installed on the system.
174    #[must_use]
175    pub fn local(name: impl Into<String>) -> Self {
176        Self::Local { name: name.into() }
177    }
178
179    /// Writes this source as a single CSS `src` entry.
180    ///
181    /// # Errors
182    ///
183    /// Returns any error produced while writing to `f`.
184    #[track_caller]
185    pub fn fmt(&self, cx: &Cx, f: &mut dyn Write) -> std::fmt::Result {
186        match self {
187            Self::Url { url, format, tech } => {
188                f.write_str("url(\"")?;
189                url.fmt(cx, &mut *f)?;
190                f.write_str("\")")?;
191                if let Some(format) = format {
192                    write!(f, " format({format})")?;
193                }
194                if let Some(tech) = tech {
195                    write!(f, " tech({tech})")?;
196                }
197            }
198            Self::Local { name } => {
199                f.write_str("local(\"")?;
200                CssString(f).write_str(name)?;
201                f.write_str("\")")?;
202            }
203        }
204        Ok(())
205    }
206
207    /// Returns `true` if the font source is [`Url`].
208    ///
209    /// [`Url`]: FontSource::Url
210    #[must_use]
211    pub fn is_url(&self) -> bool {
212        matches!(self, Self::Url { .. })
213    }
214
215    /// Returns `true` if the font source is [`Local`].
216    ///
217    /// [`Local`]: FontSource::Local
218    #[must_use]
219    pub fn is_local(&self) -> bool {
220        matches!(self, Self::Local { .. })
221    }
222
223    /// Folds this source into a running content hash.
224    pub(crate) fn hash(&self, h: Fnv1a<u64>) -> Fnv1a<u64> {
225        match self {
226            Self::Url { url, format, tech } => {
227                let h = url.hash(h.write(b"u"));
228                let h = match format {
229                    Some(format) => format.hash(h.write(&[1])),
230                    None => h.write(&[0]),
231                };
232                match tech {
233                    Some(tech) => tech.hash(h.write(&[1])),
234                    None => h.write(&[0]),
235                }
236            }
237            Self::Local { name } => h.write(b"l").write(name.as_bytes()),
238        }
239    }
240}
241
242/// An ordered, non-empty list of [`FontSource`]s, the value of a CSS
243/// `@font-face` `src` descriptor.
244///
245/// Renders as the comma-separated list CSS expects, with the browser using the
246/// first source it supports. Order from most to least preferred.
247#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub struct FontSources(Vec<FontSource>);
249
250impl FontSources {
251    /// Creates a list of `sources`.
252    ///
253    /// # Panics
254    ///
255    /// Panics if `sources` is empty; a CSS `src` descriptor requires at least
256    /// one source.
257    #[must_use]
258    #[track_caller]
259    pub fn new(sources: impl Into<Vec<FontSource>>) -> Self {
260        let sources = sources.into();
261        assert!(!sources.is_empty(), "font sources must not be empty");
262        Self(sources)
263    }
264
265    /// Folds these sources into a running content hash.
266    pub(crate) fn hash(&self, mut h: Fnv1a<u64>) -> Fnv1a<u64> {
267        for source in &self.0 {
268            h = source.hash(h);
269        }
270        h
271    }
272
273    /// Writes the sources as a comma-separated CSS `src` descriptor value.
274    ///
275    /// # Errors
276    ///
277    /// Returns any error produced while writing to `f`.
278    #[track_caller]
279    pub fn fmt(&self, cx: &Cx, f: &mut dyn Write) -> std::fmt::Result {
280        for (index, source) in self.0.iter().enumerate() {
281            if index > 0 {
282                f.write_str(", ")?;
283            }
284            source.fmt(cx, f)?;
285        }
286        Ok(())
287    }
288
289    /// Returns the sources as a slice.
290    ///
291    /// The slice is never empty, mirroring the non-empty invariant of
292    /// [`FontSources`].
293    #[must_use]
294    pub fn as_slice(&self) -> &[FontSource] {
295        &self.0
296    }
297
298    /// Builds a [`FontSources`] from `sources`, validating the non-empty invariant.
299    fn try_from_vec(sources: Vec<FontSource>) -> Result<Self, EmptyFontSourcesError> {
300        if sources.is_empty() {
301            return Err(EmptyFontSourcesError);
302        }
303        Ok(Self(sources))
304    }
305}
306
307/// Error returned when converting an empty collection into [`FontSources`].
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct EmptyFontSourcesError;
310
311impl std::fmt::Display for EmptyFontSourcesError {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.write_str("font sources must not be empty")
314    }
315}
316
317impl std::error::Error for EmptyFontSourcesError {}
318
319impl TryFrom<Vec<FontSource>> for FontSources {
320    type Error = EmptyFontSourcesError;
321
322    fn try_from(sources: Vec<FontSource>) -> Result<Self, Self::Error> {
323        Self::try_from_vec(sources)
324    }
325}
326
327impl TryFrom<&[FontSource]> for FontSources {
328    type Error = EmptyFontSourcesError;
329
330    fn try_from(sources: &[FontSource]) -> Result<Self, Self::Error> {
331        Self::try_from_vec(sources.to_vec())
332    }
333}
334
335impl Deref for FontSources {
336    type Target = [FontSource];
337
338    fn deref(&self) -> &Self::Target {
339        self.as_slice()
340    }
341}