Skip to main content

fleuron/lines/
measure.rs

1//! The bands a line is set in: where each one starts and how wide
2//! it is.
3
4/// One place a line may start and how wide it may run.
5///
6/// A band of the page is set in one span or in several, and a band
7/// set in several is still one line: the text crosses from span to
8/// span in reading order, and only the last of them ends where a
9/// line ends.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Span {
12    /// Points from the block's leading edge the span starts at.
13    pub origin: f32,
14    /// Points the span runs.
15    pub width: f32,
16    /// Whether the band ends here.
17    pub ends_band: bool,
18}
19
20impl Span {
21    /// A span that is a band on its own.
22    pub fn band(origin: f32, width: f32) -> Span {
23        Span {
24            origin,
25            width,
26            ends_band: true,
27        }
28    }
29
30    /// A band `width` wide ending where a band of `end` points ends.
31    /// A first-line indent and a drop cap beside the line both leave
32    /// one: the line is shortened at its start.
33    pub fn ending(end: f32, width: f32) -> Span {
34        Span::band(end - width, width)
35    }
36}
37
38/// The spans a paragraph breaks to, in reading order.
39///
40/// One entry per span rather than per line, so a band set in several
41/// spans is several entries. The listed spans run out and `rest`
42/// answers for every one past them, so a profile covers a paragraph
43/// before anything has broken it and found its length. `rest` is a
44/// band of its own: a band that never ended would take the rest of
45/// the paragraph.
46#[derive(Debug, Clone, PartialEq)]
47pub struct Measure {
48    leading: Vec<Span>,
49    rest: Span,
50}
51
52impl Measure {
53    /// One band of one width for every line.
54    pub fn uniform(points: f32) -> Measure {
55        Measure {
56            leading: Vec::new(),
57            rest: Span::band(0.0, points),
58        }
59    }
60
61    /// The spans a paragraph opens with, and the band every line
62    /// past them is set in.
63    pub fn new(leading: Vec<Span>, rest: Span) -> Measure {
64        Measure {
65            leading,
66            rest: Span::band(rest.origin, rest.width),
67        }
68    }
69
70    /// The band every span past the listed ones is set in.
71    pub fn rest(&self) -> Span {
72        self.rest
73    }
74
75    /// The span at `index`.
76    pub fn at(&self, index: usize) -> Span {
77        self.leading.get(index).copied().unwrap_or(self.rest)
78    }
79
80    /// Spans past which every span is the same one. Two paths that
81    /// have reached here differ in nothing the rest of the paragraph
82    /// can see.
83    pub(super) fn settled(&self) -> usize {
84        self.leading.len()
85    }
86}
87
88impl From<f32> for Measure {
89    fn from(points: f32) -> Measure {
90        Measure::uniform(points)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use crate::content::{Attributes, Inline, NodeId};
97    use crate::lines::testing::{
98        OPENING, body, divided_band, justified, line_text, one_run, registry, span_text,
99        span_width_pt, units_per_em,
100    };
101    use crate::lines::{Line, LineBreakOptions, LineLayout, Measure, Span};
102
103    /// A band set in two spans sets text in both, in reading order:
104    /// the paragraph crosses from the first to the second and comes
105    /// back off them in the order it was written.
106    #[test]
107    fn a_band_of_two_spans_sets_text_in_both() {
108        let layout = LineLayout::new(registry());
109        let lines = layout.layout(
110            &one_run(OPENING),
111            &body(),
112            divided_band(80.0, 20.0),
113            LineBreakOptions::default(),
114        );
115        let first = &lines[0];
116        assert_eq!(first.spans.len(), 2, "the band was set in one span");
117        assert!(
118            !span_text(first, 0).is_empty() && !span_text(first, 1).is_empty(),
119            "a span of the band holds no text: {first:?}"
120        );
121        assert_eq!(
122            OPENING
123                .replace(' ', "")
124                .find(&span_text(first, 1).replace(' ', "")),
125            Some(span_text(first, 0).replace(' ', "").len()),
126            "the second span does not carry on from the first"
127        );
128        // The second span opens where the profile put it, which is
129        // past the gutter rather than at the line's own edge.
130        assert_eq!(first.spans[1].offset, 100.0);
131        assert!(
132            lines[1..].iter().all(|line| line.spans.len() == 1),
133            "a band under the divided one was set in more than one span"
134        );
135    }
136
137    /// Justification flushes an interior span at both edges: the
138    /// text in the first span of a band fills it, and only the last
139    /// line of a paragraph is left short.
140    #[test]
141    fn justification_flushes_an_interior_span() {
142        let layout = LineLayout::new(registry());
143        let lines = layout.layout(
144            &one_run(OPENING),
145            &body(),
146            divided_band(80.0, 20.0),
147            justified(),
148        );
149        assert!(
150            (span_width_pt(&lines[0], 0) - 80.0).abs() < 0.01,
151            "the first span of the band is {}pt of 80pt",
152            span_width_pt(&lines[0], 0),
153        );
154    }
155
156    /// A drop cap shortens the lines beside it: the first few break
157    /// to a narrower measure, and the rest go back to the full one.
158    #[test]
159    fn a_shortened_measure_only_holds_for_the_lines_it_names() {
160        let text = "one two three four five six seven eight nine ten eleven twelve";
161        let measure = Measure::new(vec![Span::band(80.0, 40.0); 2], Span::band(0.0, 120.0));
162        let layout = LineLayout::new(registry());
163        let inlines = vec![Inline::Text {
164            id: NodeId::UNASSIGNED,
165            value: text.to_string(),
166            attributes: Attributes::default(),
167            position: None,
168            span: None,
169        }];
170        let lines = layout.layout(&inlines, &body(), measure.clone(), Default::default());
171        assert!(lines.len() > 3, "expected several lines: {lines:?}");
172        let width_pt = |line: &Line| line.width as f32 / units_per_em() as f32 * body().size;
173        for (index, line) in lines.iter().enumerate() {
174            let allowed = measure.at(index).width;
175            assert!(
176                width_pt(line) <= allowed,
177                "line {index} is {}pt against a measure of {allowed}pt",
178                width_pt(line),
179            );
180        }
181        // The lines that were not shortened use the width the
182        // shortened ones could not: nothing is lost, and the same
183        // text set at one measure breaks differently.
184        assert!(
185            width_pt(&lines[2]) > measure.at(0).width,
186            "the measure never widened"
187        );
188        let uniform = layout.layout(&inlines, &body(), 120.0, Default::default());
189        assert!(uniform.len() < lines.len());
190        assert_eq!(
191            lines.iter().map(line_text).collect::<Vec<_>>().join(" "),
192            text,
193        );
194    }
195}