git_tailor/views/
hunk_groups.rs1use crate::fragmap::{self, SquashableScope, TouchKind};
21use ratatui::{
22 Frame,
23 layout::Rect,
24 style::{Color, Style},
25 text::{Line, Span},
26 widgets::{Cell, Scrollbar, ScrollbarOrientation, ScrollbarState},
27};
28
29const CLUSTER_TOUCHED_CONFLICTING: &str = "█";
31const CLUSTER_TOUCHED_SQUASHABLE: &str = "█";
32const CLUSTER_CONNECTOR_CONFLICTING: &str = "│";
33const CLUSTER_CONNECTOR_SQUASHABLE: &str = "│";
34
35pub const COLOR_CONFLICTING: Color = Color::Red;
37pub const COLOR_SQUASHABLE: Color = Color::Yellow;
38
39const COLOR_TOUCHED_CONFLICTING: Color = Color::White;
41const COLOR_TOUCHED_SQUASHABLE: Color = Color::DarkGray;
42
43const COLOR_SELECTED_FRAGMAP_BG: Color = Color::Rgb(60, 60, 80);
45
46fn cluster_relation(
50 fragmap: &fragmap::FragMap,
51 commit_idx: usize,
52 cluster_idx: usize,
53) -> Option<fragmap::SquashRelation> {
54 if fragmap.matrix[commit_idx][cluster_idx] == TouchKind::None {
55 return None;
56 }
57 for earlier_idx in 0..commit_idx {
58 if fragmap.matrix[earlier_idx][cluster_idx] != TouchKind::None {
59 return Some(fragmap.cluster_relation(earlier_idx, commit_idx, cluster_idx));
60 }
61 }
62 None
63}
64
65fn fragmap_cell_content(
72 fragmap: &fragmap::FragMap,
73 commit_idx: usize,
74 cluster_idx: usize,
75 scope: SquashableScope,
76) -> Option<(&'static str, Style)> {
77 if fragmap.matrix[commit_idx][cluster_idx] == TouchKind::None {
78 return None;
79 }
80
81 let is_squashable = match scope {
82 SquashableScope::Group => {
83 matches!(
84 cluster_relation(fragmap, commit_idx, cluster_idx),
85 Some(fragmap::SquashRelation::Squashable)
86 )
87 }
88 SquashableScope::Commit => fragmap.is_fully_squashable(commit_idx),
89 };
90
91 if is_squashable {
92 Some((
93 CLUSTER_TOUCHED_SQUASHABLE,
94 Style::new().fg(COLOR_TOUCHED_SQUASHABLE),
95 ))
96 } else {
97 Some((
98 CLUSTER_TOUCHED_CONFLICTING,
99 Style::new().fg(COLOR_TOUCHED_CONFLICTING),
100 ))
101 }
102}
103
104fn fragmap_connector_content(
110 fragmap: &fragmap::FragMap,
111 commit_idx: usize,
112 cluster_idx: usize,
113 scope: SquashableScope,
114) -> Option<(&'static str, Style)> {
115 let has_above = (0..commit_idx)
116 .rev()
117 .any(|i| fragmap.matrix[i][cluster_idx] != TouchKind::None);
118
119 let below = ((commit_idx + 1)..fragmap.commits.len())
120 .find(|&i| fragmap.matrix[i][cluster_idx] != TouchKind::None);
121
122 match (has_above, below) {
123 (true, Some(below_idx)) => {
124 match fragmap.connector_squashable(below_idx, cluster_idx, scope) {
125 Some(true) => Some((
126 CLUSTER_CONNECTOR_SQUASHABLE,
127 Style::new().fg(COLOR_SQUASHABLE),
128 )),
129 Some(false) => Some((
130 CLUSTER_CONNECTOR_CONFLICTING,
131 Style::new().fg(COLOR_CONFLICTING),
132 )),
133 None => None,
134 }
135 }
136 _ => None,
137 }
138}
139
140pub fn commit_text_style(
148 fragmap: &fragmap::FragMap,
149 selection_idx: usize,
150 commit_idx: usize,
151) -> Style {
152 let is_squash_partner = fragmap
153 .squash_target(selection_idx)
154 .is_some_and(|t| t == commit_idx)
155 || fragmap
156 .squash_target(commit_idx)
157 .is_some_and(|t| t == selection_idx);
158
159 if is_squash_partner {
160 Style::new().fg(COLOR_SQUASHABLE)
161 } else if fragmap.shares_cluster_with(selection_idx, commit_idx) {
162 Style::new().fg(COLOR_CONFLICTING)
163 } else if fragmap.is_fully_squashable(commit_idx) {
164 Style::new().fg(COLOR_TOUCHED_SQUASHABLE)
165 } else {
166 Style::default()
167 }
168}
169
170pub fn build_fragmap_cell<'a>(
176 fragmap: &fragmap::FragMap,
177 commit_idx: usize,
178 display_clusters: &[usize],
179 is_selected: bool,
180 scope: SquashableScope,
181) -> Cell<'a> {
182 let spans: Vec<Span> = display_clusters
183 .iter()
184 .map(|&cluster_idx| {
185 let base_style = if is_selected {
186 Style::new().bg(COLOR_SELECTED_FRAGMAP_BG)
187 } else {
188 Style::new()
189 };
190 if let Some((symbol, style)) =
191 fragmap_cell_content(fragmap, commit_idx, cluster_idx, scope)
192 {
193 Span::styled(symbol, base_style.patch(style))
194 } else if let Some((symbol, style)) =
195 fragmap_connector_content(fragmap, commit_idx, cluster_idx, scope)
196 {
197 Span::styled(symbol, base_style.patch(style))
198 } else {
199 Span::styled(" ", base_style)
200 }
201 })
202 .collect();
203 let cell = Cell::from(Line::from(spans));
204 if is_selected {
205 cell.style(Style::new().bg(COLOR_SELECTED_FRAGMAP_BG))
206 } else {
207 cell
208 }
209}
210
211pub fn render_horizontal_scrollbar(
213 frame: &mut Frame,
214 hs_area: Rect,
215 title_width: u16,
216 fragmap_col_width: u16,
217 total_clusters: usize,
218 fragmap_available_width: usize,
219 h_scroll_offset: usize,
220) {
221 let fragmap_x = hs_area.x + 10 + 1 + title_width + 1;
222 let area = Rect {
223 x: fragmap_x,
224 width: fragmap_col_width,
225 ..hs_area
226 };
227
228 let mut state = ScrollbarState::new(total_clusters.saturating_sub(fragmap_available_width))
229 .position(h_scroll_offset);
230
231 let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
232 .begin_symbol(None)
233 .end_symbol(None)
234 .track_symbol(Some("─"));
235
236 frame.render_stateful_widget(scrollbar, area, &mut state);
237}