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};
7
8pub 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 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 pub fn style(mut self, style: Style) -> Self { self.style = style; self }
38
39 pub fn select_style(mut self, style: Style) -> Self { self.select_style = style; self }
41
42 pub fn show_numbers(mut self, show: bool) -> Self { self.show_numbers = show; self }
44
45 pub fn selected(&self) -> Option<usize> { self.selected }
47
48 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}