tui_widget_list/legacy/
widget.rs

1#![allow(clippy::cast_possible_truncation, deprecated)]
2use ratatui::{
3    prelude::{Buffer, Rect},
4    style::{Style, Styled},
5    widgets::{Block, StatefulWidget, Widget},
6};
7
8use crate::{legacy::utils::layout_on_viewport, ListState, PreRender, ScrollAxis};
9
10/// A [`List`] is a widget for Ratatui that can render an arbitrary list of widgets.
11/// It is generic over `T`, where each widget `T` should implement the [`PreRender`]
12/// trait.
13/// `List` is no longer developed. Consider using `ListView`.
14#[derive(Clone)]
15#[deprecated(since = "0.11.0", note = "Use ListView with ListBuilder instead.")]
16pub struct List<'a, T: PreRender> {
17    /// The list's items.
18    pub items: Vec<T>,
19
20    /// Style used as a base style for the widget.
21    style: Style,
22
23    /// Block surrounding the widget list.
24    block: Option<Block<'a>>,
25
26    /// Specifies the scroll axis. Either `Vertical` or `Horizontal`.
27    scroll_axis: ScrollAxis,
28}
29
30#[allow(deprecated)]
31impl<'a, T: PreRender> List<'a, T> {
32    /// Instantiates a widget list with elements.
33    ///
34    /// # Arguments
35    ///
36    /// * `items` - A vector of elements implementing the [`PreRender`] trait.
37    #[must_use]
38    pub fn new(items: Vec<T>) -> Self {
39        Self {
40            items,
41            style: Style::default(),
42            block: None,
43            scroll_axis: ScrollAxis::default(),
44        }
45    }
46
47    /// Sets the block style that surrounds the whole List.
48    #[must_use]
49    pub fn block(mut self, block: Block<'a>) -> Self {
50        self.block = Some(block);
51        self
52    }
53
54    /// Checks whether the widget list is empty.
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.items.is_empty()
58    }
59
60    /// Returns the length of the widget list.
61    #[must_use]
62    pub fn len(&self) -> usize {
63        self.items.len()
64    }
65
66    /// Set the base style of the List.
67    #[must_use]
68    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
69        self.style = style.into();
70        self
71    }
72
73    /// Set the scroll direction of the list.
74    #[must_use]
75    pub fn scroll_direction(mut self, scroll_axis: ScrollAxis) -> Self {
76        self.scroll_axis = scroll_axis;
77        self
78    }
79}
80
81impl<T: PreRender> Styled for List<'_, T> {
82    type Item = Self;
83    fn style(&self) -> Style {
84        self.style
85    }
86
87    fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
88        self.style = style.into();
89        self
90    }
91}
92
93impl<T: PreRender> From<Vec<T>> for List<'_, T> {
94    /// Instantiates a [`List`] from a vector of elements implementing
95    /// the [`PreRender`] trait.
96    fn from(items: Vec<T>) -> Self {
97        Self::new(items)
98    }
99}
100
101impl<T: PreRender> StatefulWidget for List<'_, T> {
102    type State = ListState;
103
104    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
105        let style = self.style;
106        let scroll_axis = self.scroll_axis;
107
108        let mut items = self.items;
109        let mut block = self.block;
110        state.set_num_elements(items.len());
111
112        // Set the base style
113        buf.set_style(area, style);
114        let area = match block.take() {
115            Some(b) => {
116                let inner_area = b.inner(area);
117                b.render(area, buf);
118                inner_area
119            }
120            None => area,
121        };
122
123        // List is empty
124        if items.is_empty() {
125            return;
126        }
127
128        // Set the dimension along the scroll axis and the cross axis
129        let (total_main_axis_size, cross_axis_size) = match scroll_axis {
130            ScrollAxis::Vertical => (area.height, area.width),
131            ScrollAxis::Horizontal => (area.width, area.height),
132        };
133
134        // Determine which widgets to show on the viewport and how much space they
135        // get assigned to.
136        let viewport_layouts = layout_on_viewport(
137            state,
138            &mut items,
139            total_main_axis_size,
140            cross_axis_size,
141            scroll_axis,
142        );
143
144        // Drain out elements that are shown on the view port from the vector of
145        // all elements.
146        let num_items_viewport = viewport_layouts.len();
147        let (start, end) = (
148            state.view_state.offset,
149            num_items_viewport + state.view_state.offset,
150        );
151        let items_viewport = items.drain(start..end);
152
153        // The starting coordinates of the current item
154        let (mut scroll_axis_pos, cross_axis_pos) = match scroll_axis {
155            ScrollAxis::Vertical => (area.top(), area.left()),
156            ScrollAxis::Horizontal => (area.left(), area.top()),
157        };
158
159        // Render the widgets on the viewport
160        for (i, (viewport_layout, item)) in
161            viewport_layouts.into_iter().zip(items_viewport).enumerate()
162        {
163            let area = match scroll_axis {
164                ScrollAxis::Vertical => Rect::new(
165                    cross_axis_pos,
166                    scroll_axis_pos,
167                    cross_axis_size,
168                    viewport_layout.main_axis_size,
169                ),
170                ScrollAxis::Horizontal => Rect::new(
171                    scroll_axis_pos,
172                    cross_axis_pos,
173                    viewport_layout.main_axis_size,
174                    cross_axis_size,
175                ),
176            };
177
178            // Check if the item needs to be truncated
179            if viewport_layout.truncated_by > 0 {
180                let trunc_top = i == 0 && num_items_viewport > 1;
181                let tot_size = viewport_layout.main_axis_size + viewport_layout.truncated_by;
182                render_trunc(item, area, buf, tot_size, scroll_axis, trunc_top, style);
183            } else {
184                item.render(area, buf);
185            }
186
187            scroll_axis_pos += viewport_layout.main_axis_size;
188        }
189    }
190}
191
192/// Renders a listable widget within a specified area of a buffer, potentially truncating the widget content based on scrolling direction.
193/// `truncate_top` indicates whether to truncate the content from the top or bottom.
194fn render_trunc<T: Widget>(
195    item: T,
196    available_area: Rect,
197    buf: &mut Buffer,
198    total_size: u16,
199    scroll_axis: ScrollAxis,
200    truncate_top: bool,
201    style: Style,
202) {
203    // Create an intermediate buffer for rendering the truncated element
204    let (width, height) = match scroll_axis {
205        ScrollAxis::Vertical => (available_area.width, total_size),
206        ScrollAxis::Horizontal => (total_size, available_area.height),
207    };
208    let mut hidden_buffer = Buffer::empty(Rect {
209        x: available_area.left(),
210        y: available_area.top(),
211        width,
212        height,
213    });
214    hidden_buffer.set_style(hidden_buffer.area, style);
215    item.render(hidden_buffer.area, &mut hidden_buffer);
216
217    // Copy the visible part from the intermediate buffer to the main buffer
218    match scroll_axis {
219        ScrollAxis::Vertical => {
220            let offset = if truncate_top {
221                total_size.saturating_sub(available_area.height)
222            } else {
223                0
224            };
225            for y in available_area.top()..available_area.bottom() {
226                let y_off = y + offset;
227                for x in available_area.left()..available_area.right() {
228                    *buf.get_mut(x, y) = hidden_buffer.get(x, y_off).clone();
229                }
230            }
231        }
232        ScrollAxis::Horizontal => {
233            let offset = if truncate_top {
234                total_size.saturating_sub(available_area.width)
235            } else {
236                0
237            };
238            for x in available_area.left()..available_area.right() {
239                let x_off = x + offset;
240                for y in available_area.top()..available_area.bottom() {
241                    *buf.get_mut(x, y) = hidden_buffer.get(x_off, y).clone();
242                }
243            }
244        }
245    };
246}
247
248#[cfg(test)]
249mod test {
250    use crate::PreRenderContext;
251
252    use super::*;
253    use ratatui::widgets::Borders;
254
255    struct TestItem {}
256    impl Widget for TestItem {
257        fn render(self, area: Rect, buf: &mut Buffer)
258        where
259            Self: Sized,
260        {
261            Block::default().borders(Borders::ALL).render(area, buf);
262        }
263    }
264
265    impl PreRender for TestItem {
266        fn pre_render(&mut self, context: &PreRenderContext) -> u16 {
267            let main_axis_size = match context.scroll_axis {
268                ScrollAxis::Vertical => 3,
269                ScrollAxis::Horizontal => 3,
270            };
271            main_axis_size
272        }
273    }
274
275    fn init(height: u16) -> (Rect, Buffer, List<'static, TestItem>, ListState) {
276        let area = Rect::new(0, 0, 5, height);
277        (
278            area,
279            Buffer::empty(area),
280            List::new(vec![TestItem {}, TestItem {}, TestItem {}]),
281            ListState::default(),
282        )
283    }
284
285    #[test]
286    fn not_truncated() {
287        // given
288        let (area, mut buf, list, mut state) = init(9);
289
290        // when
291        list.render(area, &mut buf, &mut state);
292
293        // then
294        assert_buffer_eq(
295            buf,
296            Buffer::with_lines(vec![
297                "┌───┐",
298                "│   │",
299                "└───┘",
300                "┌───┐",
301                "│   │",
302                "└───┘",
303                "┌───┐",
304                "│   │",
305                "└───┘",
306            ]),
307        )
308    }
309
310    #[test]
311    fn empty_list() {
312        // given
313        let (area, mut buf, _, mut state) = init(2);
314        let list = List::new(Vec::<TestItem>::new());
315
316        // when
317        list.render(area, &mut buf, &mut state);
318
319        // then
320        assert_buffer_eq(buf, Buffer::with_lines(vec!["     ", "     "]))
321    }
322
323    #[test]
324    fn zero_size() {
325        // given
326        let (area, mut buf, list, mut state) = init(0);
327
328        // when
329        list.render(area, &mut buf, &mut state);
330
331        // then
332        assert_buffer_eq(buf, Buffer::empty(area))
333    }
334
335    #[test]
336    fn bottom_is_truncated() {
337        // given
338        let (area, mut buf, list, mut state) = init(8);
339
340        // when
341        list.render(area, &mut buf, &mut state);
342
343        // then
344        assert_buffer_eq(
345            buf,
346            Buffer::with_lines(vec![
347                "┌───┐",
348                "│   │",
349                "└───┘",
350                "┌───┐",
351                "│   │",
352                "└───┘",
353                "┌───┐",
354                "│   │",
355            ]),
356        )
357    }
358
359    #[test]
360    fn top_is_truncated() {
361        // given
362        let (area, mut buf, list, mut state) = init(8);
363        state.select(Some(2));
364
365        // when
366        list.render(area, &mut buf, &mut state);
367
368        // then
369        assert_buffer_eq(
370            buf,
371            Buffer::with_lines(vec![
372                "│   │",
373                "└───┘",
374                "┌───┐",
375                "│   │",
376                "└───┘",
377                "┌───┐",
378                "│   │",
379                "└───┘",
380            ]),
381        )
382    }
383
384    fn assert_buffer_eq(actual: Buffer, expected: Buffer) {
385        if actual.area != expected.area {
386            panic!(
387                "buffer areas not equal expected: {:?} actual: {:?}",
388                expected, actual
389            );
390        }
391        let diff = expected.diff(&actual);
392        if !diff.is_empty() {
393            panic!(
394                "buffer contents not equal\nexpected: {:?}\nactual: {:?}",
395                expected, actual,
396            );
397        }
398        assert_eq!(actual, expected, "buffers not equal");
399    }
400}