git_igitt/widgets/
files_view.rs

1use crate::widgets::list::ListState;
2use tui::buffer::Buffer;
3use tui::layout::{Corner, Rect};
4use tui::style::Style;
5use tui::text::Span;
6use tui::widgets::{Block, StatefulWidget, Widget};
7use unicode_width::UnicodeWidthStr;
8
9const SCROLL_MARGIN: usize = 2;
10const SCROLLBAR_STR: &str = "\u{2588}";
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct FileListItem<'a> {
14    pub content: Span<'a>,
15    pub prefix: Span<'a>,
16    pub style: Style,
17}
18
19impl<'a> FileListItem<'a> {
20    pub fn new<T>(content: T, prefix: T) -> FileListItem<'a>
21    where
22        T: Into<Span<'a>>,
23    {
24        FileListItem {
25            content: content.into(),
26            prefix: prefix.into(),
27            style: Style::default(),
28        }
29    }
30
31    pub fn style(mut self, style: Style) -> FileListItem<'a> {
32        self.style = style;
33        self
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct FileList<'a> {
39    block: Option<Block<'a>>,
40    items: Vec<FileListItem<'a>>,
41    /// Style used as a base style for the widget
42    style: Style,
43    start_corner: Corner,
44    /// Style used to render selected item
45    highlight_style: Style,
46    /// Symbol in front of the selected item (Shift all items to the right)
47    highlight_symbol: Option<&'a str>,
48}
49
50impl<'a> FileList<'a> {
51    pub fn new<T>(items: T) -> FileList<'a>
52    where
53        T: Into<Vec<FileListItem<'a>>>,
54    {
55        FileList {
56            block: None,
57            style: Style::default(),
58            items: items.into(),
59            start_corner: Corner::TopLeft,
60            highlight_style: Style::default(),
61            highlight_symbol: None,
62        }
63    }
64
65    pub fn block(mut self, block: Block<'a>) -> FileList<'a> {
66        self.block = Some(block);
67        self
68    }
69
70    pub fn style(mut self, style: Style) -> FileList<'a> {
71        self.style = style;
72        self
73    }
74
75    pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> FileList<'a> {
76        self.highlight_symbol = Some(highlight_symbol);
77        self
78    }
79
80    pub fn highlight_style(mut self, style: Style) -> FileList<'a> {
81        self.highlight_style = style;
82        self
83    }
84
85    pub fn start_corner(mut self, corner: Corner) -> FileList<'a> {
86        self.start_corner = corner;
87        self
88    }
89}
90
91impl<'a> StatefulWidget for FileList<'a> {
92    type State = ListState;
93
94    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
95        buf.set_style(area, self.style);
96        let list_area = match self.block.take() {
97            Some(b) => {
98                let inner_area = b.inner(area);
99                b.render(area, buf);
100                inner_area
101            }
102            None => area,
103        };
104
105        if list_area.width < 1 || list_area.height < 1 {
106            return;
107        }
108
109        if self.items.is_empty() {
110            return;
111        }
112        let list_height = list_area.height as usize;
113
114        let mut start = state.offset;
115        let height = std::cmp::min(list_height, self.items.len().saturating_sub(state.offset));
116        let mut end = start + height;
117
118        let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
119
120        let move_to_end = (selected + SCROLL_MARGIN).min(self.items.len() - 1);
121        let move_to_start = selected.saturating_sub(SCROLL_MARGIN);
122
123        if move_to_end >= end {
124            let diff = move_to_end + 1 - end;
125            end += diff;
126            start += diff;
127        }
128        if move_to_start < start {
129            let diff = start - move_to_start;
130            end -= diff;
131            start -= diff;
132        }
133        state.offset = start;
134
135        let highlight_symbol = self.highlight_symbol.unwrap_or("");
136        let blank_symbol = " ".repeat(highlight_symbol.width());
137
138        let mut max_scroll = 0;
139        let mut current_height = 0;
140        for (i, item) in self
141            .items
142            .iter_mut()
143            .enumerate()
144            .skip(state.offset)
145            .take(end - start)
146        {
147            let (x, y) = match self.start_corner {
148                Corner::BottomLeft => {
149                    current_height += 1;
150                    (list_area.left(), list_area.bottom() - current_height)
151                }
152                _ => {
153                    let pos = (list_area.left(), list_area.top() + current_height);
154                    current_height += 1;
155                    pos
156                }
157            };
158            let area = Rect {
159                x,
160                y,
161                width: list_area.width,
162                height: 1,
163            };
164            let item_style = self.style.patch(item.style);
165            buf.set_style(area, item_style);
166
167            let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
168            let elem_x = {
169                let symbol = if is_selected {
170                    highlight_symbol
171                } else {
172                    &blank_symbol
173                };
174                let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
175                x
176            };
177
178            let max_element_width = (list_area.width - (elem_x - x)) as usize;
179            let max_width_2 = max_element_width.saturating_sub(item.prefix.width());
180
181            buf.set_span(elem_x, y, &item.prefix, max_element_width as u16);
182            if state.scroll_x > 0 && item.content.content.width() > max_width_2 {
183                if item.content.content.width() - max_width_2 > max_scroll {
184                    max_scroll = item.content.content.width() - max_width_2;
185                }
186
187                let start = std::cmp::min(
188                    item.content.content.width().saturating_sub(max_width_2) + 2,
189                    std::cmp::min(item.content.content.width(), state.scroll_x as usize + 2),
190                );
191                let span = Span::styled(
192                    format!("..{}", &item.content.content[start..]),
193                    item.content.style,
194                );
195                buf.set_span(
196                    elem_x + item.prefix.width() as u16,
197                    y,
198                    &span,
199                    max_width_2 as u16,
200                );
201            } else {
202                buf.set_span(
203                    elem_x + item.prefix.width() as u16,
204                    y,
205                    &item.content,
206                    max_width_2 as u16,
207                );
208            }
209            if is_selected {
210                buf.set_style(area, self.highlight_style);
211            }
212        }
213        if state.scroll_x > max_scroll as u16 {
214            state.scroll_x = max_scroll as u16;
215        }
216
217        let scroll_start = list_area.top() as usize
218            + (((list_height * start) as f32 / self.items.len() as f32).ceil() as usize)
219                .min(list_height - 1);
220        let scroll_height = (((list_height * list_height) as f32 / self.items.len() as f32).floor()
221            as usize)
222            .clamp(1, list_height);
223
224        if scroll_height < list_height {
225            for y in scroll_start..(scroll_start + scroll_height) {
226                buf.set_string(
227                    list_area.left() + list_area.width,
228                    y as u16,
229                    SCROLLBAR_STR,
230                    self.style,
231                );
232            }
233        }
234    }
235}
236
237impl<'a> Widget for FileList<'a> {
238    fn render(self, area: Rect, buf: &mut Buffer) {
239        let mut state = ListState::default();
240        StatefulWidget::render(self, area, buf, &mut state);
241    }
242}