use std::collections::VecDeque;
use crate::buffer::Cell;
#[derive(Debug, Clone)]
pub struct StyledLine {
pub content: Vec<Cell>,
pub wrapped: bool,
}
impl StyledLine {
pub const fn new(content: Vec<Cell>, wrapped: bool) -> Self {
Self { content, wrapped }
}
pub const fn empty() -> Self {
Self {
content: Vec::new(),
wrapped: false,
}
}
}
#[derive(Debug)]
pub struct ScrollBuffer {
lines: VecDeque<StyledLine>,
max_lines: usize,
scroll_offset: usize,
}
impl ScrollBuffer {
pub fn new(max_lines: usize) -> Self {
let mut lines = VecDeque::with_capacity(max_lines);
lines.push_back(StyledLine::empty());
Self {
lines,
max_lines,
scroll_offset: 0,
}
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn current_line(&self) -> &StyledLine {
self.lines.back().expect("Buffer should never be empty")
}
pub fn current_line_mut(&mut self) -> &mut StyledLine {
self.lines.back_mut().expect("Buffer should never be empty")
}
pub fn append(&mut self, cells: impl IntoIterator<Item = Cell>) {
self.current_line_mut().content.extend(cells);
}
pub fn newline(&mut self, wrapped: bool) {
while self.lines.len() >= self.max_lines {
self.lines.pop_front();
}
self.lines.push_back(StyledLine::new(Vec::new(), wrapped));
}
pub fn get(&self, index: usize) -> Option<&StyledLine> {
self.lines.get(index)
}
pub fn visible_lines(&self, viewport_height: usize) -> impl Iterator<Item = &StyledLine> {
let total = self.lines.len();
let end = total.saturating_sub(self.scroll_offset);
let start = end.saturating_sub(viewport_height);
self.lines.range(start..end)
}
pub fn scroll_up(&mut self, lines: usize) {
let max_offset = self.lines.len().saturating_sub(1);
self.scroll_offset = (self.scroll_offset + lines).min(max_offset);
}
pub const fn scroll_down(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
pub const fn scroll_to_bottom(&mut self) {
self.scroll_offset = 0;
}
pub const fn at_bottom(&self) -> bool {
self.scroll_offset == 0
}
pub fn clear(&mut self) {
self.lines.clear();
self.lines.push_back(StyledLine::empty());
self.scroll_offset = 0;
}
pub fn current_line_len(&self) -> usize {
self.current_line().content.len()
}
pub fn rewrap(&mut self, new_width: usize) {
if new_width == 0 {
return;
}
let mut logical_lines: Vec<Vec<Cell>> = Vec::new();
let mut current_logical: Vec<Cell> = Vec::new();
for line in &self.lines {
current_logical.extend(line.content.iter().copied());
if !line.wrapped {
logical_lines.push(std::mem::take(&mut current_logical));
}
}
if !current_logical.is_empty() || logical_lines.is_empty() {
logical_lines.push(current_logical);
}
self.lines.clear();
for logical in logical_lines {
if logical.is_empty() {
self.lines.push_back(StyledLine::empty());
} else {
let chunks: Vec<_> = logical.chunks(new_width).collect();
let chunk_count = chunks.len();
for (i, chunk) in chunks.into_iter().enumerate() {
let wrapped = i < chunk_count - 1;
self.lines.push_back(StyledLine::new(chunk.to_vec(), wrapped));
}
}
}
if self.lines.is_empty() {
self.lines.push_back(StyledLine::empty());
}
while self.lines.len() > self.max_lines {
self.lines.pop_front();
}
self.scroll_offset = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text_to_cells(text: &str) -> Vec<Cell> {
text.chars().map(Cell::from_char).collect()
}
#[test]
fn test_scroll_buffer_new() {
let buf = ScrollBuffer::new(100);
assert_eq!(buf.len(), 1);
assert!(buf.current_line().content.is_empty());
}
#[test]
fn test_scroll_buffer_append() {
let mut buf = ScrollBuffer::new(100);
buf.append(text_to_cells("Hello"));
buf.append(text_to_cells(", world!"));
let content: String = buf.current_line().content.iter()
.map(|c| c.grapheme().unwrap_or(""))
.collect();
assert_eq!(content, "Hello, world!");
}
#[test]
fn test_scroll_buffer_newline() {
let mut buf = ScrollBuffer::new(100);
buf.append(text_to_cells("Line 1"));
buf.newline(false);
buf.append(text_to_cells("Line 2"));
assert_eq!(buf.len(), 2);
let l1: String = buf.get(0).unwrap().content.iter().map(|c| c.grapheme().unwrap_or("")).collect();
assert_eq!(l1, "Line 1");
}
#[test]
fn test_scroll_buffer_capacity() {
let mut buf = ScrollBuffer::new(3);
buf.append(text_to_cells("Line 1"));
buf.newline(false);
buf.append(text_to_cells("Line 2"));
buf.newline(false);
buf.append(text_to_cells("Line 3"));
buf.newline(false);
buf.append(text_to_cells("Line 4"));
assert_eq!(buf.len(), 3);
let l0: String = buf.get(0).unwrap().content.iter().map(|c| c.grapheme().unwrap_or("")).collect();
assert_eq!(l0, "Line 2");
}
#[test]
fn test_scroll_buffer_scroll() {
let mut buf = ScrollBuffer::new(100);
for i in 0..10 {
buf.append(text_to_cells(&format!("Line {i}")));
buf.newline(false);
}
assert!(buf.at_bottom());
buf.scroll_up(3);
assert!(!buf.at_bottom());
assert_eq!(buf.scroll_offset, 3);
buf.scroll_down(1);
assert_eq!(buf.scroll_offset, 2);
buf.scroll_to_bottom();
assert!(buf.at_bottom());
}
}