lv_tui/widgets/
virtuallist.rs1use 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
9pub 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 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 pub fn highlight_symbol(mut self, sym: impl Into<String>) -> Self {
46 self.highlight_symbol = sym.into();
47 self
48 }
49
50 pub fn selected(&self) -> Option<usize> { self.selected }
52
53 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 assert!(tb.buffer.cells.iter().any(|c| c.symbol == "i"));
188 }
189}