Skip to main content

fleuron/style/properties/
value.rs

1//! The simple values: a length, a colour, and the keywords a
2//! property takes one of.
3
4use serde::de::{Error as _, Unexpected};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::fonts::{FeatureSetting, GenericFamily};
8use crate::pages::{Side, fade};
9
10/// A CSS length, before it is resolved against what it is relative to.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Length {
13    /// An absolute length, already in points.
14    Points(f32),
15    /// A multiple of the font size in force.
16    Em(f32),
17    /// A multiple of the root font size.
18    Rem(f32),
19    /// A fraction of the reference the property names.
20    Percent(f32),
21}
22
23impl Length {
24    /// The length in points. `relative` is what `em` and percentages
25    /// are measured against — the parent font size for `font-size`,
26    /// the element's own for everything else.
27    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/// `line-height`, before the font size it multiplies is known.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum LineHeight {
40    /// The font's own idea of leading.
41    Normal,
42    /// A multiple of the font size.
43    Number(f32),
44    /// A length, which computes to the multiple it works out as.
45    Length(Length),
46}
47
48impl LineHeight {
49    /// The unitless multiple this computes to at `size`.
50    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
65/// What `line-height: normal` works out to. The strut takes its
66/// ascent and descent from the font; this is the factor over them.
67pub(super) const NORMAL_LINE_HEIGHT: f32 = 1.2;
68
69/// A colour: three eight-bit channels and an eight-bit alpha, where
70/// 255 is opaque.
71///
72/// Serialization has two forms, `#rrggbb` where a person reads it
73/// and the four bytes on the wire. A colour that is not opaque reads
74/// as `#rrggbbaa`.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct Color {
77    /// Red channel.
78    pub r: u8,
79    /// Green channel.
80    pub g: u8,
81    /// Blue channel.
82    pub b: u8,
83    /// Alpha: 0 is transparent and 255 is opaque.
84    pub a: u8,
85}
86
87impl Color {
88    /// What a page is set in until a rule sets something else.
89    pub const BLACK: Color = Color::rgb(0, 0, 0);
90
91    /// An opaque colour from its three channels.
92    pub const fn rgb(r: u8, g: u8, b: u8) -> Color {
93        Color { r, g, b, a: 255 }
94    }
95
96    /// A colour from its three channels and its alpha.
97    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
98        Color { r, g, b, a }
99    }
100
101    /// Whether nothing under the colour shows through it.
102    pub const fn opaque(self) -> bool {
103        self.a == 255
104    }
105
106    /// The same colour with its alpha scaled by `opacity`, from 0 to 1.
107    pub fn faded(self, opacity: f32) -> Color {
108        Color {
109            a: fade(self.a, opacity),
110            ..self
111        }
112    }
113
114    /// The colour written the way CSS writes it, for a painter
115    /// that takes a string.
116    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    /// A colour from `#rrggbb` or `#rrggbbaa`, and `None` for anything
126    /// else.
127    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/// A font family as a stylesheet names it.
163#[derive(Debug, Clone, PartialEq, Serialize)]
164#[serde(rename_all = "snake_case")]
165pub enum Family {
166    /// A family name to match against the registry.
167    Named(String),
168    /// A generic keyword the registry binds to a face.
169    Generic(GenericFamily),
170}
171
172/// Upright or italic.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
174#[serde(rename_all = "snake_case")]
175pub enum FontStyle {
176    /// `normal`
177    Normal,
178    /// `italic`, and `oblique` with it.
179    Italic,
180}
181
182/// How a line's inline content is distributed across the measure.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
184#[serde(rename_all = "snake_case")]
185pub enum TextAlign {
186    /// `left`
187    Left,
188    /// `right`
189    Right,
190    /// `center`
191    Center,
192    /// `justify`
193    Justify,
194}
195
196/// What justification opens up to fill the measure.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
198#[serde(rename_all = "snake_case")]
199pub enum TextJustify {
200    /// `auto`, and `inter-word` with it: the space between words is
201    /// the only thing that gives.
202    InterWord,
203    /// `inter-character`: the space between letters gives too, a
204    /// little.
205    InterCharacter,
206}
207
208/// Which capitals a run is drawn with, from `font-variant-caps`.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
210#[serde(rename_all = "snake_case")]
211pub enum FontVariantCaps {
212    /// `normal`: the letters the text is written in.
213    Normal,
214    /// `small-caps`: lowercase letters set as small capitals.
215    SmallCaps,
216}
217
218/// Which ligatures a run is set with, from
219/// `font-variant-ligatures`. A group left at `None` keeps whatever
220/// the shaper does with it, which is `liga`, `clig` and `calt` on
221/// and the rest off.
222#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
223pub struct FontVariantLigatures {
224    /// `common-ligatures` or `no-common-ligatures`: `liga` and
225    /// `clig`.
226    pub common: Option<bool>,
227    /// `discretionary-ligatures` or `no-discretionary-ligatures`:
228    /// `dlig`.
229    pub discretionary: Option<bool>,
230    /// `historical-ligatures` or `no-historical-ligatures`: `hlig`.
231    pub historical: Option<bool>,
232    /// `contextual` or `no-contextual`: `calt`.
233    pub contextual: Option<bool>,
234}
235
236impl FontVariantLigatures {
237    /// `normal`: every group left to the shaper.
238    pub const NORMAL: FontVariantLigatures = FontVariantLigatures {
239        common: None,
240        discretionary: None,
241        historical: None,
242        contextual: None,
243    };
244
245    /// `none`: every ligature off.
246    pub const NONE: FontVariantLigatures = FontVariantLigatures {
247        common: Some(false),
248        discretionary: Some(false),
249        historical: Some(false),
250        contextual: Some(false),
251    };
252
253    /// The features this value asks the face for.
254    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/// Which figures a run is set with, from `font-variant-numeric`.
273#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
274pub struct FontVariantNumeric {
275    /// `lining-nums` or `oldstyle-nums`.
276    pub figures: Option<Figures>,
277    /// `proportional-nums` or `tabular-nums`.
278    pub spacing: Option<NumericSpacing>,
279    /// `diagonal-fractions` or `stacked-fractions`.
280    pub fractions: Option<Fractions>,
281    /// `ordinal`: the letters after a number in `1st`.
282    pub ordinal: bool,
283    /// `slashed-zero`.
284    pub slashed_zero: bool,
285}
286
287/// Which shape the figures take.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
289#[serde(rename_all = "kebab-case")]
290pub enum Figures {
291    /// `lining-nums`: figures that stand on the baseline at cap
292    /// height.
293    Lining,
294    /// `oldstyle-nums`: figures that rise and fall around it.
295    OldStyle,
296}
297
298/// How much width each figure takes.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
300#[serde(rename_all = "kebab-case")]
301pub enum NumericSpacing {
302    /// `proportional-nums`: each figure takes the width it draws.
303    Proportional,
304    /// `tabular-nums`: every figure takes one width, so columns of
305    /// them line up.
306    Tabular,
307}
308
309/// How a fraction is set.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
311#[serde(rename_all = "kebab-case")]
312pub enum Fractions {
313    /// `diagonal-fractions`: the figures stand either side of a
314    /// slash.
315    Diagonal,
316    /// `stacked-fractions`: one figure over the other.
317    Stacked,
318}
319
320impl FontVariantNumeric {
321    /// `normal`: the face's own figures.
322    pub const NORMAL: FontVariantNumeric = FontVariantNumeric {
323        figures: None,
324        spacing: None,
325        fractions: None,
326        ordinal: false,
327        slashed_zero: false,
328    };
329
330    /// The features this value asks the face for.
331    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/// Which alternate glyphs a run is set with, from
356/// `font-variant-alternates`.
357#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize)]
358#[serde(rename_all = "kebab-case")]
359pub enum FontVariantAlternates {
360    /// `normal`: the glyphs the face draws by default.
361    #[default]
362    Normal,
363    /// `historical-forms`: `hist`, such as the long s.
364    HistoricalForms,
365}
366
367impl FontVariantAlternates {
368    /// The features this value asks the face for.
369    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/// What a run's letters are transformed to before they are shaped,
380/// from `text-transform`.
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
382#[serde(rename_all = "snake_case")]
383pub enum TextTransform {
384    /// `none`
385    None,
386    /// `uppercase`
387    Uppercase,
388    /// `lowercase`
389    Lowercase,
390    /// `capitalize`: the first letter of every word.
391    Capitalize,
392}
393
394impl TextTransform {
395    /// Writes one character as this transform spells it, and answers
396    /// whether that differs from what was read. Case mapping is not
397    /// one for one, since `ß` uppercases to two letters, so what is
398    /// written is a stretch of text rather than a character.
399    ///
400    /// `word_start` is whether the character opens a word, which is
401    /// the only thing `capitalize` reads.
402    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/// Whether words may be broken at syllable boundaries.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
418#[serde(rename_all = "snake_case")]
419pub enum Hyphens {
420    /// `none`, and `manual` with it: only explicit soft hyphens break.
421    None,
422    /// `auto`
423    Auto,
424}
425
426/// A fragmentation instruction: the value of `break-before`,
427/// `break-after` and `break-inside`. `recto` and `verso` are the book's names
428/// for `right` and `left`.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
430#[serde(rename_all = "snake_case")]
431pub enum Break {
432    /// `auto`
433    Auto,
434    /// `avoid`
435    Avoid,
436    /// `column`: the flow moves to the next column, and only the last
437    /// column of a page moving on ends the page.
438    Column,
439    /// `page`
440    Page,
441    /// Break to the next page that falls on the given side, leaving a
442    /// blank behind if the flow sits on the wrong one.
443    Side(Side),
444}
445
446/// What a block's decoration does where a page break splits it, from
447/// `box-decoration-break`.
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
449#[serde(rename_all = "snake_case")]
450pub enum BoxDecorationBreak {
451    /// `slice`: the box is drawn as one and cut, so the two edges
452    /// the break made are open.
453    Slice,
454    /// `clone`: each piece is a box of its own, closed on all four
455    /// edges.
456    Clone,
457}
458
459/// The size a box is asked to take on one axis, from `width`,
460/// `height` or `min-height`.
461#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
462#[serde(rename_all = "snake_case")]
463pub enum Width {
464    /// `auto`: the box takes the size that layout gives it.
465    Auto,
466    /// A length, in points.
467    Points(f32),
468    /// A percentage of the same axis of the box around it.
469    Percent(f32),
470}
471
472impl Width {
473    /// The size in points inside a box `of` points on the same axis,
474    /// or `None` for `auto`.
475    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/// Whether the cells of a table share their borders, from
485/// `border-collapse`.
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
487#[serde(rename_all = "snake_case")]
488pub enum BorderCollapse {
489    /// `separate`: every cell draws its own border, beside the border
490    /// of the cell next to it.
491    Separate,
492    /// `collapse`: two cells that meet draw one border between them.
493    Collapse,
494}
495
496/// How many columns of a divided page a block is set across, from
497/// `column-span`.
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
499#[serde(rename_all = "snake_case")]
500pub enum ColumnSpan {
501    /// `none`: the block is set in one column.
502    None,
503    /// `all`: the block is set across the whole content box, with
504    /// columns above it and columns below it.
505    All,
506}
507
508/// Which rules a run has drawn across it, from
509/// `text-decoration-line`.
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
511pub struct DecorationLine {
512    /// `underline`: a rule under the text.
513    pub under: bool,
514    /// `overline`: a rule over it.
515    pub over: bool,
516    /// `line-through`: a rule across it.
517    pub through: bool,
518}
519
520impl DecorationLine {
521    /// No rule at all, which is `none`.
522    pub const NONE: DecorationLine = DecorationLine {
523        under: false,
524        over: false,
525        through: false,
526    };
527
528    /// Whether any rule is drawn.
529    pub fn draws(self) -> bool {
530        self.under || self.over || self.through
531    }
532}
533
534/// How one rule is drawn, from `text-decoration-style`.
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
536#[serde(rename_all = "snake_case")]
537pub enum DecorationStyle {
538    /// `solid`: one rule.
539    #[default]
540    Solid,
541    /// `double`: two rules, one thickness apart.
542    Double,
543}
544
545impl DecorationStyle {
546    /// How many rules one decoration draws.
547    pub fn rules(self) -> u8 {
548        match self {
549            DecorationStyle::Solid => 1,
550            DecorationStyle::Double => 2,
551        }
552    }
553}
554
555/// What a run has drawn across it: the rules, what they are painted
556/// in, how they are drawn, and how thick they are.
557#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
558pub struct TextDecoration {
559    /// Which rules are drawn.
560    pub line: DecorationLine,
561    /// What they are painted in. `None` is the colour of the text.
562    pub color: Option<Color>,
563    /// How each one is drawn.
564    pub style: DecorationStyle,
565    /// How thick each one is, in points. `None` is the thickness the
566    /// face declares.
567    pub thickness: Option<f32>,
568}
569
570impl TextDecoration {
571    /// Nothing drawn, which is what a run takes until a rule asks
572    /// for something.
573    pub const NONE: TextDecoration = TextDecoration {
574        line: DecorationLine::NONE,
575        color: None,
576        style: DecorationStyle::Solid,
577        thickness: None,
578    };
579
580    /// Whether anything is drawn.
581    pub fn draws(&self) -> bool {
582        self.line.draws()
583    }
584}