use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Pos, Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::{Color, Style};
use crate::text::Text;
pub struct VirtualList {
items: Vec<Text>,
scroll_y: u16,
rect: Rect,
style: Style,
selected: Option<usize>,
select_style: Style,
show_numbers: bool,
highlight_symbol: String,
}
impl VirtualList {
pub fn new(items: Vec<impl Into<Text>>) -> Self {
let items = items.into_iter().map(|o| o.into()).collect();
Self {
items,
scroll_y: 0,
rect: Rect::default(),
style: Style::default(),
selected: None,
select_style: Style::default().bg(Color::White).fg(Color::Black),
show_numbers: false,
highlight_symbol: String::new(),
}
}
pub fn style(mut self, style: Style) -> Self { self.style = style; self }
pub fn select_style(mut self, style: Style) -> Self { self.select_style = style; self }
pub fn show_numbers(mut self, show: bool) -> Self { self.show_numbers = show; self }
pub fn highlight_symbol(mut self, sym: impl Into<String>) -> Self {
self.highlight_symbol = sym.into();
self
}
pub fn selected(&self) -> Option<usize> { self.selected }
pub fn set_selected(&mut self, index: Option<usize>, cx: &mut EventCx) {
self.selected = index;
cx.invalidate_paint();
}
}
impl Component for VirtualList {
fn render(&self, cx: &mut RenderCx) {
let vp = if self.rect.height > 0 { self.rect } else { cx.rect };
if vp.height == 0 || self.items.is_empty() {
return;
}
let visible_count = vp.height as usize;
let max_scroll = self.items.len().saturating_sub(visible_count);
let scroll = (self.scroll_y as usize).min(max_scroll);
for i in 0..visible_count {
let idx = scroll + i;
if idx >= self.items.len() { break; }
let row_y = vp.y.saturating_add(i as u16);
let is_sel = self.selected == Some(idx);
let s = if is_sel { &self.select_style } else { &self.style };
let text = if is_sel && !self.highlight_symbol.is_empty() {
format!("{}{}", self.highlight_symbol, if self.show_numbers {
format!("{:4}: {}", idx, self.items[idx].first_text())
} else {
self.items[idx].first_text().to_string()
})
} else if self.show_numbers {
format!("{:4}: {}", idx, self.items[idx].first_text())
} else {
self.items[idx].first_text().to_string()
};
cx.buffer.write_text(
Pos { x: vp.x, y: row_y }, vp, &text, s,
);
}
}
fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
Size { width: constraint.max.width, height: constraint.max.height }
}
fn focusable(&self) -> bool { false }
fn event(&mut self, event: &Event, cx: &mut EventCx) {
if matches!(event, Event::Focus | Event::Blur) { return; }
if self.items.is_empty() { return; }
if let Event::Key(key_event) = event {
let visible = self.rect.height.max(1) as usize;
match &key_event.key {
crate::event::Key::Up => {
if let Some(idx) = self.selected {
if idx > 0 {
self.selected = Some(idx - 1);
self.scroll_to_visible(idx - 1, visible);
cx.invalidate_paint();
}
}
return;
}
crate::event::Key::Down => {
let new_idx = match self.selected {
Some(i) if i + 1 < self.items.len() => i + 1,
None => 0,
_ => return,
};
self.selected = Some(new_idx);
self.scroll_to_visible(new_idx, visible);
cx.invalidate_paint();
return;
}
crate::event::Key::PageUp => {
let new_idx = match self.selected {
Some(i) => i.saturating_sub(visible),
None => 0,
};
self.selected = Some(new_idx);
self.scroll_y = self.scroll_y.saturating_sub(visible as u16);
self.scroll_to_visible(new_idx, visible);
cx.invalidate_paint();
return;
}
crate::event::Key::PageDown => {
let new_idx = match self.selected {
Some(i) => (i + visible).min(self.items.len() - 1),
None => (visible - 1).min(self.items.len() - 1),
};
self.selected = Some(new_idx);
self.scroll_to_visible(new_idx, visible);
cx.invalidate_paint();
return;
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
self.rect = rect;
}
fn for_each_child(&self, _f: &mut dyn FnMut(&crate::node::Node)) {}
fn for_each_child_mut(&mut self, _f: &mut dyn FnMut(&mut crate::node::Node)) {}
fn style(&self) -> Style { self.style.clone() }
}
impl VirtualList {
fn scroll_to_visible(&mut self, idx: usize, visible: usize) {
if idx < self.scroll_y as usize {
self.scroll_y = idx as u16;
} else if idx >= self.scroll_y as usize + visible {
self.scroll_y = (idx + 1).saturating_sub(visible) as u16;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testbuffer::TestBuffer;
#[test]
fn test_highlight_symbol() {
let mut tb = TestBuffer::new(20, 3);
let list = VirtualList::new(vec![Text::from("item")]).highlight_symbol("> ");
tb.render(&list);
assert!(tb.buffer.cells.iter().any(|c| c.symbol == "i"));
}
}