Skip to main content

git_igitt/widgets/
graph_view.rs

1use crate::util::ctrl_chars::CtrlChars;
2use crate::widgets::branches_view::BranchItem;
3use crate::widgets::list::StatefulList;
4use gleisbau::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 StatefulWidget for GraphView<'_> {
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)
170                .max(move_to_selected + SCROLL_MARGIN)
171                .min(state.graph_lines.len() - 1)
172        };
173        let move_to_start = move_to_selected.saturating_sub(SCROLL_MARGIN);
174
175        if move_to_end >= end {
176            let diff = move_to_end + 1 - end;
177            end += diff;
178            start += diff;
179        }
180        if move_to_start < start {
181            let diff = start - move_to_start;
182            end -= diff;
183            start -= diff;
184        }
185        state.offset = start;
186
187        let highlight_symbol = self.highlight_symbol.unwrap_or("");
188        let secondary_highlight_symbol = self.secondary_highlight_symbol.unwrap_or("");
189
190        let blank_symbol = " ".repeat(highlight_symbol.width());
191
192        let style = Style::default();
193        for (current_height, (i, (graph_item, text_item))) in state
194            .graph_lines
195            .iter()
196            .zip(state.text_lines.iter())
197            .enumerate()
198            .skip(state.offset)
199            .take(end - start)
200            .enumerate()
201        {
202            let (x, y) = (list_area.left(), list_area.top() + current_height as u16);
203
204            let is_selected = selected_row.map(|s| s == i).unwrap_or(false);
205            let is_sec_selected = secondary_selected_row.map(|s| s == i).unwrap_or(false);
206            let elem_x = {
207                let symbol = if is_selected {
208                    highlight_symbol
209                } else if is_sec_selected {
210                    secondary_highlight_symbol
211                } else {
212                    &blank_symbol
213                };
214                let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, style);
215                x
216            };
217
218            let area = Rect {
219                x,
220                y,
221                width: list_area.width,
222                height: 1,
223            };
224
225            let max_element_width = (list_area.width - (elem_x - x)) as usize;
226
227            let mut body = CtrlChars::parse(graph_item).into_text();
228            body.extend(CtrlChars::parse(&format!("  {}", text_item)).into_text());
229
230            let mut x = elem_x;
231            let mut remaining_width = max_element_width as u16;
232            for txt in body {
233                for line in txt.lines {
234                    if remaining_width == 0 {
235                        break;
236                    }
237                    let pos = buf.set_spans(x, y, &line, remaining_width);
238                    let w = pos.0.saturating_sub(x);
239                    x = pos.0;
240                    remaining_width = remaining_width.saturating_sub(w);
241                }
242            }
243
244            if is_selected || is_sec_selected {
245                buf.set_style(area, self.highlight_style);
246            }
247        }
248
249        let scroll_start = list_area.top() as usize
250            + (((list_height * start) as f32 / state.graph_lines.len() as f32).ceil() as usize)
251                .min(list_height - 1);
252        let scroll_height = (((list_height * list_height) as f32 / state.graph_lines.len() as f32)
253            .floor() as usize)
254            .clamp(1, list_height);
255
256        if scroll_height < list_height {
257            for y in scroll_start..(scroll_start + scroll_height) {
258                buf.set_string(
259                    list_area.left() + list_area.width,
260                    y as u16,
261                    SCROLLBAR_STR,
262                    self.style,
263                );
264            }
265        }
266    }
267}
268
269impl Widget for GraphView<'_> {
270    fn render(self, area: Rect, buf: &mut Buffer) {
271        let mut state = GraphViewState::default();
272        StatefulWidget::render(self, area, buf, &mut state);
273    }
274}