Skip to main content

lv_tui/widgets/
virtuallist.rs

1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Pos, Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::{Color, Style};
7use crate::text::Text;
8
9/// A virtual-scrolling list for large datasets.
10///
11/// Only visible rows are rendered to the buffer. Rows are 1 line high.
12/// Use Up/Down to scroll. Each item is rendered as a single line of text.
13pub struct VirtualList {
14    items: Vec<Text>,
15    scroll_y: u16,
16    rect: Rect,
17    style: Style,
18    selected: Option<usize>,
19    select_style: Style,
20    show_numbers: bool,
21    highlight_symbol: String,
22}
23
24impl VirtualList {
25    /// Creates a virtual list with the given items.
26    pub fn new(items: Vec<impl Into<Text>>) -> Self {
27        let items = items.into_iter().map(|o| o.into()).collect();
28        Self {
29            items,
30            scroll_y: 0,
31            rect: Rect::default(),
32            style: Style::default(),
33            selected: None,
34            select_style: Style::default().bg(Color::White).fg(Color::Black),
35            show_numbers: false,
36            highlight_symbol: String::new(),
37        }
38    }
39
40    pub fn style(mut self, style: Style) -> Self { self.style = style; self }
41    pub fn select_style(mut self, style: Style) -> Self { self.select_style = style; self }
42    pub fn show_numbers(mut self, show: bool) -> Self { self.show_numbers = show; self }
43
44    /// Sets a highlight symbol shown before the selected item (e.g. "❯ ").
45    pub fn highlight_symbol(mut self, sym: impl Into<String>) -> Self {
46        self.highlight_symbol = sym.into();
47        self
48    }
49
50    /// Returns the currently selected item index.
51    pub fn selected(&self) -> Option<usize> { self.selected }
52
53    /// Sets the selected item.
54    pub fn set_selected(&mut self, index: Option<usize>, cx: &mut EventCx) {
55        self.selected = index;
56        cx.invalidate_paint();
57    }
58}
59
60impl Component for VirtualList {
61    fn render(&self, cx: &mut RenderCx) {
62        let vp = if self.rect.height > 0 { self.rect } else { cx.rect };
63        if vp.height == 0 || self.items.is_empty() {
64            return;
65        }
66
67        let visible_count = vp.height as usize;
68        let max_scroll = self.items.len().saturating_sub(visible_count);
69        let scroll = (self.scroll_y as usize).min(max_scroll);
70
71        for i in 0..visible_count {
72            let idx = scroll + i;
73            if idx >= self.items.len() { break; }
74
75            let row_y = vp.y.saturating_add(i as u16);
76            let is_sel = self.selected == Some(idx);
77            let s = if is_sel { &self.select_style } else { &self.style };
78
79            let text = if is_sel && !self.highlight_symbol.is_empty() {
80                format!("{}{}", self.highlight_symbol, if self.show_numbers {
81                    format!("{:4}: {}", idx, self.items[idx].first_text())
82                } else {
83                    self.items[idx].first_text().to_string()
84                })
85            } else if self.show_numbers {
86                format!("{:4}: {}", idx, self.items[idx].first_text())
87            } else {
88                self.items[idx].first_text().to_string()
89            };
90
91            cx.buffer.write_text(
92                Pos { x: vp.x, y: row_y }, vp, &text, s,
93            );
94        }
95    }
96
97    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
98        Size { width: constraint.max.width, height: constraint.max.height }
99    }
100
101    fn focusable(&self) -> bool { false }
102
103    fn event(&mut self, event: &Event, cx: &mut EventCx) {
104        if matches!(event, Event::Focus | Event::Blur) { return; }
105        if self.items.is_empty() { return; }
106
107        if let Event::Key(key_event) = event {
108            let visible = self.rect.height.max(1) as usize;
109            match &key_event.key {
110                crate::event::Key::Up => {
111                    if let Some(idx) = self.selected {
112                        if idx > 0 {
113                            self.selected = Some(idx - 1);
114                            self.scroll_to_visible(idx - 1, visible);
115                            cx.invalidate_paint();
116                        }
117                    }
118                    return;
119                }
120                crate::event::Key::Down => {
121                    let new_idx = match self.selected {
122                        Some(i) if i + 1 < self.items.len() => i + 1,
123                        None => 0,
124                        _ => return,
125                    };
126                    self.selected = Some(new_idx);
127                    self.scroll_to_visible(new_idx, visible);
128                    cx.invalidate_paint();
129                    return;
130                }
131                crate::event::Key::PageUp => {
132                    let new_idx = match self.selected {
133                        Some(i) => i.saturating_sub(visible),
134                        None => 0,
135                    };
136                    self.selected = Some(new_idx);
137                    self.scroll_y = self.scroll_y.saturating_sub(visible as u16);
138                    self.scroll_to_visible(new_idx, visible);
139                    cx.invalidate_paint();
140                    return;
141                }
142                crate::event::Key::PageDown => {
143                    let new_idx = match self.selected {
144                        Some(i) => (i + visible).min(self.items.len() - 1),
145                        None => (visible - 1).min(self.items.len() - 1),
146                    };
147                    self.selected = Some(new_idx);
148                    self.scroll_to_visible(new_idx, visible);
149                    cx.invalidate_paint();
150                    return;
151                }
152                _ => {}
153            }
154        }
155    }
156
157    fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
158        self.rect = rect;
159    }
160
161    fn for_each_child(&self, _f: &mut dyn FnMut(&crate::node::Node)) {}
162    fn for_each_child_mut(&mut self, _f: &mut dyn FnMut(&mut crate::node::Node)) {}
163    fn style(&self) -> Style { self.style.clone() }
164}
165
166impl VirtualList {
167    fn scroll_to_visible(&mut self, idx: usize, visible: usize) {
168        if idx < self.scroll_y as usize {
169            self.scroll_y = idx as u16;
170        } else if idx >= self.scroll_y as usize + visible {
171            self.scroll_y = (idx + 1).saturating_sub(visible) as u16;
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::testbuffer::TestBuffer;
180
181    #[test]
182    fn test_highlight_symbol() {
183        let mut tb = TestBuffer::new(20, 3);
184        let list = VirtualList::new(vec![Text::from("item")]).highlight_symbol("> ");
185        tb.render(&list);
186        // Items should render even without selection
187        assert!(tb.buffer.cells.iter().any(|c| c.symbol == "i"));
188    }
189}