Skip to main content

ctl_text/
wrap.rs

1//! Fitting a line to a width: wrapping and truncation.
2
3use anstyle::Style;
4
5use crate::cells::{prefix_len, text_width};
6use crate::line::{Line, Piece};
7
8/// The text an [`Overflow::Ellipsis`] cut ends with. One cell wide.
9pub const ELLIPSIS: &str = "…";
10
11/// What happens to a line wider than the width it is given.
12///
13/// Each variant is a policy a layout offers a column or a paragraph. No
14/// policy splits a grapheme cluster.
15#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
16#[non_exhaustive]
17pub enum Overflow {
18    /// Break lines at spaces only. A word wider than the width keeps a line
19    /// of its own and runs past the width.
20    #[default]
21    KeepWords,
22    /// Break lines at spaces, and break a word wider than the width between
23    /// clusters. A cluster wider than the width keeps a line of its own.
24    Fold,
25    /// Keep one line and cut it with an [`ELLIPSIS`] when it is too wide.
26    Ellipsis,
27}
28
29impl Line {
30    /// The width of the widest part of the line that `overflow` will not
31    /// break: the minimum width it can be laid out in.
32    pub(crate) fn minimum_width(&self, overflow: Overflow) -> u16 {
33        let pieces = self.pieces();
34        match overflow {
35            Overflow::KeepWords => pieces
36                .split(|piece| piece.space)
37                .map(total_width)
38                .max()
39                .unwrap_or(0),
40            Overflow::Fold => pieces
41                .iter()
42                .filter(|piece| !piece.space)
43                .map(|piece| piece.width)
44                .max()
45                .unwrap_or(0),
46            Overflow::Ellipsis => u16::from(self.width() > 0),
47        }
48    }
49
50    /// The line cut to exactly `width` cells and ended with [`ELLIPSIS`], or
51    /// the line itself when it fits.
52    ///
53    /// The text is cut at a cluster boundary. A wide cluster that straddles
54    /// the cut is replaced by a space, so a cut line is never narrower than
55    /// `width`. The padding and the ellipsis take the style of the text they
56    /// replace. A width of 0 gives an empty line.
57    #[must_use]
58    pub fn truncate(&self, width: u16) -> Self {
59        if self.width() <= width {
60            return self.clone();
61        }
62        let Some(budget) = width.checked_sub(ELLIPSIS_WIDTH) else {
63            return Self::new();
64        };
65        let mut cut = Self::new();
66        let mut used: u16 = 0;
67        let mut style = Style::new();
68        for span in self.spans() {
69            style = span.style();
70            let span_width = span.width();
71            if used.saturating_add(span_width) <= budget {
72                cut.push(span.clone());
73                used = used.saturating_add(span_width);
74            } else {
75                let room = budget.saturating_sub(used);
76                let kept = &span.text()[..prefix_len(span.text(), room)];
77                let gap = usize::from(room.saturating_sub(text_width(kept)));
78                cut.append(kept, style);
79                cut.append(&" ".repeat(gap), style);
80                break;
81            }
82        }
83        cut.append(ELLIPSIS, style);
84        cut
85    }
86
87    /// The line fitted to `width` cells by `overflow`: wrapped into lines for
88    /// [`Overflow::KeepWords`] and [`Overflow::Fold`], or cut into one line
89    /// for [`Overflow::Ellipsis`].
90    ///
91    /// A wrapped line never ends in a space: the spaces at the end of the line
92    /// and at each break are dropped. An ellipsis line that fits keeps the
93    /// spaces it ends with, as [`Line::truncate`] does. Call
94    /// [`trim_end`](Line::trim_end) first; [`Text`](crate::Text) does this for
95    /// you. A width of 0 counts as 1 when wrapping.
96    #[must_use]
97    pub fn fit(&self, width: u16, overflow: Overflow) -> Vec<Self> {
98        match overflow {
99            Overflow::KeepWords | Overflow::Fold => self.wrap(width, overflow),
100            Overflow::Ellipsis => vec![self.truncate(width)],
101        }
102    }
103
104    /// The line broken into lines no wider than `width`, except where
105    /// `overflow` lets a word or a cluster run past it.
106    fn wrap(&self, width: u16, overflow: Overflow) -> Vec<Self> {
107        let pieces = self.pieces();
108        let mut wrapper = Wrapper::new(width.max(1), overflow);
109        let mut rest = pieces.as_slice();
110        while !rest.is_empty() {
111            let space_len = rest.iter().take_while(|piece| piece.space).count();
112            let (space, tail) = rest.split_at(space_len);
113            let word_len = tail.iter().take_while(|piece| !piece.space).count();
114            let (word, next) = tail.split_at(word_len);
115            if word.is_empty() {
116                break;
117            }
118            wrapper.place(space, word);
119            rest = next;
120        }
121        wrapper.finish()
122    }
123}
124
125const ELLIPSIS_WIDTH: u16 = 1;
126
127fn total_width(pieces: &[Piece<'_>]) -> u16 {
128    pieces
129        .iter()
130        .fold(0, |total, piece| total.saturating_add(piece.width))
131}
132
133/// The line being built and the lines already done.
134///
135/// A word is placed after the spaces that came before it. Leading spaces
136/// stay only on the first line, as indentation, and only when the word still
137/// fits behind them. Every other space at a break is dropped, so no line
138/// ends or starts with the spaces of a break.
139struct Wrapper<'a> {
140    width: u16,
141    overflow: Overflow,
142    lines: Vec<Line>,
143    current: Vec<Piece<'a>>,
144    current_width: u16,
145}
146
147impl<'a> Wrapper<'a> {
148    fn new(width: u16, overflow: Overflow) -> Self {
149        Self {
150            width,
151            overflow,
152            lines: Vec::new(),
153            current: Vec::new(),
154            current_width: 0,
155        }
156    }
157
158    fn place(&mut self, space: &[Piece<'a>], word: &[Piece<'a>]) {
159        let space_width = total_width(space);
160        let word_width = total_width(word);
161        if self.current.is_empty() {
162            let indented =
163                self.lines.is_empty() && space_width.saturating_add(word_width) <= self.width;
164            if indented {
165                self.extend(space);
166            }
167            self.put_word(word, word_width);
168        } else if self
169            .current_width
170            .saturating_add(space_width)
171            .saturating_add(word_width)
172            <= self.width
173        {
174            self.extend(space);
175            self.extend(word);
176        } else {
177            self.break_line();
178            self.put_word(word, word_width);
179        }
180    }
181
182    /// Adds a word to the empty or fitting current line, folding it between
183    /// clusters when the policy allows and it is wider than a line.
184    fn put_word(&mut self, word: &[Piece<'a>], word_width: u16) {
185        if self.overflow == Overflow::Fold && word_width > self.width {
186            for piece in word {
187                let overflows = self.current_width.saturating_add(piece.width) > self.width;
188                if overflows && !self.current.is_empty() {
189                    self.break_line();
190                }
191                self.push(*piece);
192            }
193        } else {
194            self.extend(word);
195        }
196    }
197
198    fn extend(&mut self, pieces: &[Piece<'a>]) {
199        for piece in pieces {
200            self.push(*piece);
201        }
202    }
203
204    fn push(&mut self, piece: Piece<'a>) {
205        self.current_width = self.current_width.saturating_add(piece.width);
206        self.current.push(piece);
207    }
208
209    fn break_line(&mut self) {
210        self.lines.push(Line::from_pieces(&self.current));
211        self.current.clear();
212        self.current_width = 0;
213    }
214
215    fn finish(mut self) -> Vec<Line> {
216        if !self.current.is_empty() || self.lines.is_empty() {
217            self.break_line();
218        }
219        self.lines
220    }
221}