revue/widget/data/
list.rs1use crate::render::Cell;
4use crate::style::Color;
5use crate::utils::Selection;
6use crate::widget::traits::{RenderContext, View, WidgetProps};
7use std::fmt::Display;
8
9pub struct List<T> {
11 items: Vec<T>,
12 selection: Selection,
13 highlight_fg: Option<Color>,
14 highlight_bg: Option<Color>,
15 props: WidgetProps,
16}
17
18impl<T> List<T> {
19 pub fn new(items: Vec<T>) -> Self {
21 let len = items.len();
22 Self {
23 items,
24 selection: Selection::new(len),
25 highlight_fg: None,
26 highlight_bg: Some(Color::BLUE),
27 props: WidgetProps::new(),
28 }
29 }
30
31 pub fn selected(mut self, idx: usize) -> Self {
33 self.selection.set(idx);
34 self
35 }
36
37 pub fn highlight_fg(mut self, color: Color) -> Self {
39 self.highlight_fg = Some(color);
40 self
41 }
42
43 pub fn highlight_bg(mut self, color: Color) -> Self {
45 self.highlight_bg = Some(color);
46 self
47 }
48
49 pub fn items(&self) -> &[T] {
51 &self.items
52 }
53
54 pub fn selected_index(&self) -> usize {
56 self.selection.index
57 }
58
59 pub fn len(&self) -> usize {
61 self.items.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
66 self.items.is_empty()
67 }
68
69 pub fn select_next(&mut self) {
71 self.selection.next();
72 }
73
74 pub fn select_prev(&mut self) {
76 self.selection.prev();
77 }
78}
79
80impl<T: Display> View for List<T> {
81 crate::impl_view_meta!("List");
82 fn render(&self, ctx: &mut RenderContext) {
83 let area = ctx.area;
84 if area.width == 0 || area.height == 0 {
85 return;
86 }
87
88 for (i, item) in self.items.iter().enumerate() {
90 if i as u16 >= area.height {
91 break;
92 }
93
94 let y = area.y + i as u16;
95 let is_selected = self.selection.is_selected(i);
96
97 let text = item.to_string();
98 let mut x = area.x;
99
100 for ch in text.chars() {
101 if x >= area.x + area.width {
102 break;
103 }
104
105 let mut cell = Cell::new(ch);
106 if is_selected {
107 cell.fg = self.highlight_fg;
108 cell.bg = self.highlight_bg;
109 }
110
111 ctx.buffer.set(x, y, cell);
112
113 let char_width = crate::utils::unicode::char_width(ch).max(1) as u16;
114 if char_width == 2 && x + 1 < area.x + area.width {
115 let mut cont = Cell::continuation();
116 if is_selected {
117 cont.bg = self.highlight_bg;
118 }
119 ctx.buffer.set(x + 1, y, cont);
120 }
121 x += char_width;
122 }
123
124 if is_selected {
126 while x < area.x + area.width {
127 let mut cell = Cell::new(' ');
128 cell.bg = self.highlight_bg;
129 ctx.buffer.set(x, y, cell);
130 x += 1;
131 }
132 }
133 }
134 }
135}
136
137impl<T: Display> crate::widget::StyledView for List<T> {
140 fn set_id(&mut self, id: impl Into<String>) {
141 self.props.id = Some(id.into());
142 }
143
144 fn add_class(&mut self, class: impl Into<String>) {
145 let class_str = class.into();
146 if !self.props.classes.iter().any(|c| c == &class_str) {
147 self.props.classes.push(class_str);
148 }
149 }
150
151 fn remove_class(&mut self, class: &str) {
152 self.props.classes.retain(|c| c != class);
153 }
154
155 fn toggle_class(&mut self, class: &str) {
156 if self.props.classes.iter().any(|c| c == class) {
157 self.props.classes.retain(|c| c != class);
158 } else {
159 self.props.classes.push(class.to_string());
160 }
161 }
162
163 fn has_class(&self, class: &str) -> bool {
164 self.props.classes.iter().any(|c| c == class)
165 }
166}
167
168pub fn list<T>(items: Vec<T>) -> List<T> {
170 List::new(items)
171}
172
173#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_list_builder() {
182 let list = List::new(vec!["A", "B", "C"])
183 .selected(1)
184 .highlight_fg(Color::WHITE)
185 .highlight_bg(Color::RED);
186
187 assert_eq!(list.selected_index(), 1);
188 assert_eq!(list.highlight_fg, Some(Color::WHITE));
189 assert_eq!(list.highlight_bg, Some(Color::RED));
190 }
191}