Skip to main content

valo_text/
style.rs

1use valo_geometry::{Color, Point};
2
3/// How one span of text looks (skparagraph's TextStyle, the subset valo implements).
4/// Families are tried in order per character (then the collection's fallback
5/// chain); `weight`/`italic` pick within a family's variants.
6#[derive(Clone, Debug, PartialEq)]
7pub struct TextStyle {
8    pub families: Vec<String>,
9    /// CSS weight, 100–900.
10    pub weight: u16,
11    pub italic: bool,
12    /// CSS `font-width` (legacy `font-stretch`) percentage; 100 is normal.
13    /// Selects among a family's registered widths — valo never synthesizes
14    /// one, and neither do browsers.
15    pub stretch: f32,
16    /// Let the font kern (`kern`). Canvas2D's `fontKerning: "none"` clears it.
17    pub kerning: bool,
18    /// OpenType capital-letter forms — Canvas2D's `fontVariantCaps`.
19    pub variant_caps: VariantCaps,
20    pub size: f32,
21    pub color: Color,
22    /// Added after every grapheme cluster (px).
23    pub letter_spacing: f32,
24    /// Added after every U+0020 cluster (px), on top of `letter_spacing`.
25    pub word_spacing: f32,
26    /// Line-height multiplier: `Some(1.5)` = 1.5 × size, metrics scaled
27    /// proportionally (skparagraph's setHeight + heightOverride). `None` =
28    /// the font's own metrics.
29    pub height: Option<f32>,
30    pub decoration: Option<Decoration>,
31    /// Painted back-to-front UNDER the text, each a blurred offset copy —
32    /// Flutter's TextStyle.shadows lowering.
33    pub shadows: Vec<Shadow>,
34}
35
36impl Default for TextStyle {
37    fn default() -> Self {
38        Self {
39            families: Vec::new(),
40            weight: 400,
41            italic: false,
42            stretch: crate::font::NORMAL_STRETCH,
43            kerning: true,
44            variant_caps: VariantCaps::Normal,
45            size: 14.0,
46            color: Color::BLACK,
47            letter_spacing: 0.0,
48            word_spacing: 0.0,
49            height: None,
50            decoration: None,
51            shadows: Vec::new(),
52        }
53    }
54}
55
56impl TextStyle {
57    pub fn new(family: &str, size: f32, color: Color) -> Self {
58        Self {
59            families: vec![family.to_owned()],
60            size,
61            color,
62            ..Default::default()
63        }
64    }
65
66    /// What this style asks of face selection — the CSS matching axes,
67    /// separated from everything shaping and painting care about.
68    pub fn font_attrs(&self) -> crate::font::FontAttrs {
69        crate::font::FontAttrs {
70            weight: self.weight,
71            italic: self.italic,
72            stretch: self.stretch,
73        }
74    }
75}
76
77/// CSS `font-variant-caps`, lowered to the OpenType features a shaper
78/// understands. Every variant here is a font capability: a face without the
79/// feature renders unchanged rather than synthesizing small capitals, which
80/// is what browsers do for `font-synthesis: none` and what valo always does.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum VariantCaps {
83    #[default]
84    Normal,
85    /// `smcp` — lowercase becomes small capitals.
86    SmallCaps,
87    /// `c2sc` + `smcp` — capitals shrink to small capitals too.
88    AllSmallCaps,
89    /// `pcap` — the lighter-weight petite variant.
90    PetiteCaps,
91    /// `c2pc` + `pcap`.
92    AllPetiteCaps,
93    /// `unic` — lowercase-looking capitals.
94    Unicase,
95    /// `titl` — capitals cut for all-caps display sizes.
96    TitlingCaps,
97}
98
99impl VariantCaps {
100    /// The OpenType tags this variant turns ON, in application order.
101    pub fn feature_tags(self) -> &'static [&'static [u8; 4]] {
102        match self {
103            Self::Normal => &[],
104            Self::SmallCaps => &[b"smcp"],
105            Self::AllSmallCaps => &[b"c2sc", b"smcp"],
106            Self::PetiteCaps => &[b"pcap"],
107            Self::AllPetiteCaps => &[b"c2pc", b"pcap"],
108            Self::Unicase => &[b"unic"],
109            Self::TitlingCaps => &[b"titl"],
110        }
111    }
112}
113
114/// An underline / strike-through / overline, drawn from the font's own
115/// decoration metrics (post/OS2 tables; skparagraph's Decorations.cpp).
116#[derive(Clone, Copy, Debug, PartialEq)]
117pub struct Decoration {
118    pub kind: DecorationKind,
119    /// `None` = the text color.
120    pub color: Option<Color>,
121    /// Multiplier over the font's suggested thickness.
122    pub thickness: f32,
123}
124
125impl Decoration {
126    pub fn new(kind: DecorationKind) -> Self {
127        Self {
128            kind,
129            color: None,
130            thickness: 1.0,
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum DecorationKind {
137    Underline,
138    LineThrough,
139    Overline,
140}
141
142/// One text shadow: an offset, optionally blurred copy in `color`.
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct Shadow {
145    pub color: Color,
146    pub offset: Point,
147    /// Gaussian σ; 0 = a hard offset copy.
148    pub blur: f32,
149}
150
151/// Paragraph-level horizontal alignment (needs a finite layout width).
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub enum TextAlign {
154    #[default]
155    Left,
156    Center,
157    Right,
158    /// Word gaps stretch to fill the width; a paragraph's last line (and
159    /// lines ending in a hard break) stay ragged, CSS-style.
160    Justify,
161}
162
163impl From<TextAlign> for ParagraphStyle {
164    fn from(align: TextAlign) -> Self {
165        Self {
166            align,
167            ..Default::default()
168        }
169    }
170}
171
172/// Which way the paragraph reads before its content gets a say.
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum TextDirection {
175    Ltr,
176    Rtl,
177}
178
179/// Paragraph-level knobs, fixed at `build` (Flutter's ParagraphStyle).
180#[derive(Clone, Debug, Default)]
181pub struct ParagraphStyle {
182    pub align: TextAlign,
183    /// The bidi base level. `None` infers it from the first strong character
184    /// (UAX #9 rules P2/P3); `Some(..)` forces it, which is what CSS
185    /// `direction` and Canvas2D's `direction` ask for. Forcing matters for
186    /// text that is entirely neutral — digits and punctuation carry no
187    /// direction of their own, so only the base level orders them.
188    pub direction: Option<TextDirection>,
189    /// Include trailing whitespace in line advances. Canvas text enables this;
190    /// paragraph layout defaults to trimmed line widths like SkParagraph.
191    pub preserve_trailing_whitespace: bool,
192    /// Stop wrapping after this many lines; content past them is dropped
193    /// (see `ellipsis`).
194    pub max_lines: Option<u32>,
195    /// Spliced onto a truncated last line (shaped in that line's trailing
196    /// style), on the visual end matching the paragraph's base direction.
197    pub ellipsis: Option<String>,
198}