flywheel/widget/
scroll_buffer.rs1use std::collections::VecDeque;
7use crate::buffer::Cell;
8
9#[derive(Debug, Clone)]
11pub struct StyledLine {
12 pub content: Vec<Cell>,
14 pub wrapped: bool,
16}
17
18impl StyledLine {
19 pub const fn new(content: Vec<Cell>, wrapped: bool) -> Self {
21 Self { content, wrapped }
22 }
23
24 pub const fn empty() -> Self {
26 Self {
27 content: Vec::new(),
28 wrapped: false,
29 }
30 }
31}
32
33#[derive(Debug)]
38pub struct ScrollBuffer {
39 lines: VecDeque<StyledLine>,
41 max_lines: usize,
43 scroll_offset: usize,
45}
46
47impl ScrollBuffer {
48 pub fn new(max_lines: usize) -> Self {
50 let mut lines = VecDeque::with_capacity(max_lines);
51 lines.push_back(StyledLine::empty());
52
53 Self {
54 lines,
55 max_lines,
56 scroll_offset: 0,
57 }
58 }
59
60 pub fn len(&self) -> usize {
62 self.lines.len()
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.lines.is_empty()
68 }
69
70 pub fn current_line(&self) -> &StyledLine {
76 self.lines.back().expect("Buffer should never be empty")
77 }
78
79 pub fn current_line_mut(&mut self) -> &mut StyledLine {
85 self.lines.back_mut().expect("Buffer should never be empty")
86 }
87
88 pub fn append(&mut self, cells: impl IntoIterator<Item = Cell>) {
90 self.current_line_mut().content.extend(cells);
91 }
92
93 pub fn newline(&mut self, wrapped: bool) {
99 while self.lines.len() >= self.max_lines {
101 self.lines.pop_front();
102 }
103
104 self.lines.push_back(StyledLine::new(Vec::new(), wrapped));
105 }
106
107 pub fn get(&self, index: usize) -> Option<&StyledLine> {
109 self.lines.get(index)
110 }
111
112 pub fn visible_lines(&self, viewport_height: usize) -> impl Iterator<Item = &StyledLine> {
117 let total = self.lines.len();
118 let end = total.saturating_sub(self.scroll_offset);
119 let start = end.saturating_sub(viewport_height);
120
121 self.lines.range(start..end)
122 }
123
124 pub fn scroll_up(&mut self, lines: usize) {
126 let max_offset = self.lines.len().saturating_sub(1);
127 self.scroll_offset = (self.scroll_offset + lines).min(max_offset);
128 }
129
130 pub const fn scroll_down(&mut self, lines: usize) {
132 self.scroll_offset = self.scroll_offset.saturating_sub(lines);
133 }
134
135 pub const fn scroll_to_bottom(&mut self) {
137 self.scroll_offset = 0;
138 }
139
140 pub const fn at_bottom(&self) -> bool {
142 self.scroll_offset == 0
143 }
144
145 pub fn clear(&mut self) {
147 self.lines.clear();
148 self.lines.push_back(StyledLine::empty());
149 self.scroll_offset = 0;
150 }
151
152 pub fn current_line_len(&self) -> usize {
154 self.current_line().content.len()
155 }
156
157 pub fn rewrap(&mut self, new_width: usize) {
162 if new_width == 0 {
163 return;
164 }
165
166 let mut logical_lines: Vec<Vec<Cell>> = Vec::new();
168 let mut current_logical: Vec<Cell> = Vec::new();
169
170 for line in &self.lines {
171 current_logical.extend(line.content.iter().copied());
172 if !line.wrapped {
173 logical_lines.push(std::mem::take(&mut current_logical));
175 }
176 }
177 if !current_logical.is_empty() || logical_lines.is_empty() {
179 logical_lines.push(current_logical);
180 }
181
182 self.lines.clear();
184 for logical in logical_lines {
185 if logical.is_empty() {
186 self.lines.push_back(StyledLine::empty());
187 } else {
188 let chunks: Vec<_> = logical.chunks(new_width).collect();
189 let chunk_count = chunks.len();
190 for (i, chunk) in chunks.into_iter().enumerate() {
191 let wrapped = i < chunk_count - 1;
192 self.lines.push_back(StyledLine::new(chunk.to_vec(), wrapped));
193 }
194 }
195 }
196
197 if self.lines.is_empty() {
199 self.lines.push_back(StyledLine::empty());
200 }
201
202 while self.lines.len() > self.max_lines {
204 self.lines.pop_front();
205 }
206
207 self.scroll_offset = 0;
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn text_to_cells(text: &str) -> Vec<Cell> {
217 text.chars().map(Cell::from_char).collect()
218 }
219
220 #[test]
221 fn test_scroll_buffer_new() {
222 let buf = ScrollBuffer::new(100);
223 assert_eq!(buf.len(), 1);
224 assert!(buf.current_line().content.is_empty());
225 }
226
227 #[test]
228 fn test_scroll_buffer_append() {
229 let mut buf = ScrollBuffer::new(100);
230 buf.append(text_to_cells("Hello"));
231 buf.append(text_to_cells(", world!"));
232
233 let content: String = buf.current_line().content.iter()
234 .map(|c| c.grapheme().unwrap_or(""))
235 .collect();
236 assert_eq!(content, "Hello, world!");
237 }
238
239 #[test]
240 fn test_scroll_buffer_newline() {
241 let mut buf = ScrollBuffer::new(100);
242 buf.append(text_to_cells("Line 1"));
243 buf.newline(false);
244 buf.append(text_to_cells("Line 2"));
245 assert_eq!(buf.len(), 2);
246
247 let l1: String = buf.get(0).unwrap().content.iter().map(|c| c.grapheme().unwrap_or("")).collect();
248 assert_eq!(l1, "Line 1");
249 }
250
251 #[test]
252 fn test_scroll_buffer_capacity() {
253 let mut buf = ScrollBuffer::new(3);
254 buf.append(text_to_cells("Line 1"));
255 buf.newline(false);
256 buf.append(text_to_cells("Line 2"));
257 buf.newline(false);
258 buf.append(text_to_cells("Line 3"));
259 buf.newline(false);
260 buf.append(text_to_cells("Line 4"));
261
262 assert_eq!(buf.len(), 3);
263 let l0: String = buf.get(0).unwrap().content.iter().map(|c| c.grapheme().unwrap_or("")).collect();
265 assert_eq!(l0, "Line 2");
266 }
267
268 #[test]
269 fn test_scroll_buffer_scroll() {
270 let mut buf = ScrollBuffer::new(100);
271 for i in 0..10 {
272 buf.append(text_to_cells(&format!("Line {i}")));
273 buf.newline(false);
274 }
275
276 assert!(buf.at_bottom());
277
278 buf.scroll_up(3);
279 assert!(!buf.at_bottom());
280 assert_eq!(buf.scroll_offset, 3);
281
282 buf.scroll_down(1);
283 assert_eq!(buf.scroll_offset, 2);
284
285 buf.scroll_to_bottom();
286 assert!(buf.at_bottom());
287 }
288}