Skip to main content

altui_core/widgets/
list.rs

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