git_igitt/widgets/
files_view.rs1use 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: Style,
43 start_corner: Corner,
44 highlight_style: Style,
46 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}