1use std::{fmt::Write, iter};
2
3use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
4
5use crate::{
6 Bar, Column, Count, List, Place, Row, Source,
7 bar::{Stack, thumb},
8 row::{Kind, Paint},
9 text::{self, blank, put, put_runs},
10};
11
12pub const CURSOR: &str = "▌ ";
13pub const MAX_COLUMNS: usize = 32;
14const GAP: u16 = 2;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Styles {
19 pub(crate) selected: Style,
20 pub(crate) ink: Style,
21 pub(crate) faint: Style,
22 pub(crate) base: Style,
23 pub(crate) header: Option<Style>,
24 pub(crate) header_key: Option<Style>,
25 pub(crate) cursor: Option<Style>,
26 pub(crate) keep: bool,
27 pub(crate) track: Option<Style>,
28 pub(crate) thumb: Option<Style>,
29 pub(crate) count: Option<Style>,
30}
31
32impl Styles {
33 pub const fn new() -> Self {
34 Styles {
35 selected: Style::new(),
36 ink: Style::new(),
37 faint: Style::new(),
38 base: Style::new(),
39 header: None,
40 header_key: None,
41 cursor: None,
42 keep: false,
43 track: None,
44 thumb: None,
45 count: None,
46 }
47 }
48
49 pub const fn selected(mut self, style: Style) -> Self {
50 self.selected = style;
51 self
52 }
53
54 pub const fn ink(mut self, style: Style) -> Self {
55 self.ink = style;
56 self
57 }
58
59 pub const fn faint(mut self, style: Style) -> Self {
60 self.faint = style;
61 self
62 }
63
64 pub const fn base(mut self, style: Style) -> Self {
65 self.base = style;
66 self
67 }
68
69 pub const fn header(mut self, style: Style) -> Self {
70 self.header = Some(style);
71 self
72 }
73
74 pub const fn header_key(mut self, style: Style) -> Self {
75 self.header_key = Some(style);
76 self
77 }
78
79 pub const fn cursor(mut self, style: Style) -> Self {
80 self.cursor = Some(style);
81 self
82 }
83
84 pub const fn keep_colours(mut self, keep: bool) -> Self {
85 self.keep = keep;
86 self
87 }
88
89 pub const fn track(mut self, style: Style) -> Self {
90 self.track = Some(style);
91 self
92 }
93
94 pub const fn thumb(mut self, style: Style) -> Self {
95 self.thumb = Some(style);
96 self
97 }
98
99 pub const fn count(mut self, style: Style) -> Self {
100 self.count = Some(style);
101 self
102 }
103
104 fn resolve(&self, paint: Paint) -> Style {
105 match paint {
106 Paint::Ink => self.ink,
107 Paint::Faint => self.faint,
108 Paint::Own(style) => style,
109 }
110 }
111}
112
113impl Default for Styles {
114 fn default() -> Self {
115 Styles::new()
116 }
117}
118
119struct Layout {
120 widths: [u16; MAX_COLUMNS],
121 kept: [bool; MAX_COLUMNS],
122 right: [bool; MAX_COLUMNS],
123 count: usize,
124 flex: usize,
125}
126
127fn small(value: usize) -> u16 {
128 u16::try_from(value).unwrap_or(u16::MAX)
129}
130
131impl Layout {
132 fn new(columns: &[Column], width: u16, lead: u16, gap: u16) -> Self {
133 let count = columns.len().min(MAX_COLUMNS);
134 let mut layout = Layout {
135 widths: [0; MAX_COLUMNS],
136 kept: [false; MAX_COLUMNS],
137 right: [false; MAX_COLUMNS],
138 count,
139 flex: 0,
140 };
141 for (right, column) in layout.right.iter_mut().zip(columns) {
142 *right = column.right;
143 }
144 if count == 0 {
145 return layout;
146 }
147 layout.flex = columns[..count]
148 .iter()
149 .position(|column| column.flex)
150 .unwrap_or(count - 1);
151 layout.kept[..count].fill(true);
152 while layout.need(columns, lead, gap) > usize::from(width) {
153 let mut pick: Option<usize> = None;
154 for at in 0..count {
155 if at != layout.flex && layout.kept[at] && !columns[at].pinned {
156 match pick {
157 Some(held) if columns[held].rank > columns[at].rank => {}
158 _ => pick = Some(at),
159 }
160 }
161 }
162 match pick {
163 Some(at) => layout.kept[at] = false,
164 None => break,
165 }
166 }
167 layout
168 }
169
170 fn need(&self, columns: &[Column], lead: u16, gap: u16) -> usize {
171 let mut total = usize::from(lead).saturating_add(usize::from(columns[self.flex].min));
172 for (at, column) in columns.iter().enumerate().take(self.count) {
173 if at != self.flex && self.kept[at] {
174 total = total
175 .saturating_add(usize::from(column.min))
176 .saturating_add(usize::from(gap));
177 }
178 }
179 total
180 }
181
182 fn fits(&self, columns: &[Column], at: usize) -> bool {
183 at != self.flex && self.kept[at] && columns[at].fit.is_some()
184 }
185
186 fn size(&mut self, columns: &[Column], width: u16, lead: u16, gap: u16, fitted: &[u16]) {
187 if self.count == 0 {
188 return;
189 }
190 let width = usize::from(width);
191 let mut extra = width.saturating_sub(self.need(columns, lead, gap));
192 let mut used = usize::from(lead);
193 for (at, column) in columns.iter().enumerate().take(self.count) {
194 if at == self.flex || !self.kept[at] {
195 continue;
196 }
197 let min = usize::from(column.min);
198 let preferred = match column.fit {
199 Some(cap) => fitted[at].min(cap),
200 None => column.preferred,
201 };
202 let grow = usize::from(preferred).saturating_sub(min).min(extra);
203 extra -= grow;
204 let size = min + grow;
205 self.widths[at] = small(size);
206 used = used.saturating_add(size).saturating_add(usize::from(gap));
207 }
208 self.widths[self.flex] = small(width.saturating_sub(used));
209 }
210
211 fn places(&self, from: u16, edge: u16, gap: u16) -> impl Iterator<Item = (usize, u16, u16)> {
212 (0..self.count)
213 .filter(|&at| self.kept[at])
214 .scan(from, move |x, at| {
215 let start = *x;
216 *x = start.saturating_add(self.widths[at]).saturating_add(gap);
217 Some((at, start, self.widths[at].min(edge.saturating_sub(start))))
218 })
219 .filter(|&(_, _, width)| width > 0)
220 }
221}
222
223fn fitted<S: Source>(
224 list: &List<S>,
225 columns: &[Column],
226 layout: &Layout,
227 header: bool,
228 lines: usize,
229) -> [u16; MAX_COLUMNS] {
230 let mut widths = [0; MAX_COLUMNS];
231 let count = layout.count;
232 if !(0..count).any(|at| layout.fits(columns, at)) {
233 return widths;
234 }
235 if header {
236 for (at, width) in widths.iter_mut().enumerate().take(count) {
237 if layout.fits(columns, at) {
238 *width = text::cells(columns[at].title);
239 }
240 }
241 }
242 let from = list.top();
243 let to = from.saturating_add(lines).min(list.len());
244 for index in from..to {
245 let row = list.row(index);
246 if row.kind != Kind::Item {
247 continue;
248 }
249 for (at, cell) in row.cells.iter().enumerate().take(count) {
250 if !layout.kept[at] {
251 continue;
252 }
253 if cell.rest {
254 break;
255 }
256 if layout.fits(columns, at) {
257 let cells = cell.pieces().fold(0usize, |total, (text, _)| {
258 total.saturating_add(text::width(text))
259 });
260 widths[at] = widths[at].max(small(cells));
261 }
262 }
263 }
264 widths
265}
266
267fn columns_line<'r, I>(
268 buf: &mut Buffer,
269 clip: Rect,
270 y: u16,
271 from: u16,
272 layout: &Layout,
273 gap: u16,
274 mut cell: impl FnMut(usize) -> Option<(I, bool)>,
275) -> u16
276where
277 I: Iterator<Item = (&'r str, Style)> + Clone,
278{
279 let mut x = from;
280 let mut ink = 0;
281 for at in 0..layout.count {
282 if !layout.kept[at] {
283 continue;
284 }
285 let size = layout.widths[at];
286 if let Some((runs, rest)) = cell(at) {
287 if rest {
288 let room = clip.right().saturating_sub(x);
289 return ink.max(put_runs(buf, clip, x, y, room, runs, layout.right[at]));
290 }
291 ink = ink.max(put_runs(buf, clip, x, y, size, runs, layout.right[at]));
292 }
293 x = x.saturating_add(size).saturating_add(gap);
294 }
295 ink
296}
297
298#[derive(Debug)]
299pub struct ListView<'a, S = Vec<Row>> {
300 list: &'a mut List<S>,
301 columns: &'a [Column<'a>],
302 styles: Styles,
303 cursor: &'a str,
304 gap: u16,
305 header: bool,
306 key: Option<usize>,
307 end: Option<&'a str>,
308 empty: Option<&'a str>,
309 bar: Option<Bar<'a>>,
310 count: Option<Count<'a>>,
311}
312
313impl<'a, S: Source> ListView<'a, S> {
314 pub fn new(list: &'a mut List<S>, columns: &'a [Column<'a>]) -> Self {
315 ListView {
316 list,
317 columns,
318 styles: Styles::new(),
319 cursor: CURSOR,
320 gap: GAP,
321 header: false,
322 key: None,
323 end: None,
324 empty: None,
325 bar: None,
326 count: None,
327 }
328 }
329
330 pub fn scrollbar(mut self, bar: Option<Bar<'a>>) -> Self {
331 self.bar = bar;
332 self
333 }
334
335 pub fn count(mut self, count: Option<Count<'a>>) -> Self {
336 self.count = count;
337 self
338 }
339
340 pub fn styles(mut self, styles: Styles) -> Self {
341 self.styles = styles;
342 self
343 }
344
345 pub fn cursor(mut self, cursor: &'a str) -> Self {
346 self.cursor = cursor;
347 self
348 }
349
350 pub fn gap(mut self, gap: u16) -> Self {
351 self.gap = gap;
352 self
353 }
354
355 pub fn header(mut self, header: bool) -> Self {
356 self.header = header;
357 self
358 }
359
360 pub fn key_column(mut self, column: Option<usize>) -> Self {
361 self.key = column;
362 self
363 }
364
365 pub fn end(mut self, end: Option<&'a str>) -> Self {
366 self.end = end;
367 self
368 }
369
370 pub fn empty(mut self, empty: Option<&'a str>) -> Self {
371 self.empty = empty;
372 self
373 }
374}
375
376struct Line<'a> {
377 styles: Styles,
378 chosen: Style,
379 layout: Layout,
380 gap: u16,
381 indent: u16,
382 lead: u16,
383 cursor: &'a str,
384 mark_width: u16,
385 area: Rect,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389enum Lit {
390 No,
391 Range,
392 Cursor,
393}
394
395impl Line<'_> {
396 fn paint(&self, paint: Paint, chosen: bool) -> Style {
397 match (chosen, self.styles.keep) {
398 (false, _) => self.styles.resolve(paint),
399 (true, false) => self.styles.selected,
400 (true, true) => self.styles.resolve(paint).patch(self.styles.selected),
401 }
402 }
403
404 fn row(&self, buf: &mut Buffer, y: u16, row: &Row, lit: Lit) -> u16 {
405 match row.kind {
406 Kind::Item => self.item(buf, y, row, lit),
407 Kind::Heading => self.heading(buf, y, row),
408 Kind::Blank => 0,
409 }
410 }
411
412 fn item(&self, buf: &mut Buffer, y: u16, row: &Row, lit: Lit) -> u16 {
413 let area = self.area;
414 let mut x = area.x;
415 let room = area.width;
416 let chosen = lit != Lit::No;
417 let mut ink = 0;
418 if chosen {
419 blank(buf, area, x, y, room, self.chosen);
420 }
421 if lit == Lit::Cursor {
422 let style = self.styles.cursor.unwrap_or(self.styles.selected);
423 ink = put(buf, area, x, y, self.indent, self.cursor, style);
424 }
425 x = x.saturating_add(self.indent);
426 if let Some(mark) = &row.mark {
427 ink = ink.max(put(
428 buf,
429 area,
430 x,
431 y,
432 self.mark_width,
433 &mark.text,
434 self.paint(mark.paint, chosen),
435 ));
436 }
437 x = x.saturating_add(self.lead - self.indent);
438 let cells = columns_line(buf, area, y, x, &self.layout, self.gap, |at| {
439 let cell = row.cells.get(at)?;
440 let runs = cell
441 .pieces()
442 .map(move |(text, paint)| (text, self.paint(paint, chosen)));
443 Some((runs, cell.rest))
444 });
445 ink.max(cells)
446 }
447
448 fn heading(&self, buf: &mut Buffer, y: u16, row: &Row) -> u16 {
449 let Some(cell) = row.cells.first() else {
450 return 0;
451 };
452 let x = self.area.x.saturating_add(self.indent);
453 let room = self.area.width.saturating_sub(self.indent);
454 let runs = cell
455 .pieces()
456 .map(|(text, paint)| (text, self.styles.resolve(paint)));
457 put_runs(buf, self.area, x, y, room, runs, false)
458 }
459
460 fn header(&self, buf: &mut Buffer, wide: Rect, columns: &[Column], key: Option<usize>) {
461 let styles = self.styles;
462 let y = wide.y;
463 if let Some(style) = styles.header {
464 blank(buf, wide, wide.x, y, wide.width, styles.base.patch(style));
465 }
466 let title = styles.header.unwrap_or(styles.faint);
467 let x = self.area.x.saturating_add(self.lead);
468 columns_line(buf, self.area, y, x, &self.layout, self.gap, |at| {
469 let style = match styles.header_key {
470 Some(style) if key == Some(at) => style,
471 _ => title,
472 };
473 Some((iter::once((columns[at].title, style)), false))
474 });
475 }
476
477 fn note(&self, buf: &mut Buffer, y: u16, text: &str) -> u16 {
478 let x = self.area.x.saturating_add(self.indent);
479 let room = self.area.width.saturating_sub(self.indent);
480 put(buf, self.area, x, y, room, text, self.styles.faint)
481 }
482
483 fn bar(&self, buf: &mut Buffer, bar: &Bar, x: u16, y: u16, top: usize, lines: usize) {
484 let height = usize::from(self.area.bottom().saturating_sub(y));
485 let unit = if bar.halves.is_some() { 2 } else { 1 };
486 let (start, size) = thumb(top, lines, height, unit);
487 let end = start.saturating_add(size);
488 let track = self.styles.track.unwrap_or(self.styles.faint);
489 let lit = self
490 .styles
491 .thumb
492 .or(self.styles.cursor)
493 .unwrap_or(self.styles.selected);
494 let clip = Rect {
495 x,
496 width: 1,
497 ..self.area
498 };
499 for offset in 0..height {
500 let from = offset * unit;
501 let upper = (start..end).contains(&from);
502 let lower = (start..end).contains(&(from + unit - 1));
503 let at = y.saturating_add(small(offset));
504 match bar.glyph(upper, lower) {
505 Some(glyph) => put(buf, clip, x, at, 1, glyph, lit),
506 None => put(buf, clip, x, at, 1, bar.track, track),
507 };
508 }
509 }
510}
511
512fn label(
513 buf: &mut Buffer,
514 count: &Count,
515 shown: (usize, usize, usize),
516 style: Style,
517 line: Rect,
518 free: Option<u16>,
519) {
520 let mut text = Stack::new();
521 let (first, last, total) = shown;
522 if write!(text, "{}", count.label(first, last, total)).is_err() {
523 return;
524 }
525 let text = text.text();
526 if let Some(free) = free
527 && line.right().saturating_sub(text::cells(text)) < free
528 {
529 return;
530 }
531 let runs = iter::once((text, style));
532 put_runs(buf, line, line.x, line.y, line.width, runs, true);
533}
534
535impl<S: Source> Widget for ListView<'_, S> {
536 fn render(self, area: Rect, buf: &mut Buffer) {
537 let area = area.intersection(buf.area);
538 if area.is_empty() {
539 self.list.place(iter::empty());
540 self.list.drawn(None, false, false);
541 return;
542 }
543 let ListView {
544 list,
545 columns,
546 styles,
547 cursor,
548 gap,
549 header,
550 key,
551 end,
552 empty,
553 bar,
554 count,
555 } = self;
556 for y in area.top()..area.bottom() {
557 blank(buf, area, area.x, y, area.width, styles.base);
558 }
559 let indent = text::cells(cursor);
560 let mark_width = list.mark_width();
561 let lead = indent
562 .saturating_add(mark_width)
563 .saturating_add(u16::from(mark_width > 0));
564 let y = area.y.saturating_add(u16::from(header));
565 let body = area.bottom().saturating_sub(y);
566 let len = list.len();
567 let lines = if len == 0 {
568 0
569 } else {
570 len.saturating_add(usize::from(end.is_some()))
571 };
572 let over = body > 0 && lines > usize::from(body);
573 let count = count.filter(|_| over);
574 let cut = count.is_some_and(|count| count.place == Place::Line && body > 1);
575 let bar = bar.filter(|_| over && area.width > 1);
576 let rows = Rect {
577 width: area.width - u16::from(bar.is_some()),
578 height: area.height - u16::from(cut),
579 ..area
580 };
581 let height = usize::from(rows.bottom().saturating_sub(y));
582 let selected = list.follow(height, end.is_some());
583 let mut layout = Layout::new(columns, rows.width, lead, gap);
584 let fitted = fitted(list, columns, &layout, header, height);
585 layout.size(columns, rows.width, lead, gap, &fitted);
586 list.place(layout.places(rows.x.saturating_add(lead), rows.right(), gap));
587 let line = Line {
588 styles,
589 chosen: styles.base.patch(styles.selected),
590 layout,
591 gap,
592 indent,
593 lead,
594 cursor,
595 mark_width,
596 area: rows,
597 };
598 if header {
599 line.header(buf, Rect { height: 1, ..area }, columns, key);
600 }
601 if list.is_empty() {
602 list.drawn(None, false, false);
603 if let Some(text) = empty
604 && height > 0
605 {
606 line.note(buf, y, text);
607 }
608 return;
609 }
610 let top = list.top();
611 let mut ink = 0;
612 for offset in 0..height {
613 let index = top.saturating_add(offset);
614 let at = y.saturating_add(small(offset));
615 if index < len {
616 let lit = if selected == Some(index) {
617 Lit::Cursor
618 } else if list.in_range(index) {
619 Lit::Range
620 } else {
621 Lit::No
622 };
623 ink = line.row(buf, at, &list.row(index), lit);
624 continue;
625 }
626 ink = 0;
627 if let Some(text) = end
628 && index == len
629 {
630 ink = line.note(buf, at, text);
631 }
632 break;
633 }
634 let on = len.saturating_sub(top).min(height);
635 let shown = (on > 0).then(|| (top + 1, top + on, len));
636 list.drawn(shown, cut, bar.is_some());
637 if let Some(bar) = bar {
638 line.bar(buf, &bar, area.right() - 1, y, top, lines);
639 }
640 let (Some(count), Some(shown)) = (count, shown) else {
641 return;
642 };
643 let style = styles.count.unwrap_or(styles.faint);
644 let (at, free) = match count.place {
645 Place::Line if cut => (
646 Rect {
647 y: rows.bottom(),
648 height: 1,
649 ..area
650 },
651 None,
652 ),
653 _ => {
654 let last = y.saturating_add(small(height)).saturating_sub(1);
655 let free = if ink == 0 {
656 rows.x
657 } else {
658 ink.saturating_add(1)
659 };
660 (
661 Rect {
662 y: last,
663 height: 1,
664 ..rows
665 },
666 Some(free),
667 )
668 }
669 };
670 label(buf, &count, shown, style, at, free);
671 }
672}