Skip to main content

fleuron/lines/
mod.rs

1//! Line layout: text in, broken lines out.
2//!
3//! Knuth-Plass total fit. A paragraph is flattened into style runs,
4//! shaped, and modelled as boxes, glue and penalties; the breaker
5//! picks the set of breaks with the fewest demerits over the whole
6//! paragraph rather than the most text on each line. The break
7//! source is UAX #14 with word boundaries from UAX #29, plus
8//! optional hyphenation, which enters as a flagged penalty.
9//!
10//! Justified text has its glue stretched or shrunk to the measure
11//! here. The adjustment lands on the glyphs' own advances, so a
12//! painter positions what it is given and never re-derives spacing.
13//! A `Line` is shaped runs: measurement happened here.
14//!
15//! Units: advances come out of the shaper in font units; the measure
16//! arrives in points and converts once, via `units_per_em * size`.
17//!
18//! The stages have a file each: `flatten` makes one string of styled
19//! spans, `shape` turns spans into glyphs, `opportunity` says where a
20//! line may end, `breaker` picks the breaks, `justify` moves the glue
21//! on a line that was set to its measure, and `line` is what comes
22//! out.
23
24use crate::content::Inline;
25use crate::fonts::FontRegistry;
26use crate::linebox::{LineBox, Strut};
27use icu_segmenter::{WordSegmenter, options::WordBreakInvariantOptions};
28
29mod breaker;
30mod flatten;
31mod inline;
32mod justify;
33mod line;
34mod measure;
35mod opportunity;
36mod paragraph;
37mod shape;
38
39#[cfg(test)]
40mod testing;
41
42pub use line::{InlineFragment, Line, LineSpan, ShapedRun, Spans};
43pub use measure::{Measure, Span};
44pub use paragraph::{
45    Face, FirstLine, Generated, HangEnd, HangingPunctuation, Inherited, InlineBox, InlineStyles,
46    LineBreakOptions, Opening, ParagraphStyle, Patterns,
47};
48
49use breaker::Breaker;
50use flatten::FlatParagraph;
51use justify::adjust;
52use line::{Widths, cut_runs, gather, tile};
53use paragraph::Lead;
54use shape::ShapedSpan;
55
56/// The layout pass: one paragraph → lines that fit the measure.
57pub struct LineLayout<'a> {
58    registry: &'a FontRegistry,
59    segmenter: WordSegmenterBorrowedStatic,
60}
61
62/// The borrowed, 'static segmenter `WordSegmenter::new_auto` returns.
63type WordSegmenterBorrowedStatic = icu_segmenter::WordSegmenterBorrowed<'static>;
64
65impl<'a> LineLayout<'a> {
66    /// A layout pass over the faces in `registry`.
67    pub fn new(registry: &'a FontRegistry) -> Self {
68        LineLayout {
69            registry,
70            segmenter: WordSegmenter::new_auto(WordBreakInvariantOptions::default()),
71        }
72    }
73}
74
75impl LineLayout<'_> {
76    /// The paragraph's strut: the minimum box every one of its lines
77    /// occupies, whatever the runs on it.
78    pub fn strut(&self, style: &ParagraphStyle) -> Strut {
79        self.registry
80            .metrics(style.font_id)
81            .map(|m| Strut::from_metrics(m, style.size, style.line_height))
82            .unwrap_or_default()
83    }
84
85    /// The box one line occupies: the strut, grown by any run taller
86    /// than it around the shared baseline.
87    pub fn line_box(&self, runs: &[ShapedRun], style: &ParagraphStyle) -> LineBox {
88        let strut = self.strut(style);
89        let mut above = strut.above;
90        let mut below = strut.below;
91        for run in runs {
92            let Some(metrics) = self.registry.metrics(run.font_id) else {
93                continue;
94            };
95            let run_strut = Strut::from_metrics(metrics, run.size, style.line_height);
96            above = above.max(run_strut.above);
97            below = below.max(run_strut.below);
98        }
99        LineBox {
100            baseline: above,
101            height: above + below,
102        }
103    }
104
105    /// Breaks one paragraph into lines of at most `measure_pt`
106    /// points, every inline taking the block's own style.
107    pub fn layout(
108        &self,
109        inlines: &[Inline],
110        style: &ParagraphStyle,
111        measure: impl Into<Measure>,
112        options: LineBreakOptions,
113    ) -> Vec<Line> {
114        self.layout_styled(
115            inlines,
116            style,
117            &Inherited,
118            &measure.into(),
119            options,
120            Opening::default(),
121        )
122    }
123
124    /// The same, with the style tree answering for each inline and
125    /// `opening` saying what sets the line the paragraph opens on
126    /// apart from the rest of it.
127    pub fn layout_styled(
128        &self,
129        inlines: &[Inline],
130        style: &ParagraphStyle,
131        styles: &dyn InlineStyles,
132        measure: &Measure,
133        options: LineBreakOptions,
134        opening: Opening,
135    ) -> Vec<Line> {
136        self.broken(inlines, style, styles, measure, options, opening)
137            .0
138    }
139
140    /// The same, handing back the shaped paragraph beside the lines.
141    ///
142    /// A caller that can break this paragraph again keeps it.
143    /// [`LineLayout::rebreak`] sets the same text to a different
144    /// profile without shaping it twice.
145    pub fn layout_shaped(
146        &self,
147        inlines: &[Inline],
148        style: &ParagraphStyle,
149        styles: &dyn InlineStyles,
150        measure: &Measure,
151        options: LineBreakOptions,
152        opening: Opening,
153    ) -> (Broken, Option<Shaped>) {
154        let (broken, _, shaped) =
155            self.broken_shaped(inlines, style, styles, measure, options, opening);
156        (broken, shaped)
157    }
158
159    /// Breaks a paragraph that was already shaped to `measure`,
160    /// starting from the end of the line `from`, which is `0` for the
161    /// whole of it.
162    ///
163    /// The style over the opening line is not held to the text it
164    /// covered before. The profile decides where the lines end. A
165    /// line pinned to a width it no longer has runs past that width.
166    pub fn rebreak(&self, shaped: &Shaped, measure: &Measure, from: usize) -> Broken {
167        self.break_shaped(shaped, measure, from, None)
168    }
169
170    /// Breaks one paragraph, and how many times the breaker ran.
171    ///
172    /// A first-line style takes two runs. What it sets changes the
173    /// width of the opening run, the width moves where the paragraph
174    /// breaks, and the break decides how much text the opening line
175    /// holds. The first run sets the paragraph's leading run in the
176    /// opening style to the end and breaks the whole of it. The
177    /// second sets as far as the extent that break gave and breaks
178    /// again with the opening line ending there, so the style covers
179    /// the text the line comes to hold. The count is fixed at two,
180    /// because a line count that depended on how long a paragraph
181    /// took to settle would not be deterministic.
182    fn broken(
183        &self,
184        inlines: &[Inline],
185        style: &ParagraphStyle,
186        styles: &dyn InlineStyles,
187        measure: &Measure,
188        options: LineBreakOptions,
189        opening: Opening,
190    ) -> (Vec<Line>, u8) {
191        let (broken, runs, _) =
192            self.broken_shaped(inlines, style, styles, measure, options, opening);
193        (broken.lines, runs)
194    }
195
196    /// The same, handing back what the paragraph was shaped from.
197    fn broken_shaped(
198        &self,
199        inlines: &[Inline],
200        style: &ParagraphStyle,
201        styles: &dyn InlineStyles,
202        measure: &Measure,
203        options: LineBreakOptions,
204        opening: Opening,
205    ) -> (Broken, u8, Option<Shaped>) {
206        let lead = Lead {
207            style: opening.first_line,
208            extent: None,
209            taken: opening.taken,
210            pseudo_element: (opening.first_line.is_some()
211                && opening.node != crate::content::NodeId::UNASSIGNED)
212                .then(|| {
213                    opening
214                        .node
215                        .pseudo(crate::content::PseudoElement::FirstLine)
216                }),
217        };
218        let once = |lead| {
219            let shaped = self.shaped(inlines, style, styles, options, lead);
220            let broken = shaped
221                .as_ref()
222                .map(|shaped| self.break_shaped(shaped, measure, 0, shaped.opening))
223                .unwrap_or_default();
224            (broken, shaped)
225        };
226        let (broken, shaped) = once(lead);
227        if lead.style.is_none() || broken.extent == 0 {
228            return (broken, 1, shaped);
229        }
230        let lead = Lead {
231            extent: Some(broken.extent),
232            ..lead
233        };
234        let (broken, shaped) = once(lead);
235        (broken, 2, shaped)
236    }
237
238    /// One paragraph flattened and shaped, which is everything about
239    /// it that the measure does not decide.
240    ///
241    /// `lead` is the style the paragraph opens in, how far it reaches
242    /// in bytes of that text, and what a drop cap took.
243    fn shaped(
244        &self,
245        inlines: &[Inline],
246        style: &ParagraphStyle,
247        styles: &dyn InlineStyles,
248        options: LineBreakOptions,
249        lead: Lead,
250    ) -> Option<Shaped> {
251        let flat = self.flatten(inlines, style, styles, lead);
252        self.shape_flat(flat, style, options, lead.extent)
253    }
254
255    /// Breaks the text of the generated box `node` into lines of
256    /// `measure`: what `::before` and `::after` generate on a block.
257    pub(crate) fn layout_generated(
258        &self,
259        text: &str,
260        node: crate::content::NodeId,
261        style: &ParagraphStyle,
262        measure: &Measure,
263        options: LineBreakOptions,
264    ) -> Vec<Line> {
265        let flat = self.flatten_generated(text, node, style);
266        self.shape_flat(flat, style, options, None)
267            .map(|shaped| self.break_shaped(&shaped, measure, 0, None).lines)
268            .unwrap_or_default()
269    }
270
271    /// Breaks the preformatted text of `node` into lines: one line per
272    /// newline the author wrote, whatever the measure.
273    ///
274    /// A line wider than the measure runs past it, because there is
275    /// nowhere else for it to end.
276    pub(crate) fn layout_preformatted(
277        &self,
278        text: &str,
279        node: crate::content::NodeId,
280        style: &ParagraphStyle,
281        measure: &Measure,
282        options: LineBreakOptions,
283    ) -> Vec<Line> {
284        let flat = self.flatten_preformatted(text, node, style);
285        self.shape_flat(flat, style, options, None)
286            .map(|shaped| self.break_shaped(&shaped, measure, 0, None).lines)
287            .unwrap_or_default()
288    }
289
290    /// One flattened paragraph shaped. `opening` is where the line the
291    /// paragraph opens on has to end.
292    fn shape_flat(
293        &self,
294        flat: flatten::FlatParagraph,
295        style: &ParagraphStyle,
296        options: LineBreakOptions,
297        opening: Option<usize>,
298    ) -> Option<Shaped> {
299        if flat.text.is_empty() {
300            return None;
301        }
302        let upem = self.registry.metrics(style.font_id)?.units_per_em as f32;
303        let spans = self.shape_spans(&flat, style, upem);
304        Some(Shaped {
305            flat,
306            spans,
307            style: style.clone(),
308            options,
309            upem,
310            opening,
311        })
312    }
313
314    /// Breaks a shaped paragraph to `measure`, starting from the
315    /// break `from`.
316    ///
317    /// `opening` is where the line the paragraph opens on has to end,
318    /// which is the extent of the style over it.
319    fn break_shaped(
320        &self,
321        shaped: &Shaped,
322        measure: &Measure,
323        from: usize,
324        opening: Option<usize>,
325    ) -> Broken {
326        let Shaped {
327            flat,
328            spans,
329            style,
330            options,
331            upem,
332            ..
333        } = shaped;
334        let (options, upem) = (*options, *upem);
335        // Points → font units: measure / size gives ems, ems *
336        // units_per_em gives font units.
337        let to_points = |units: f32| units / upem * style.size;
338
339        // Points into the paragraph's own font units: what an
340        // inline box's edges are charged in.
341        let units = if style.size > 0.0 {
342            upem / style.size
343        } else {
344            0.0
345        };
346        let widths = Widths::build(flat, spans, units);
347        let hyphen = self.hyphen_advance(style) as f32;
348        let breaks = self.break_points(&flat.text, &widths, hyphen, options);
349        let breaker = Breaker {
350            breaks: &breaks,
351            widths: &widths,
352            measure,
353            rest: measure.at(measure.settled()),
354            settled: measure.settled(),
355            first_band: (0..).find(|slot| measure.at(*slot).ends_band).unwrap_or(0),
356            upem,
357            size: style.size,
358            hyphen,
359            options,
360            from,
361            opening: opening
362                .and_then(|extent| breaks.iter().position(|at| at.content_end == extent)),
363        };
364
365        let mut broken = Broken::default();
366        let mut start = breaks[from.min(breaks.len() - 1)].next;
367        // The band being filled, and where its first span was set:
368        // a span's offset is from there, so a painter handed the
369        // line's leading edge places the rest. One buffer gathers the
370        // spans of every band.
371        let mut band: Option<(Line, f32)> = None;
372        let mut spans = Vec::new();
373        for fit in breaker.run() {
374            let at = &breaks[fit.at];
375            let span = measure.at(fit.slot);
376            if at.content_end > start {
377                let (line, origin) = band.get_or_insert_with(|| {
378                    let mut line = Line::empty();
379                    line.protrusion = to_points(fit.protrusion);
380                    (line, span.origin)
381                });
382                let first = line.runs.len();
383                line.runs
384                    .extend(cut_runs(flat, &shaped.spans, start, at.content_end));
385                adjust(&mut line.runs[first..], &flat.text, fit.ratio, options);
386                if at.hyphen {
387                    self.hyphenate(&mut line.runs, style);
388                }
389                let mut boxes = self.inline_fragments(
390                    flat,
391                    &mut line.runs[first..],
392                    spans.len(),
393                    start..at.content_end,
394                );
395                line.boxes.append(&mut boxes);
396                let offset = span.origin - *origin;
397                let width = line.runs[first..].iter().map(|run| run.advance).sum();
398                spans.push(LineSpan {
399                    runs: first..line.runs.len(),
400                    offset,
401                    width,
402                });
403                line.width += width;
404            }
405            start = at.next;
406            let hard = at.forced && fit.at != breaker.end();
407            if !span.ends_band && !hard {
408                continue;
409            }
410            // A blank line of preformatted text takes a line of its
411            // own, so what the author wrote under it stays where it
412            // was written.
413            if band.is_none() && options.preformatted && hard {
414                spans.push(LineSpan {
415                    runs: 0..0,
416                    offset: 0.0,
417                    width: 0,
418                });
419                band = Some((Line::empty(), span.origin));
420            }
421            let Some((mut line, _)) = band.take() else {
422                continue;
423            };
424            line.overhang = to_points(fit.overhang);
425            line.box_ = self.line_box(&line.runs, style);
426            line.spans = gather(&mut spans);
427            if broken.lines.is_empty() {
428                broken.extent = at.content_end;
429            }
430            broken.lines.push(line);
431            broken.ends.push(fit.at);
432        }
433        // A paragraph that ran out inside a band still sets what it
434        // reached.
435        if let Some((mut line, _)) = band.take() {
436            line.box_ = self.line_box(&line.runs, style);
437            line.spans = gather(&mut spans);
438            broken.lines.push(line);
439            broken.ends.push(breaker.end());
440        }
441        tile(&mut broken.lines, flat);
442        broken
443    }
444}
445
446/// A paragraph shaped once, and everything about it the measure does
447/// not decide: the flattened text, the runs it shaped into, and the
448/// style and options it was set with.
449///
450/// Shaping is the expensive half of setting a paragraph, and it does
451/// not depend on where the lines end. A caller that can break the
452/// same paragraph again against a different profile keeps this. It
453/// breaks the paragraph through [`LineLayout::rebreak`].
454pub struct Shaped {
455    flat: FlatParagraph,
456    spans: Vec<ShapedSpan>,
457    style: ParagraphStyle,
458    options: LineBreakOptions,
459    upem: f32,
460    /// Where the line the paragraph opens on has to end, when an
461    /// opening style covers exactly that much of the text.
462    opening: Option<usize>,
463}
464
465impl std::fmt::Debug for Shaped {
466    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467        f.debug_struct("Shaped")
468            .field("text", &self.flat.text)
469            .finish_non_exhaustive()
470    }
471}
472
473impl Shaped {
474    /// The style the paragraph is set in.
475    pub fn style(&self) -> &ParagraphStyle {
476        &self.style
477    }
478}
479
480/// One paragraph broken into lines: the lines themselves, and where
481/// each of them ended.
482#[derive(Debug, Default)]
483pub struct Broken {
484    /// The lines, in reading order.
485    pub lines: Vec<Line>,
486    /// Where each line ended, as the paragraph counts the places it
487    /// can be broken. A break from the end of a line sets the text
488    /// that line left.
489    pub ends: Vec<usize>,
490    /// Where the first line ended in the shaped text.
491    extent: usize,
492}
493
494#[cfg(test)]
495mod tests {
496    use crate::content::NodeId;
497    use crate::fonts::FaceAttributes;
498    use crate::lines::flatten::SMALL_CAPS_RATIO;
499    use crate::lines::testing::{
500        DISPLAY, OPENING, body, divided_band, drawn, emphasis, layout_body, layout_first,
501        line_text, one_run, registry, two_families, units_per_em, with_breaks,
502    };
503    use crate::lines::{
504        Face, FirstLine, Inherited, InlineStyles, Line, LineBreakOptions, LineLayout, Measure,
505        Opening, ParagraphStyle,
506    };
507    use crate::style::{FontVariantCaps, TextTransform};
508
509    /// The opening line set in `face` and nothing else.
510    fn faced(face: Face) -> Option<FirstLine> {
511        Some(FirstLine {
512            face: Some(face),
513            ..FirstLine::default()
514        })
515    }
516
517    /// The cut of the bundled family at one slope and weight.
518    fn cut(italic: bool, weight: u16) -> u16 {
519        registry()
520            .select("eb garamond", FaceAttributes { italic, weight })
521            .expect("the bundled family has cuts")
522            .id
523    }
524
525    /// The face every run of `line` is shaped with, where they agree.
526    fn face_of(line: &Line) -> u16 {
527        let first = line.runs[0].font_id;
528        assert!(
529            line.runs.iter().all(|run| run.font_id == first),
530            "the line is set in more than one face: {:?}",
531            line.runs.iter().map(|run| run.font_id).collect::<Vec<_>>(),
532        );
533        first
534    }
535
536    /// A hard break ends the band it falls in. The text after a break
537    /// in the first span of a divided band opens the band under it,
538    /// not the span beside it.
539    #[test]
540    fn a_hard_break_in_a_divided_band_opens_the_next_band() {
541        let layout = LineLayout::new(registry());
542        let lines = layout.layout(
543            &with_breaks("one\ntwo"),
544            &body(),
545            divided_band(100.0, 20.0),
546            LineBreakOptions::default(),
547        );
548        assert_eq!(drawn(&lines), ["one", "two"]);
549        assert_eq!(lines[0].spans.len(), 1, "{:?}", lines[0].spans);
550        assert_eq!(lines[1].spans[0].offset, 0.0);
551    }
552
553    /// A paragraph shaped once breaks to the same lines as the same
554    /// text shaped and broken together. A break from the end of one
555    /// line sets the text that line left.
556    #[test]
557    fn a_shaped_paragraph_breaks_again_to_the_same_lines() {
558        let layout = LineLayout::new(registry());
559        let measure = Measure::uniform(120.0);
560        let (first, shaped) = layout.layout_shaped(
561            &one_run(OPENING),
562            &body(),
563            &Inherited,
564            &measure,
565            LineBreakOptions::default(),
566            Opening::default(),
567        );
568        let shaped = shaped.expect("the paragraph shaped");
569        let lines = first.lines;
570        assert!(lines.len() > 3, "{} lines is too few to cut", lines.len());
571
572        let again = layout.rebreak(&shaped, &measure, 0);
573        assert_eq!(drawn(&again.lines), drawn(&lines));
574        assert_eq!(again.ends.len(), again.lines.len());
575
576        let tail = layout.rebreak(&shaped, &measure, again.ends[1]);
577        assert_eq!(drawn(&tail.lines), drawn(&lines[2..]));
578    }
579
580    /// The same paragraph broken to a narrower measure holds the same
581    /// words in more lines. No line runs past the measure it was set
582    /// to.
583    #[test]
584    fn a_shaped_paragraph_breaks_again_to_a_narrower_measure() {
585        let layout = LineLayout::new(registry());
586        let (wide, shaped) = layout.layout_shaped(
587            &one_run(OPENING),
588            &body(),
589            &Inherited,
590            &Measure::uniform(200.0),
591            LineBreakOptions::default(),
592            Opening::default(),
593        );
594        let shaped = shaped.expect("the paragraph shaped");
595        let lines = wide.lines;
596        let narrow = Measure::uniform(100.0);
597        let again = layout.rebreak(&shaped, &narrow, 0);
598        assert!(again.lines.len() > lines.len());
599        assert_eq!(
600            drawn(&again.lines).join(" ").replace("  ", " "),
601            drawn(&lines).join(" ").replace("  ", " ")
602        );
603        let upem = units_per_em() as f32;
604        for line in &again.lines {
605            let width = line.width as f32 / upem * body().size;
606            assert!(width <= 100.0 + 0.5, "{width} runs past the measure");
607        }
608    }
609
610    /// Empty paragraph → no lines.
611    #[test]
612    fn empty_paragraph_yields_no_lines() {
613        assert!(layout_body("", 200.0).is_empty());
614    }
615
616    /// A word that fits stays on one line, and its width is the
617    /// shaped advance of exactly its glyphs.
618    #[test]
619    fn short_text_is_one_line() {
620        let lines = layout_body("hello", 200.0);
621        assert_eq!(lines.len(), 1);
622        assert_eq!(line_text(&lines[0]), "hello");
623        let glyph_count: usize = lines[0].runs.iter().map(|r| r.glyphs.len()).sum();
624        assert_eq!(glyph_count, 5);
625    }
626
627    /// Words flow to later lines once the measure overflows; nothing
628    /// is lost and nothing is reordered.
629    #[test]
630    fn text_wraps_and_preserves_every_word() {
631        let text = "one two three four five six seven eight";
632        let lines = layout_body(text, 60.0);
633        assert!(lines.len() >= 2, "expected wrapping, got {lines:?}");
634        let reconstructed: String = lines
635            .iter()
636            .map(|l| line_text(l).trim_end().to_string())
637            .collect::<Vec<_>>()
638            .join(" ");
639        assert_eq!(reconstructed, text);
640    }
641
642    /// A paragraph with only one sensible answer gets it: everything
643    /// that fits on one line stays on it.
644    #[test]
645    fn a_paragraph_with_one_answer_gets_it() {
646        let text = "aa bb cc";
647        let lines = layout_body(text, 100.0);
648        assert_eq!(lines.len(), 1, "everything fits: {lines:?}");
649        let lines = layout_body(text, 30.0);
650        assert_eq!(line_text(&lines[0]), "aa bb");
651    }
652
653    /// A first-line style takes two runs of the breaker and a
654    /// paragraph without one takes a single run, whether or not the
655    /// two runs agree on where the paragraph breaks.
656    #[test]
657    fn a_first_line_style_breaks_the_paragraph_twice() {
658        let layout = LineLayout::new(registry());
659        let inlines = one_run(OPENING);
660        let broken = |first_line| {
661            layout
662                .broken(
663                    &inlines,
664                    &body(),
665                    &Inherited,
666                    &Measure::uniform(160.0),
667                    LineBreakOptions::default(),
668                    Opening {
669                        first_line,
670                        ..Opening::default()
671                    },
672                )
673                .1
674        };
675        assert_eq!(broken(None), 1);
676        assert_eq!(
677            broken(Some(FirstLine {
678                caps: Some(FontVariantCaps::SmallCaps),
679                ..FirstLine::default()
680            })),
681            2,
682        );
683    }
684
685    /// Small capitals on the opening line stop at the break the
686    /// second run chose: the last word of the first line is set in
687    /// them throughout and the first word of the second line is none
688    /// of it. What the author wrote comes back off the runs either
689    /// way.
690    #[test]
691    fn a_first_line_is_small_capitals_as_far_as_the_break() {
692        // The face with no substitutions of its own synthesises its
693        // small capitals, which puts the boundary in the drawn text.
694        let bare = crate::fonts::registry_without_substitutions();
695        let layout = LineLayout::new(&bare);
696        let lines = layout_first(
697            &layout,
698            160.0,
699            Some(FirstLine {
700                caps: Some(FontVariantCaps::SmallCaps),
701                ..FirstLine::default()
702            }),
703        );
704        assert!(lines.len() > 2, "the paragraph did not break");
705
706        let opening = line_text(&lines[0]);
707        let next = line_text(&lines[1]);
708        let last_word = opening.split_whitespace().next_back().expect("a word");
709        let first_word = next.split_whitespace().next().expect("a word");
710        assert_eq!(
711            last_word,
712            last_word.to_uppercase(),
713            "the first line's last word is not small capitals throughout: {opening:?}",
714        );
715        assert_eq!(
716            first_word,
717            first_word.to_lowercase(),
718            "the small capitals ran past the first line: {next:?}",
719        );
720        assert_eq!(opening, opening.to_uppercase());
721        assert_eq!(next, next.to_lowercase());
722        assert!(
723            lines[0]
724                .runs
725                .iter()
726                .any(|run| run.size == body().size * SMALL_CAPS_RATIO),
727            "nothing on the opening line was set at the reduced size",
728        );
729        assert!(
730            lines[1].runs.iter().all(|run| run.size == body().size),
731            "the reduced size reached the second line",
732        );
733
734        // What the author wrote is on the runs beside what was drawn.
735        let written: String = lines[0]
736            .runs
737            .iter()
738            .map(|run| match run.source.is_empty() {
739                true => run.text.as_str(),
740                false => run.source.as_str(),
741            })
742            .collect();
743        assert!(
744            OPENING.starts_with(&written),
745            "the opening line lost the manuscript: {written:?}",
746        );
747    }
748
749    /// Tracking on the opening line is width like any other, so the
750    /// line the paragraph breaks at holds less than it does
751    /// untracked.
752    #[test]
753    fn a_tracked_first_line_breaks_earlier() {
754        let layout = LineLayout::new(registry());
755        let plain = layout_first(&layout, 160.0, None);
756        let tracked = layout_first(
757            &layout,
758            160.0,
759            Some(FirstLine {
760                letter_spacing: Some(0.12 * body().size),
761                ..FirstLine::default()
762            }),
763        );
764        assert!(
765            line_text(&tracked[0]).len() < line_text(&plain[0]).len(),
766            "the tracked line held as much: {:?} against {:?}",
767            line_text(&tracked[0]),
768            line_text(&plain[0]),
769        );
770    }
771
772    /// A first line set larger grows its own line box around the
773    /// baseline the paragraph shares, and leaves the lines under it
774    /// the size they were.
775    #[test]
776    fn a_first_line_set_larger_grows_its_line_box() {
777        let layout = LineLayout::new(registry());
778        let lines = layout_first(
779            &layout,
780            240.0,
781            Some(FirstLine {
782                size: Some(body().size * 1.6),
783                ..FirstLine::default()
784            }),
785        );
786        assert!(lines.len() > 1, "the paragraph did not break");
787        assert!(
788            lines[0].box_.height > lines[1].box_.height,
789            "the opening line did not grow: {:?} against {:?}",
790            lines[0].box_,
791            lines[1].box_,
792        );
793        assert!(
794            lines[0].box_.baseline > lines[1].box_.baseline,
795            "the opening line grew below the baseline alone",
796        );
797        assert_eq!(
798            lines[0].runs[0].size,
799            body().size * 1.6,
800            "the opening run was not set larger",
801        );
802        assert_eq!(lines[1].runs[0].size, body().size);
803    }
804
805    /// Acceptance: `font-style: italic` on the opening line sets it
806    /// in the italic of the family the paragraph is set in, and
807    /// leaves the lines under it upright.
808    #[test]
809    fn an_italic_first_line_is_set_in_the_italic_cut() {
810        let layout = LineLayout::new(registry());
811        let lines = layout_first(
812            &layout,
813            160.0,
814            faced(Face {
815                italic: Some(true),
816                ..Face::default()
817            }),
818        );
819        assert!(lines.len() > 1, "the paragraph did not break");
820        assert_eq!(face_of(&lines[0]), cut(true, 400));
821        assert_eq!(face_of(&lines[1]), body().font_id);
822    }
823
824    /// Acceptance: `font-family` on the opening line sets it in
825    /// another family, and the lines under it keep the paragraph's
826    /// own.
827    #[test]
828    fn a_first_line_takes_another_family() {
829        let registry = two_families();
830        let layout = LineLayout::new(registry);
831        let display = registry
832            .select(DISPLAY, FaceAttributes::REGULAR)
833            .expect("the second family is registered")
834            .id;
835        let lines = layout_first(
836            &layout,
837            160.0,
838            faced(Face {
839                family: Some(display),
840                ..Face::default()
841            }),
842        );
843        assert!(lines.len() > 1, "the paragraph did not break");
844        assert_eq!(face_of(&lines[0]), display);
845        assert_eq!(face_of(&lines[1]), body().font_id);
846    }
847
848    /// Acceptance: `font-weight` on the opening line picks another
849    /// cut of the same family.
850    #[test]
851    fn a_first_line_set_bold_picks_the_bold_cut() {
852        let layout = LineLayout::new(registry());
853        let lines = layout_first(
854            &layout,
855            160.0,
856            faced(Face {
857                weight: Some(700),
858                ..Face::default()
859            }),
860        );
861        assert!(lines.len() > 1, "the paragraph did not break");
862        assert_eq!(face_of(&lines[0]), cut(false, 700));
863        assert_eq!(face_of(&lines[1]), body().font_id);
864    }
865
866    /// What the rule leaves alone an inline element keeps: an italic
867    /// opening line over an inline element set bold is set in the
868    /// bold italic, not in the regular one.
869    #[test]
870    fn a_face_on_the_first_line_keeps_the_cut_an_inline_element_brought() {
871        /// A style tree that sets one node bold and everything else
872        /// as the block is set.
873        struct Bold(NodeId);
874        impl InlineStyles for Bold {
875            fn style(&self, id: NodeId, block: &ParagraphStyle) -> ParagraphStyle {
876                let font_id = match id == self.0 {
877                    true => cut(false, 700),
878                    false => block.font_id,
879                };
880                ParagraphStyle {
881                    font_id,
882                    ..block.clone()
883                }
884            }
885        }
886
887        let bold = NodeId::new(3);
888        let mut inlines = one_run("she said ");
889        inlines.push(emphasis(bold, one_run("never")));
890        let layout = LineLayout::new(registry());
891        let lines = layout.layout_styled(
892            &inlines,
893            &body(),
894            &Bold(bold),
895            &Measure::uniform(200.0),
896            LineBreakOptions::default(),
897            Opening {
898                first_line: faced(Face {
899                    italic: Some(true),
900                    ..Face::default()
901                }),
902                ..Opening::default()
903            },
904        );
905        let faces: Vec<u16> = lines[0].runs.iter().map(|run| run.font_id).collect();
906        assert_eq!(
907            faces,
908            [cut(true, 400), cut(true, 700)],
909            "{:?}",
910            lines[0].runs
911        );
912    }
913
914    /// Acceptance: a face on the opening line re-breaks it. The bold
915    /// cut is wider, so the line holds less, and the word it dropped
916    /// opens the line under it.
917    #[test]
918    fn a_face_on_the_first_line_rebreaks_it() {
919        let layout = LineLayout::new(registry());
920        let plain = layout_first(&layout, 160.0, None);
921        let bold = layout_first(
922            &layout,
923            160.0,
924            faced(Face {
925                weight: Some(700),
926                ..Face::default()
927            }),
928        );
929        assert!(
930            line_text(&bold[0]).len() < line_text(&plain[0]).len(),
931            "the bold line held as much: {:?} against {:?}",
932            line_text(&bold[0]),
933            line_text(&plain[0]),
934        );
935        let dropped = line_text(&plain[0])
936            .split_whitespace()
937            .next_back()
938            .expect("a word")
939            .to_string();
940        assert!(
941            line_text(&bold[1]).starts_with(&dropped),
942            "the word the opening line dropped did not open the second: {:?}",
943            line_text(&bold[1]),
944        );
945    }
946
947    /// `text-transform` on the opening line changes what is shaped
948    /// and leaves what the author wrote on the run beside it, the way
949    /// it does on a whole paragraph.
950    #[test]
951    fn a_transformed_first_line_keeps_the_manuscript() {
952        let layout = LineLayout::new(registry());
953        let lines = layout_first(
954            &layout,
955            160.0,
956            Some(FirstLine {
957                transform: Some(TextTransform::Uppercase),
958                ..FirstLine::default()
959            }),
960        );
961        let opening = line_text(&lines[0]);
962        assert_eq!(opening, opening.to_uppercase());
963        assert_eq!(line_text(&lines[1]), line_text(&lines[1]).to_lowercase());
964        let written: String = lines[0]
965            .runs
966            .iter()
967            .map(|run| run.source.as_str())
968            .collect();
969        assert!(
970            OPENING.starts_with(&written) && written == written.to_lowercase(),
971            "the opening line lost the manuscript: {written:?}",
972        );
973    }
974}