use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use super::{qc, set_cell};
use crate::event::Rect as ERect;
use crate::primitives::diff_view::{
row_colors, unified_hunk_header, unified_row_style, unified_row_text, DiffLineContent,
DiffMode, DiffView, DiffViewGeometry, DiffViewLayout,
};
use crate::theme::Theme;
fn cell_rect(r: ERect) -> Rect {
Rect::new(r.x as u16, r.y as u16, r.width as u16, r.height as u16)
}
pub fn draw_diff_view(
buf: &mut Buffer,
area: Rect,
view: &DiffView,
theme: &Theme,
) -> DiffViewLayout {
if area.width == 0 || area.height == 0 {
return DiffViewLayout {
visible_rows: 0,
total_rows: view.total_rows(),
};
}
let viewport = ERect::new(
area.x as f32,
area.y as f32,
area.width as f32,
area.height as f32,
);
let geometry = view.layout(viewport, 1.0);
match view.mode {
DiffMode::SideBySide => draw_side_by_side(buf, area, view, theme, &geometry),
DiffMode::Unified => draw_unified(buf, area, view, theme, &geometry),
}
geometry.as_layout()
}
fn draw_side_by_side(
buf: &mut Buffer,
area: Rect,
view: &DiffView,
theme: &Theme,
geometry: &DiffViewGeometry,
) {
let flat = view.flat_rows();
if let Some(header) = &geometry.header {
let hdr_bg = qc(theme.header_bg);
let hdr_fg = qc(theme.header_fg);
let left_r = cell_rect(header.left);
let right_r = cell_rect(header.right);
let divider_r = cell_rect(header.divider);
for col in 0..left_r.width {
set_cell(buf, left_r.x + col, left_r.y, ' ', hdr_fg, hdr_bg);
}
for col in 0..right_r.width {
set_cell(buf, right_r.x + col, right_r.y, ' ', hdr_fg, hdr_bg);
}
set_cell(
buf,
divider_r.x,
divider_r.y,
'│',
qc(theme.border_fg),
hdr_bg,
);
if let Some(label) = &view.left_label {
draw_text_in(buf, left_r.x, left_r.y, left_r.width, label, hdr_fg, hdr_bg);
}
if let Some(label) = &view.right_label {
draw_text_in(
buf,
right_r.x,
right_r.y,
right_r.width,
label,
hdr_fg,
hdr_bg,
);
}
}
for line in &geometry.lines {
let DiffLineContent::Row { row_idx } = line.content else {
continue;
};
let row = flat[row_idx];
let (left_fg, left_bg, right_fg, right_bg) = row_colors(row.kind, theme);
let (left_fg, left_bg, right_fg, right_bg) =
(qc(left_fg), qc(left_bg), qc(right_fg), qc(right_bg));
let left_r = cell_rect(line.left.expect("side-by-side row has left bounds"));
let right_r = cell_rect(line.right.expect("side-by-side row has right bounds"));
let divider_r = cell_rect(line.divider.expect("side-by-side row has divider bounds"));
for col in 0..left_r.width {
set_cell(buf, left_r.x + col, left_r.y, ' ', left_fg, left_bg);
}
for col in 0..right_r.width {
set_cell(buf, right_r.x + col, right_r.y, ' ', right_fg, right_bg);
}
set_cell(
buf,
divider_r.x,
divider_r.y,
'│',
qc(theme.border_fg),
left_bg,
);
if let Some(text) = &row.left {
draw_text_in(
buf,
left_r.x,
left_r.y,
left_r.width,
text,
left_fg,
left_bg,
);
}
if let Some(text) = &row.right {
draw_text_in(
buf,
right_r.x,
right_r.y,
right_r.width,
text,
right_fg,
right_bg,
);
}
}
if let Some(panes) = &geometry.panes {
let header_rows: u16 = if geometry.header.is_some() { 1 } else { 0 };
let content_area_h = area.height.saturating_sub(header_rows);
let divider_x = panes.divider_x as u16;
let left_w = panes.left_w as u16;
let right_w = panes.right_w as u16;
let rows_drawn = geometry.lines.len() as u16;
for blank_row in rows_drawn..content_area_h {
let screen_y = area.y + header_rows + blank_row;
let bg = qc(theme.background);
let fg = qc(theme.muted_fg);
for col in 0..left_w {
set_cell(buf, area.x + col, screen_y, ' ', fg, bg);
}
set_cell(buf, divider_x, screen_y, '│', qc(theme.border_fg), bg);
for col in 0..right_w {
set_cell(buf, divider_x + 1 + col, screen_y, ' ', fg, bg);
}
}
}
}
fn draw_unified(
buf: &mut Buffer,
area: Rect,
view: &DiffView,
theme: &Theme,
geometry: &DiffViewGeometry,
) {
let flat = view.flat_rows();
for line in &geometry.lines {
let screen = cell_rect(line.bounds);
match line.content {
DiffLineContent::UnifiedHeader { hunk_idx } => {
let header_text = unified_hunk_header(&view.hunks[hunk_idx]);
let bg = qc(theme.background);
let fg = qc(theme.accent_fg);
for col in 0..screen.width {
set_cell(buf, screen.x + col, screen.y, ' ', fg, bg);
}
draw_text_in(buf, screen.x, screen.y, screen.width, &header_text, fg, bg);
}
DiffLineContent::Row { row_idx } => {
let row = flat[row_idx];
let (prefix, fg, bg) = unified_row_style(row.kind, theme);
let (fg, bg) = (qc(fg), qc(bg));
for col in 0..screen.width {
set_cell(buf, screen.x + col, screen.y, ' ', fg, bg);
}
if screen.width > 0 {
set_cell(buf, screen.x, screen.y, prefix, fg, bg);
}
let text = unified_row_text(row);
if screen.width > 1 {
draw_text_in(
buf,
screen.x + 1,
screen.y,
screen.width.saturating_sub(1),
text,
fg,
bg,
);
}
}
}
}
let rows_drawn = geometry.lines.len() as u16;
for blank in rows_drawn..area.height {
let screen_y = area.y + blank;
let bg = qc(theme.background);
let fg = qc(theme.muted_fg);
for col in 0..area.width {
set_cell(buf, area.x + col, screen_y, ' ', fg, bg);
}
}
}
fn draw_text_in(
buf: &mut Buffer,
x: u16,
y: u16,
max_width: u16,
text: &str,
fg: ratatui::style::Color,
bg: ratatui::style::Color,
) {
for (col, ch) in (0_u16..).zip(text.chars()) {
if col >= max_width {
break;
}
set_cell(buf, x + col, y, ch, fg, bg);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::diff_view::{DiffEditability, DiffHunk, DiffPane, DiffRow, DiffRowKind};
use crate::types::WidgetId;
fn cell_char(buf: &Buffer, x: u16, y: u16) -> char {
buf[(x, y)].symbol().chars().next().unwrap_or(' ')
}
fn cell_row_str(buf: &Buffer, y: u16, width: u16) -> String {
(0..width).map(|x| cell_char(buf, x, y)).collect()
}
fn make_view(mode: DiffMode) -> DiffView {
DiffView {
id: WidgetId::new("test"),
left: String::new(),
right: String::new(),
left_label: None,
right_label: None,
hunks: vec![
DiffHunk {
left_start: 1,
right_start: 1,
rows: vec![
DiffRow {
left: Some("alpha".into()),
right: Some("ALPHA".into()),
kind: DiffRowKind::Changed,
},
DiffRow {
left: Some("beta".into()),
right: Some("beta".into()),
kind: DiffRowKind::Same,
},
],
},
DiffHunk {
left_start: 10,
right_start: 10,
rows: vec![
DiffRow {
left: Some("gamma".into()),
right: None,
kind: DiffRowKind::Removed,
},
DiffRow {
left: None,
right: Some("delta".into()),
kind: DiffRowKind::Added,
},
],
},
],
mode,
editability: DiffEditability::ReadOnly,
scroll_offset: 0,
focused_pane: DiffPane::Left,
has_focus: false,
}
}
#[test]
fn zero_size_area_side_by_side_returns_empty_layout_without_panic() {
let buf_area = Rect::new(0, 0, 40, 10);
let mut buf = Buffer::empty(buf_area);
let view = make_view(DiffMode::SideBySide);
let layout = draw_diff_view(&mut buf, Rect::new(0, 0, 0, 0), &view, &Theme::default());
assert_eq!(layout.visible_rows, 0);
assert_eq!(layout.total_rows, view.total_rows());
}
#[test]
fn zero_size_area_unified_returns_empty_layout_without_panic() {
let buf_area = Rect::new(0, 0, 40, 10);
let mut buf = Buffer::empty(buf_area);
let view = make_view(DiffMode::Unified);
let layout = draw_diff_view(&mut buf, Rect::new(0, 0, 0, 0), &view, &Theme::default());
assert_eq!(layout.visible_rows, 0);
assert_eq!(layout.total_rows, view.total_rows());
}
#[test]
fn scroll_offset_zero_paints_first_row_at_top() {
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
let view = make_view(DiffMode::SideBySide);
draw_diff_view(&mut buf, area, &view, &Theme::default());
let row0 = cell_row_str(&buf, 0, area.width);
assert!(
row0.contains("alpha"),
"expected 'alpha' on row 0, got: {row0:?}"
);
}
#[test]
fn scroll_offset_one_paints_second_row_at_top() {
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
let mut view = make_view(DiffMode::SideBySide);
view.scroll_offset = 1;
draw_diff_view(&mut buf, area, &view, &Theme::default());
let row0 = cell_row_str(&buf, 0, area.width);
assert!(
row0.contains("beta"),
"expected 'beta' on row 0 at offset=1, got: {row0:?}"
);
assert!(
!row0.contains("alpha"),
"unexpected 'alpha' on row 0 at offset=1: {row0:?}"
);
}
#[test]
fn unified_hunk_header_appears_before_first_row() {
let area = Rect::new(0, 0, 30, 6);
let mut buf = Buffer::empty(area);
let view = make_view(DiffMode::Unified);
draw_diff_view(&mut buf, area, &view, &Theme::default());
let row0 = cell_row_str(&buf, 0, area.width);
assert!(
row0.contains("@@"),
"expected '@@ header' on row 0 in unified mode, got: {row0:?}"
);
}
#[test]
fn unified_scroll_reaches_last_row() {
let area = Rect::new(0, 0, 30, 2);
let mut buf = Buffer::empty(area);
let view = make_view(DiffMode::Unified);
let layout = draw_diff_view(&mut buf, area, &view, &Theme::default());
assert_eq!(layout.total_rows, 6, "2 hunks × 2 rows + 2 headers = 6");
assert_eq!(layout.visible_rows, 2);
let max_offset = layout.total_rows.saturating_sub(layout.visible_rows);
let mut buf2 = Buffer::empty(area);
let mut view2 = make_view(DiffMode::Unified);
view2.scroll_offset = max_offset;
draw_diff_view(&mut buf2, area, &view2, &Theme::default());
let row1 = cell_row_str(&buf2, 1, area.width);
assert!(
row1.contains("delta"),
"expected 'delta' on screen row 1 at max_offset={max_offset}, got: {row1:?}"
);
}
#[test]
fn unified_header_counts_exclude_padding() {
let view = DiffView {
id: WidgetId::new("hdr"),
left: String::new(),
right: String::new(),
left_label: None,
right_label: None,
hunks: vec![crate::primitives::diff_view::DiffHunk {
left_start: 5,
right_start: 7,
rows: vec![
DiffRow {
left: Some("old1".into()),
right: Some("new1".into()),
kind: DiffRowKind::Changed,
},
DiffRow {
left: Some("old2".into()),
right: None,
kind: DiffRowKind::Removed,
},
DiffRow {
left: None,
right: Some("new3".into()),
kind: DiffRowKind::Added,
},
],
}],
mode: DiffMode::Unified,
editability: DiffEditability::ReadOnly,
scroll_offset: 0,
focused_pane: DiffPane::Left,
has_focus: false,
};
let area = Rect::new(0, 0, 40, 4);
let mut buf = Buffer::empty(area);
draw_diff_view(&mut buf, area, &view, &Theme::default());
let row0 = cell_row_str(&buf, 0, area.width);
assert!(
row0.contains("@@ -5,2 +7,2 @@"),
"expected header '@@ -5,2 +7,2 @@', got: {row0:?}"
);
assert!(
!row0.contains("@@ -5,3 +7,3 @@"),
"header still reports raw row count: {row0:?}"
);
}
}