ratatui/widgets/
reflow.rs

1use std::{collections::VecDeque, mem};
2
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6use crate::{layout::Alignment, text::StyledGrapheme};
7
8/// A state machine to pack styled symbols into lines.
9/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
10/// iterators for that).
11pub trait LineComposer<'a> {
12    fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>>;
13}
14
15pub struct WrappedLine<'lend, 'text> {
16    /// One line reflowed to the correct width
17    pub line: &'lend [StyledGrapheme<'text>],
18    /// The width of the line
19    pub width: u16,
20    /// Whether the line was aligned left or right
21    pub alignment: Alignment,
22}
23
24/// A state machine that wraps lines on word boundaries.
25#[derive(Debug, Default, Clone)]
26pub struct WordWrapper<'a, O, I>
27where
28    // Outer iterator providing the individual lines
29    O: Iterator<Item = (I, Alignment)>,
30    // Inner iterator providing the styled symbols of a line Each line consists of an alignment and
31    // a series of symbols
32    I: Iterator<Item = StyledGrapheme<'a>>,
33{
34    /// The given, unprocessed lines
35    input_lines: O,
36    max_line_width: u16,
37    wrapped_lines: VecDeque<Vec<StyledGrapheme<'a>>>,
38    current_alignment: Alignment,
39    current_line: Vec<StyledGrapheme<'a>>,
40    /// Removes the leading whitespace from lines
41    trim: bool,
42
43    // These are cached allocations that hold no state across next_line invocations
44    pending_word: Vec<StyledGrapheme<'a>>,
45    pending_whitespace: VecDeque<StyledGrapheme<'a>>,
46    pending_line_pool: Vec<Vec<StyledGrapheme<'a>>>,
47}
48
49impl<'a, O, I> WordWrapper<'a, O, I>
50where
51    O: Iterator<Item = (I, Alignment)>,
52    I: Iterator<Item = StyledGrapheme<'a>>,
53{
54    pub const fn new(lines: O, max_line_width: u16, trim: bool) -> Self {
55        Self {
56            input_lines: lines,
57            max_line_width,
58            wrapped_lines: VecDeque::new(),
59            current_alignment: Alignment::Left,
60            current_line: vec![],
61            trim,
62
63            pending_word: Vec::new(),
64            pending_line_pool: Vec::new(),
65            pending_whitespace: VecDeque::new(),
66        }
67    }
68
69    /// Split an input line (`line_symbols`) into wrapped lines
70    /// and cache them to be emitted later
71    fn process_input(&mut self, line_symbols: impl IntoIterator<Item = StyledGrapheme<'a>>) {
72        let mut pending_line = self.pending_line_pool.pop().unwrap_or_default();
73        let mut line_width = 0;
74        let mut word_width = 0;
75        let mut whitespace_width = 0;
76        let mut non_whitespace_previous = false;
77
78        self.pending_word.clear();
79        self.pending_whitespace.clear();
80        pending_line.clear();
81
82        for grapheme in line_symbols {
83            let is_whitespace = grapheme.is_whitespace();
84            let symbol_width = grapheme.symbol.width() as u16;
85
86            // ignore symbols wider than line limit
87            if symbol_width > self.max_line_width {
88                continue;
89            }
90
91            let word_found = non_whitespace_previous && is_whitespace;
92            // current word would overflow after removing whitespace
93            let trimmed_overflow = pending_line.is_empty()
94                && self.trim
95                && word_width + symbol_width > self.max_line_width;
96            // separated whitespace would overflow on its own
97            let whitespace_overflow = pending_line.is_empty()
98                && self.trim
99                && whitespace_width + symbol_width > self.max_line_width;
100            // current full word (including whitespace) would overflow
101            let untrimmed_overflow = pending_line.is_empty()
102                && !self.trim
103                && word_width + whitespace_width + symbol_width > self.max_line_width;
104
105            // append finished segment to current line
106            if word_found || trimmed_overflow || whitespace_overflow || untrimmed_overflow {
107                if !pending_line.is_empty() || !self.trim {
108                    pending_line.extend(self.pending_whitespace.drain(..));
109                    line_width += whitespace_width;
110                }
111
112                pending_line.append(&mut self.pending_word);
113                line_width += word_width;
114
115                self.pending_whitespace.clear();
116                whitespace_width = 0;
117                word_width = 0;
118            }
119
120            // pending line fills up limit
121            let line_full = line_width >= self.max_line_width;
122            // pending word would overflow line limit
123            let pending_word_overflow = symbol_width > 0
124                && line_width + whitespace_width + word_width >= self.max_line_width;
125
126            // add finished wrapped line to remaining lines
127            if line_full || pending_word_overflow {
128                let mut remaining_width = u16::saturating_sub(self.max_line_width, line_width);
129
130                self.wrapped_lines.push_back(mem::take(&mut pending_line));
131                line_width = 0;
132
133                // remove whitespace up to the end of line
134                while let Some(grapheme) = self.pending_whitespace.front() {
135                    let width = grapheme.symbol.width() as u16;
136
137                    if width > remaining_width {
138                        break;
139                    }
140
141                    whitespace_width -= width;
142                    remaining_width -= width;
143                    self.pending_whitespace.pop_front();
144                }
145
146                // don't count first whitespace toward next word
147                if is_whitespace && self.pending_whitespace.is_empty() {
148                    continue;
149                }
150            }
151
152            // append symbol to a pending buffer
153            if is_whitespace {
154                whitespace_width += symbol_width;
155                self.pending_whitespace.push_back(grapheme);
156            } else {
157                word_width += symbol_width;
158                self.pending_word.push(grapheme);
159            }
160
161            non_whitespace_previous = !is_whitespace;
162        }
163
164        // append remaining text parts
165        if pending_line.is_empty()
166            && self.pending_word.is_empty()
167            && !self.pending_whitespace.is_empty()
168        {
169            self.wrapped_lines.push_back(vec![]);
170        }
171        if !pending_line.is_empty() || !self.trim {
172            pending_line.extend(self.pending_whitespace.drain(..));
173        }
174        pending_line.append(&mut self.pending_word);
175
176        #[allow(clippy::else_if_without_else)]
177        if !pending_line.is_empty() {
178            self.wrapped_lines.push_back(pending_line);
179        } else if pending_line.capacity() > 0 {
180            self.pending_line_pool.push(pending_line);
181        }
182        if self.wrapped_lines.is_empty() {
183            self.wrapped_lines.push_back(vec![]);
184        }
185    }
186
187    fn replace_current_line(&mut self, line: Vec<StyledGrapheme<'a>>) {
188        let cache = mem::replace(&mut self.current_line, line);
189        if cache.capacity() > 0 {
190            self.pending_line_pool.push(cache);
191        }
192    }
193}
194
195impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
196where
197    O: Iterator<Item = (I, Alignment)>,
198    I: Iterator<Item = StyledGrapheme<'a>>,
199{
200    #[allow(clippy::too_many_lines)]
201    fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
202        if self.max_line_width == 0 {
203            return None;
204        }
205
206        loop {
207            // emit next cached line if present
208            if let Some(line) = self.wrapped_lines.pop_front() {
209                let line_width = line
210                    .iter()
211                    .map(|grapheme| grapheme.symbol.width() as u16)
212                    .sum();
213
214                self.replace_current_line(line);
215                return Some(WrappedLine {
216                    line: &self.current_line,
217                    width: line_width,
218                    alignment: self.current_alignment,
219                });
220            }
221
222            // otherwise, process pending wrapped lines from input
223            let (line_symbols, line_alignment) = self.input_lines.next()?;
224            self.current_alignment = line_alignment;
225            self.process_input(line_symbols);
226        }
227    }
228}
229
230/// A state machine that truncates overhanging lines.
231#[derive(Debug, Default, Clone)]
232pub struct LineTruncator<'a, O, I>
233where
234    // Outer iterator providing the individual lines
235    O: Iterator<Item = (I, Alignment)>,
236    // Inner iterator providing the styled symbols of a line Each line consists of an alignment and
237    // a series of symbols
238    I: Iterator<Item = StyledGrapheme<'a>>,
239{
240    /// The given, unprocessed lines
241    input_lines: O,
242    max_line_width: u16,
243    current_line: Vec<StyledGrapheme<'a>>,
244    /// Record the offset to skip render
245    horizontal_offset: u16,
246}
247
248impl<'a, O, I> LineTruncator<'a, O, I>
249where
250    O: Iterator<Item = (I, Alignment)>,
251    I: Iterator<Item = StyledGrapheme<'a>>,
252{
253    pub const fn new(lines: O, max_line_width: u16) -> Self {
254        Self {
255            input_lines: lines,
256            max_line_width,
257            horizontal_offset: 0,
258            current_line: vec![],
259        }
260    }
261
262    pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
263        self.horizontal_offset = horizontal_offset;
264    }
265}
266
267impl<'a, O, I> LineComposer<'a> for LineTruncator<'a, O, I>
268where
269    O: Iterator<Item = (I, Alignment)>,
270    I: Iterator<Item = StyledGrapheme<'a>>,
271{
272    fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
273        if self.max_line_width == 0 {
274            return None;
275        }
276
277        self.current_line.truncate(0);
278        let mut current_line_width = 0;
279
280        let mut lines_exhausted = true;
281        let mut horizontal_offset = self.horizontal_offset as usize;
282        let mut current_alignment = Alignment::Left;
283        if let Some((current_line, alignment)) = &mut self.input_lines.next() {
284            lines_exhausted = false;
285            current_alignment = *alignment;
286
287            for StyledGrapheme { symbol, style } in current_line {
288                // Ignore characters wider that the total max width.
289                if symbol.width() as u16 > self.max_line_width {
290                    continue;
291                }
292
293                if current_line_width + symbol.width() as u16 > self.max_line_width {
294                    // Truncate line
295                    break;
296                }
297
298                let symbol = if horizontal_offset == 0 || Alignment::Left != *alignment {
299                    symbol
300                } else {
301                    let w = symbol.width();
302                    if w > horizontal_offset {
303                        let t = trim_offset(symbol, horizontal_offset);
304                        horizontal_offset = 0;
305                        t
306                    } else {
307                        horizontal_offset -= w;
308                        ""
309                    }
310                };
311                current_line_width += symbol.width() as u16;
312                self.current_line.push(StyledGrapheme { symbol, style });
313            }
314        }
315
316        if lines_exhausted {
317            None
318        } else {
319            Some(WrappedLine {
320                line: &self.current_line,
321                width: current_line_width,
322                alignment: current_alignment,
323            })
324        }
325    }
326}
327
328/// This function will return a str slice which start at specified offset.
329/// As src is a unicode str, start offset has to be calculated with each character.
330fn trim_offset(src: &str, mut offset: usize) -> &str {
331    let mut start = 0;
332    for c in UnicodeSegmentation::graphemes(src, true) {
333        let w = c.width();
334        if w <= offset {
335            offset -= w;
336            start += c.len();
337        } else {
338            break;
339        }
340    }
341    #[allow(clippy::string_slice)] // Is safe as it comes from UnicodeSegmentation
342    &src[start..]
343}
344
345#[cfg(test)]
346mod test {
347    use super::*;
348    use crate::{
349        style::Style,
350        text::{Line, Text},
351    };
352
353    #[derive(Clone, Copy)]
354    enum Composer {
355        WordWrapper { trim: bool },
356        LineTruncator,
357    }
358
359    fn run_composer<'a>(
360        which: Composer,
361        text: impl Into<Text<'a>>,
362        text_area_width: u16,
363    ) -> (Vec<String>, Vec<u16>, Vec<Alignment>) {
364        let text = text.into();
365        let styled_lines = text.iter().map(|line| {
366            (
367                line.iter()
368                    .flat_map(|span| span.styled_graphemes(Style::default())),
369                line.alignment.unwrap_or(Alignment::Left),
370            )
371        });
372
373        let mut composer: Box<dyn LineComposer> = match which {
374            Composer::WordWrapper { trim } => {
375                Box::new(WordWrapper::new(styled_lines, text_area_width, trim))
376            }
377            Composer::LineTruncator => Box::new(LineTruncator::new(styled_lines, text_area_width)),
378        };
379        let mut lines = vec![];
380        let mut widths = vec![];
381        let mut alignments = vec![];
382        while let Some(WrappedLine {
383            line: styled,
384            width,
385            alignment,
386        }) = composer.next_line()
387        {
388            let line = styled
389                .iter()
390                .map(|StyledGrapheme { symbol, .. }| *symbol)
391                .collect::<String>();
392            assert!(width <= text_area_width);
393            lines.push(line);
394            widths.push(width);
395            alignments.push(alignment);
396        }
397        (lines, widths, alignments)
398    }
399
400    #[test]
401    fn line_composer_one_line() {
402        let width = 40;
403        for i in 1..width {
404            let text = "a".repeat(i);
405            let (word_wrapper, _, _) =
406                run_composer(Composer::WordWrapper { trim: true }, &*text, width as u16);
407            let (line_truncator, _, _) =
408                run_composer(Composer::LineTruncator, &*text, width as u16);
409            let expected = vec![text];
410            assert_eq!(word_wrapper, expected);
411            assert_eq!(line_truncator, expected);
412        }
413    }
414
415    #[test]
416    fn line_composer_short_lines() {
417        let width = 20;
418        let text =
419            "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
420        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
421        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
422
423        let wrapped: Vec<&str> = text.split('\n').collect();
424        assert_eq!(word_wrapper, wrapped);
425        assert_eq!(line_truncator, wrapped);
426    }
427
428    #[test]
429    fn line_composer_long_word() {
430        let width = 20;
431        let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
432        let (word_wrapper, _, _) =
433            run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
434        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
435
436        let wrapped = vec![
437            text.get(..width).unwrap(),
438            text.get(width..width * 2).unwrap(),
439            text.get(width * 2..width * 3).unwrap(),
440            text.get(width * 3..).unwrap(),
441        ];
442        assert_eq!(
443            word_wrapper, wrapped,
444            "WordWrapper should detect the line cannot be broken on word boundary and \
445             break it at line width limit."
446        );
447        assert_eq!(line_truncator, [text.get(..width).unwrap()]);
448    }
449
450    #[test]
451    fn line_composer_long_sentence() {
452        let width = 20;
453        let text =
454            "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
455        let text_multi_space =
456            "abcd efghij    klmnopabcd efgh     ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
457             m n o";
458        let (word_wrapper_single_space, _, _) =
459            run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
460        let (word_wrapper_multi_space, _, _) = run_composer(
461            Composer::WordWrapper { trim: true },
462            text_multi_space,
463            width as u16,
464        );
465        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
466
467        let word_wrapped = vec![
468            "abcd efghij",
469            "klmnopabcd efgh",
470            "ijklmnopabcdefg",
471            "hijkl mnopab c d e f",
472            "g h i j k l m n o",
473        ];
474        assert_eq!(word_wrapper_single_space, word_wrapped);
475        assert_eq!(word_wrapper_multi_space, word_wrapped);
476
477        assert_eq!(line_truncator, [text.get(..width).unwrap()]);
478    }
479
480    #[test]
481    fn line_composer_zero_width() {
482        let width = 0;
483        let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
484        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
485        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
486
487        let expected: Vec<&str> = Vec::new();
488        assert_eq!(word_wrapper, expected);
489        assert_eq!(line_truncator, expected);
490    }
491
492    #[test]
493    fn line_composer_max_line_width_of_1() {
494        let width = 1;
495        let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
496        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
497        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
498
499        let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
500            .filter(|g| g.chars().any(|c| !c.is_whitespace()))
501            .collect();
502        assert_eq!(word_wrapper, expected);
503        assert_eq!(line_truncator, ["a"]);
504    }
505
506    #[test]
507    fn line_composer_max_line_width_of_1_double_width_characters() {
508        let width = 1;
509        let text =
510            "コンピュータ上で文字を扱う場合、典型的には文字\naaa\naによる通信を行う場合にその\
511                    両端点では、";
512        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
513        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
514        assert_eq!(word_wrapper, ["", "a", "a", "a", "a"]);
515        assert_eq!(line_truncator, ["", "a", "a"]);
516    }
517
518    /// Tests `WordWrapper` with words some of which exceed line length and some not.
519    #[test]
520    fn line_composer_word_wrapper_mixed_length() {
521        let width = 20;
522        let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
523        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
524        assert_eq!(
525            word_wrapper,
526            vec![
527                "abcd efghij",
528                "klmnopabcdefghijklmn",
529                "opabcdefghijkl",
530                "mnopab cdefghi j",
531                "klmno",
532            ]
533        );
534    }
535
536    #[test]
537    fn line_composer_double_width_chars() {
538        let width = 20;
539        let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
540                    では、";
541        let (word_wrapper, word_wrapper_width, _) =
542            run_composer(Composer::WordWrapper { trim: true }, text, width);
543        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
544        assert_eq!(line_truncator, ["コンピュータ上で文字"]);
545        let wrapped = [
546            "コンピュータ上で文字",
547            "を扱う場合、典型的に",
548            "は文字による通信を行",
549            "う場合にその両端点で",
550            "は、",
551        ];
552        assert_eq!(word_wrapper, wrapped);
553        assert_eq!(word_wrapper_width, [width, width, width, width, 4]);
554    }
555
556    #[test]
557    fn line_composer_leading_whitespace_removal() {
558        let width = 20;
559        let text = "AAAAAAAAAAAAAAAAAAAA    AAA";
560        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
561        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
562        assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAAAAAAA", "AAA"]);
563        assert_eq!(line_truncator, ["AAAAAAAAAAAAAAAAAAAA"]);
564    }
565
566    /// Tests truncation of leading whitespace.
567    #[test]
568    fn line_composer_lots_of_spaces() {
569        let width = 20;
570        let text = "                                                                     ";
571        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
572        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
573        assert_eq!(word_wrapper, [""]);
574        assert_eq!(line_truncator, ["                    "]);
575    }
576
577    /// Tests an input starting with a letter, followed by spaces - some of the behaviour is
578    /// incidental.
579    #[test]
580    fn line_composer_char_plus_lots_of_spaces() {
581        let width = 20;
582        let text = "a                                                                     ";
583        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
584        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
585        // What's happening below is: the first line gets consumed, trailing spaces discarded,
586        // after 20 of which a word break occurs (probably shouldn't). The second line break
587        // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
588        // that much.
589        assert_eq!(word_wrapper, ["a", ""]);
590        assert_eq!(line_truncator, ["a                   "]);
591    }
592
593    #[test]
594    fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
595        let width = 20;
596        // Japanese seems not to use spaces but we should break on spaces anyway... We're using it
597        // to test double-width chars.
598        // You are more than welcome to add word boundary detection based of alterations of
599        // hiragana and katakana...
600        // This happens to also be a test case for mixed width because regular spaces are single
601        // width.
602        let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
603        let (word_wrapper, word_wrapper_width, _) =
604            run_composer(Composer::WordWrapper { trim: true }, text, width);
605        assert_eq!(
606            word_wrapper,
607            vec![
608                "コンピュ",
609                "ータ上で文字を扱う場",
610                "合、 典型的には文",
611                "字による 通信を行",
612                "う場合にその両端点で",
613                "は、",
614            ]
615        );
616        // Odd-sized lines have a space in them.
617        assert_eq!(word_wrapper_width, [8, 20, 17, 17, 20, 4]);
618    }
619
620    /// Ensure words separated by nbsp are wrapped as if they were a single one.
621    #[test]
622    fn line_composer_word_wrapper_nbsp() {
623        let width = 20;
624        let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
625        let (word_wrapper, word_wrapper_widths, _) =
626            run_composer(Composer::WordWrapper { trim: true }, text, width);
627        assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA"]);
628        assert_eq!(word_wrapper_widths, [15, 8]);
629
630        // Ensure that if the character was a regular space, it would be wrapped differently.
631        let text_space = text.replace('\u{00a0}', " ");
632        let (word_wrapper_space, word_wrapper_widths, _) =
633            run_composer(Composer::WordWrapper { trim: true }, text_space, width);
634        assert_eq!(word_wrapper_space, ["AAAAAAAAAAAAAAA AAAA", "AAA"]);
635        assert_eq!(word_wrapper_widths, [20, 3]);
636    }
637
638    #[test]
639    fn line_composer_word_wrapper_preserve_indentation() {
640        let width = 20;
641        let text = "AAAAAAAAAAAAAAAAAAAA    AAA";
642        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
643        assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAAAAAAA", "   AAA"]);
644    }
645
646    #[test]
647    fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
648        let width = 10;
649        let text = "AAA AAA AAAAA AA AAAAAA\n B\n  C\n   D";
650        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
651        assert_eq!(
652            word_wrapper,
653            vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", "  C", "   D"]
654        );
655    }
656
657    #[test]
658    fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
659        let width = 10;
660        let text = "               4 Indent\n                 must wrap!";
661        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
662        assert_eq!(
663            word_wrapper,
664            vec![
665                "          ",
666                "    4",
667                "Indent",
668                "          ",
669                "      must",
670                "wrap!"
671            ]
672        );
673    }
674
675    #[test]
676    fn line_composer_zero_width_at_end() {
677        let width = 3;
678        let line = "foo\u{200B}";
679        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
680        let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
681        assert_eq!(word_wrapper, ["foo"]);
682        assert_eq!(line_truncator, ["foo\u{200B}"]);
683    }
684
685    #[test]
686    fn line_composer_preserves_line_alignment() {
687        let width = 20;
688        let lines = vec![
689            Line::from("Something that is left aligned.").alignment(Alignment::Left),
690            Line::from("This is right aligned and half short.").alignment(Alignment::Right),
691            Line::from("This should sit in the center.").alignment(Alignment::Center),
692        ];
693        let (_, _, wrapped_alignments) =
694            run_composer(Composer::WordWrapper { trim: true }, lines.clone(), width);
695        let (_, _, truncated_alignments) = run_composer(Composer::LineTruncator, lines, width);
696        assert_eq!(
697            wrapped_alignments,
698            vec![
699                Alignment::Left,
700                Alignment::Left,
701                Alignment::Right,
702                Alignment::Right,
703                Alignment::Right,
704                Alignment::Center,
705                Alignment::Center
706            ]
707        );
708        assert_eq!(
709            truncated_alignments,
710            vec![Alignment::Left, Alignment::Right, Alignment::Center]
711        );
712    }
713
714    #[test]
715    fn line_composer_zero_width_white_space() {
716        let width = 3;
717        let line = "foo\u{200b}bar";
718        let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
719        assert_eq!(word_wrapper, ["foo", "bar"]);
720    }
721}