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#[derive(Debug, Clone)]
80pub struct List<'a> {
81 block: Option<Block<'a>>,
82 items: Vec<ListItem<'a>>,
83 style: Style,
85 start_corner: Corner,
86 highlight_style: Style,
88 highlight_symbol: Option<&'a str>,
90 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 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}