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}