1use std::{fmt::Write, ops::Deref};
4
5use topcoat_core::{context::Cx, fnv1a::Fnv1a};
6
7use crate::{CssString, FontFormat, FontTech};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum FontSourceUrl {
17 Str(String),
19 #[cfg(feature = "asset")]
21 Asset(topcoat_asset::Asset),
22}
23
24impl FontSourceUrl {
25 #[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 #[must_use]
46 pub fn is_str(&self) -> bool {
47 matches!(self, Self::Str(..))
48 }
49
50 #[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 #[must_use]
66 #[cfg(feature = "asset")]
67 pub fn is_asset(&self) -> bool {
68 matches!(self, Self::Asset(..))
69 }
70
71 #[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 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub enum FontSource {
141 Url {
143 url: FontSourceUrl,
145 format: Option<FontFormat>,
147 tech: Option<FontTech>,
149 },
150 Local {
152 name: String,
154 },
155}
156
157impl FontSource {
158 #[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 #[must_use]
175 pub fn local(name: impl Into<String>) -> Self {
176 Self::Local { name: name.into() }
177 }
178
179 #[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 #[must_use]
211 pub fn is_url(&self) -> bool {
212 matches!(self, Self::Url { .. })
213 }
214
215 #[must_use]
219 pub fn is_local(&self) -> bool {
220 matches!(self, Self::Local { .. })
221 }
222
223 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub struct FontSources(Vec<FontSource>);
249
250impl FontSources {
251 #[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 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 #[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 #[must_use]
294 pub fn as_slice(&self) -> &[FontSource] {
295 &self.0
296 }
297
298 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#[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}