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