use crate::base::{Rect, Size};
use super::cell::Cell;
use super::surface::Surface;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Run {
pub y: i32,
pub x: i32,
pub len: i32,
}
impl Run {
pub const fn end(&self) -> i32 {
self.x + self.len
}
}
#[derive(Default)]
pub struct FrameDiff {
pub(super) spans: Vec<(i32, i32, i32)>,
pub(super) runs: Vec<Run>,
pub(super) fp_prev: Vec<u64>,
pub(super) fp_next: Vec<u64>,
}
impl FrameDiff {
pub fn new() -> FrameDiff {
FrameDiff::default()
}
pub fn compute_full<'a>(&'a mut self, prev: &Surface, next: &Surface) -> &'a [Run] {
let all = [Rect::from_size(next.size())];
self.compute(prev, next, &all)
}
pub fn compute<'a>(&'a mut self, prev: &Surface, next: &Surface, damage: &[Rect]) -> &'a [Run] {
self.runs.clear();
self.spans.clear();
let size = next.size();
if size.is_empty() {
return &self.runs;
}
if prev.size() != size {
self.emit_all(next);
return &self.runs;
}
self.collect_spans(damage, size);
self.spans.sort_unstable();
let mut i = 0;
while i < self.spans.len() {
let (y, x0, mut x1) = self.spans[i];
i += 1;
while i < self.spans.len() {
let (ny, nx0, nx1) = self.spans[i];
if ny != y || nx0 > x1 {
break;
}
x1 = x1.max(nx1);
i += 1;
}
self.scan_interval(prev, next, y, x0, x1);
}
&self.runs
}
fn emit_all(&mut self, next: &Surface) {
let size = next.size();
for y in 0..size.h {
self.runs.push(Run {
y,
x: 0,
len: size.w,
});
}
}
pub(super) fn collect_spans(&mut self, damage: &[Rect], size: Size) {
let bounds = Rect::from_size(size);
for rect in damage {
let r = rect.intersect(bounds);
if r.is_empty() {
continue;
}
let x0 = (r.x - 1).max(0);
let x1 = (r.right() + 1).min(size.w);
for y in r.y..r.bottom() {
self.spans.push((y, x0, x1));
}
}
}
fn scan_interval(&mut self, prev: &Surface, next: &Surface, y: i32, x0: i32, x1: i32) {
let prev_row = prev.row(y);
let next_row = next.row(y);
let mut run_start: Option<i32> = None;
for x in x0..x1 {
let a = &prev_row[x as usize];
let b = &next_row[x as usize];
let equal = cells_equal(prev, next, a, b);
if equal {
if let Some(start) = run_start.take() {
self.push_run(y, start, x);
}
} else if run_start.is_none() {
let start = if b.is_continuation() {
(x - 1).max(0)
} else {
x
};
run_start = Some(start);
}
}
if let Some(start) = run_start {
self.push_run(y, start, x1);
}
}
pub(super) fn push_run(&mut self, y: i32, x0: i32, x1: i32) {
if x1 <= x0 {
return;
}
if let Some(last) = self.runs.last_mut() {
if last.y == y && x0 <= last.end() {
let end = last.end().max(x1);
last.len = end - last.x;
return;
}
}
self.runs.push(Run {
y,
x: x0,
len: x1 - x0,
});
}
}
pub(super) fn cells_equal(prev: &Surface, next: &Surface, a: &Cell, b: &Cell) -> bool {
if a.attrs != b.attrs || a.fg != b.fg || a.bg != b.bg || a.ul != b.ul {
return false;
}
if !a.glyph.content_eq(&b.glyph, prev.pool(), next.pool()) {
return false;
}
if a.link == 0 && b.link == 0 {
return true;
}
prev.link_uri(a.link) == next.link_uri(b.link)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::{Rgba, Size};
use crate::render::style::Style;
fn surf(w: i32, h: i32) -> Surface {
Surface::new(Size::new(w, h), Cell::EMPTY)
}
fn full(s: &Surface) -> Vec<Rect> {
vec![s.bounds()]
}
#[test]
fn identical_surfaces_no_runs() {
let a = surf(10, 3);
let b = surf(10, 3);
let mut diff = FrameDiff::new();
assert!(diff.compute(&a, &b, &full(&a)).is_empty());
}
#[test]
fn single_cell_change_single_tiny_run() {
let a = surf(20, 5);
let mut b = surf(20, 5);
b.draw_text(7, 2, "x", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &[Rect::new(7, 2, 1, 1)]).to_vec();
assert_eq!(runs, vec![Run { y: 2, x: 7, len: 1 }]);
}
#[test]
fn damage_bounds_the_scan() {
let a = surf(20, 5);
let mut b = surf(20, 5);
b.draw_text(0, 0, "changed", Style::new());
b.draw_text(0, 4, "also changed", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &[Rect::new(0, 0, 20, 1)]).to_vec();
assert!(runs.iter().all(|r| r.y == 0));
assert_eq!(runs.len(), 1);
}
#[test]
fn empty_damage_means_no_changes() {
let a = surf(10, 2);
let mut b = surf(10, 2);
b.draw_text(0, 0, "hidden", Style::new());
let mut diff = FrameDiff::new();
assert!(diff.compute(&a, &b, &[]).is_empty());
}
#[test]
fn equal_interior_splits_runs() {
let mut a = surf(10, 1);
a.draw_text(0, 0, "aaaaaaaaaa", Style::new());
let mut b = surf(10, 1);
b.draw_text(0, 0, "bbbaaabbbb", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &full(&a)).to_vec();
assert_eq!(
runs,
vec![Run { y: 0, x: 0, len: 3 }, Run { y: 0, x: 6, len: 4 }]
);
}
#[test]
fn wide_to_narrow_re_emits_both_columns() {
let mut a = surf(6, 1);
a.draw_text(0, 0, "世", Style::new());
let mut b = surf(6, 1);
b.draw_text(0, 0, "ab", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &[Rect::new(0, 0, 2, 1)]).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 2 }]);
}
#[test]
fn narrow_to_wide_covers_continuation() {
let mut a = surf(6, 1);
a.draw_text(0, 0, "ab", Style::new());
let mut b = surf(6, 1);
b.draw_text(0, 0, "世", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &full(&a)).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 2 }]);
}
#[test]
fn wide_to_wide_same_style_emits_leader_only() {
let mut a = surf(6, 1);
a.draw_text(0, 0, "世", Style::new());
let mut b = surf(6, 1);
b.draw_text(0, 0, "界", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &full(&a)).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 1 }]);
}
#[test]
fn damage_slicing_a_pair_still_reaches_the_leader() {
let mut a = surf(6, 1);
a.draw_text(0, 0, "ab", Style::new());
let mut b = surf(6, 1);
b.draw_text(0, 0, "世", Style::new());
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &[Rect::new(1, 0, 1, 1)]).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 2 }]);
}
#[test]
fn style_only_change_is_detected() {
let mut a = surf(4, 1);
a.draw_text(0, 0, "hi", Style::new());
let mut b = surf(4, 1);
b.draw_text(0, 0, "hi", Style::new().fg(Rgba::rgb(255, 0, 0)));
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &full(&a)).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 2 }]);
}
#[test]
fn pooled_glyphs_compare_by_content_across_pools() {
let family = "👨\u{200D}👩\u{200D}👧\u{200D}👦";
let mut a = surf(6, 1);
a.draw_text(0, 0, "👩\u{200D}🚀", Style::new());
a.draw_text(0, 0, family, Style::new());
let mut b = surf(6, 1);
b.draw_text(0, 0, family, Style::new());
let mut diff = FrameDiff::new();
assert!(
diff.compute(&a, &b, &full(&a)).is_empty(),
"same cluster in different pools must compare equal"
);
}
#[test]
fn link_identity_is_by_uri_not_id() {
let mut a = surf(4, 1);
let la = a.register_link("https://a.example");
a.draw_text(0, 0, "x", Style::new().link(la));
let mut b = surf(4, 1);
b.register_link("https://padding.example"); let lb = b.register_link("https://a.example");
b.draw_text(0, 0, "x", Style::new().link(lb));
let mut diff = FrameDiff::new();
assert!(diff.compute(&a, &b, &full(&a)).is_empty());
let mut c = surf(4, 1);
let lc = c.register_link("https://other.example");
c.draw_text(0, 0, "x", Style::new().link(lc));
assert_eq!(diff.compute(&a, &c, &full(&a)).len(), 1);
}
#[test]
fn size_mismatch_emits_everything() {
let a = surf(4, 2);
let b = surf(6, 3);
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &[]).to_vec();
assert_eq!(runs.len(), 3);
assert!(runs.iter().all(|r| r.len == 6));
}
#[test]
fn overlapping_damage_rects_do_not_duplicate_runs() {
let a = surf(10, 1);
let mut b = surf(10, 1);
b.draw_text(2, 0, "xxx", Style::new());
let damage = vec![Rect::new(1, 0, 4, 1), Rect::new(3, 0, 4, 1)];
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &damage).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 2, len: 3 }]);
}
#[test]
fn steady_state_reuses_scratch() {
let a = surf(80, 24);
let mut b = surf(80, 24);
b.draw_text(0, 0, "warmup", Style::new());
let mut diff = FrameDiff::new();
diff.compute(&a, &b, &full(&a));
let cap_runs = diff.runs.capacity();
let cap_spans = diff.spans.capacity();
for _ in 0..10 {
diff.compute(&a, &b, &full(&a));
}
assert_eq!(diff.runs.capacity(), cap_runs);
assert_eq!(diff.spans.capacity(), cap_spans);
}
#[test]
fn no_change_frame_is_scratch_only_and_byteless() {
use crate::render::present::{PresentCaps, Presenter};
let mut frame = surf(80, 24);
frame.draw_text(0, 0, "steady content", Style::new().fg(Rgba::rgb(9, 9, 9)));
let mut diff = FrameDiff::new();
let mut presenter = Presenter::new();
let mut out = Vec::new();
let runs = diff.compute_full(&frame, &frame).to_vec();
presenter.emit(&runs, &frame, &PresentCaps::FULL, &mut out);
out.clear();
let (cap_spans, cap_runs) = (diff.spans.capacity(), diff.runs.capacity());
for _ in 0..5 {
let runs = diff.compute_full(&frame, &frame);
assert!(runs.is_empty(), "identical frames produce no runs");
let runs = runs.to_vec();
presenter.emit(&runs, &frame, &PresentCaps::FULL, &mut out);
assert!(out.is_empty(), "identical frames emit zero bytes");
}
assert_eq!(diff.spans.capacity(), cap_spans, "span scratch reused");
assert_eq!(diff.runs.capacity(), cap_runs, "run scratch reused");
}
#[test]
fn moved_layer_style_change_via_point_damage() {
let a = surf(10, 1);
let mut b = surf(10, 1);
b.draw_text(0, 0, "abcd", Style::new());
let damage: Vec<Rect> = (0..4).map(|x| Rect::new(x, 0, 1, 1)).collect();
let mut diff = FrameDiff::new();
let runs = diff.compute(&a, &b, &damage).to_vec();
assert_eq!(runs, vec![Run { y: 0, x: 0, len: 4 }]);
}
}