git_igitt/widgets/
graph_view.rs1use 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}