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};
7
8/// A virtual-scrolling list for large datasets.
9///
10/// Only visible rows are rendered to the buffer. Rows are 1 line high.
11/// Use Up/Down to scroll. Each item is rendered as a single line of text.
12pub struct VirtualList {
13    items: Vec<String>,
14    scroll_y: u16,
15    rect: Rect,
16    style: Style,
17    selected: Option<usize>,
18    select_style: Style,
19    show_numbers: bool,
20}
21
22impl VirtualList {
23    /// Creates a virtual list with the given items.
24    pub fn new(items: Vec<String>) -> Self {
25        Self {
26            items,
27            scroll_y: 0,
28            rect: Rect::default(),
29            style: Style::default(),
30            selected: None,
31            select_style: Style::default().bg(Color::White).fg(Color::Black),
32            show_numbers: false,
33        }
34    }
35
36    /// Builder: sets the default row style.
37    pub fn style(mut self, style: Style) -> Self { self.style = style; self }
38
39    /// Builder: sets the selected row style.
40    pub fn select_style(mut self, style: Style) -> Self { self.select_style = style; self }
41
42    /// Builder: enables row numbering.
43    pub fn show_numbers(mut self, show: bool) -> Self { self.show_numbers = show; self }
44
45    /// Returns the currently selected item index.
46    pub fn selected(&self) -> Option<usize> { self.selected }
47
48    /// Sets the selected item.
49    pub fn set_selected(&mut self, index: Option<usize>, cx: &mut EventCx) {
50        self.selected = index;
51        cx.invalidate_paint();
52    }
53}
54
55impl Component for VirtualList {
56    fn render(&self, cx: &mut RenderCx) {
57        let vp = self.rect;
58        if vp.height == 0 || self.items.is_empty() {
59            return;
60        }
61
62        let visible_count = vp.height as usize;
63        let max_scroll = self.items.len().saturating_sub(visible_count);
64        let scroll = (self.scroll_y as usize).min(max_scroll);
65
66        for i in 0..visible_count {
67            let idx = scroll + i;
68            if idx >= self.items.len() { break; }
69
70            let row_y = vp.y.saturating_add(i as u16);
71            let is_sel = self.selected == Some(idx);
72            let s = if is_sel { &self.select_style } else { &self.style };
73
74            let text = if self.show_numbers {
75                format!("{:4}: {}", idx, self.items[idx])
76            } else {
77                self.items[idx].clone()
78            };
79
80            cx.buffer.write_text(
81                Pos { x: vp.x, y: row_y }, vp, &text, s,
82            );
83        }
84    }
85
86    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
87        Size { width: constraint.max.width, height: constraint.max.height }
88    }
89
90    fn focusable(&self) -> bool { false }
91
92    fn event(&mut self, event: &Event, cx: &mut EventCx) {
93        if matches!(event, Event::Focus | Event::Blur) { return; }
94        if self.items.is_empty() { return; }
95
96        if let Event::Key(key_event) = event {
97            let visible = self.rect.height.max(1) as usize;
98            match &key_event.key {
99                crate::event::Key::Up => {
100                    if let Some(idx) = self.selected {
101                        if idx > 0 {
102                            self.selected = Some(idx - 1);
103                            self.scroll_to_visible(idx - 1, visible);
104                            cx.invalidate_paint();
105                        }
106                    }
107                    return;
108                }
109                crate::event::Key::Down => {
110                    let new_idx = match self.selected {
111                        Some(i) if i + 1 < self.items.len() => i + 1,
112                        None => 0,
113                        _ => return,
114                    };
115                    self.selected = Some(new_idx);
116                    self.scroll_to_visible(new_idx, visible);
117                    cx.invalidate_paint();
118                    return;
119                }
120                crate::event::Key::PageUp => {
121                    let new_idx = match self.selected {
122                        Some(i) => i.saturating_sub(visible),
123                        None => 0,
124                    };
125                    self.selected = Some(new_idx);
126                    self.scroll_y = self.scroll_y.saturating_sub(visible as u16);
127                    self.scroll_to_visible(new_idx, visible);
128                    cx.invalidate_paint();
129                    return;
130                }
131                crate::event::Key::PageDown => {
132                    let new_idx = match self.selected {
133                        Some(i) => (i + visible).min(self.items.len() - 1),
134                        None => (visible - 1).min(self.items.len() - 1),
135                    };
136                    self.selected = Some(new_idx);
137                    self.scroll_to_visible(new_idx, visible);
138                    cx.invalidate_paint();
139                    return;
140                }
141                _ => {}
142            }
143        }
144    }
145
146    fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
147        self.rect = rect;
148    }
149
150    fn for_each_child(&self, _f: &mut dyn FnMut(&crate::node::Node)) {}
151    fn for_each_child_mut(&mut self, _f: &mut dyn FnMut(&mut crate::node::Node)) {}
152    fn style(&self) -> Style { self.style.clone() }
153}
154
155impl VirtualList {
156    fn scroll_to_visible(&mut self, idx: usize, visible: usize) {
157        if idx < self.scroll_y as usize {
158            self.scroll_y = idx as u16;
159        } else if idx >= self.scroll_y as usize + visible {
160            self.scroll_y = (idx + 1).saturating_sub(visible) as u16;
161        }
162    }
163}