1use crate::color::Color;
2
3#[derive(Debug, Clone, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct RichText {
7 pub segments: Vec<RichSegment>,
8 pub color: Color,
9 pub font_size: f32,
10 pub font: String,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct RichSegment {
19 pub text: char,
20 pub bold: bool,
21 pub italic: bool,
22 pub underline: bool,
23 pub superscript: bool,
24 pub subscript: bool,
25 pub greek: bool,
26 #[cfg_attr(
28 feature = "serde",
29 serde(default, skip_serializing_if = "Option::is_none")
30 )]
31 pub color: Option<Color>,
32 #[cfg_attr(
35 feature = "serde",
36 serde(default, skip_serializing_if = "Option::is_none")
37 )]
38 pub font_size: Option<f32>,
39 #[cfg_attr(
44 feature = "serde",
45 serde(default, skip_serializing_if = "Option::is_none")
46 )]
47 pub field_em: Option<f32>,
48 #[cfg_attr(
52 feature = "serde",
53 serde(default, skip_serializing_if = "std::ops::Not::not")
54 )]
55 pub rule: bool,
56 #[cfg_attr(
59 feature = "serde",
60 serde(default, skip_serializing_if = "Option::is_none")
61 )]
62 pub rule_dash: Option<Vec<f32>>,
63}
64
65impl RichSegment {
66 pub fn plain(text: char) -> Self {
69 Self {
70 text,
71 bold: false,
72 italic: false,
73 underline: false,
74 superscript: false,
75 subscript: false,
76 greek: false,
77 color: None,
78 font_size: None,
79 field_em: None,
80 rule: false,
81 rule_dash: None,
82 }
83 }
84
85 pub fn rule(width_em: f32, color: Option<Color>) -> Self {
89 Self {
90 field_em: Some(width_em),
91 rule: true,
92 color,
93 ..Self::plain('—')
94 }
95 }
96
97 pub fn dashed_rule(width_em: f32, color: Option<Color>, dash_em: Vec<f32>) -> Self {
100 Self {
101 rule_dash: (!dash_em.is_empty()).then_some(dash_em),
102 ..Self::rule(width_em, color)
103 }
104 }
105
106 pub fn fielded(text: char, width_em: f32, color: Option<Color>) -> Self {
108 Self {
109 field_em: Some(width_em),
110 color,
111 ..Self::plain(text)
112 }
113 }
114}
115
116impl RichText {
117 pub fn plain(text: &str, color: Color, font_size: f32, font: impl Into<String>) -> Self {
118 Self {
119 segments: rich_segments_from_text(text),
120 color,
121 font_size,
122 font: font.into(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
132pub struct TextExtents {
133 pub width: f32,
134 pub ascent: f32,
135 pub descent: f32,
136}
137
138impl TextExtents {
139 pub fn height(&self) -> f32 {
140 self.ascent + self.descent
141 }
142}
143
144pub trait MeasureText {
150 fn measure_rich(&self, rt: &RichText) -> TextExtents;
151}
152
153pub fn rich_segments_from_text(text: &str) -> Vec<RichSegment> {
157 text.chars().map(rich_segment_from_char).collect()
158}
159
160fn rich_segment_from_char(c: char) -> RichSegment {
161 let (text, superscript, subscript) = match c {
162 '⁰' => ('0', true, false),
163 '¹' => ('1', true, false),
164 '²' => ('2', true, false),
165 '³' => ('3', true, false),
166 '⁴' => ('4', true, false),
167 '⁵' => ('5', true, false),
168 '⁶' => ('6', true, false),
169 '⁷' => ('7', true, false),
170 '⁸' => ('8', true, false),
171 '⁹' => ('9', true, false),
172 '⁺' => ('+', true, false),
173 '⁻' => ('-', true, false),
174 '⁽' => ('(', true, false),
175 '⁾' => (')', true, false),
176 'ⁿ' => ('n', true, false),
177 'ᵗ' => ('t', true, false),
178 '₀' => ('0', false, true),
179 '₁' => ('1', false, true),
180 '₂' => ('2', false, true),
181 '₃' => ('3', false, true),
182 '₄' => ('4', false, true),
183 '₅' => ('5', false, true),
184 '₆' => ('6', false, true),
185 '₇' => ('7', false, true),
186 '₈' => ('8', false, true),
187 '₉' => ('9', false, true),
188 '₊' => ('+', false, true),
189 '₋' => ('-', false, true),
190 '₍' => ('(', false, true),
191 '₎' => (')', false, true),
192 other => (other, false, false),
193 };
194 RichSegment {
195 superscript,
196 subscript,
197 ..RichSegment::plain(text)
198 }
199}
200
201pub fn greek_char(c: char) -> char {
204 match c {
205 'a' => 'α',
206 'b' => 'β',
207 'c' => 'χ',
208 'd' => 'δ',
209 'e' => 'ε',
210 'f' => 'φ',
211 'g' => 'γ',
212 'h' => 'η',
213 'i' => 'ι',
214 'j' => 'ϕ',
215 'k' => 'κ',
216 'l' => 'λ',
217 'm' => 'μ',
218 'n' => 'ν',
219 'o' => 'ο',
220 'p' => 'π',
221 'q' => 'θ',
222 'r' => 'ρ',
223 's' => 'σ',
224 't' => 'τ',
225 'u' => 'υ',
226 'v' => 'ϖ',
227 'w' => 'ω',
228 'x' => 'ξ',
229 'y' => 'ψ',
230 'z' => 'ζ',
231 'A' => 'Α',
232 'B' => 'Β',
233 'C' => 'Χ',
234 'D' => 'Δ',
235 'E' => 'Ε',
236 'F' => 'Φ',
237 'G' => 'Γ',
238 'H' => 'Η',
239 'I' => 'Ι',
240 'J' => 'ϑ',
241 'K' => 'Κ',
242 'L' => 'Λ',
243 'M' => 'Μ',
244 'N' => 'Ν',
245 'O' => 'Ο',
246 'P' => 'Π',
247 'Q' => 'Θ',
248 'R' => 'Ρ',
249 'S' => 'Σ',
250 'T' => 'Τ',
251 'U' => 'Υ',
252 'V' => 'ς',
253 'W' => 'Ω',
254 'X' => 'Ξ',
255 'Y' => 'Ψ',
256 'Z' => 'Ζ',
257 other => other,
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn lowercase_full_table() {
267 let pairs: &[(char, char)] = &[
268 ('a', 'α'),
269 ('b', 'β'),
270 ('c', 'χ'),
271 ('d', 'δ'),
272 ('e', 'ε'),
273 ('f', 'φ'),
274 ('g', 'γ'),
275 ('h', 'η'),
276 ('i', 'ι'),
277 ('j', 'ϕ'),
278 ('k', 'κ'),
279 ('l', 'λ'),
280 ('m', 'μ'),
281 ('n', 'ν'),
282 ('o', 'ο'),
283 ('p', 'π'),
284 ('q', 'θ'),
285 ('r', 'ρ'),
286 ('s', 'σ'),
287 ('t', 'τ'),
288 ('u', 'υ'),
289 ('v', 'ϖ'),
290 ('w', 'ω'),
291 ('x', 'ξ'),
292 ('y', 'ψ'),
293 ('z', 'ζ'),
294 ];
295 for &(l, g) in pairs {
296 assert_eq!(greek_char(l), g, "lower '{}' -> '{}'", l, g);
297 }
298 }
299
300 #[test]
301 fn uppercase_full_table() {
302 let pairs: &[(char, char)] = &[
303 ('A', 'Α'),
304 ('B', 'Β'),
305 ('C', 'Χ'),
306 ('D', 'Δ'),
307 ('E', 'Ε'),
308 ('F', 'Φ'),
309 ('G', 'Γ'),
310 ('H', 'Η'),
311 ('I', 'Ι'),
312 ('J', 'ϑ'),
313 ('K', 'Κ'),
314 ('L', 'Λ'),
315 ('M', 'Μ'),
316 ('N', 'Ν'),
317 ('O', 'Ο'),
318 ('P', 'Π'),
319 ('Q', 'Θ'),
320 ('R', 'Ρ'),
321 ('S', 'Σ'),
322 ('T', 'Τ'),
323 ('U', 'Υ'),
324 ('V', 'ς'),
325 ('W', 'Ω'),
326 ('X', 'Ξ'),
327 ('Y', 'Ψ'),
328 ('Z', 'Ζ'),
329 ];
330 for &(l, g) in pairs {
331 assert_eq!(greek_char(l), g, "upper '{}' -> '{}'", l, g);
332 }
333 }
334
335 #[test]
336 fn non_latin_passthrough() {
337 for c in ['0', '9', ' ', '+', '=', '.', '-', '_', 'α', 'Ω', '漢'] {
338 assert_eq!(greek_char(c), c);
339 }
340 }
341
342 #[cfg(feature = "serde")]
345 #[test]
346 fn segment_overrides_stay_backward_compatible_and_compact() {
347 let old = r#"{"text":"a","bold":false,"italic":false,"underline":false,"superscript":false,"subscript":false,"greek":false}"#;
348 let seg: RichSegment = serde_json::from_str(old).expect("pre-override document parses");
349 assert_eq!(seg, RichSegment::plain('a'));
350
351 let json = serde_json::to_string(&seg).unwrap();
352 assert!(
353 !json.contains("color"),
354 "None color must be skipped: {json}"
355 );
356 assert!(
357 !json.contains("font_size"),
358 "None font_size must be skipped: {json}"
359 );
360 }
361}