1use crate::widgets::list::{ListItem, 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 enum BranchItemType {
14 LocalBranch,
15 RemoteBranch,
16 Tag,
17 Heading,
18}
19
20impl BranchItemType {
21 pub fn is_selectable(&self) -> bool {
22 self != &BranchItemType::Heading
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct BranchListItem<'a> {
28 pub content: Span<'a>,
29 pub style: Style,
30 pub item_type: &'a BranchItemType,
31}
32
33impl<'a> BranchListItem<'a> {
34 pub fn new<T>(content: T, item_type: &'a BranchItemType) -> BranchListItem<'a>
35 where
36 T: Into<Span<'a>>,
37 {
38 BranchListItem {
39 content: content.into(),
40 style: Style::default(),
41 item_type,
42 }
43 }
44
45 pub fn style(mut self, style: Style) -> BranchListItem<'a> {
46 self.style = style;
47 self
48 }
49}
50
51pub struct BranchItem {
52 pub(crate) name: String,
53 pub(crate) index: Option<usize>,
54 pub(crate) color: u8,
55 pub(crate) branch_type: BranchItemType,
56}
57
58impl BranchItem {
59 pub fn new(name: String, index: Option<usize>, color: u8, branch_type: BranchItemType) -> Self {
60 Self {
61 name,
62 index,
63 color,
64 branch_type,
65 }
66 }
67}
68
69impl ListItem for BranchItem {
70 fn is_selectable(&self) -> bool {
71 self.branch_type != BranchItemType::Heading
72 }
73}
74
75impl<'a> ListItem for BranchListItem<'a> {
76 fn is_selectable(&self) -> bool {
77 self.item_type != &BranchItemType::Heading
78 }
79}
80
81#[derive(Debug, Clone)]
82pub struct BranchList<'a> {
83 block: Option<Block<'a>>,
84 items: Vec<BranchListItem<'a>>,
85 style: Style,
86 start_corner: Corner,
87 highlight_style: Style,
89 highlight_symbol: Option<&'a str>,
91}
92
93impl<'a> BranchList<'a> {
94 pub fn new<T>(items: T) -> BranchList<'a>
95 where
96 T: Into<Vec<BranchListItem<'a>>>,
97 {
98 BranchList {
99 block: None,
100 style: Style::default(),
101 items: items.into(),
102 start_corner: Corner::TopLeft,
103 highlight_style: Style::default(),
104 highlight_symbol: None,
105 }
106 }
107
108 pub fn block(mut self, block: Block<'a>) -> BranchList<'a> {
109 self.block = Some(block);
110 self
111 }
112
113 pub fn style(mut self, style: Style) -> BranchList<'a> {
114 self.style = style;
115 self
116 }
117
118 pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> BranchList<'a> {
119 self.highlight_symbol = Some(highlight_symbol);
120 self
121 }
122
123 pub fn highlight_style(mut self, style: Style) -> BranchList<'a> {
124 self.highlight_style = style;
125 self
126 }
127
128 pub fn start_corner(mut self, corner: Corner) -> BranchList<'a> {
129 self.start_corner = corner;
130 self
131 }
132}
133
134impl<'a> StatefulWidget for BranchList<'a> {
135 type State = ListState;
136
137 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
138 buf.set_style(area, self.style);
139 let list_area = match self.block.take() {
140 Some(b) => {
141 let inner_area = b.inner(area);
142 b.render(area, buf);
143 inner_area
144 }
145 None => area,
146 };
147
148 if list_area.width < 1 || list_area.height < 1 {
149 return;
150 }
151
152 if self.items.is_empty() {
153 return;
154 }
155 let list_height = list_area.height as usize;
156
157 let mut start = state.offset;
158 let height = std::cmp::min(list_height, self.items.len().saturating_sub(state.offset));
159 let mut end = start + height;
160
161 let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
162
163 let move_to_end = (selected + SCROLL_MARGIN).min(self.items.len() - 1);
164 let move_to_start = selected.saturating_sub(SCROLL_MARGIN);
165
166 if move_to_end >= end {
167 let diff = move_to_end + 1 - end;
168 end += diff;
169 start += diff;
170 }
171 if move_to_start < start {
172 let diff = start - move_to_start;
173 end -= diff;
174 start -= diff;
175 }
176 state.offset = start;
177
178 let highlight_symbol = self.highlight_symbol.unwrap_or("");
179 let blank_symbol = " ".repeat(highlight_symbol.width());
180
181 let mut max_scroll = 0;
182 let mut current_height = 0;
183 for (i, item) in self
184 .items
185 .iter_mut()
186 .enumerate()
187 .skip(state.offset)
188 .take(end - start)
189 {
190 let (x, y) = match self.start_corner {
191 Corner::BottomLeft => {
192 current_height += 1;
193 (list_area.left(), list_area.bottom() - current_height)
194 }
195 _ => {
196 let pos = (list_area.left(), list_area.top() + current_height);
197 current_height += 1;
198 pos
199 }
200 };
201 let area = Rect {
202 x,
203 y,
204 width: list_area.width,
205 height: 1,
206 };
207 let item_style = self.style.patch(item.style);
208 buf.set_style(area, item_style);
209
210 let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
211 let elem_x = if item.is_selectable() {
212 let symbol = if is_selected {
213 highlight_symbol
214 } else {
215 &blank_symbol
216 };
217 let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
218 x
219 } else {
220 x
221 };
222
223 let max_element_width = (list_area.width - (elem_x - x)) as usize;
224
225 if state.scroll_x > 0 && item.content.content.width() > max_element_width {
226 if item.content.content.width() - max_element_width > max_scroll {
227 max_scroll = item.content.content.width() - max_element_width;
228 }
229
230 let start = std::cmp::min(
231 item.content
232 .content
233 .width()
234 .saturating_sub(max_element_width)
235 + 2,
236 std::cmp::min(item.content.content.width(), state.scroll_x as usize + 2),
237 );
238 let span = Span::styled(
239 format!("..{}", &item.content.content[start..]),
240 item.content.style,
241 );
242 buf.set_span(elem_x, y, &span, max_element_width as u16);
243 } else {
244 buf.set_span(elem_x, y, &item.content, max_element_width as u16);
245 }
246 if is_selected {
247 buf.set_style(area, self.highlight_style);
248 }
249 }
250 if state.scroll_x > max_scroll as u16 {
251 state.scroll_x = max_scroll as u16;
252 }
253
254 let scroll_start = list_area.top() as usize
255 + (((list_height * start) as f32 / self.items.len() as f32).ceil() as usize)
256 .min(list_height - 1);
257 let scroll_height = (((list_height * list_height) as f32 / self.items.len() as f32).floor()
258 as usize)
259 .clamp(1, list_height);
260
261 if scroll_height < list_height {
262 for y in scroll_start..(scroll_start + scroll_height) {
263 buf.set_string(
264 list_area.left() + list_area.width,
265 y as u16,
266 SCROLLBAR_STR,
267 self.style,
268 );
269 }
270 }
271 }
272}
273
274impl<'a> Widget for BranchList<'a> {
275 fn render(self, area: Rect, buf: &mut Buffer) {
276 let mut state = ListState::default();
277 StatefulWidget::render(self, area, buf, &mut state);
278 }
279}