use crate::tui::app_states::SourceSide;
use crate::tui::app_states::source::SourceDiffState;
use crate::tui::shared::source::{render_source_panel, render_str};
use crate::tui::theme::colors;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Scrollbar, ScrollbarOrientation, ScrollbarState};
use std::fmt::Write;
pub fn render_source(frame: &mut Frame, area: Rect, source: &mut SourceDiffState) {
let show_detail = source.show_detail;
let main_area = if show_detail {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(38),
Constraint::Percentage(38),
Constraint::Percentage(24),
])
.split(area);
render_detail_panel(frame, chunks[2], source);
(chunks[0], chunks[1])
} else {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
(chunks[0], chunks[1])
};
let active = source.active_side;
let sync_label = if source.is_synced() { " [sync]" } else { "" };
let (old_added, old_removed, old_modified) =
SourceDiffState::annotation_counts(&source.old_panel);
let (new_added, new_removed, new_modified) =
SourceDiffState::annotation_counts(&source.new_panel);
let old_badge = format_change_badge(old_added, old_removed, old_modified);
let new_badge = format_change_badge(new_added, new_removed, new_modified);
let old_title = format!("Old SBOM{sync_label}{old_badge}");
let new_title = format!("New SBOM{sync_label}{new_badge}");
if !source.old_panel.flat_cache_valid || !source.new_panel.flat_cache_valid {
source.alignment_applied = false;
}
source
.old_panel
.prepare_source_render(main_area.0.height.saturating_sub(2) as usize);
source
.new_panel
.prepare_source_render(main_area.1.height.saturating_sub(2) as usize);
source.align_component_panels();
render_source_panel(
frame,
main_area.0,
&mut source.old_panel,
&old_title,
active == SourceSide::Old,
);
render_source_panel(
frame,
main_area.1,
&mut source.new_panel,
&new_title,
active == SourceSide::New,
);
}
fn render_detail_panel(frame: &mut Frame, area: Rect, source: &mut SourceDiffState) {
let scheme = colors();
let detail_text = source.get_selected_detail().unwrap_or_default();
let lines: Vec<&str> = detail_text.lines().collect();
let total_lines = lines.len();
let block = Block::default()
.title(" Detail ")
.title_style(Style::default().fg(scheme.accent).bold())
.borders(Borders::ALL)
.border_style(Style::default().fg(scheme.accent));
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.width < 2 || inner.height < 1 {
return;
}
let visible_height = inner.height as usize;
if source.detail_scroll > total_lines.saturating_sub(visible_height) {
source.detail_scroll = total_lines.saturating_sub(visible_height);
}
for (i, line) in lines
.iter()
.skip(source.detail_scroll)
.take(visible_height)
.enumerate()
{
let y = inner.y + i as u16;
render_str(
frame.buffer_mut(),
inner.x,
y,
line,
inner.width,
Style::default().fg(scheme.text),
);
}
if total_lines > visible_height {
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(scheme.accent))
.track_style(Style::default().fg(scheme.muted));
let mut sb_state = ScrollbarState::new(total_lines).position(source.detail_scroll);
frame.render_stateful_widget(scrollbar, inner, &mut sb_state);
}
}
fn format_change_badge(added: usize, removed: usize, modified: usize) -> String {
if added == 0 && removed == 0 && modified == 0 {
return String::new();
}
let mut badge = String::new();
if added > 0 {
let _ = write!(badge, " +{added}");
}
if removed > 0 {
let _ = write!(badge, " -{removed}");
}
if modified > 0 {
let _ = write!(badge, " ~{modified}");
}
badge
}