zui_widgets/widgets/
list.rs

1use crate::{
2    buffer::Buffer,
3    layout::{Corner, Rect},
4    style::Style,
5    text::Text,
6    widgets::{Block, StatefulWidget, Widget},
7};
8use unicode_width::UnicodeWidthStr;
9
10#[derive(Debug, Clone)]
11pub struct ListState {
12    offset: usize,
13    selected: Option<usize>,
14}
15
16impl Default for ListState {
17    fn default() -> ListState {
18        ListState {
19            offset: 0,
20            selected: None,
21        }
22    }
23}
24
25impl ListState {
26    pub fn selected(&self) -> Option<usize> {
27        self.selected
28    }
29
30    pub fn select(&mut self, index: Option<usize>) {
31        self.selected = index;
32        if index.is_none() {
33            self.offset = 0;
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct ListItem<'a> {
40    content: Text<'a>,
41    style: Style,
42}
43
44impl<'a> ListItem<'a> {
45    pub fn new<T>(content: T) -> ListItem<'a>
46    where
47        T: Into<Text<'a>>,
48    {
49        ListItem {
50            content: content.into(),
51            style: Style::default(),
52        }
53    }
54
55    pub fn style(mut self, style: Style) -> ListItem<'a> {
56        self.style = style;
57        self
58    }
59
60    pub fn height(&self) -> usize {
61        self.content.height()
62    }
63}
64
65/// A widget to display several items among which one can be selected (optional)
66///
67/// # Examples
68///
69/// ```
70/// # use zui_widgets::widgets::{Block, Borders, List, ListItem};
71/// # use zui_widgets::style::{Style, Color, Modifier};
72/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
73/// List::new(items)
74///     .block(Block::default().title("List").borders(Borders::ALL))
75///     .style(Style::default().fg(Color::White))
76///     .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
77///     .highlight_symbol(">>");
78/// ```
79#[derive(Debug, Clone)]
80pub struct List<'a> {
81    block: Option<Block<'a>>,
82    items: Vec<ListItem<'a>>,
83    /// Style used as a base style for the widget
84    style: Style,
85    start_corner: Corner,
86    /// Style used to render selected item
87    highlight_style: Style,
88    /// Symbol in front of the selected item (Shift all items to the right)
89    highlight_symbol: Option<&'a str>,
90    /// Whether to repeat the highlight symbol for each line of the selected item
91    repeat_highlight_symbol: bool,
92}
93
94impl<'a> List<'a> {
95    pub fn new<T>(items: T) -> List<'a>
96    where
97        T: Into<Vec<ListItem<'a>>>,
98    {
99        List {
100            block: None,
101            style: Style::default(),
102            items: items.into(),
103            start_corner: Corner::TopLeft,
104            highlight_style: Style::default(),
105            highlight_symbol: None,
106            repeat_highlight_symbol: false,
107        }
108    }
109
110    pub fn block(mut self, block: Block<'a>) -> List<'a> {
111        self.block = Some(block);
112        self
113    }
114
115    pub fn style(mut self, style: Style) -> List<'a> {
116        self.style = style;
117        self
118    }
119
120    pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
121        self.highlight_symbol = Some(highlight_symbol);
122        self
123    }
124
125    pub fn highlight_style(mut self, style: Style) -> List<'a> {
126        self.highlight_style = style;
127        self
128    }
129
130    pub fn repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> {
131        self.repeat_highlight_symbol = repeat;
132        self
133    }
134
135    pub fn start_corner(mut self, corner: Corner) -> List<'a> {
136        self.start_corner = corner;
137        self
138    }
139
140    fn get_items_bounds(
141        &self,
142        selected: Option<usize>,
143        offset: usize,
144        max_height: usize,
145    ) -> (usize, usize) {
146        let offset = offset.min(self.items.len().saturating_sub(1));
147        let mut start = offset;
148        let mut end = offset;
149        let mut height = 0;
150        for item in self.items.iter().skip(offset) {
151            if height + item.height() > max_height {
152                break;
153            }
154            height += item.height();
155            end += 1;
156        }
157
158        let selected = selected.unwrap_or(0).min(self.items.len() - 1);
159        while selected >= end {
160            height = height.saturating_add(self.items[end].height());
161            end += 1;
162            while height > max_height {
163                height = height.saturating_sub(self.items[start].height());
164                start += 1;
165            }
166        }
167        while selected < start {
168            start -= 1;
169            height = height.saturating_add(self.items[start].height());
170            while height > max_height {
171                end -= 1;
172                height = height.saturating_sub(self.items[end].height());
173            }
174        }
175        (start, end)
176    }
177}
178
179impl<'a> StatefulWidget for List<'a> {
180    type State = ListState;
181
182    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
183        buf.set_style(area, self.style);
184        let list_area = match self.block.take() {
185            Some(b) => {
186                let inner_area = b.inner(area);
187                b.render(area, buf);
188                inner_area
189            }
190            None => area,
191        };
192
193        if list_area.width < 1 || list_area.height < 1 {
194            return;
195        }
196
197        if self.items.is_empty() {
198            return;
199        }
200        let list_height = list_area.height as usize;
201
202        let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
203        state.offset = start;
204
205        let highlight_symbol = self.highlight_symbol.unwrap_or("");
206        let blank_symbol = " ".repeat(highlight_symbol.width());
207
208        let mut current_height = 0;
209        let has_selection = state.selected.is_some();
210        for (i, item) in self
211            .items
212            .iter_mut()
213            .enumerate()
214            .skip(state.offset)
215            .take(end - start)
216        {
217            let (x, y) = match self.start_corner {
218                Corner::BottomLeft => {
219                    current_height += item.height() as u16;
220                    (list_area.left(), list_area.bottom() - current_height)
221                }
222                _ => {
223                    let pos = (list_area.left(), list_area.top() + current_height);
224                    current_height += item.height() as u16;
225                    pos
226                }
227            };
228            let area = Rect {
229                x,
230                y,
231                width: list_area.width,
232                height: item.height() as u16,
233            };
234            let item_style = self.style.patch(item.style);
235            buf.set_style(area, item_style);
236
237            let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
238            for (j, line) in item.content.lines.iter().enumerate() {
239                // if the item is selected, we need to display the hightlight symbol:
240                // - either for the first line of the item only,
241                // - or for each line of the item if the appropriate option is set
242                let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
243                    highlight_symbol
244                } else {
245                    &blank_symbol
246                };
247                let (elem_x, max_element_width) = if has_selection {
248                    let (elem_x, _) = buf.set_stringn(
249                        x,
250                        y + j as u16,
251                        symbol,
252                        list_area.width as usize,
253                        item_style,
254                    );
255                    (elem_x, (list_area.width - (elem_x - x)) as u16)
256                } else {
257                    (x, list_area.width)
258                };
259                buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
260            }
261            if is_selected {
262                buf.set_style(area, self.highlight_style);
263            }
264        }
265    }
266}
267
268impl<'a> Widget for List<'a> {
269    fn render(self, area: Rect, buf: &mut Buffer) {
270        let mut state = ListState::default();
271        StatefulWidget::render(self, area, buf, &mut state);
272    }
273}