itui/widgets/
reflow.rs

1use crate::style::Style;
2use unicode_width::UnicodeWidthStr;
3
4const NBSP: &str = "\u{00a0}";
5
6#[derive(Copy, Clone, Debug)]
7pub struct Styled<'a>(pub &'a str, pub Style);
8
9/// A state machine to pack styled symbols into lines.
10/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
11/// iterators for that).
12pub trait LineComposer<'a> {
13    fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)>;
14}
15
16/// A state machine that wraps lines on word boundaries.
17pub struct WordWrapper<'a, 'b> {
18    symbols: &'b mut Iterator<Item = Styled<'a>>,
19    max_line_width: u16,
20    current_line: Vec<Styled<'a>>,
21    next_line: Vec<Styled<'a>>,
22}
23
24impl<'a, 'b> WordWrapper<'a, 'b> {
25    pub fn new(
26        symbols: &'b mut Iterator<Item = Styled<'a>>,
27        max_line_width: u16,
28    ) -> WordWrapper<'a, 'b> {
29        WordWrapper {
30            symbols,
31            max_line_width,
32            current_line: vec![],
33            next_line: vec![],
34        }
35    }
36}
37
38impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
39    fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
40        if self.max_line_width == 0 {
41            return None;
42        }
43        std::mem::swap(&mut self.current_line, &mut self.next_line);
44        self.next_line.truncate(0);
45
46        let mut current_line_width = self
47            .current_line
48            .iter()
49            .map(|Styled(c, _)| c.width() as u16)
50            .sum();
51
52        let mut symbols_to_last_word_end: usize = 0;
53        let mut width_to_last_word_end: u16 = 0;
54        let mut prev_whitespace = false;
55        let mut symbols_exhausted = true;
56        for Styled(symbol, style) in &mut self.symbols {
57            symbols_exhausted = false;
58            let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
59
60            // Ignore characters wider that the total max width.
61            if symbol.width() as u16 > self.max_line_width
62                // Skip leading whitespace.
63                || symbol_whitespace && symbol != "\n" && current_line_width == 0
64            {
65                continue;
66            }
67
68            // Break on newline and discard it.
69            if symbol == "\n" {
70                if prev_whitespace {
71                    current_line_width = width_to_last_word_end;
72                    self.current_line.truncate(symbols_to_last_word_end);
73                }
74                break;
75            }
76
77            // Mark the previous symbol as word end.
78            if symbol_whitespace && !prev_whitespace && symbol != NBSP {
79                symbols_to_last_word_end = self.current_line.len();
80                width_to_last_word_end = current_line_width;
81            }
82
83            self.current_line.push(Styled(symbol, style));
84            current_line_width += symbol.width() as u16;
85
86            if current_line_width > self.max_line_width {
87                // If there was no word break in the text, wrap at the end of the line.
88                let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
89                    (symbols_to_last_word_end, width_to_last_word_end)
90                } else {
91                    (self.current_line.len() - 1, self.max_line_width)
92                };
93
94                // Push the remainder to the next line but strip leading whitespace:
95                {
96                    let remainder = &self.current_line[truncate_at..];
97                    if let Some(remainder_nonwhite) = remainder
98                        .iter()
99                        .position(|Styled(c, _)| !c.chars().all(&char::is_whitespace))
100                    {
101                        self.next_line
102                            .extend_from_slice(&remainder[remainder_nonwhite..]);
103                    }
104                }
105                self.current_line.truncate(truncate_at);
106                current_line_width = truncated_width;
107                break;
108            }
109
110            prev_whitespace = symbol_whitespace;
111        }
112
113        // Even if the iterator is exhausted, pass the previous remainder.
114        if symbols_exhausted && self.current_line.is_empty() {
115            None
116        } else {
117            Some((&self.current_line[..], current_line_width))
118        }
119    }
120}
121
122/// A state machine that truncates overhanging lines.
123pub struct LineTruncator<'a, 'b> {
124    symbols: &'b mut Iterator<Item = Styled<'a>>,
125    max_line_width: u16,
126    current_line: Vec<Styled<'a>>,
127}
128
129impl<'a, 'b> LineTruncator<'a, 'b> {
130    pub fn new(
131        symbols: &'b mut Iterator<Item = Styled<'a>>,
132        max_line_width: u16,
133    ) -> LineTruncator<'a, 'b> {
134        LineTruncator {
135            symbols,
136            max_line_width,
137            current_line: vec![],
138        }
139    }
140}
141
142impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
143    fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
144        if self.max_line_width == 0 {
145            return None;
146        }
147
148        self.current_line.truncate(0);
149        let mut current_line_width = 0;
150
151        let mut skip_rest = false;
152        let mut symbols_exhausted = true;
153        for Styled(symbol, style) in &mut self.symbols {
154            symbols_exhausted = false;
155
156            // Ignore characters wider that the total max width.
157            if symbol.width() as u16 > self.max_line_width {
158                continue;
159            }
160
161            // Break on newline and discard it.
162            if symbol == "\n" {
163                break;
164            }
165
166            if current_line_width + symbol.width() as u16 > self.max_line_width {
167                // Exhaust the remainder of the line.
168                skip_rest = true;
169                break;
170            }
171
172            current_line_width += symbol.width() as u16;
173            self.current_line.push(Styled(symbol, style));
174        }
175
176        if skip_rest {
177            for Styled(symbol, _) in &mut self.symbols {
178                if symbol == "\n" {
179                    break;
180                }
181            }
182        }
183
184        if symbols_exhausted && self.current_line.is_empty() {
185            None
186        } else {
187            Some((&self.current_line[..], current_line_width))
188        }
189    }
190}
191
192#[cfg(test)]
193mod test {
194    use super::*;
195    use unicode_segmentation::UnicodeSegmentation;
196
197    enum Composer {
198        WordWrapper,
199        LineTruncator,
200    }
201
202    fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
203        let style = Default::default();
204        let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style));
205        let mut composer: Box<dyn LineComposer> = match which {
206            Composer::WordWrapper => Box::new(WordWrapper::new(&mut styled, text_area_width)),
207            Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
208        };
209        let mut lines = vec![];
210        let mut widths = vec![];
211        while let Some((styled, width)) = composer.next_line() {
212            let line = styled
213                .iter()
214                .map(|Styled(g, _style)| *g)
215                .collect::<String>();
216            assert!(width <= text_area_width);
217            lines.push(line);
218            widths.push(width);
219        }
220        (lines, widths)
221    }
222
223    #[test]
224    fn line_composer_one_line() {
225        let width = 40;
226        for i in 1..width {
227            let text = "a".repeat(i);
228            let (word_wrapper, _) = run_composer(Composer::WordWrapper, &text, width as u16);
229            let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
230            let expected = vec![text];
231            assert_eq!(word_wrapper, expected);
232            assert_eq!(line_truncator, expected);
233        }
234    }
235
236    #[test]
237    fn line_composer_short_lines() {
238        let width = 20;
239        let text =
240            "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
241        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
242        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
243
244        let wrapped: Vec<&str> = text.split('\n').collect();
245        assert_eq!(word_wrapper, wrapped);
246        assert_eq!(line_truncator, wrapped);
247    }
248
249    #[test]
250    fn line_composer_long_word() {
251        let width = 20;
252        let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
253        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width as u16);
254        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
255
256        let wrapped = vec![
257            &text[..width],
258            &text[width..width * 2],
259            &text[width * 2..width * 3],
260            &text[width * 3..],
261        ];
262        assert_eq!(
263            word_wrapper, wrapped,
264            "WordWrapper should deect the line cannot be broken on word boundary and \
265             break it at line width limit."
266        );
267        assert_eq!(line_truncator, vec![&text[..width]]);
268    }
269
270    #[test]
271    fn line_composer_long_sentence() {
272        let width = 20;
273        let text =
274            "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
275        let text_multi_space =
276            "abcd efghij    klmnopabcd efgh     ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
277             m n o";
278        let (word_wrapper_single_space, _) =
279            run_composer(Composer::WordWrapper, text, width as u16);
280        let (word_wrapper_multi_space, _) =
281            run_composer(Composer::WordWrapper, text_multi_space, width as u16);
282        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
283
284        let word_wrapped = vec![
285            "abcd efghij",
286            "klmnopabcd efgh",
287            "ijklmnopabcdefg",
288            "hijkl mnopab c d e f",
289            "g h i j k l m n o",
290        ];
291        assert_eq!(word_wrapper_single_space, word_wrapped);
292        assert_eq!(word_wrapper_multi_space, word_wrapped);
293
294        assert_eq!(line_truncator, vec![&text[..width]]);
295    }
296
297    #[test]
298    fn line_composer_zero_width() {
299        let width = 0;
300        let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
301        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
302        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
303
304        let expected: Vec<&str> = Vec::new();
305        assert_eq!(word_wrapper, expected);
306        assert_eq!(line_truncator, expected);
307    }
308
309    #[test]
310    fn line_composer_max_line_width_of_1() {
311        let width = 1;
312        let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
313        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
314        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
315
316        let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
317            .filter(|g| g.chars().any(|c| !c.is_whitespace()))
318            .collect();
319        assert_eq!(word_wrapper, expected);
320        assert_eq!(line_truncator, vec!["a"]);
321    }
322
323    #[test]
324    fn line_composer_max_line_width_of_1_double_width_characters() {
325        let width = 1;
326        let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
327                    両端点では、";
328        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
329        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
330        assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
331        assert_eq!(line_truncator, vec!["", "a"]);
332    }
333
334    /// Tests WordWrapper with words some of which exceed line length and some not.
335    #[test]
336    fn line_composer_word_wrapper_mixed_length() {
337        let width = 20;
338        let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
339        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
340        assert_eq!(
341            word_wrapper,
342            vec![
343                "abcd efghij",
344                "klmnopabcdefghijklmn",
345                "opabcdefghijkl",
346                "mnopab cdefghi j",
347                "klmno",
348            ]
349        )
350    }
351
352    #[test]
353    fn line_composer_double_width_chars() {
354        let width = 20;
355        let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
356                    では、";
357        let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, &text, width);
358        let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
359        assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
360        let wrapped = vec![
361            "コンピュータ上で文字",
362            "を扱う場合、典型的に",
363            "は文字による通信を行",
364            "う場合にその両端点で",
365            "は、",
366        ];
367        assert_eq!(word_wrapper, wrapped);
368        assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
369    }
370
371    #[test]
372    fn line_composer_leading_whitespace_removal() {
373        let width = 20;
374        let text = "AAAAAAAAAAAAAAAAAAAA    AAA";
375        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
376        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
377        assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
378        assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
379    }
380
381    /// Tests truncation of leading whitespace.
382    #[test]
383    fn line_composer_lots_of_spaces() {
384        let width = 20;
385        let text = "                                                                     ";
386        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
387        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
388        assert_eq!(word_wrapper, vec![""]);
389        assert_eq!(line_truncator, vec!["                    "]);
390    }
391
392    /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
393    /// incidental.
394    #[test]
395    fn line_composer_char_plus_lots_of_spaces() {
396        let width = 20;
397        let text = "a                                                                     ";
398        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
399        let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
400        // What's happening below is: the first line gets consumed, trailing spaces discarded,
401        // after 20 of which a word break occurs (probably shouldn't). The second line break
402        // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
403        // that much.
404        assert_eq!(word_wrapper, vec!["a", ""]);
405        assert_eq!(line_truncator, vec!["a                   "]);
406    }
407
408    #[test]
409    fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
410        let width = 20;
411        // Japanese seems not to use spaces but we should break on spaces anyway... We're using it
412        // to test double-width chars.
413        // You are more than welcome to add word boundary detection based of alterations of
414        // hiragana and katakana...
415        // This happens to also be a test case for mixed width because regular spaces are single width.
416        let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
417        let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, text, width);
418        assert_eq!(
419            word_wrapper,
420            vec![
421                "コンピュ",
422                "ータ上で文字を扱う場",
423                "合、 典型的には文",
424                "字による 通信を行",
425                "う場合にその両端点で",
426                "は、",
427            ]
428        );
429        // Odd-sized lines have a space in them.
430        assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
431    }
432
433    /// Ensure words separated by nbsp are wrapped as if they were a single one.
434    #[test]
435    fn line_composer_word_wrapper_nbsp() {
436        let width = 20;
437        let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
438        let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
439        assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
440
441        // Ensure that if the character was a regular space, it would be wrapped differently.
442        let text_space = text.replace("\u{00a0}", " ");
443        let (word_wrapper_space, _) = run_composer(Composer::WordWrapper, &text_space, width);
444        assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
445    }
446}