1use std::ops::Range;
4
5use crate::fonts::{Features, ShapedGlyph};
6
7use super::LineLayout;
8use super::flatten::{FlatParagraph, StyleSpan};
9use super::line::{ShapedRun, cut_runs};
10use super::paragraph::ParagraphStyle;
11
12impl LineLayout<'_> {
13 pub fn shape(&self, text: &str, style: &ParagraphStyle) -> Option<Vec<ShapedRun>> {
17 let upem = self.registry.metrics(style.font_id)?.units_per_em as f32;
18 let mut flat = FlatParagraph::new();
19 flat.push_styled(text, style, self.small_caps(style));
20 let shaped = self.shape_spans(&flat, style, upem);
21 Some(cut_runs(&flat, &shaped, 0, flat.text.len()))
22 }
23
24 pub(super) fn shape_spans(
32 &self,
33 flat: &FlatParagraph,
34 style: &ParagraphStyle,
35 upem: f32,
36 ) -> Vec<ShapedSpan> {
37 flat.spans
38 .iter()
39 .map(|span| {
40 let mut glyphs = self
41 .registry
42 .shape_with(span.font_id, &flat.text[span.range.clone()], &span.features)
43 .unwrap_or_default();
44 let track = self.tracking_units(span);
45 if track != 0 {
46 for at in 0..glyphs.len() {
47 let last = glyphs
48 .get(at + 1)
49 .is_none_or(|next| next.cluster != glyphs[at].cluster);
50 if last {
51 glyphs[at].x_advance =
52 (glyphs[at].x_advance as i64 + track).max(0) as u32;
53 }
54 }
55 }
56 let scale = self.scale(span.font_id, span.size, style, upem);
57 ShapedSpan {
58 range: span.range.clone(),
59 scale,
60 track,
61 tracking: track as f32 * scale,
62 glyphs,
63 }
64 })
65 .collect()
66 }
67
68 fn tracking_units(&self, span: &StyleSpan) -> i64 {
70 if span.tracking == 0.0 || span.size <= 0.0 {
71 return 0;
72 }
73 let upem = self
74 .registry
75 .metrics(span.font_id)
76 .map(|m| m.units_per_em as f32)
77 .unwrap_or(1000.0);
78 (span.tracking / span.size * upem).round() as i64
79 }
80
81 fn scale(&self, font_id: u16, size: f32, style: &ParagraphStyle, upem: f32) -> f32 {
85 let span_upem = self
86 .registry
87 .metrics(font_id)
88 .map(|m| m.units_per_em as f32)
89 .unwrap_or(upem);
90 if span_upem <= 0.0 || style.size <= 0.0 {
91 return 1.0;
92 }
93 size / span_upem * upem / style.size
94 }
95
96 pub(super) fn hyphenate(&self, runs: &mut Vec<ShapedRun>, style: &ParagraphStyle) {
103 let Some(id) = self.registry.char_glyph(style.font_id, '-') else {
104 return;
105 };
106 let advance = self
107 .registry
108 .advance_width(style.font_id, id)
109 .unwrap_or_default() as u32;
110 let ending = runs
111 .last()
112 .map(|run| run.text_start + run.text.len() as u32)
113 .unwrap_or_default();
114 let color = runs.last().map_or(style.color, |run| run.color);
118 let decoration = runs.last().map_or(style.decoration, |run| run.decoration);
121 let inline = runs.last().and_then(|run| run.inline);
124 match runs.last_mut() {
125 Some(run) if run.font_id == style.font_id && run.size == style.size => {
126 run.text.push('-');
127 if let Some(end) = run.source_map.last().copied() {
131 run.source_map.push(end);
132 }
133 run.glyphs.push(ShapedGlyph {
134 id,
135 x_advance: advance,
136 cluster: ending,
137 });
138 run.advance += advance;
139 }
140 _ => runs.push(ShapedRun {
144 font_id: style.font_id,
145 size: style.size,
146 text: "-".to_string(),
147 source: String::new(),
148 source_map: Vec::new(),
149 text_start: ending,
150 origin: None,
151 pseudo_element: None,
152 inline,
153 lead: 0.0,
154 trail: 0.0,
155 features: Features::NONE,
156 color,
157 decoration,
158 glyphs: vec![ShapedGlyph {
159 id,
160 x_advance: advance,
161 cluster: ending,
162 }],
163 advance,
164 }),
165 }
166 }
167
168 pub(super) fn hyphen_advance(&self, style: &ParagraphStyle) -> u32 {
169 self.registry
170 .char_glyph(style.font_id, '-')
171 .and_then(|g| self.registry.advance_width(style.font_id, g))
172 .unwrap_or(0) as u32
173 }
174}
175
176pub(super) struct ShapedSpan {
179 pub(super) range: Range<usize>,
181 pub(super) scale: f32,
184 pub(super) track: i64,
187 pub(super) tracking: f32,
189 pub(super) glyphs: Vec<ShapedGlyph>,
190}
191
192impl ShapedSpan {
193 pub(super) fn glyphs_in(&self, start: usize, end: usize) -> Vec<ShapedGlyph> {
196 self.glyphs
197 .iter()
198 .filter(|g| {
199 let cluster = self.range.start + g.cluster as usize;
200 cluster >= start && cluster < end
201 })
202 .map(|g| ShapedGlyph {
203 cluster: g.cluster + self.range.start as u32,
204 ..*g
205 })
206 .collect()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use crate::content::{Attributes, Inline, NodeId};
213 use crate::lines::flatten::SMALL_CAPS_RATIO;
214 use crate::lines::testing::{
215 body, hyphenated, layout_body_opts, layout_style, line_text, registry, units_per_em,
216 };
217 use crate::lines::{LineBreakOptions, LineLayout, Measure, Opening, ParagraphStyle};
218 use crate::style::FontVariantCaps;
219
220 #[test]
223 fn a_hyphenated_line_paints_the_hyphen_it_paid_for() {
224 let text = "extraordinarily";
225 let lines = layout_body_opts(text, 53.0, hyphenated());
226 let first = &lines[0];
227 assert_eq!(line_text(first), "extraordi-");
228
229 let run = first.runs.last().expect("the line has no runs");
230 assert_eq!(
231 run.glyphs.len(),
232 run.text.chars().count(),
233 "the hyphen is in the text and not in the glyphs",
234 );
235 let ranges = run.glyph_ranges();
236 let last = ranges.last().expect("the run has no glyphs");
237 assert_eq!(
238 &run.text[last.start as usize..last.end as usize],
239 "-",
240 "the last glyph does not stand for the hyphen",
241 );
242
243 let hyphen = run.glyphs.last().expect("the run has no glyphs").x_advance;
246 assert!(hyphen > 0, "the hyphen has no advance");
247 assert_eq!(
248 first.width,
249 first.runs.iter().map(|r| r.advance).sum::<u32>(),
250 "the line's width and its runs disagree",
251 );
252
253 let last_line = lines.last().expect("no lines");
254 assert!(
255 !line_text(last_line).ends_with('-'),
256 "the last line was hyphenated: {:?}",
257 line_text(last_line),
258 );
259 }
260
261 #[test]
266 fn emphasis_shapes_on_its_own_face() {
267 let mut book = crate::content::Book {
268 metadata: Default::default(),
269 sections: vec![crate::content::Section {
270 attributes: Default::default(),
271 blocks: vec![crate::content::Block::Paragraph {
272 id: NodeId::UNASSIGNED,
273 inlines: vec![
274 Inline::Text {
275 id: NodeId::UNASSIGNED,
276 value: "He said ".into(),
277 attributes: Attributes::default(),
278 position: None,
279 span: None,
280 },
281 Inline::Emphasis {
282 id: NodeId::UNASSIGNED,
283 children: vec![
284 Inline::Text {
285 id: NodeId::UNASSIGNED,
286 value: "never ".into(),
287 attributes: Attributes::default(),
288 position: None,
289 span: None,
290 },
291 Inline::Strong {
292 id: NodeId::UNASSIGNED,
293 children: vec![Inline::Text {
294 id: NodeId::UNASSIGNED,
295 value: "again".into(),
296 attributes: Attributes::default(),
297 position: None,
298 span: None,
299 }],
300 attributes: Attributes::default(),
301 position: None,
302 span: None,
303 },
304 ],
305 attributes: Attributes::default(),
306 position: None,
307 span: None,
308 },
309 Inline::Text {
310 id: NodeId::UNASSIGNED,
311 value: " to her.".into(),
312 attributes: Attributes::default(),
313 position: None,
314 span: None,
315 },
316 ],
317 attributes: Attributes::default(),
318 position: None,
319 span: None,
320 }],
321 ..Default::default()
322 }],
323 };
324 book.assign_node_ids();
325 let styles = crate::style::defaults(&book, registry());
326 let crate::content::Block::Paragraph { id, inlines, .. } = &book.sections[0].blocks[0]
327 else {
328 unreachable!()
329 };
330 let lines = LineLayout::new(registry()).layout_styled(
331 inlines,
332 &styles.paragraph(*id),
333 &styles,
334 &Measure::uniform(400.0),
335 Default::default(),
336 Opening::default(),
337 );
338 assert_eq!(lines.len(), 1);
339 let runs: Vec<(u16, &str)> = lines[0]
340 .runs
341 .iter()
342 .map(|run| (run.font_id, run.text.as_str()))
343 .collect();
344 let face = |italic, weight| {
345 registry()
346 .select(
347 "eb garamond",
348 crate::fonts::FaceAttributes { italic, weight },
349 )
350 .unwrap()
351 .id
352 };
353 assert_eq!(
354 runs,
355 vec![
356 (face(false, 400), "He said "),
357 (face(true, 400), "never "),
358 (face(true, 700), "again"),
359 (face(false, 400), " to her."),
360 ],
361 );
362 }
363
364 #[test]
369 fn letter_spacing_opens_the_gaps_between_glyphs() {
370 let plain = layout_style("HANDGLOVES", 400.0, &body());
371 let tracking = 0.08 * body().size;
372 let tracked = layout_style(
373 "HANDGLOVES",
374 400.0,
375 &ParagraphStyle {
376 letter_spacing: tracking,
377 ..body()
378 },
379 );
380 let glyphs = plain[0].runs[0].glyphs.len();
381 assert_eq!(glyphs, 10, "the title shaped to one glyph a letter");
382 let units = tracking / body().size * units_per_em() as f32;
383 assert_eq!(
384 tracked[0].width - plain[0].width,
385 (units * (glyphs - 1) as f32).round() as u32,
386 "tracking did not open exactly the gaps between the glyphs",
387 );
388 for (loose, tight) in tracked[0].runs[0]
389 .glyphs
390 .iter()
391 .zip(&plain[0].runs[0].glyphs)
392 .take(glyphs - 1)
393 {
394 assert_eq!(
395 loose.x_advance - tight.x_advance,
396 units.round() as u32,
397 "a glyph has no tracking of its own",
398 );
399 }
400 }
401
402 #[test]
409 fn small_caps_take_the_feature_or_a_synthesis() {
410 let style = ParagraphStyle {
411 caps: FontVariantCaps::SmallCaps,
412 ..body()
413 };
414 let plain = layout_style("hello", 400.0, &body());
415 let feature = layout_style("hello", 400.0, &style);
416 assert_eq!(feature[0].runs.len(), 1, "the feature split the run");
417 assert_eq!(feature[0].runs[0].size, body().size);
418 assert_ne!(
419 feature[0].runs[0]
420 .glyphs
421 .iter()
422 .map(|g| g.id)
423 .collect::<Vec<_>>(),
424 plain[0].runs[0]
425 .glyphs
426 .iter()
427 .map(|g| g.id)
428 .collect::<Vec<_>>(),
429 "the face's small capitals drew the lowercase glyphs",
430 );
431
432 let bare = crate::fonts::registry_without_substitutions();
433 assert!(!bare.has_small_caps(0), "the face still substitutes");
434 let inlines = vec![Inline::Text {
435 id: NodeId::UNASSIGNED,
436 value: "hi Ho".to_string(),
437 attributes: Attributes::default(),
438 position: None,
439 span: None,
440 }];
441 let synthesized =
442 LineLayout::new(&bare).layout(&inlines, &style, 400.0, LineBreakOptions::default());
443 let runs: Vec<(f32, &str, &str)> = synthesized[0]
444 .runs
445 .iter()
446 .map(|run| (run.size, run.text.as_str(), run.source.as_str()))
447 .collect();
448 assert_eq!(
449 runs,
450 vec![
451 (body().size * SMALL_CAPS_RATIO, "HI", "hi"),
452 (body().size, " H", ""),
453 (body().size * SMALL_CAPS_RATIO, "O", "o"),
454 ],
455 "the synthesis did not raise what was lowercase, leave the rest, \
456 and keep what the author wrote",
457 );
458 }
459}