1use serde::de::{Error as _, Unexpected};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::fonts::{FeatureSetting, GenericFamily};
8use crate::pages::{Side, fade};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Length {
13 Points(f32),
15 Em(f32),
17 Rem(f32),
19 Percent(f32),
21}
22
23impl Length {
24 pub fn to_points(self, relative: f32, root: f32) -> f32 {
28 match self {
29 Length::Points(pt) => pt,
30 Length::Em(em) => em * relative,
31 Length::Rem(rem) => rem * root,
32 Length::Percent(percent) => percent / 100.0 * relative,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum LineHeight {
40 Normal,
42 Number(f32),
44 Length(Length),
46}
47
48impl LineHeight {
49 pub fn to_multiple(self, size: f32, root: f32) -> f32 {
51 match self {
52 LineHeight::Normal => NORMAL_LINE_HEIGHT,
53 LineHeight::Number(number) => number,
54 LineHeight::Length(length) => {
55 if size > 0.0 {
56 length.to_points(size, root) / size
57 } else {
58 NORMAL_LINE_HEIGHT
59 }
60 }
61 }
62 }
63}
64
65pub(super) const NORMAL_LINE_HEIGHT: f32 = 1.2;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct Color {
77 pub r: u8,
79 pub g: u8,
81 pub b: u8,
83 pub a: u8,
85}
86
87impl Color {
88 pub const BLACK: Color = Color::rgb(0, 0, 0);
90
91 pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
93 Color { r, g, b, a: 255 }
94 }
95
96 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
98 Color { r, g, b, a }
99 }
100
101 pub const fn opaque(self) -> bool {
103 self.a == 255
104 }
105
106 pub fn faded(self, opacity: f32) -> Color {
108 Color {
109 a: fade(self.a, opacity),
110 ..self
111 }
112 }
113
114 pub fn to_hex(self) -> String {
117 let rgb = format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b);
118 if self.opaque() {
119 rgb
120 } else {
121 format!("{rgb}{:02x}", self.a)
122 }
123 }
124
125 pub fn from_hex(text: &str) -> Option<Color> {
128 let digits = text.strip_prefix('#')?;
129 if !matches!(digits.len(), 6 | 8) || !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) {
130 return None;
131 }
132 let channel = |at: usize| u8::from_str_radix(&digits[at..at + 2], 16).ok();
133 let alpha = if digits.len() == 8 { channel(6)? } else { 255 };
134 Some(Color::rgba(channel(0)?, channel(2)?, channel(4)?, alpha))
135 }
136}
137
138impl Serialize for Color {
139 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
140 if serializer.is_human_readable() {
141 serializer.serialize_str(&self.to_hex())
142 } else {
143 [self.r, self.g, self.b, self.a].serialize(serializer)
144 }
145 }
146}
147
148impl<'de> Deserialize<'de> for Color {
149 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Color, D::Error> {
150 if deserializer.is_human_readable() {
151 let hex = String::deserialize(deserializer)?;
152 Color::from_hex(&hex).ok_or_else(|| {
153 D::Error::invalid_value(Unexpected::Str(&hex), &"#rrggbb or #rrggbbaa")
154 })
155 } else {
156 let [r, g, b, a] = <[u8; 4]>::deserialize(deserializer)?;
157 Ok(Color::rgba(r, g, b, a))
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize)]
164#[serde(rename_all = "snake_case")]
165pub enum Family {
166 Named(String),
168 Generic(GenericFamily),
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
174#[serde(rename_all = "snake_case")]
175pub enum FontStyle {
176 Normal,
178 Italic,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
184#[serde(rename_all = "snake_case")]
185pub enum TextAlign {
186 Left,
188 Right,
190 Center,
192 Justify,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
198#[serde(rename_all = "snake_case")]
199pub enum TextJustify {
200 InterWord,
203 InterCharacter,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
210#[serde(rename_all = "snake_case")]
211pub enum FontVariantCaps {
212 Normal,
214 SmallCaps,
216}
217
218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
223pub struct FontVariantLigatures {
224 pub common: Option<bool>,
227 pub discretionary: Option<bool>,
230 pub historical: Option<bool>,
232 pub contextual: Option<bool>,
234}
235
236impl FontVariantLigatures {
237 pub const NORMAL: FontVariantLigatures = FontVariantLigatures {
239 common: None,
240 discretionary: None,
241 historical: None,
242 contextual: None,
243 };
244
245 pub const NONE: FontVariantLigatures = FontVariantLigatures {
247 common: Some(false),
248 discretionary: Some(false),
249 historical: Some(false),
250 contextual: Some(false),
251 };
252
253 pub fn settings(&self) -> Vec<FeatureSetting> {
255 let groups: [(Option<bool>, &[&[u8; 4]]); 4] = [
256 (self.common, &[b"liga", b"clig"]),
257 (self.discretionary, &[b"dlig"]),
258 (self.historical, &[b"hlig"]),
259 (self.contextual, &[b"calt"]),
260 ];
261 groups
262 .iter()
263 .filter_map(|(asked, tags)| Some((asked.as_ref()?, tags)))
264 .flat_map(|(on, tags)| {
265 tags.iter()
266 .map(move |tag| FeatureSetting::new(**tag, u32::from(*on)))
267 })
268 .collect()
269 }
270}
271
272#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
274pub struct FontVariantNumeric {
275 pub figures: Option<Figures>,
277 pub spacing: Option<NumericSpacing>,
279 pub fractions: Option<Fractions>,
281 pub ordinal: bool,
283 pub slashed_zero: bool,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
289#[serde(rename_all = "kebab-case")]
290pub enum Figures {
291 Lining,
294 OldStyle,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
300#[serde(rename_all = "kebab-case")]
301pub enum NumericSpacing {
302 Proportional,
304 Tabular,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
311#[serde(rename_all = "kebab-case")]
312pub enum Fractions {
313 Diagonal,
316 Stacked,
318}
319
320impl FontVariantNumeric {
321 pub const NORMAL: FontVariantNumeric = FontVariantNumeric {
323 figures: None,
324 spacing: None,
325 fractions: None,
326 ordinal: false,
327 slashed_zero: false,
328 };
329
330 pub fn settings(&self) -> Vec<FeatureSetting> {
332 let tags = [
333 self.figures.map(|figures| match figures {
334 Figures::Lining => b"lnum",
335 Figures::OldStyle => b"onum",
336 }),
337 self.spacing.map(|spacing| match spacing {
338 NumericSpacing::Proportional => b"pnum",
339 NumericSpacing::Tabular => b"tnum",
340 }),
341 self.fractions.map(|fractions| match fractions {
342 Fractions::Diagonal => b"frac",
343 Fractions::Stacked => b"afrc",
344 }),
345 self.ordinal.then_some(b"ordn"),
346 self.slashed_zero.then_some(b"zero"),
347 ];
348 tags.into_iter()
349 .flatten()
350 .map(|tag| FeatureSetting::new(*tag, 1))
351 .collect()
352 }
353}
354
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
358#[serde(rename_all = "kebab-case")]
359pub enum FontVariantAlternates {
360 #[default]
362 Normal,
363 HistoricalForms,
365}
366
367impl FontVariantAlternates {
368 pub fn settings(&self) -> Vec<FeatureSetting> {
370 match self {
371 FontVariantAlternates::Normal => Vec::new(),
372 FontVariantAlternates::HistoricalForms => {
373 vec![FeatureSetting::new(*b"hist", 1)]
374 }
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
382#[serde(rename_all = "snake_case")]
383pub enum TextTransform {
384 None,
386 Uppercase,
388 Lowercase,
390 Capitalize,
392}
393
394impl TextTransform {
395 pub fn write(self, letter: char, word_start: bool, out: &mut String) -> bool {
403 let at = out.len();
404 match self {
405 TextTransform::None => out.push(letter),
406 TextTransform::Uppercase => out.extend(letter.to_uppercase()),
407 TextTransform::Lowercase => out.extend(letter.to_lowercase()),
408 TextTransform::Capitalize if word_start => out.extend(letter.to_uppercase()),
409 TextTransform::Capitalize => out.push(letter),
410 }
411 let mut one = [0u8; 4];
412 out[at..] != *letter.encode_utf8(&mut one)
413 }
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
418#[serde(rename_all = "snake_case")]
419pub enum Hyphens {
420 None,
422 Auto,
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
430#[serde(rename_all = "snake_case")]
431pub enum Break {
432 Auto,
434 Avoid,
436 Column,
439 Page,
441 Side(Side),
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
449#[serde(rename_all = "snake_case")]
450pub enum BoxDecorationBreak {
451 Slice,
454 Clone,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
462#[serde(rename_all = "snake_case")]
463pub enum Width {
464 Auto,
466 Points(f32),
468 Percent(f32),
470}
471
472impl Width {
473 pub fn resolve(self, of: f32) -> Option<f32> {
476 match self {
477 Width::Auto => None,
478 Width::Points(points) => Some(points),
479 Width::Percent(percent) => Some(percent / 100.0 * of),
480 }
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
487#[serde(rename_all = "snake_case")]
488pub enum BorderCollapse {
489 Separate,
492 Collapse,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
499#[serde(rename_all = "snake_case")]
500pub enum ColumnSpan {
501 None,
503 All,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
511pub struct DecorationLine {
512 pub under: bool,
514 pub over: bool,
516 pub through: bool,
518}
519
520impl DecorationLine {
521 pub const NONE: DecorationLine = DecorationLine {
523 under: false,
524 over: false,
525 through: false,
526 };
527
528 pub fn draws(self) -> bool {
530 self.under || self.over || self.through
531 }
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
536#[serde(rename_all = "snake_case")]
537pub enum DecorationStyle {
538 #[default]
540 Solid,
541 Double,
543}
544
545impl DecorationStyle {
546 pub fn rules(self) -> u8 {
548 match self {
549 DecorationStyle::Solid => 1,
550 DecorationStyle::Double => 2,
551 }
552 }
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
558pub struct TextDecoration {
559 pub line: DecorationLine,
561 pub color: Option<Color>,
563 pub style: DecorationStyle,
565 pub thickness: Option<f32>,
568}
569
570impl TextDecoration {
571 pub const NONE: TextDecoration = TextDecoration {
574 line: DecorationLine::NONE,
575 color: None,
576 style: DecorationStyle::Solid,
577 thickness: None,
578 };
579
580 pub fn draws(&self) -> bool {
582 self.line.draws()
583 }
584}