datui_lib/widgets/textarea/
render.rs1use ratatui::{
10 buffer::Buffer,
11 layout::Rect,
12 style::Style,
13 widgets::{StatefulWidget, Widget},
14};
15use unicode_width::UnicodeWidthChar;
16
17use super::{TextArea, Viewport};
18
19struct DisplayCell {
21 symbol: String,
22 width: usize,
23 style: Style,
24}
25
26impl TextArea {
27 pub fn display_col(&self, row: usize, col: usize) -> usize {
29 let Some(line) = self.lines.get(row) else {
30 return 0;
31 };
32 let mut width = 0;
33 for c in line.chars().take(col) {
34 width += self.char_width(c, width);
35 }
36 width
37 }
38
39 pub(super) fn char_width(&self, c: char, at: usize) -> usize {
41 if c == '\t' {
42 self.tab_len - (at % self.tab_len)
43 } else {
44 UnicodeWidthChar::width(c).unwrap_or(0)
45 }
46 }
47
48 fn viewport_for(&self, area: Rect) -> Viewport {
51 let prev = self.viewport.get();
52 let (cursor_row, cursor_col) = self.cursor;
53
54 let height = area.height as usize;
55 let mut row = prev.row.min(self.lines.len().saturating_sub(1));
56 if cursor_row < row {
57 row = cursor_row;
58 } else if height > 0 && cursor_row >= row + height {
59 row = cursor_row + 1 - height;
60 }
61
62 let width = area.width as usize;
63 let cursor_x = self.display_col(cursor_row, cursor_col);
64 let mut col = prev.col;
65 if cursor_x < col {
66 col = cursor_x;
67 } else if width > 0 && cursor_x >= col + width {
68 col = cursor_x + 1 - width;
69 }
70
71 Viewport {
72 row,
73 col,
74 height: area.height,
75 width: area.width,
76 }
77 }
78
79 pub(super) fn style_at(&self, row: usize, col: usize) -> Style {
82 let mut style = self.style;
83 if let Some((start, end)) = self.selection()
84 && (row, col) >= start
85 && (row, col) < end
86 {
87 style = style.patch(self.selection_style);
88 }
89 if self.cursor_visible && self.cursor == (row, col) {
90 style = style.patch(self.cursor_style);
91 }
92 style
93 }
94
95 fn cells_for_line(&self, row: usize) -> Vec<DisplayCell> {
98 let line = &self.lines[row];
99 let mut cells = Vec::with_capacity(line.len() + 1);
100 let mut width = 0;
101
102 for (col, c) in line.chars().enumerate() {
103 let style = self.style_at(row, col);
104 if c == '\t' {
105 let spaces = self.tab_len - (width % self.tab_len);
106 for _ in 0..spaces {
107 cells.push(DisplayCell {
108 symbol: " ".to_string(),
109 width: 1,
110 style,
111 });
112 }
113 width += spaces;
114 continue;
115 }
116 let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
117 if char_width == 0 {
118 continue;
119 }
120 cells.push(DisplayCell {
121 symbol: c.to_string(),
122 width: char_width,
123 style,
124 });
125 width += char_width;
126 }
127
128 let line_len = line.chars().count();
129 if self.cursor == (row, line_len) {
130 cells.push(DisplayCell {
131 symbol: " ".to_string(),
132 width: 1,
133 style: self.style_at(row, line_len),
134 });
135 }
136 cells
137 }
138
139 fn render_line(cells: &[DisplayCell], area: Rect, y: u16, scroll: usize, buf: &mut Buffer) {
142 let mut x = area.x;
143 let mut column = 0;
144 for cell in cells {
145 let end = column + cell.width;
146 if end <= scroll {
147 column = end;
148 continue;
149 }
150 if x >= area.right() {
151 return;
152 }
153 let clipped_left = column < scroll;
156 let overflows_right = x as usize + cell.width > area.right() as usize;
157 if clipped_left || overflows_right {
158 let visible = if clipped_left {
159 end - scroll
160 } else {
161 cell.width
162 };
163 for _ in 0..visible {
164 if x >= area.right() {
165 break;
166 }
167 buf[(x, y)].set_symbol(" ").set_style(cell.style);
168 x += 1;
169 }
170 column = end;
171 continue;
172 }
173 buf[(x, y)].set_symbol(&cell.symbol).set_style(cell.style);
174 for offset in 1..cell.width as u16 {
175 buf[(x + offset, y)].set_symbol(" ").set_style(cell.style);
176 }
177 x += cell.width as u16;
178 column = end;
179 }
180 }
181}
182
183impl Widget for &TextArea {
184 fn render(self, area: Rect, buf: &mut Buffer) {
185 if area.width == 0 || area.height == 0 {
186 return;
187 }
188 if self.wrap {
189 self.render_wrapped(area, buf);
190 return;
191 }
192 let viewport = self.viewport_for(area);
193 self.viewport.set(viewport);
194
195 buf.set_style(area, self.style);
196
197 for offset in 0..area.height {
198 let row = viewport.row + offset as usize;
199 if row >= self.lines.len() {
200 break;
201 }
202 let cells = self.cells_for_line(row);
203 TextArea::render_line(&cells, area, area.y + offset, viewport.col, buf);
204 }
205 }
206}
207
208impl StatefulWidget for &TextArea {
209 type State = (usize, usize);
212
213 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
214 Widget::render(self, area, buf);
215 *state = self.scroll_offsets();
216 }
217}