Skip to main content

revue/widget/data/
list.rs

1//! List widget
2
3use crate::render::Cell;
4use crate::style::Color;
5use crate::utils::Selection;
6use crate::widget::traits::{RenderContext, View, WidgetProps};
7use std::fmt::Display;
8
9/// A list widget for displaying items
10pub 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    /// Create a new list with items
20    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    /// Set selected index
32    pub fn selected(mut self, idx: usize) -> Self {
33        self.selection.set(idx);
34        self
35    }
36
37    /// Set highlight foreground color
38    pub fn highlight_fg(mut self, color: Color) -> Self {
39        self.highlight_fg = Some(color);
40        self
41    }
42
43    /// Set highlight background color
44    pub fn highlight_bg(mut self, color: Color) -> Self {
45        self.highlight_bg = Some(color);
46        self
47    }
48
49    /// Get items
50    pub fn items(&self) -> &[T] {
51        &self.items
52    }
53
54    /// Get selected index
55    pub fn selected_index(&self) -> usize {
56        self.selection.index
57    }
58
59    /// Get number of items
60    pub fn len(&self) -> usize {
61        self.items.len()
62    }
63
64    /// Check if empty
65    pub fn is_empty(&self) -> bool {
66        self.items.is_empty()
67    }
68
69    /// Select next item (wraps around)
70    pub fn select_next(&mut self) {
71        self.selection.next();
72    }
73
74    /// Select previous item (wraps around)
75    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        // Render each visible item
89        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            // Fill rest of line for selected item
125            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
137// Note: Cannot use impl_styled_view! macro with generic types
138// Implement StyledView manually for List<T>
139impl<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
168/// Helper function to create a list widget
169pub fn list<T>(items: Vec<T>) -> List<T> {
170    List::new(items)
171}
172
173// Most tests moved to tests/widget_tests.rs
174// Tests below access private fields and must stay inline
175
176#[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}