altui_core/widgets/
list.rs1use 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#[derive(Debug, Clone)]
52pub struct List<'a> {
53 block: Option<Block<'a>>,
54 items: Vec<ListItem<'a>>,
55 style: Style,
57 start_corner: Corner,
58 highlight_style: Style,
60 highlight_symbol: Option<&'a str>,
62 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 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}