genpdf/
wrap.rs

1// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4//! Utilities for text wrapping.
5
6use std::mem;
7
8use crate::style;
9use crate::Context;
10use crate::Mm;
11
12/// Combines a sequence of styled words into lines with a maximum width.
13///
14/// If a word does not fit into a line, the wrapper tries to split it using the `split` function.
15pub struct Wrapper<'c, 's, I: Iterator<Item = style::StyledStr<'s>>> {
16    iter: I,
17    context: &'c Context,
18    width: Mm,
19    x: Mm,
20    buf: Vec<style::StyledCow<'s>>,
21}
22
23impl<'c, 's, I: Iterator<Item = style::StyledStr<'s>>> Wrapper<'c, 's, I> {
24    /// Creates a new wrapper for the given word sequence and with the given maximum width.
25    pub fn new(iter: I, context: &'c Context, width: Mm) -> Wrapper<'c, 's, I> {
26        Wrapper {
27            iter,
28            context,
29            width,
30            x: Mm(0.0),
31            buf: Vec::new(),
32        }
33    }
34}
35
36impl<'c, 's, I: Iterator<Item = style::StyledStr<'s>>> Iterator for Wrapper<'c, 's, I> {
37    // This iterator yields pairs of lines and the length difference between the input words and
38    // the line.
39    type Item = (Vec<style::StyledCow<'s>>, usize);
40
41    fn next(&mut self) -> Option<(Vec<style::StyledCow<'s>>, usize)> {
42        // Append words to self.buf until the maximum line length is reached
43        while let Some(s) = self.iter.next() {
44            let mut width = s.width(&self.context.font_cache);
45
46            if self.x + width > self.width {
47                // The word does not fit into the current line (at least not completely)
48
49                let mut delta = 0;
50                // Try to split the word so that the first part fits into the current line
51                let s = if let Some((start, end)) = split(self.context, s, self.width - self.x) {
52                    // Calculate the number of bytes that we added to the string when splitting it
53                    // (for the hyphen, if required).
54                    delta = start.s.len() + end.s.len() - s.s.len();
55                    self.buf.push(start);
56                    width = end.width(&self.context.font_cache);
57                    end
58                } else {
59                    s.into()
60                };
61
62                if width > self.width {
63                    // The remainder of the word is longer than the current page – we will never be
64                    // able to render it completely.
65                    // TODO: return error?
66                    break;
67                }
68
69                // Return the current line and add the word that did not fit to the next line
70                let v = std::mem::take(&mut self.buf);
71                self.buf.push(s);
72                self.x = width;
73                return Some((v, delta));
74            } else {
75                // The word fits in the current line, so just append it
76                self.buf.push(s.into());
77                self.x += width;
78            }
79        }
80
81        if self.buf.is_empty() {
82            None
83        } else {
84            Some((mem::take(&mut self.buf), 0))
85        }
86    }
87}
88
89#[cfg(not(feature = "hyphenation"))]
90fn split<'s>(
91    _context: &Context,
92    _s: style::StyledStr<'s>,
93    _len: Mm,
94) -> Option<(style::StyledCow<'s>, style::StyledCow<'s>)> {
95    None
96}
97
98/// Tries to split the given string into two parts so that the first part is shorter than the given
99/// width.
100#[cfg(feature = "hyphenation")]
101fn split<'s>(
102    context: &Context,
103    s: style::StyledStr<'s>,
104    width: Mm,
105) -> Option<(style::StyledCow<'s>, style::StyledCow<'s>)> {
106    use hyphenation::{Hyphenator, Iter};
107
108    let hyphenator = if let Some(hyphenator) = &context.hyphenator {
109        hyphenator
110    } else {
111        return None;
112    };
113
114    let mark = "-";
115    let mark_width = s.style.str_width(&context.font_cache, mark);
116
117    let hyphenated = hyphenator.hyphenate(s.s);
118    let segments: Vec<_> = hyphenated.iter().segments().collect();
119
120    // Find the hyphenation with the longest first part so that the first part (and the hyphen) are
121    // shorter than or equals to the required width.
122    let idx = segments
123        .iter()
124        .scan(Mm(0.0), |acc, t| {
125            *acc += s.style.str_width(&context.font_cache, t);
126            Some(*acc)
127        })
128        .position(|w| w + mark_width > width)
129        .unwrap_or_default();
130    if idx > 0 {
131        let idx = hyphenated.breaks[idx - 1];
132        let start = s.s[..idx].to_owned() + mark;
133        let end = &s.s[idx..];
134        Some((
135            style::StyledCow::new(start, s.style),
136            style::StyledCow::new(end, s.style),
137        ))
138    } else {
139        None
140    }
141}
142
143/// Splits a sequence of styled strings into words.
144pub struct Words<I: Iterator<Item = style::StyledString>> {
145    iter: I,
146    s: Option<style::StyledString>,
147}
148
149impl<I: Iterator<Item = style::StyledString>> Words<I> {
150    /// Creates a new words iterator.
151    pub fn new<IntoIter: IntoIterator<Item = style::StyledString, IntoIter = I>>(
152        iter: IntoIter,
153    ) -> Words<I> {
154        Words {
155            iter: iter.into_iter(),
156            s: None,
157        }
158    }
159}
160
161impl<I: Iterator<Item = style::StyledString>> Iterator for Words<I> {
162    type Item = style::StyledString;
163
164    fn next(&mut self) -> Option<style::StyledString> {
165        if self.s.as_ref().map(|s| s.s.is_empty()).unwrap_or(true) {
166            self.s = self.iter.next();
167        }
168
169        if let Some(s) = &mut self.s {
170            // Split at the first space or use the complete string
171            let n = s.s.find(' ').map(|i| i + 1).unwrap_or_else(|| s.s.len());
172            let mut tmp = s.s.split_off(n);
173            mem::swap(&mut tmp, &mut s.s);
174            Some(style::StyledString::new(tmp, s.style))
175        } else {
176            None
177        }
178    }
179}