git_igitt/widgets/
graph_view.rs

1use crate::util::ctrl_chars::CtrlChars;
2use crate::widgets::branches_view::BranchItem;
3use crate::widgets::list::StatefulList;
4use git_graph::graph::GitGraph;
5use std::iter::Iterator;
6use tui::buffer::Buffer;
7use tui::layout::Rect;
8use tui::style::Style;
9use tui::widgets::{Block, StatefulWidget, Widget};
10use unicode_width::UnicodeWidthStr;
11
12const SCROLL_MARGIN: usize = 3;
13const SCROLLBAR_STR: &str = "\u{2588}";
14
15#[derive(Default)]
16pub struct GraphViewState {
17    pub graph: Option<GitGraph>,
18    pub graph_lines: Vec<String>,
19    pub text_lines: Vec<String>,
20    pub indices: Vec<usize>,
21    pub offset: usize,
22    pub selected: Option<usize>,
23    pub branches: Option<StatefulList<BranchItem>>,
24    pub secondary_selected: Option<usize>,
25    pub secondary_changed: bool,
26}
27
28impl GraphViewState {
29    pub fn move_selection(&mut self, steps: usize, down: bool) -> bool {
30        let changed = if let Some(sel) = self.selected {
31            let new_idx = if down {
32                std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1)
33            } else {
34                std::cmp::max(sel.saturating_sub(steps), 0)
35            };
36            self.selected = Some(new_idx);
37            new_idx != sel
38        } else if !self.graph_lines.is_empty() {
39            self.selected = Some(0);
40            true
41        } else {
42            false
43        };
44        if changed {
45            self.secondary_changed = false;
46        }
47        changed
48    }
49    pub fn move_secondary_selection(&mut self, steps: usize, down: bool) -> bool {
50        let changed = if let Some(sel) = self.secondary_selected {
51            let new_idx = if down {
52                std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1)
53            } else {
54                std::cmp::max(sel.saturating_sub(steps), 0)
55            };
56            self.secondary_selected = Some(new_idx);
57            new_idx != sel
58        } else if !self.graph_lines.is_empty() {
59            if let Some(sel) = self.selected {
60                let new_idx = if down {
61                    std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1)
62                } else {
63                    std::cmp::max(sel.saturating_sub(steps), 0)
64                };
65                self.secondary_selected = Some(new_idx);
66                new_idx != sel
67            } else {
68                false
69            }
70        } else {
71            false
72        };
73        if changed {
74            self.secondary_changed = true;
75        }
76        changed
77    }
78}
79
80#[derive(Default)]
81pub struct GraphView<'a> {
82    block: Option<Block<'a>>,
83    highlight_symbol: Option<&'a str>,
84    secondary_highlight_symbol: Option<&'a str>,
85    style: Style,
86    highlight_style: Style,
87}
88
89impl<'a> GraphView<'a> {
90    pub fn block(mut self, block: Block<'a>) -> GraphView<'a> {
91        self.block = Some(block);
92        self
93    }
94
95    pub fn style(mut self, style: Style) -> GraphView<'a> {
96        self.style = style;
97        self
98    }
99
100    pub fn highlight_symbol(
101        mut self,
102        highlight_symbol: &'a str,
103        secondary_highlight_symbol: &'a str,
104    ) -> GraphView<'a> {
105        self.highlight_symbol = Some(highlight_symbol);
106        self.secondary_highlight_symbol = Some(secondary_highlight_symbol);
107        self
108    }
109
110    pub fn highlight_style(mut self, style: Style) -> GraphView<'a> {
111        self.highlight_style = style;
112        self
113    }
114}
115
116impl<'a> StatefulWidget for GraphView<'a> {
117    type State = GraphViewState;
118
119    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
120        buf.set_style(area, self.style);
121        let list_area = match self.block.take() {
122            Some(b) => {
123                let inner_area = b.inner(area);
124                b.render(area, buf);
125                inner_area
126            }
127            None => area,
128        };
129
130        if list_area.width < 1 || list_area.height < 1 {
131            return;
132        }
133
134        if state.graph_lines.is_empty() {
135            return;
136        }
137        let list_height = list_area.height as usize;
138
139        let mut start = state.offset;
140
141        let height = std::cmp::min(
142            list_height,
143            state.graph_lines.len().saturating_sub(state.offset),
144        );
145        let mut end = start + height;
146
147        let selected_row = state.selected.map(|idx| state.indices[idx]);
148        let selected = selected_row.unwrap_or(0).min(state.graph_lines.len() - 1);
149
150        let secondary_selected_row = state.secondary_selected.map(|idx| state.indices[idx]);
151        let secondary_selected = secondary_selected_row
152            .unwrap_or(0)
153            .min(state.graph_lines.len() - 1);
154
155        let selected_index = if state.secondary_changed {
156            state.secondary_selected.unwrap_or(0)
157        } else {
158            state.selected.unwrap_or(0)
159        };
160        let move_to_selected = if state.secondary_changed {
161            secondary_selected
162        } else {
163            selected
164        };
165
166        let move_to_end = if selected_index >= state.indices.len() - 1 {
167            state.graph_lines.len() - 1
168        } else {
169            (state.indices[selected_index + 1] - 1).clamp(
170                move_to_selected + SCROLL_MARGIN,
171                state.graph_lines.len() - 1,
172            )
173        };
174        let move_to_start = move_to_selected.saturating_sub(SCROLL_MARGIN);
175
176        if move_to_end >= end {
177            let diff = move_to_end + 1 - end;
178            end += diff;
179            start += diff;
180        }
181        if move_to_start < start {
182            let diff = start - move_to_start;
183            end -= diff;
184            start -= diff;
185        }
186        state.offset = start;
187
188        let highlight_symbol = self.highlight_symbol.unwrap_or("");
189        let secondary_highlight_symbol = self.secondary_highlight_symbol.unwrap_or("");
190
191        let blank_symbol = " ".repeat(highlight_symbol.width());
192
193        let style = Style::default();
194        for (current_height, (i, (graph_item, text_item))) in state
195            .graph_lines
196            .iter()
197            .zip(state.text_lines.iter())
198            .enumerate()
199            .skip(state.offset)
200            .take(end - start)
201            .enumerate()
202        {
203            let (x, y) = (list_area.left(), list_area.top() + current_height as u16);
204
205            let is_selected = selected_row.map(|s| s == i).unwrap_or(false);
206            let is_sec_selected = secondary_selected_row.map(|s| s == i).unwrap_or(false);
207            let elem_x = {
208                let symbol = if is_selected {
209                    highlight_symbol
210                } else if is_sec_selected {
211                    secondary_highlight_symbol
212                } else {
213                    &blank_symbol
214                };
215                let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, style);
216                x
217            };
218
219            let area = Rect {
220                x,
221                y,
222                width: list_area.width,
223                height: 1,
224            };
225
226            let max_element_width = (list_area.width - (elem_x - x)) as usize;
227
228            let mut body = CtrlChars::parse(graph_item).into_text();
229            body.extend(CtrlChars::parse(&format!("  {}", text_item)).into_text());
230
231            let mut x = elem_x;
232            let mut remaining_width = max_element_width as u16;
233            for txt in body {
234                for line in txt.lines {
235                    if remaining_width == 0 {
236                        break;
237                    }
238                    let pos = buf.set_spans(x, y, &line, remaining_width);
239                    let w = pos.0.saturating_sub(x);
240                    x = pos.0;
241                    remaining_width = remaining_width.saturating_sub(w);
242                }
243            }
244
245            if is_selected || is_sec_selected {
246                buf.set_style(area, self.highlight_style);
247            }
248        }
249
250        let scroll_start = list_area.top() as usize
251            + (((list_height * start) as f32 / state.graph_lines.len() as f32).ceil() as usize)
252                .min(list_height - 1);
253        let scroll_height = (((list_height * list_height) as f32 / state.graph_lines.len() as f32)
254            .floor() as usize)
255            .clamp(1, list_height);
256
257        if scroll_height < list_height {
258            for y in scroll_start..(scroll_start + scroll_height) {
259                buf.set_string(
260                    list_area.left() + list_area.width,
261                    y as u16,
262                    SCROLLBAR_STR,
263                    self.style,
264                );
265            }
266        }
267    }
268}
269
270impl<'a> Widget for GraphView<'a> {
271    fn render(self, area: Rect, buf: &mut Buffer) {
272        let mut state = GraphViewState::default();
273        StatefulWidget::render(self, area, buf, &mut state);
274    }
275}