Skip to main content

datui_lib/widgets/textarea/
wrap.rs

1//! Soft wrapping: a long line breaks onto the rows below instead of scrolling
2//! sideways.
3//!
4//! Wrapping is a matter of display only. The buffer keeps its lines as typed,
5//! and a visual row is a character range of one of them. Rows break after the
6//! last space that fits, or mid-word when a word is wider than the field.
7
8use ratatui::{buffer::Buffer, layout::Rect};
9
10use super::{TextArea, Viewport};
11
12/// One visual row: buffer line and the character range `[start, end)` on it.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub(super) struct VisualRow {
15    pub line: usize,
16    pub start: usize,
17    pub end: usize,
18}
19
20impl TextArea {
21    /// Break long lines onto the rows below rather than scrolling sideways.
22    pub fn set_wrap(&mut self, wrap: bool) {
23        self.wrap = wrap;
24    }
25
26    /// Wrapping, and drawn at least once: before the first frame there is no
27    /// width to wrap to, and movement goes by buffer line.
28    pub(super) fn wraps_now(&self) -> bool {
29        self.wrap && self.viewport.get().width > 0
30    }
31
32    /// Rows the buffer takes at `width` columns: one per line without
33    /// wrapping.
34    pub fn visual_rows(&self, width: u16) -> usize {
35        if !self.wrap || width == 0 {
36            return self.lines.len();
37        }
38        (0..self.lines.len())
39            .map(|line| self.line_rows(line, width as usize).len())
40            .sum()
41    }
42
43    /// The rows one line wraps into. A line that exactly fills its last row
44    /// gets an empty row after it, where the cursor at its end is drawn, so
45    /// the height does not change as the cursor moves.
46    fn line_rows(&self, line: usize, width: usize) -> Vec<VisualRow> {
47        let chars: Vec<char> = self.lines[line].chars().collect();
48        let mut rows = Vec::new();
49        let mut start = 0;
50        let mut used = 0;
51        // Just past the last space in the current row: where it breaks.
52        let mut after_space = None;
53        let mut i = 0;
54        while i < chars.len() {
55            let w = self.char_width(chars[i], used);
56            if used + w > width && i > start {
57                let end = after_space.filter(|&b| b > start).unwrap_or(i);
58                rows.push(VisualRow { line, start, end });
59                start = end;
60                used = 0;
61                after_space = None;
62                i = start;
63                continue;
64            }
65            used += w;
66            if chars[i] == ' ' {
67                after_space = Some(i + 1);
68            }
69            i += 1;
70        }
71        rows.push(VisualRow {
72            line,
73            start,
74            end: chars.len(),
75        });
76        if used >= width {
77            rows.push(VisualRow {
78                line,
79                start: chars.len(),
80                end: chars.len(),
81            });
82        }
83        rows
84    }
85
86    /// Every visual row of the buffer, in order.
87    pub(super) fn layout(&self, width: usize) -> Vec<VisualRow> {
88        (0..self.lines.len())
89            .flat_map(|line| self.line_rows(line, width))
90            .collect()
91    }
92
93    /// The visual row holding `(line, col)`: the last row of that line
94    /// starting at or before the column.
95    fn row_of(layout: &[VisualRow], (line, col): (usize, usize)) -> usize {
96        layout
97            .iter()
98            .rposition(|r| r.line == line && r.start <= col)
99            .unwrap_or(0)
100    }
101
102    /// Display width of `[start, col)` on a line.
103    fn width_between(&self, line: usize, start: usize, col: usize) -> usize {
104        let mut used = 0;
105        for c in self.lines[line].chars().skip(start).take(col - start) {
106            used += self.char_width(c, used);
107        }
108        used
109    }
110
111    /// The position one visual row up or down from the cursor, keeping its
112    /// display column where the row allows. `None` at the first or last row.
113    pub(super) fn visual_step(&self, down: bool) -> Option<(usize, usize)> {
114        let width = self.viewport.get().width as usize;
115        let layout = self.layout(width);
116        let here = Self::row_of(&layout, self.cursor);
117        let there = if down {
118            Some(here + 1).filter(|&r| r < layout.len())?
119        } else {
120            here.checked_sub(1)?
121        };
122        let from = layout[here];
123        let x = self.width_between(from.line, from.start, self.cursor.1);
124        let to = layout[there];
125        // The end of a row that continues below is the next row's start, so a
126        // cursor landing there would show on the wrong row.
127        let continues = layout
128            .get(there + 1)
129            .is_some_and(|next| next.line == to.line);
130        let last = if continues && to.end > to.start {
131            to.end - 1
132        } else {
133            to.end
134        };
135        let mut col = to.start;
136        let mut used = 0;
137        for c in self.lines[to.line]
138            .chars()
139            .skip(to.start)
140            .take(last - to.start)
141        {
142            let w = self.char_width(c, used);
143            if used + w > x {
144                break;
145            }
146            used += w;
147            col += 1;
148        }
149        Some((to.line, col))
150    }
151
152    /// Draw the buffer wrapped to `area`, scrolled by whole rows to keep the
153    /// cursor in view.
154    pub(super) fn render_wrapped(&self, area: Rect, buf: &mut Buffer) {
155        let width = area.width as usize;
156        let layout = self.layout(width);
157        let cursor_row = Self::row_of(&layout, self.cursor);
158        let height = area.height as usize;
159        let mut top = self.viewport.get().row.min(layout.len().saturating_sub(1));
160        if cursor_row < top {
161            top = cursor_row;
162        } else if height > 0 && cursor_row >= top + height {
163            top = cursor_row + 1 - height;
164        }
165        self.viewport.set(Viewport {
166            row: top,
167            col: 0,
168            height: area.height,
169            width: area.width,
170        });
171        buf.set_style(area, self.style);
172
173        for (offset, row) in layout.iter().skip(top).take(height).enumerate() {
174            let y = area.y + offset as u16;
175            let mut x = area.x;
176            let mut used = 0;
177            let chars = self.lines[row.line].chars().skip(row.start);
178            for (i, c) in chars.take(row.end - row.start).enumerate() {
179                let col = row.start + i;
180                let w = self.char_width(c, used);
181                used += w;
182                if w == 0 {
183                    continue;
184                }
185                let style = self.style_at(row.line, col);
186                let symbol = if c == '\t' {
187                    " ".to_string()
188                } else {
189                    c.to_string()
190                };
191                buf[(x, y)].set_symbol(&symbol).set_style(style);
192                for extra in 1..w as u16 {
193                    if x + extra < area.right() {
194                        buf[(x + extra, y)].set_symbol(" ").set_style(style);
195                    }
196                }
197                x = x.saturating_add(w as u16);
198                if x >= area.right() {
199                    break;
200                }
201            }
202            let line_len = self.lines[row.line].chars().count();
203            if row.end == line_len
204                && self.cursor == (row.line, line_len)
205                && Self::row_of(&layout, self.cursor) == top + offset
206                && x < area.right()
207            {
208                buf[(x, y)]
209                    .set_symbol(" ")
210                    .set_style(self.style_at(row.line, line_len));
211            }
212        }
213    }
214}