#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignmentMode {
#[default]
Grouped,
Aligned,
Unified,
}
impl AlignmentMode {
pub const fn toggle(&mut self) {
*self = match self {
Self::Grouped => Self::Aligned,
Self::Aligned => Self::Unified,
Self::Unified => Self::Grouped,
};
}
pub const fn name(self) -> &'static str {
match self {
Self::Grouped => "Grouped",
Self::Aligned => "Aligned",
Self::Unified => "Unified",
}
}
pub const fn uses_row_selection(self) -> bool {
matches!(self, Self::Aligned | Self::Unified)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollSyncMode {
#[default]
Independent,
Locked,
}
impl ScrollSyncMode {
pub const fn toggle(&mut self) {
*self = match self {
Self::Independent => Self::Locked,
Self::Locked => Self::Independent,
};
}
pub const fn name(self) -> &'static str {
match self {
Self::Independent => "Independent",
Self::Locked => "Locked",
}
}
}
#[derive(Debug, Clone)]
pub struct ChangeTypeFilter {
pub show_added: bool,
pub show_removed: bool,
pub show_modified: bool,
}
impl Default for ChangeTypeFilter {
fn default() -> Self {
Self {
show_added: true,
show_removed: true,
show_modified: true,
}
}
}
impl ChangeTypeFilter {
pub const fn toggle_added(&mut self) {
self.show_added = !self.show_added;
}
pub const fn toggle_removed(&mut self) {
self.show_removed = !self.show_removed;
}
pub const fn toggle_modified(&mut self) {
self.show_modified = !self.show_modified;
}
pub const fn show_all(&mut self) {
self.show_added = true;
self.show_removed = true;
self.show_modified = true;
}
pub const fn is_filtered(&self) -> bool {
!self.show_added || !self.show_removed || !self.show_modified
}
pub fn summary(&self) -> String {
if !self.is_filtered() {
return "All".to_string();
}
let mut parts = Vec::new();
if self.show_added {
parts.push("+");
}
if self.show_removed {
parts.push("-");
}
if self.show_modified {
parts.push("~");
}
parts.join("")
}
}
pub struct SideBySideState {
pub left_scroll: usize,
pub right_scroll: usize,
pub left_total: usize,
pub right_total: usize,
pub focus_right: bool,
pub alignment_mode: AlignmentMode,
pub sync_mode: ScrollSyncMode,
pub filter: ChangeTypeFilter,
pub selected_row: usize,
pub total_rows: usize,
pub change_indices: Vec<usize>,
pub current_change_idx: Option<usize>,
pub search_query: Option<String>,
pub search_matches: Vec<usize>,
pub current_match_idx: usize,
pub search_active: bool,
pub show_detail_modal: bool,
pub detail_component_left: Option<String>,
pub detail_component_right: Option<String>,
}
impl SideBySideState {
pub fn new() -> Self {
Self {
left_scroll: 0,
right_scroll: 0,
left_total: 0,
right_total: 0,
focus_right: false,
alignment_mode: AlignmentMode::default(),
sync_mode: ScrollSyncMode::default(),
filter: ChangeTypeFilter::default(),
selected_row: 0,
total_rows: 0,
change_indices: Vec::new(),
current_change_idx: None,
search_query: None,
search_matches: Vec::new(),
current_match_idx: 0,
search_active: false,
show_detail_modal: false,
detail_component_left: None,
detail_component_right: None,
}
}
pub fn scroll_up(&mut self) {
match self.sync_mode {
ScrollSyncMode::Independent => {
if self.focus_right {
self.right_scroll = self.right_scroll.saturating_sub(1);
} else {
self.left_scroll = self.left_scroll.saturating_sub(1);
}
}
ScrollSyncMode::Locked => {
self.scroll_both_up();
}
}
if self.alignment_mode.uses_row_selection() {
self.selected_row = self.selected_row.saturating_sub(1);
}
}
pub fn scroll_down(&mut self) {
match self.sync_mode {
ScrollSyncMode::Independent => {
if self.focus_right {
if self.right_total > 0
&& self.right_scroll < self.right_total.saturating_sub(1)
{
self.right_scroll += 1;
}
} else if self.left_total > 0
&& self.left_scroll < self.left_total.saturating_sub(1)
{
self.left_scroll += 1;
}
}
ScrollSyncMode::Locked => {
self.scroll_both_down();
}
}
if self.alignment_mode.uses_row_selection() && self.total_rows > 0 {
self.selected_row = (self.selected_row + 1).min(self.total_rows.saturating_sub(1));
}
}
pub fn page_up(&mut self) {
let page_size = crate::tui::constants::PAGE_SIZE;
match self.sync_mode {
ScrollSyncMode::Independent => {
if self.focus_right {
self.right_scroll = self.right_scroll.saturating_sub(page_size);
} else {
self.left_scroll = self.left_scroll.saturating_sub(page_size);
}
}
ScrollSyncMode::Locked => {
self.left_scroll = self.left_scroll.saturating_sub(page_size);
self.right_scroll = self.right_scroll.saturating_sub(page_size);
}
}
if self.alignment_mode.uses_row_selection() {
self.selected_row = self.selected_row.saturating_sub(page_size);
}
}
pub fn page_down(&mut self) {
let page_size = crate::tui::constants::PAGE_SIZE;
match self.sync_mode {
ScrollSyncMode::Independent => {
if self.focus_right {
self.right_scroll =
(self.right_scroll + page_size).min(self.right_total.saturating_sub(1));
} else {
self.left_scroll =
(self.left_scroll + page_size).min(self.left_total.saturating_sub(1));
}
}
ScrollSyncMode::Locked => {
self.left_scroll =
(self.left_scroll + page_size).min(self.left_total.saturating_sub(1));
self.right_scroll =
(self.right_scroll + page_size).min(self.right_total.saturating_sub(1));
}
}
if self.alignment_mode.uses_row_selection() && self.total_rows > 0 {
self.selected_row =
(self.selected_row + page_size).min(self.total_rows.saturating_sub(1));
}
}
pub const fn toggle_focus(&mut self) {
self.focus_right = !self.focus_right;
}
pub const fn toggle_alignment(&mut self) {
self.alignment_mode.toggle();
}
pub const fn toggle_sync(&mut self) {
self.sync_mode.toggle();
}
pub const fn scroll_both_up(&mut self) {
self.left_scroll = self.left_scroll.saturating_sub(1);
self.right_scroll = self.right_scroll.saturating_sub(1);
}
pub const fn scroll_both_down(&mut self) {
if self.left_total > 0 && self.left_scroll < self.left_total.saturating_sub(1) {
self.left_scroll += 1;
}
if self.right_total > 0 && self.right_scroll < self.right_total.saturating_sub(1) {
self.right_scroll += 1;
}
}
pub const fn set_totals(&mut self, left: usize, right: usize) {
self.left_total = left;
self.right_total = right;
}
pub fn go_to_top(&mut self) {
if self.focus_right {
self.right_scroll = 0;
} else {
self.left_scroll = 0;
}
if self.alignment_mode.uses_row_selection() {
self.selected_row = 0;
}
}
pub fn go_to_bottom(&mut self) {
if self.focus_right {
self.right_scroll = self.right_total.saturating_sub(1);
} else {
self.left_scroll = self.left_total.saturating_sub(1);
}
if self.alignment_mode.uses_row_selection() && self.total_rows > 0 {
self.selected_row = self.total_rows - 1;
}
}
pub fn next_change(&mut self) {
if self.change_indices.is_empty() {
return;
}
let next_idx = match self.current_change_idx {
Some(idx) => {
if idx + 1 < self.change_indices.len() {
idx + 1
} else {
0 }
}
None => 0,
};
self.current_change_idx = Some(next_idx);
self.scroll_to_row(self.change_indices[next_idx]);
}
pub fn prev_change(&mut self) {
if self.change_indices.is_empty() {
return;
}
let prev_idx = match self.current_change_idx {
Some(idx) => {
if idx > 0 {
idx - 1
} else {
self.change_indices.len() - 1 }
}
None => self.change_indices.len() - 1,
};
self.current_change_idx = Some(prev_idx);
self.scroll_to_row(self.change_indices[prev_idx]);
}
pub const fn scroll_to_row(&mut self, row: usize) {
self.selected_row = row;
let visible_rows = 20;
if row < self.left_scroll {
self.left_scroll = row;
self.right_scroll = row;
} else if row >= self.left_scroll + visible_rows {
self.left_scroll = row.saturating_sub(visible_rows / 2);
self.right_scroll = row.saturating_sub(visible_rows / 2);
}
}
pub fn start_search(&mut self) {
self.search_active = true;
self.search_query = Some(String::new());
self.search_matches.clear();
self.current_match_idx = 0;
}
pub fn cancel_search(&mut self) {
self.search_active = false;
self.search_query = None;
self.search_matches.clear();
}
pub const fn confirm_search(&mut self) {
self.search_active = false;
}
pub fn search_push(&mut self, c: char) {
if let Some(ref mut query) = self.search_query {
query.push(c);
}
}
pub fn search_pop(&mut self) {
if let Some(ref mut query) = self.search_query {
query.pop();
}
}
pub fn update_search_matches(&mut self, matches: Vec<usize>) {
self.search_matches = matches;
self.current_match_idx = 0;
if !self.search_matches.is_empty() {
self.scroll_to_row(self.search_matches[0]);
}
}
pub fn next_match(&mut self) {
if self.search_matches.is_empty() {
return;
}
self.current_match_idx = (self.current_match_idx + 1) % self.search_matches.len();
self.scroll_to_row(self.search_matches[self.current_match_idx]);
}
pub fn prev_match(&mut self) {
if self.search_matches.is_empty() {
return;
}
if self.current_match_idx > 0 {
self.current_match_idx -= 1;
} else {
self.current_match_idx = self.search_matches.len() - 1;
}
self.scroll_to_row(self.search_matches[self.current_match_idx]);
}
pub const fn toggle_detail_modal(&mut self) {
self.show_detail_modal = !self.show_detail_modal;
}
pub fn close_detail_modal(&mut self) {
self.show_detail_modal = false;
self.detail_component_left = None;
self.detail_component_right = None;
}
pub fn change_position(&self) -> String {
if self.change_indices.is_empty() {
return "0/0".to_string();
}
self.current_change_idx.map_or_else(
|| format!("-/{}", self.change_indices.len()),
|idx| format!("{}/{}", idx + 1, self.change_indices.len()),
)
}
pub fn match_position(&self) -> String {
if self.search_matches.is_empty() {
return "0/0".to_string();
}
format!(
"{}/{}",
self.current_match_idx + 1,
self.search_matches.len()
)
}
}
impl Default for SideBySideState {
fn default() -> Self {
Self::new()
}
}