ratatui_interact/components/
paragraph_ext.rs1use ratatui::{buffer::Buffer, layout::Rect, style::Style, text::Line, widgets::Widget};
31
32pub struct ParagraphExt<'a> {
40 lines: Vec<Line<'a>>,
41 scroll: u16,
42 width: Option<u16>,
43}
44
45impl<'a> ParagraphExt<'a> {
46 pub fn new(lines: Vec<Line<'a>>) -> Self {
48 Self {
49 lines,
50 scroll: 0,
51 width: None,
52 }
53 }
54
55 pub fn scroll(mut self, scroll: u16) -> Self {
57 self.scroll = scroll;
58 self
59 }
60
61 pub fn width(mut self, width: u16) -> Self {
65 self.width = Some(width);
66 self
67 }
68
69 fn wrap_lines(&self, width: u16) -> Vec<Vec<(char, Style)>> {
73 let width = width as usize;
74 if width == 0 {
75 return vec![];
76 }
77
78 let mut wrapped = Vec::new();
79
80 for line in &self.lines {
81 let mut chars: Vec<(char, Style)> = Vec::new();
83 for span in &line.spans {
84 for ch in span.content.chars() {
85 chars.push((ch, span.style));
86 }
87 }
88
89 if chars.is_empty() {
90 wrapped.push(vec![]);
91 continue;
92 }
93
94 let mut start = 0;
96 while start < chars.len() {
97 let remaining = chars.len() - start;
98 if remaining <= width {
99 wrapped.push(chars[start..].to_vec());
100 break;
101 }
102
103 let end = start + width;
104 let mut break_at = end;
105
106 for i in (start..end).rev() {
108 if chars[i].0 == ' ' {
109 break_at = i + 1;
110 break;
111 }
112 }
113
114 wrapped.push(chars[start..break_at].to_vec());
115 start = break_at;
116
117 while start < chars.len() && chars[start].0 == ' ' {
119 start += 1;
120 }
121 }
122 }
123
124 wrapped
125 }
126
127 pub fn line_count(&self, width: u16) -> usize {
131 self.wrap_lines(width).len()
132 }
133}
134
135impl Widget for ParagraphExt<'_> {
136 fn render(self, area: Rect, buf: &mut Buffer) {
137 let width = self.width.unwrap_or(area.width);
138 let wrapped = self.wrap_lines(width);
139 let scroll = self.scroll as usize;
140
141 for y in area.y..area.y + area.height {
143 for x in area.x..area.x + area.width {
144 buf[(x, y)].reset();
145 }
146 }
147
148 let visible = wrapped.iter().skip(scroll).take(area.height as usize);
149
150 for (row, line_chars) in visible.enumerate() {
151 let y = area.y + row as u16;
152 if y >= area.y + area.height {
153 break;
154 }
155
156 for (col, (ch, style)) in line_chars.iter().enumerate() {
158 let x = area.x + col as u16;
159 if x >= area.x + area.width {
160 break;
161 }
162 buf[(x, y)].set_char(*ch).set_style(*style);
163 }
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use ratatui::style::Color;
172 use ratatui::text::Span;
173
174 #[test]
175 fn test_empty_lines() {
176 let widget = ParagraphExt::new(vec![]);
177 let area = Rect::new(0, 0, 20, 5);
178 let mut buf = Buffer::empty(area);
179 widget.render(area, &mut buf);
180 }
182
183 #[test]
184 fn test_simple_render() {
185 let lines = vec![Line::from("Hello")];
186 let widget = ParagraphExt::new(lines);
187 let area = Rect::new(0, 0, 20, 5);
188 let mut buf = Buffer::empty(area);
189 widget.render(area, &mut buf);
190
191 assert_eq!(buf[(0, 0)].symbol(), "H");
193 assert_eq!(buf[(1, 0)].symbol(), "e");
194 assert_eq!(buf[(2, 0)].symbol(), "l");
195 assert_eq!(buf[(3, 0)].symbol(), "l");
196 assert_eq!(buf[(4, 0)].symbol(), "o");
197 }
198
199 #[test]
200 fn test_word_wrap() {
201 let lines = vec![Line::from("Hello world this is a test")];
202 let widget = ParagraphExt::new(lines).width(10);
203 let area = Rect::new(0, 0, 10, 5);
204 let mut buf = Buffer::empty(area);
205 widget.render(area, &mut buf);
206
207 assert_eq!(buf[(0, 0)].symbol(), "H");
209 assert_eq!(buf[(0, 1)].symbol(), "w");
211 }
212
213 #[test]
214 fn test_scroll() {
215 let lines = vec![
216 Line::from("Line 1"),
217 Line::from("Line 2"),
218 Line::from("Line 3"),
219 ];
220 let widget = ParagraphExt::new(lines).scroll(1);
221 let area = Rect::new(0, 0, 20, 2);
222 let mut buf = Buffer::empty(area);
223 widget.render(area, &mut buf);
224
225 assert_eq!(buf[(0, 0)].symbol(), "L");
227 assert_eq!(buf[(5, 0)].symbol(), "2");
228 }
229
230 #[test]
231 fn test_styled_text() {
232 let lines = vec![Line::from(vec![
233 Span::styled("Red", Style::default().fg(Color::Red)),
234 Span::raw(" "),
235 Span::styled("Blue", Style::default().fg(Color::Blue)),
236 ])];
237 let widget = ParagraphExt::new(lines);
238 let area = Rect::new(0, 0, 20, 1);
239 let mut buf = Buffer::empty(area);
240 widget.render(area, &mut buf);
241
242 assert_eq!(buf[(0, 0)].fg, Color::Red);
244 assert_eq!(buf[(4, 0)].fg, Color::Blue);
245 }
246
247 #[test]
248 fn test_line_count() {
249 let lines = vec![Line::from("Hello world this is a long line")];
250 let widget = ParagraphExt::new(lines);
251
252 let count = widget.line_count(10);
254 assert!(count > 1);
255 }
256
257 #[test]
258 fn test_empty_line_preserved() {
259 let lines = vec![Line::from("Line 1"), Line::from(""), Line::from("Line 3")];
260 let widget = ParagraphExt::new(lines);
261 let count = widget.line_count(20);
262 assert_eq!(count, 3);
263 }
264}