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#[derive(Clone)]
15#[deprecated(since = "0.11.0", note = "Use ListView with ListBuilder instead.")]
16pub struct List<'a, T: PreRender> {
17 pub items: Vec<T>,
19
20 style: Style,
22
23 block: Option<Block<'a>>,
25
26 scroll_axis: ScrollAxis,
28}
29
30#[allow(deprecated)]
31impl<'a, T: PreRender> List<'a, T> {
32 #[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 #[must_use]
49 pub fn block(mut self, block: Block<'a>) -> Self {
50 self.block = Some(block);
51 self
52 }
53
54 #[must_use]
56 pub fn is_empty(&self) -> bool {
57 self.items.is_empty()
58 }
59
60 #[must_use]
62 pub fn len(&self) -> usize {
63 self.items.len()
64 }
65
66 #[must_use]
68 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
69 self.style = style.into();
70 self
71 }
72
73 #[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 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 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 if items.is_empty() {
125 return;
126 }
127
128 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 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 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 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 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 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
192fn 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 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 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 let (area, mut buf, list, mut state) = init(9);
289
290 list.render(area, &mut buf, &mut state);
292
293 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 let (area, mut buf, _, mut state) = init(2);
314 let list = List::new(Vec::<TestItem>::new());
315
316 list.render(area, &mut buf, &mut state);
318
319 assert_buffer_eq(buf, Buffer::with_lines(vec![" ", " "]))
321 }
322
323 #[test]
324 fn zero_size() {
325 let (area, mut buf, list, mut state) = init(0);
327
328 list.render(area, &mut buf, &mut state);
330
331 assert_buffer_eq(buf, Buffer::empty(area))
333 }
334
335 #[test]
336 fn bottom_is_truncated() {
337 let (area, mut buf, list, mut state) = init(8);
339
340 list.render(area, &mut buf, &mut state);
342
343 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 let (area, mut buf, list, mut state) = init(8);
363 state.select(Some(2));
364
365 list.render(area, &mut buf, &mut state);
367
368 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}