1use std::collections::HashMap;
2
3use ratatui_core::{
4 buffer::Buffer,
5 layout::{Position, Rect},
6 style::{Style, Styled},
7 widgets::{StatefulWidget, Widget},
8};
9use ratatui_widgets::block::Block;
10use ratatui_widgets::block::BlockExt;
11use ratatui_widgets::scrollbar::Scrollbar;
12
13use crate::{utils::layout_on_viewport, ListState};
14
15#[allow(clippy::module_name_repetitions)]
18pub struct ListView<'a, T> {
19 pub item_count: usize,
21
22 pub builder: ListBuilder<'a, T>,
24
25 pub scroll_axis: ScrollAxis,
27
28 pub style: Style,
30
31 pub block: Option<Block<'a>>,
33
34 pub scrollbar: Option<Scrollbar<'a>>,
36
37 pub(crate) scroll_padding: u16,
39
40 pub(crate) infinite_scrolling: bool,
43}
44
45impl<'a, T> ListView<'a, T> {
46 #[must_use]
48 pub fn new(builder: ListBuilder<'a, T>, item_count: usize) -> Self {
49 Self {
50 builder,
51 item_count,
52 scroll_axis: ScrollAxis::Vertical,
53 style: Style::default(),
54 block: None,
55 scrollbar: None,
56 scroll_padding: 0,
57 infinite_scrolling: true,
58 }
59 }
60
61 #[must_use]
63 pub fn is_empty(&self) -> bool {
64 self.item_count == 0
65 }
66
67 #[must_use]
69 pub fn len(&self) -> usize {
70 self.item_count
71 }
72
73 #[must_use]
75 pub fn block(mut self, block: Block<'a>) -> Self {
76 self.block = Some(block);
77 self
78 }
79
80 #[must_use]
82 pub fn scrollbar(mut self, scrollbar: Scrollbar<'a>) -> Self {
83 self.scrollbar = Some(scrollbar);
84 self
85 }
86
87 #[must_use]
89 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
90 self.style = style.into();
91 self
92 }
93
94 #[must_use]
96 pub fn scroll_axis(mut self, scroll_axis: ScrollAxis) -> Self {
97 self.scroll_axis = scroll_axis;
98 self
99 }
100
101 #[must_use]
103 pub fn scroll_padding(mut self, scroll_padding: u16) -> Self {
104 self.scroll_padding = scroll_padding;
105 self
106 }
107
108 #[must_use]
110 pub fn infinite_scrolling(mut self, infinite_scrolling: bool) -> Self {
111 self.infinite_scrolling = infinite_scrolling;
112 self
113 }
114}
115
116impl<T> Styled for ListView<'_, T> {
117 type Item = Self;
118
119 fn style(&self) -> Style {
120 self.style
121 }
122
123 fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
124 self.style = style.into();
125 self
126 }
127}
128
129impl<'a, T: Copy + 'a> From<Vec<T>> for ListView<'a, T> {
130 fn from(value: Vec<T>) -> Self {
131 let item_count = value.len();
132 let builder = ListBuilder::new(move |context| (value[context.index], 1));
133
134 ListView::new(builder, item_count)
135 }
136}
137
138pub struct ListBuildContext {
141 pub index: usize,
143
144 pub is_selected: bool,
146
147 pub scroll_axis: ScrollAxis,
149
150 pub cross_axis_size: u16,
152}
153
154type ListBuilderClosure<'a, T> = dyn Fn(&ListBuildContext) -> (T, u16) + 'a;
156
157pub struct ListBuilder<'a, T> {
159 closure: Box<ListBuilderClosure<'a, T>>,
160}
161
162impl<'a, T> ListBuilder<'a, T> {
163 pub fn new<F>(closure: F) -> Self
180 where
181 F: Fn(&ListBuildContext) -> (T, u16) + 'a,
182 {
183 ListBuilder {
184 closure: Box::new(closure),
185 }
186 }
187
188 pub(crate) fn call_closure(&self, context: &ListBuildContext) -> (T, u16) {
190 (self.closure)(context)
191 }
192}
193
194#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
196pub enum ScrollAxis {
197 #[default]
199 Vertical,
200
201 Horizontal,
203}
204
205impl<T: Widget> StatefulWidget for ListView<'_, T> {
206 type State = ListState;
207
208 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
209 state.set_num_elements(self.item_count);
210 state.set_infinite_scrolling(self.infinite_scrolling);
211
212 buf.set_style(area, self.style);
214
215 if let Some(ref block) = self.block {
217 block.render(area, buf);
218 }
219 let inner_area = self.block.inner_if_some(area);
220 state.set_inner_area(inner_area);
221 state.set_scroll_axis(self.scroll_axis);
222
223 if self.item_count == 0 {
225 return;
226 }
227
228 let (main_axis_size, cross_axis_size) = match self.scroll_axis {
230 ScrollAxis::Vertical => (inner_area.height, inner_area.width),
231 ScrollAxis::Horizontal => (inner_area.width, inner_area.height),
232 };
233
234 let (mut scroll_axis_pos, cross_axis_pos) = match self.scroll_axis {
236 ScrollAxis::Vertical => (inner_area.top(), inner_area.left()),
237 ScrollAxis::Horizontal => (inner_area.left(), inner_area.top()),
238 };
239
240 let mut viewport = layout_on_viewport(
243 state,
244 &self.builder,
245 self.item_count,
246 main_axis_size,
247 cross_axis_size,
248 self.scroll_axis,
249 self.scroll_padding,
250 );
251 state.update_scrollbar_state(
252 &self.builder,
253 self.item_count,
254 main_axis_size,
255 cross_axis_size,
256 self.scroll_axis,
257 );
258
259 let (start, end) = (
260 state.view_state.offset,
261 viewport.len() + state.view_state.offset,
262 );
263 let mut cached_sizes: std::collections::HashMap<usize, u16> = HashMap::new();
265
266 for i in start..end {
267 let Some(element) = viewport.remove(&i) else {
268 break;
269 };
270 let visible_main_axis_size = element
271 .main_axis_size
272 .saturating_sub(element.truncation.value());
273
274 cached_sizes.insert(i, visible_main_axis_size);
275
276 let area = match self.scroll_axis {
277 ScrollAxis::Vertical => Rect::new(
278 cross_axis_pos,
279 scroll_axis_pos,
280 cross_axis_size,
281 visible_main_axis_size,
282 ),
283 ScrollAxis::Horizontal => Rect::new(
284 scroll_axis_pos,
285 cross_axis_pos,
286 visible_main_axis_size,
287 cross_axis_size,
288 ),
289 };
290
291 if element.truncation.value() > 0 {
293 render_truncated(
294 element.widget,
295 area,
296 buf,
297 element.main_axis_size,
298 &element.truncation,
299 self.style,
300 self.scroll_axis,
301 );
302 } else {
303 element.widget.render(area, buf);
304 }
305
306 scroll_axis_pos += visible_main_axis_size;
307 }
308
309 state.set_visible_main_axis_sizes(cached_sizes);
311
312 if let Some(scrollbar) = self.scrollbar {
314 scrollbar.render(area, buf, &mut state.scrollbar_state);
315 }
316 }
317}
318
319fn render_truncated<T: Widget>(
322 item: T,
323 available_area: Rect,
324 buf: &mut Buffer,
325 untruncated_size: u16,
326 truncation: &Truncation,
327 base_style: Style,
328 scroll_axis: ScrollAxis,
329) {
330 let (width, height) = match scroll_axis {
332 ScrollAxis::Vertical => (available_area.width, untruncated_size),
333 ScrollAxis::Horizontal => (untruncated_size, available_area.height),
334 };
335 let mut hidden_buffer = Buffer::empty(Rect {
336 x: available_area.left(),
337 y: available_area.top(),
338 width,
339 height,
340 });
341 hidden_buffer.set_style(hidden_buffer.area, base_style);
342 item.render(hidden_buffer.area, &mut hidden_buffer);
343
344 match scroll_axis {
346 ScrollAxis::Vertical => {
347 let offset = match truncation {
348 Truncation::Top(value) => *value,
349 _ => 0,
350 };
351 for y in available_area.top()..available_area.bottom() {
352 let y_off = y + offset;
353 for x in available_area.left()..available_area.right() {
354 if let Some(to) = buf.cell_mut(Position::new(x, y)) {
355 if let Some(from) = hidden_buffer.cell(Position::new(x, y_off)) {
356 *to = from.clone();
357 }
358 }
359 }
360 }
361 }
362 ScrollAxis::Horizontal => {
363 let offset = match truncation {
364 Truncation::Top(value) => *value,
365 _ => 0,
366 };
367 for x in available_area.left()..available_area.right() {
368 let x_off = x + offset;
369 for y in available_area.top()..available_area.bottom() {
370 if let Some(to) = buf.cell_mut(Position::new(x, y)) {
371 if let Some(from) = hidden_buffer.cell(Position::new(x_off, y)) {
372 *to = from.clone();
373 }
374 }
375 }
376 }
377 }
378 }
379}
380
381#[derive(Debug, Clone, Default, PartialEq, PartialOrd, Eq, Ord)]
382pub(crate) enum Truncation {
383 #[default]
384 None,
385 Top(u16),
386 Bot(u16),
387}
388
389impl Truncation {
390 pub(crate) fn value(&self) -> u16 {
391 match self {
392 Self::Top(value) | Self::Bot(value) => *value,
393 Self::None => 0,
394 }
395 }
396}
397
398#[cfg(test)]
399mod test {
400 use crate::ListBuilder;
401 use ratatui::widgets::Block;
402
403 use super::*;
404 use ratatui::widgets::Borders;
405
406 struct TestItem {}
407 impl Widget for TestItem {
408 fn render(self, area: Rect, buf: &mut Buffer)
409 where
410 Self: Sized,
411 {
412 Block::default().borders(Borders::ALL).render(area, buf);
413 }
414 }
415
416 fn test_data(total_height: u16) -> (Rect, Buffer, ListView<'static, TestItem>, ListState) {
417 let area = Rect::new(0, 0, 5, total_height);
418 let list = ListView::new(ListBuilder::new(|_| (TestItem {}, 3)), 3);
419 (area, Buffer::empty(area), list, ListState::default())
420 }
421
422 #[test]
423 fn not_truncated() {
424 let (area, mut buf, list, mut state) = test_data(9);
426
427 list.render(area, &mut buf, &mut state);
429
430 assert_buffer_eq(
432 buf,
433 Buffer::with_lines(vec![
434 "┌───┐",
435 "│ │",
436 "└───┘",
437 "┌───┐",
438 "│ │",
439 "└───┘",
440 "┌───┐",
441 "│ │",
442 "└───┘",
443 ]),
444 )
445 }
446
447 #[test]
448 fn empty_list() {
449 let area = Rect::new(0, 0, 5, 2);
451 let mut buf = Buffer::empty(area);
452 let mut state = ListState::default();
453 let builder = ListBuilder::new(|_| (TestItem {}, 0));
454 let list = ListView::new(builder, 0);
455
456 list.render(area, &mut buf, &mut state);
458
459 assert_buffer_eq(buf, Buffer::with_lines(vec![" ", " "]))
461 }
462
463 #[test]
464 fn zero_size() {
465 let (area, mut buf, list, mut state) = test_data(0);
467
468 list.render(area, &mut buf, &mut state);
470
471 assert_buffer_eq(buf, Buffer::empty(area))
473 }
474
475 #[test]
476 fn truncated_bot() {
477 let (area, mut buf, list, mut state) = test_data(8);
479
480 list.render(area, &mut buf, &mut state);
482
483 assert_buffer_eq(
485 buf,
486 Buffer::with_lines(vec![
487 "┌───┐",
488 "│ │",
489 "└───┘",
490 "┌───┐",
491 "│ │",
492 "└───┘",
493 "┌───┐",
494 "│ │",
495 ]),
496 )
497 }
498
499 #[test]
500 fn truncated_top() {
501 let (area, mut buf, list, mut state) = test_data(8);
503 state.select(Some(2));
504
505 list.render(area, &mut buf, &mut state);
507
508 assert_buffer_eq(
510 buf,
511 Buffer::with_lines(vec![
512 "│ │",
513 "└───┘",
514 "┌───┐",
515 "│ │",
516 "└───┘",
517 "┌───┐",
518 "│ │",
519 "└───┘",
520 ]),
521 )
522 }
523
524 #[test]
525 fn scroll_up() {
526 let (area, mut buf, list, mut state) = test_data(8);
527 state.select(Some(2));
529 list.render(area, &mut buf, &mut state);
530 assert_buffer_eq(
531 buf,
532 Buffer::with_lines(vec![
533 "│ │",
534 "└───┘",
535 "┌───┐",
536 "│ │",
537 "└───┘",
538 "┌───┐",
539 "│ │",
540 "└───┘",
541 ]),
542 );
543
544 let (_, mut buf, list, _) = test_data(8);
546 state.select(Some(1));
547 list.render(area, &mut buf, &mut state);
548 assert_buffer_eq(
549 buf,
550 Buffer::with_lines(vec![
551 "│ │",
552 "└───┘",
553 "┌───┐",
554 "│ │",
555 "└───┘",
556 "┌───┐",
557 "│ │",
558 "└───┘",
559 ]),
560 )
561 }
562
563 fn assert_buffer_eq(actual: Buffer, expected: Buffer) {
564 if actual.area != expected.area {
565 panic!(
566 "buffer areas not equal expected: {:?} actual: {:?}",
567 expected, actual
568 );
569 }
570 let diff = expected.diff(&actual);
571 if !diff.is_empty() {
572 panic!(
573 "buffer contents not equal\nexpected: {:?}\nactual: {:?}",
574 expected, actual,
575 );
576 }
577 assert_eq!(actual, expected, "buffers not equal");
578 }
579}