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