1use ratatui::{
10 buffer::Buffer,
11 layout::Rect,
12 style::Style,
13 text::{Line, Span},
14 widgets::{Paragraph, Widget, Wrap},
15};
16use ratatui_textarea::TextArea;
17use unicode_width::UnicodeWidthChar;
18
19pub fn render_wrapped_input(
22 textarea: &TextArea,
23 area: Rect,
24 buf: &mut Buffer,
25 cursor_style: Style,
26) {
27 let lines = textarea.lines();
28 let (cursor_row, cursor_col) = textarea.cursor();
29 let width = area.width as usize;
30
31 if width == 0 || area.height == 0 {
32 return;
33 }
34
35 let display_lines: Vec<Line<'_>> = lines
37 .iter()
38 .map(|l| Line::from(Span::raw(l.as_str())))
39 .collect();
40
41 let paragraph = Paragraph::new(display_lines).wrap(Wrap { trim: false });
43 paragraph.render(area, buf);
44
45 let (vis_row, vis_col) = logical_to_visual(lines, cursor_row, cursor_col, width);
47
48 let cursor_y = area.y + vis_row as u16;
50 let cursor_x = area.x + vis_col as u16;
51 if cursor_y < area.y + area.height && cursor_x < area.x + area.width {
52 let cell = &mut buf[(cursor_x, cursor_y)];
53 cell.set_style(cursor_style);
54 }
55}
56
57pub fn wrapped_height(textarea: &TextArea, width: usize) -> usize {
61 if width == 0 {
62 return textarea.lines().len().max(1);
63 }
64 textarea
65 .lines()
66 .iter()
67 .map(|line| visual_line_count(line, width))
68 .sum::<usize>()
69 .max(1)
70}
71
72fn logical_to_visual(
75 lines: &[String],
76 cursor_row: usize,
77 cursor_col: usize,
78 width: usize,
79) -> (usize, usize) {
80 let mut visual_row = 0usize;
81
82 for line in lines.iter().take(cursor_row) {
84 visual_row += visual_line_count(line, width);
85 }
86
87 let cursor_line = lines.get(cursor_row).map(|s| s.as_str()).unwrap_or("");
89 let (extra_rows, vis_col) = cursor_visual_offset(cursor_line, cursor_col, width);
90 visual_row += extra_rows;
91
92 (visual_row, vis_col)
93}
94
95fn visual_line_count(line: &str, width: usize) -> usize {
99 crate::wrap_util::visual_line_count(line, width)
100}
101
102fn cursor_visual_offset(line: &str, cursor_col: usize, width: usize) -> (usize, usize) {
106 let w = width.max(1);
107 let mut visual_row = 0usize;
108 let mut col = 0usize;
109 let mut word_start_col = 0usize;
110 let mut in_word = false;
111 for (char_idx, ch) in line.chars().enumerate() {
112 if char_idx == cursor_col {
113 return (visual_row, col);
114 }
115
116 let char_w = ch.width().unwrap_or(0);
117 let is_space = ch == ' ' || ch == '\t';
118
119 if is_space {
120 in_word = false;
121 if col + char_w > w {
122 visual_row += 1;
123 col = char_w;
124 } else {
125 col += char_w;
126 }
127 word_start_col = col;
128 } else {
129 if !in_word {
130 word_start_col = col;
131 in_word = true;
132 }
133 if col + char_w > w {
134 if word_start_col > 0 && word_start_col <= w {
135 visual_row += 1;
136 let word_len_so_far = col - word_start_col;
137 col = word_len_so_far + char_w;
138 word_start_col = 0;
139 } else {
140 visual_row += 1;
141 col = char_w;
142 word_start_col = 0;
143 }
144 } else {
145 col += char_w;
146 }
147 }
148 }
149
150 (visual_row, col)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_visual_line_count_short() {
160 assert_eq!(visual_line_count("hello", 80), 1);
161 }
162
163 #[test]
164 fn test_visual_line_count_empty() {
165 assert_eq!(visual_line_count("", 80), 1);
166 }
167
168 #[test]
169 fn test_visual_line_count_wrap() {
170 let line = "x".repeat(100);
172 assert_eq!(visual_line_count(&line, 80), 2);
173 }
174
175 #[test]
176 fn test_visual_line_count_word_wrap() {
177 let line = format!("{} {}", "a".repeat(75), "b".repeat(75));
181 assert_eq!(visual_line_count(&line, 80), 2);
182 }
183
184 #[test]
185 fn test_cursor_visual_offset_no_wrap() {
186 assert_eq!(cursor_visual_offset("hello world", 5, 80), (0, 5));
187 }
188
189 #[test]
190 fn test_cursor_visual_offset_after_wrap() {
191 let line = format!("{}abc", "x".repeat(80));
195 assert_eq!(cursor_visual_offset(&line, 82, 80), (1, 2));
196 }
197
198 #[test]
199 fn test_cursor_visual_offset_at_end() {
200 let line = "hello";
201 assert_eq!(cursor_visual_offset(line, 5, 80), (0, 5));
202 }
203
204 #[test]
205 fn test_wrapped_height_multiline() {
206 let mut ta = TextArea::default();
207 ta.insert_str("short line");
208 ta.insert_newline();
209 ta.insert_str("another line");
210 assert_eq!(wrapped_height(&ta, 80), 2);
212 }
213
214 #[test]
215 fn test_wrapped_height_long_line() {
216 let mut ta = TextArea::default();
217 ta.insert_str("x".repeat(200));
218 assert_eq!(wrapped_height(&ta, 80), 3);
220 }
221
222 #[test]
223 fn test_logical_to_visual_first_line() {
224 let lines = vec!["hello world".to_string()];
225 assert_eq!(logical_to_visual(&lines, 0, 5, 80), (0, 5));
226 }
227
228 #[test]
229 fn test_logical_to_visual_second_line() {
230 let lines = vec!["first".to_string(), "second".to_string()];
231 assert_eq!(logical_to_visual(&lines, 1, 3, 80), (1, 3));
232 }
233
234 #[test]
235 fn test_logical_to_visual_with_wrapped_previous() {
236 let lines = vec!["x".repeat(200), "hello".to_string()];
238 assert_eq!(logical_to_visual(&lines, 1, 3, 80), (3, 3));
240 }
241}