1use std::borrow::Cow;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 prelude::StatefulWidget,
7 style::Style,
8 widgets::{Block, Borders, Widget},
9};
10use tui_widget_list::{ListBuilder, ListState, ListView, ScrollAxis};
11use unicode_width::UnicodeWidthStr;
12
13use super::AsWidget;
14
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
17pub enum HighlightSymbolMode {
18 First,
20 Repeat,
22 Last,
24}
25
26pub struct CustomList<'a, T: AsWidget> {
28 block: Option<Block<'a>>,
30 axis: ScrollAxis,
32 items: Vec<T>,
34 focus: bool,
36 state: ListState,
38 highlight_symbol: Option<String>,
40 highlight_symbol_mode: HighlightSymbolMode,
42 highlight_symbol_style: Style,
44}
45
46impl<'a, T: AsWidget> CustomList<'a, T> {
47 pub fn new(style: impl Into<Style>, inline: bool, mut items: Vec<T>) -> Self {
49 let style = style.into();
50 let mut state = ListState::default();
51 if let Some(first) = items.first_mut() {
52 first.set_highlighted(true);
53 state.select(Some(0));
54 }
55 Self {
56 block: (!inline).then(|| Block::default().borders(Borders::ALL).style(style)),
57 axis: ScrollAxis::Vertical,
58 items,
59 focus: true,
60 state,
61 highlight_symbol_style: style,
62 highlight_symbol: None,
63 highlight_symbol_mode: HighlightSymbolMode::First,
64 }
65 }
66
67 pub fn horizontal(mut self) -> Self {
69 self.axis = ScrollAxis::Horizontal;
70 self
71 }
72
73 pub fn vertical(mut self) -> Self {
75 self.axis = ScrollAxis::Vertical;
76 self
77 }
78
79 pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
81 self.set_title(title);
82 self
83 }
84
85 pub fn highlight_symbol(mut self, highlight_symbol: String) -> Self {
87 self.highlight_symbol = Some(highlight_symbol).filter(|s| !s.is_empty());
88 self
89 }
90
91 pub fn highlight_symbol_mode(mut self, highlight_symbol_mode: HighlightSymbolMode) -> Self {
93 self.highlight_symbol_mode = highlight_symbol_mode;
94 self
95 }
96
97 pub fn highlight_symbol_style(mut self, highlight_symbol_style: Style) -> Self {
99 self.highlight_symbol_style = highlight_symbol_style;
100 self
101 }
102
103 pub fn set_title(&mut self, new_title: impl Into<Cow<'a, str>>) {
105 if let Some(ref mut block) = self.block {
106 *block = block.clone().title(new_title.into());
107 }
108 }
109
110 pub fn set_focus(&mut self, focus: bool) {
112 if focus != self.focus {
113 self.focus = focus;
114 if let Some(selected) = self.state.selected
115 && let Some(selected) = self.items.get_mut(selected)
116 {
117 selected.set_highlighted(focus);
118 }
119 }
120 }
121
122 pub fn is_focused(&self) -> bool {
124 self.focus
125 }
126
127 pub fn len(&self) -> usize {
129 self.items.len()
130 }
131
132 #[must_use]
134 pub fn is_empty(&self) -> bool {
135 self.items.is_empty()
136 }
137
138 pub fn items(&self) -> &Vec<T> {
140 &self.items
141 }
142
143 pub fn update_items(&mut self, items: Vec<T>) {
145 self.items = items;
146
147 if self.items.is_empty() {
148 self.state.select(None);
149 } else if let Some(selected) = self.state.selected {
150 if selected > self.items.len() - 1 {
151 self.state.select(Some(self.items.len() - 1));
152 }
153 } else {
154 self.state.select(Some(0));
155 }
156 if let Some(selected) = self.state.selected
157 && let Some(selected) = self.items.get_mut(selected)
158 {
159 selected.set_highlighted(true);
160 }
161 }
162
163 pub fn reset_selection(&mut self) {
165 if self.focus {
166 if let Some(selected) = self.state.selected
167 && let Some(selected) = self.items.get_mut(selected)
168 {
169 selected.set_highlighted(false);
170 }
171 self.state = ListState::default();
172 if !self.items.is_empty() {
173 self.state.select(Some(0));
174 if let Some(selected) = self.items.get_mut(0) {
175 selected.set_highlighted(true);
176 }
177 }
178 }
179 }
180
181 pub fn select_next(&mut self) {
183 if self.focus {
184 if let Some(selected) = self.state.selected {
185 if let Some(selected) = self.items.get_mut(selected) {
186 selected.set_highlighted(false);
187 }
188 if self.items.is_empty() {
189 self.state.select(None);
190 } else {
191 let i = if selected >= self.items.len() - 1 {
192 0
193 } else {
194 selected + 1
195 };
196 self.state.select(Some(i));
197 if let Some(selected) = self.items.get_mut(i) {
198 selected.set_highlighted(true);
199 }
200 }
201 }
202 }
203 }
204
205 pub fn select_prev(&mut self) {
207 if self.focus {
208 if let Some(selected) = self.state.selected {
209 if let Some(selected) = self.items.get_mut(selected) {
210 selected.set_highlighted(false);
211 }
212 if self.items.is_empty() {
213 self.state.select(None);
214 } else {
215 let i = if selected == 0 {
216 self.items.len() - 1
217 } else {
218 selected - 1
219 };
220 self.state.select(Some(i));
221 if let Some(selected) = self.items.get_mut(i) {
222 selected.set_highlighted(true);
223 }
224 }
225 }
226 }
227 }
228
229 pub fn select_first(&mut self) {
231 if self.focus && !self.items.is_empty() {
232 if let Some(selected) = self.state.selected
233 && let Some(selected) = self.items.get_mut(selected)
234 {
235 selected.set_highlighted(false);
236 }
237 self.state.select(Some(0));
238 if let Some(selected) = self.items.get_mut(0) {
239 selected.set_highlighted(true);
240 }
241 }
242 }
243
244 pub fn select_last(&mut self) {
246 if self.focus && !self.items.is_empty() {
247 if let Some(selected) = self.state.selected
248 && let Some(selected) = self.items.get_mut(selected)
249 {
250 selected.set_highlighted(false);
251 }
252 let i = self.items.len() - 1;
253 self.state.select(Some(i));
254 if let Some(selected) = self.items.get_mut(i) {
255 selected.set_highlighted(true);
256 }
257 }
258 }
259
260 pub fn select(&mut self, index: usize) {
262 if self.focus && index < self.items.len() {
263 if let Some(selected) = self.state.selected
264 && let Some(selected) = self.items.get_mut(selected)
265 {
266 selected.set_highlighted(false);
267 }
268 self.state.select(Some(index));
269 if let Some(selected) = self.items.get_mut(index) {
270 selected.set_highlighted(true);
271 }
272 }
273 }
274
275 pub fn selected_mut(&mut self) -> Option<&mut T> {
277 if self.focus {
278 if let Some(selected) = self.state.selected {
279 self.items.get_mut(selected)
280 } else {
281 None
282 }
283 } else {
284 None
285 }
286 }
287
288 pub fn selected(&self) -> Option<&T> {
290 if self.focus {
291 if let Some(selected) = self.state.selected {
292 self.items.get(selected)
293 } else {
294 None
295 }
296 } else {
297 None
298 }
299 }
300
301 pub fn delete_selected(&mut self) -> Option<T> {
303 if self.focus {
304 let selected = self.state.selected?;
305 let mut deleted = self.items.remove(selected);
306 deleted.set_highlighted(false);
307
308 if self.items.is_empty() {
309 self.state.select(None);
310 } else if selected >= self.items.len() {
311 self.state.select(Some(self.items.len() - 1));
312 }
313 if let Some(selected) = self.state.selected
314 && let Some(selected) = self.items.get_mut(selected)
315 {
316 selected.set_highlighted(true);
317 }
318
319 Some(deleted)
320 } else {
321 None
322 }
323 }
324}
325
326impl<'a, T: AsWidget> Widget for &mut CustomList<'a, T> {
327 fn render(self, area: Rect, buf: &mut Buffer)
328 where
329 Self: Sized,
330 {
331 let mut default_state = ListState::default();
332 let state = if self.focus {
333 &mut self.state
334 } else {
335 &mut default_state
336 };
337 if let Some(ref highlight_symbol) = self.highlight_symbol {
338 render_list_view(
340 ListBuilder::new(|ctx| {
341 let (item_widget, item_size) = self.items[ctx.index].as_widget(ctx.is_selected);
343
344 let item = SymbolAndWidget {
346 content: item_widget,
347 content_height: item_size.height,
348 symbol: if ctx.is_selected { highlight_symbol.as_str() } else { "" },
349 symbol_width: highlight_symbol.width() as u16,
350 symbol_mode: self.highlight_symbol_mode,
351 symbol_style: self.highlight_symbol_style,
352 };
353
354 let main_axis_size = match ctx.scroll_axis {
355 ScrollAxis::Vertical => item_size.height,
356 ScrollAxis::Horizontal => item_size.width + 1,
357 };
358
359 (item, main_axis_size)
360 }),
361 self.axis,
362 self.block.is_none(),
363 self.items.len(),
364 self.block.clone(),
365 state,
366 area,
367 buf,
368 );
369 } else {
370 render_list_view(
372 ListBuilder::new(|ctx| {
373 let (item_widget, item_size) = self.items[ctx.index].as_widget(ctx.is_selected);
374 let main_axis_size = match ctx.scroll_axis {
375 ScrollAxis::Vertical => item_size.height,
376 ScrollAxis::Horizontal => item_size.width + 1,
377 };
378 (item_widget, main_axis_size)
379 }),
380 self.axis,
381 self.block.is_none(),
382 self.items.len(),
383 self.block.clone(),
384 state,
385 area,
386 buf,
387 );
388 }
389 }
390}
391
392#[allow(clippy::too_many_arguments)]
394fn render_list_view<'a, W: Widget>(
395 builder: ListBuilder<'a, W>,
396 axis: ScrollAxis,
397 inline: bool,
398 item_count: usize,
399 block: Option<Block<'a>>,
400 state: &mut ListState,
401 area: Rect,
402 buf: &mut Buffer,
403) {
404 let mut view = ListView::new(builder, item_count)
405 .scroll_axis(axis)
406 .infinite_scrolling(false)
407 .scroll_padding(1 + (!inline as u16));
408 if let Some(block) = block {
409 view = view.block(block);
410 }
411 view.render(area, buf, state)
412}
413
414struct SymbolAndWidget<'a, W: Widget> {
416 content: W,
418 content_height: u16,
420 symbol: &'a str,
422 symbol_width: u16,
424 symbol_mode: HighlightSymbolMode,
426 symbol_style: Style,
428}
429
430impl<'a, W: Widget> Widget for SymbolAndWidget<'a, W> {
431 fn render(self, area: Rect, buf: &mut Buffer) {
432 let mut content_area = area;
433 let mut symbol_area = Rect::default();
434
435 if self.symbol_width > 0 && area.width > 0 {
437 symbol_area = Rect {
438 x: area.x,
439 y: area.y,
440 width: self.symbol_width.min(area.width),
441 height: area.height,
442 };
443
444 content_area.x = area.x.saturating_add(symbol_area.width);
446 content_area.width = area.width.saturating_sub(symbol_area.width);
447 }
448
449 if content_area.width > 0 && content_area.height > 0 {
451 self.content.render(content_area, buf);
452 }
453
454 if !self.symbol.is_empty() && symbol_area.width > 0 && symbol_area.height > 0 {
456 if let Some(bg_color) = self.symbol_style.bg {
458 for y_coord in symbol_area.top()..symbol_area.bottom() {
459 for x_coord in symbol_area.left()..symbol_area.right() {
460 if let Some(cell) = buf.cell_mut((x_coord, y_coord)) {
461 cell.set_bg(bg_color);
462 }
463 }
464 }
465 }
466 match self.symbol_mode {
468 HighlightSymbolMode::First => {
469 buf.set_stringn(
471 symbol_area.x,
472 symbol_area.y,
473 self.symbol,
474 symbol_area.width as usize,
475 self.symbol_style,
476 );
477 }
478 HighlightSymbolMode::Repeat => {
479 for i in 0..self.content_height {
481 let y_pos = symbol_area.y + i;
482 if y_pos < symbol_area.bottom() && i < symbol_area.height {
484 buf.set_stringn(
485 symbol_area.x,
486 y_pos,
487 self.symbol,
488 symbol_area.width as usize,
489 self.symbol_style,
490 );
491 } else {
492 break;
494 }
495 }
496 }
497 HighlightSymbolMode::Last => {
498 if self.content_height > 0 {
500 let y_pos = symbol_area.y + self.content_height - 1;
501 if y_pos < symbol_area.bottom() && (self.content_height - 1) < symbol_area.height {
503 buf.set_stringn(
504 symbol_area.x,
505 y_pos,
506 self.symbol,
507 symbol_area.width as usize,
508 self.symbol_style,
509 );
510 }
511 }
512 }
513 }
514 }
515 }
516}