datui_lib/widgets/textarea/
wrap.rs1use ratatui::{buffer::Buffer, layout::Rect};
9
10use super::{TextArea, Viewport};
11
12#[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 pub fn set_wrap(&mut self, wrap: bool) {
23 self.wrap = wrap;
24 }
25
26 pub(super) fn wraps_now(&self) -> bool {
29 self.wrap && self.viewport.get().width > 0
30 }
31
32 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 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 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 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 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 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 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 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 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}