Skip to main content

rumatui_tui/widgets/
list.rs

1use std::iter::{self, Iterator};
2
3use unicode_width::UnicodeWidthStr;
4
5use crate::buffer::Buffer;
6use crate::layout::{Corner, Rect};
7use crate::style::Style;
8use crate::widgets::{Block, StatefulWidget, Text, Widget};
9
10#[derive(Copy, Clone, Debug)]
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/// A widget to display several items among which one can be selected (optional)
39///
40/// # Examples
41///
42/// ```
43/// # use rumatui_tui::widgets::{Block, Borders, List, Text};
44/// # use rumatui_tui::style::{Style, Color, Modifier};
45/// let items = ["Item 1", "Item 2", "Item 3"].iter().map(|i| Text::raw(*i));
46/// List::new(items)
47///     .block(Block::default().title("List").borders(Borders::ALL))
48///     .style(Style::default().fg(Color::White))
49///     .highlight_style(Style::default().modifier(Modifier::ITALIC))
50///     .highlight_symbol(">>");
51/// ```
52pub struct List<'b, L>
53where
54    L: Iterator<Item = Text<'b>>,
55{
56    block: Option<Block<'b>>,
57    items: L,
58    start_corner: Corner,
59    /// Base style of the widget
60    style: Style,
61    /// Style used to render selected item
62    highlight_style: Style,
63    /// Symbol in front of the selected item (Shift all items to the right)
64    highlight_symbol: Option<&'b str>,
65}
66
67impl<'b, L> Default for List<'b, L>
68where
69    L: Iterator<Item = Text<'b>> + Default,
70{
71    fn default() -> List<'b, L> {
72        List {
73            block: None,
74            items: L::default(),
75            style: Default::default(),
76            start_corner: Corner::TopLeft,
77            highlight_style: Style::default(),
78            highlight_symbol: None,
79        }
80    }
81}
82
83impl<'b, L> List<'b, L>
84where
85    L: Iterator<Item = Text<'b>>,
86{
87    pub fn new(items: L) -> List<'b, L> {
88        List {
89            block: None,
90            items,
91            style: Default::default(),
92            start_corner: Corner::TopLeft,
93            highlight_style: Style::default(),
94            highlight_symbol: None,
95        }
96    }
97
98    pub fn block(mut self, block: Block<'b>) -> List<'b, L> {
99        self.block = Some(block);
100        self
101    }
102
103    pub fn items<I>(mut self, items: I) -> List<'b, L>
104    where
105        I: IntoIterator<Item = Text<'b>, IntoIter = L>,
106    {
107        self.items = items.into_iter();
108        self
109    }
110
111    pub fn style(mut self, style: Style) -> List<'b, L> {
112        self.style = style;
113        self
114    }
115
116    pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> List<'b, L> {
117        self.highlight_symbol = Some(highlight_symbol);
118        self
119    }
120
121    pub fn highlight_style(mut self, highlight_style: Style) -> List<'b, L> {
122        self.highlight_style = highlight_style;
123        self
124    }
125
126    pub fn start_corner(mut self, corner: Corner) -> List<'b, L> {
127        self.start_corner = corner;
128        self
129    }
130}
131
132impl<'b, L> StatefulWidget for List<'b, L>
133where
134    L: Iterator<Item = Text<'b>>,
135{
136    type State = ListState;
137
138    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
139        let list_area = match self.block {
140            Some(ref mut b) => {
141                b.render(area, buf);
142                b.inner(area)
143            }
144            None => area,
145        };
146
147        if list_area.width < 1 || list_area.height < 1 {
148            return;
149        }
150
151        let list_height = list_area.height as usize;
152
153        buf.set_background(list_area, self.style.bg);
154
155        // Use highlight_style only if something is selected
156        let (selected, highlight_style) = match state.selected {
157            Some(i) => (Some(i), self.highlight_style),
158            None => (None, self.style),
159        };
160        let highlight_symbol = self.highlight_symbol.unwrap_or("");
161        let blank_symbol = iter::repeat(" ")
162            .take(highlight_symbol.width())
163            .collect::<String>();
164
165        // Make sure the list show the selected item
166        state.offset = if let Some(selected) = selected {
167            if selected >= list_height + state.offset - 1 {
168                selected + 1 - list_height
169            } else if selected < state.offset {
170                selected
171            } else {
172                state.offset
173            }
174        } else {
175            0
176        };
177
178        for (i, item) in self
179            .items
180            .skip(state.offset)
181            .enumerate()
182            .take(list_area.height as usize)
183        {
184            let (x, y) = match self.start_corner {
185                Corner::TopLeft => (list_area.left(), list_area.top() + i as u16),
186                Corner::BottomLeft => (list_area.left(), list_area.bottom() - (i + 1) as u16),
187                // Not supported
188                _ => (list_area.left(), list_area.top() + i as u16),
189            };
190            let (x, style) = if let Some(s) = selected {
191                if s == i + state.offset {
192                    let (x, _) = buf.set_stringn(
193                        x,
194                        y,
195                        highlight_symbol,
196                        list_area.width as usize,
197                        highlight_style,
198                    );
199                    (x + 1, Some(highlight_style))
200                } else {
201                    let (x, _) = buf.set_stringn(
202                        x,
203                        y,
204                        &blank_symbol,
205                        list_area.width as usize,
206                        highlight_style,
207                    );
208                    (x + 1, None)
209                }
210            } else {
211                (x, None)
212            };
213            match item {
214                Text::Raw(ref v) => {
215                    buf.set_stringn(
216                        x,
217                        y,
218                        v,
219                        list_area.width as usize,
220                        style.unwrap_or(self.style),
221                    );
222                }
223                Text::Styled(ref v, s) => {
224                    buf.set_stringn(x, y, v, list_area.width as usize, style.unwrap_or(s));
225                }
226            };
227        }
228    }
229}
230
231impl<'b, L> Widget for List<'b, L>
232where
233    L: Iterator<Item = Text<'b>>,
234{
235    fn render(self, area: Rect, buf: &mut Buffer) {
236        let mut state = ListState::default();
237        StatefulWidget::render(self, area, buf, &mut state);
238    }
239}