git_igitt/widgets/
branches_view.rs

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    /// Style used to render selected item
88    highlight_style: Style,
89    /// Symbol in front of the selected item (Shift all items to the right)
90    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}