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