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    /// Select the first item
80    pub fn select_first(&mut self) {
81        self.selection.set(0);
82    }
83
84    /// Select the last item
85    pub fn select_last(&mut self) {
86        if !self.items.is_empty() {
87            self.selection.set(self.items.len() - 1);
88        }
89    }
90
91    /// Handle key input for navigation
92    pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
93        use crate::event::Key;
94        match key {
95            Key::Up | Key::Char('k') => {
96                self.select_prev();
97                true
98            }
99            Key::Down | Key::Char('j') => {
100                self.select_next();
101                true
102            }
103            Key::Home => {
104                self.select_first();
105                true
106            }
107            Key::End => {
108                self.select_last();
109                true
110            }
111            _ => false,
112        }
113    }
114}
115
116impl<T: Display> View for List<T> {
117    crate::impl_view_meta!("List");
118    fn render(&self, ctx: &mut RenderContext) {
119        let area = ctx.area;
120        if area.width == 0 || area.height == 0 {
121            return;
122        }
123
124        // Render each visible item
125        for (i, item) in self.items.iter().enumerate() {
126            if i as u16 >= area.height {
127                break;
128            }
129
130            let y = i as u16;
131            let is_selected = self.selection.is_selected(i);
132
133            let text = item.to_string();
134            let mut x = 0u16;
135
136            for ch in text.chars() {
137                if x >= area.width {
138                    break;
139                }
140
141                let mut cell = Cell::new(ch);
142                if is_selected {
143                    cell.fg = self.highlight_fg;
144                    cell.bg = self.highlight_bg;
145                }
146
147                ctx.set(x, y, cell);
148
149                let char_width = crate::utils::unicode::char_width(ch).max(1) as u16;
150                if char_width == 2 && x + 1 < area.width {
151                    let mut cont = Cell::continuation();
152                    if is_selected {
153                        cont.bg = self.highlight_bg;
154                    }
155                    ctx.set(x + 1, y, cont);
156                }
157                x += char_width;
158            }
159
160            // Fill rest of line for selected item
161            if is_selected {
162                while x < area.width {
163                    let mut cell = Cell::new(' ');
164                    cell.bg = self.highlight_bg;
165                    ctx.set(x, y, cell);
166                    x += 1;
167                }
168            }
169        }
170    }
171}
172
173// Note: Cannot use impl_styled_view! macro with generic types
174// Implement StyledView manually for List<T>
175impl<T: Display> crate::widget::StyledView for List<T> {
176    fn set_id(&mut self, id: impl Into<String>) {
177        self.props.id = Some(id.into());
178    }
179
180    fn add_class(&mut self, class: impl Into<String>) {
181        let class_str = class.into();
182        if !self.props.classes.iter().any(|c| c == &class_str) {
183            self.props.classes.push(class_str);
184        }
185    }
186
187    fn remove_class(&mut self, class: &str) {
188        self.props.classes.retain(|c| c != class);
189    }
190
191    fn toggle_class(&mut self, class: &str) {
192        if self.props.classes.iter().any(|c| c == class) {
193            self.props.classes.retain(|c| c != class);
194        } else {
195            self.props.classes.push(class.to_string());
196        }
197    }
198
199    fn has_class(&self, class: &str) -> bool {
200        self.props.classes.iter().any(|c| c == class)
201    }
202}
203
204/// Helper function to create a list widget
205pub fn list<T>(items: Vec<T>) -> List<T> {
206    List::new(items)
207}
208
209// Tests moved to tests/widget/data/list.rs