use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use std::collections::HashMap;
#[derive(Clone)]
pub struct VItem {
pub line: Line<'static>,
}
impl VItem {
pub fn new(line: Line<'static>) -> Self {
Self { line }
}
pub fn height(&self) -> u16 {
1
}
}
pub struct VirtualList {
committed_items: Vec<VItem>,
streaming_items: Vec<VItem>,
pub scroll_offset: u16,
pub viewport_height: u16,
pub sticky_bottom: bool,
last_width: u16,
}
impl VirtualList {
pub fn new() -> Self {
Self {
committed_items: Vec::new(),
streaming_items: Vec::new(),
scroll_offset: 0,
viewport_height: 0,
sticky_bottom: true,
last_width: 0,
}
}
pub fn total_height(&self) -> u16 {
(self.committed_items.len() + self.streaming_items.len()) as u16
}
fn max_offset(&self) -> u16 {
self.total_height().saturating_sub(self.viewport_height)
}
pub fn effective_offset(&self) -> u16 {
if self.sticky_bottom {
self.max_offset()
} else {
self.scroll_offset.min(self.max_offset())
}
}
pub fn set_committed(&mut self, items: Vec<VItem>) {
self.committed_items = items;
}
pub fn set_streaming(&mut self, items: Vec<VItem>) {
self.streaming_items = items;
}
pub fn set_viewport(&mut self, height: u16) {
self.viewport_height = height;
}
pub fn width_changed(&self, new_width: u16) -> bool {
self.last_width != new_width
}
pub fn set_width(&mut self, width: u16) {
self.last_width = width;
}
pub fn scroll_up(&mut self, n: u16) {
self.sticky_bottom = false;
self.scroll_offset = self.scroll_offset.saturating_sub(n);
}
pub fn scroll_down(&mut self, n: u16) {
self.sticky_bottom = false;
self.scroll_offset = (self.scroll_offset + n).min(self.max_offset());
if self.scroll_offset >= self.max_offset() {
self.sticky_bottom = true;
}
}
pub fn page_up(&mut self) {
self.scroll_up(self.viewport_height.saturating_sub(2));
}
pub fn page_down(&mut self) {
self.scroll_down(self.viewport_height.saturating_sub(2));
}
pub fn scroll_to_bottom(&mut self) {
self.sticky_bottom = true;
self.scroll_offset = self.max_offset();
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
let offset = self.effective_offset();
let total = self.total_height();
let committed_len = self.committed_items.len() as u16;
if area.height == 0 {
return;
}
for y in area.y..area.y + area.height {
for x in area.x..area.x + area.width {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.reset();
}
}
}
if total == 0 {
return;
}
let mut screen_y = area.y;
let end_y = area.y + area.height;
for idx in 0..total {
if screen_y >= end_y {
break; }
if idx < offset {
continue; }
let item = if idx < committed_len {
&self.committed_items[idx as usize]
} else {
let stream_idx = (idx - committed_len) as usize;
if stream_idx < self.streaming_items.len() {
&self.streaming_items[stream_idx]
} else {
continue;
}
};
let line_area = Rect {
x: area.x,
y: screen_y,
width: area.width,
height: 1,
};
let para = Paragraph::new(vec![item.line.clone()]);
para.render(line_area, buf);
screen_y += 1;
}
}
}
impl Default for VirtualList {
fn default() -> Self {
Self::new()
}
}