use crate::error::DotmaxError;
use crate::grid::BrailleGrid;
use crate::render::TerminalRenderer;
use crossterm::{cursor::MoveTo, QueueableCommand};
use std::io::Write;
use tracing::debug;
#[derive(Debug)]
pub struct DifferentialRenderer {
last_frame: Option<BrailleGrid>,
}
impl DifferentialRenderer {
#[must_use]
pub const fn new() -> Self {
Self { last_frame: None }
}
pub fn render_diff(
&mut self,
current: &BrailleGrid,
renderer: &mut TerminalRenderer,
) -> Result<(), DotmaxError> {
let should_full_render = self
.last_frame
.as_ref()
.map_or(true, |last| {
if last.width() != current.width() || last.height() != current.height() {
debug!(
old_width = last.width(),
old_height = last.height(),
new_width = current.width(),
new_height = current.height(),
"Dimension mismatch - performing full frame render"
);
true
} else {
false
}
});
let last = match (should_full_render, &self.last_frame) {
(true, _) | (_, None) => {
debug!("First render or dimension change - performing full frame render");
renderer.render(current)?;
self.last_frame = Some(current.clone());
return Ok(());
}
(false, Some(last)) => last,
};
let mut stdout = std::io::stdout();
let mut changed_count = 0;
for y in 0..current.height() {
for x in 0..current.width() {
if Self::cells_differ(current, last, x, y) {
#[allow(clippy::cast_possible_truncation)]
stdout.queue(MoveTo(x as u16, y as u16))?;
let ch = current.get_char(x, y);
if let Some(color) = current.get_color(x, y) {
write!(
stdout,
"\x1b[38;2;{};{};{}m{}\x1b[0m",
color.r, color.g, color.b, ch
)?;
} else {
write!(stdout, "{ch}")?;
}
changed_count += 1;
}
}
}
stdout.flush()?;
debug!(changed_cells = changed_count, "Differential render complete");
self.last_frame = Some(current.clone());
Ok(())
}
pub fn invalidate(&mut self) {
debug!("Invalidating differential renderer - next render will be full");
self.last_frame = None;
}
fn cells_differ(current: &BrailleGrid, last: &BrailleGrid, x: usize, y: usize) -> bool {
let current_patterns = current.get_raw_patterns();
let last_patterns = last.get_raw_patterns();
let index = y * current.width() + x;
if current_patterns[index] != last_patterns[index] {
return true;
}
if current.get_color(x, y) != last.get_color(x, y) {
return true;
}
false
}
#[must_use]
pub fn count_changed_cells(&self, current: &BrailleGrid, previous: &BrailleGrid) -> usize {
if current.width() != previous.width() || current.height() != previous.height() {
return current.width() * current.height();
}
let mut count = 0;
for y in 0..current.height() {
for x in 0..current.width() {
if Self::cells_differ(current, previous, x, y) {
count += 1;
}
}
}
count
}
#[must_use]
pub const fn has_previous_frame(&self) -> bool {
self.last_frame.is_some()
}
}
impl Default for DifferentialRenderer {
fn default() -> Self {
Self::new()
}
}
impl Clone for DifferentialRenderer {
fn clone(&self) -> Self {
Self {
last_frame: self.last_frame.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_creates_renderer_with_no_last_frame() {
let renderer = DifferentialRenderer::new();
assert!(!renderer.has_previous_frame());
}
#[test]
fn test_default_same_as_new() {
let renderer1 = DifferentialRenderer::new();
let renderer2 = DifferentialRenderer::default();
assert!(!renderer1.has_previous_frame());
assert!(!renderer2.has_previous_frame());
}
#[test]
fn test_invalidate_clears_last_frame() {
let mut renderer = DifferentialRenderer::new();
renderer.last_frame = Some(BrailleGrid::new(10, 10).unwrap());
assert!(renderer.has_previous_frame());
renderer.invalidate();
assert!(!renderer.has_previous_frame());
}
#[test]
fn test_count_changed_cells_identical_frames() {
let renderer = DifferentialRenderer::new();
let frame1 = BrailleGrid::new(10, 10).unwrap();
let frame2 = BrailleGrid::new(10, 10).unwrap();
let changed = renderer.count_changed_cells(&frame1, &frame2);
assert_eq!(changed, 0);
}
#[test]
fn test_count_changed_cells_single_change() {
let renderer = DifferentialRenderer::new();
let frame1 = BrailleGrid::new(10, 10).unwrap();
let mut frame2 = BrailleGrid::new(10, 10).unwrap();
frame2.set_dot(0, 0).unwrap();
let changed = renderer.count_changed_cells(&frame2, &frame1);
assert_eq!(changed, 1);
}
#[test]
fn test_count_changed_cells_dimension_mismatch() {
let renderer = DifferentialRenderer::new();
let frame1 = BrailleGrid::new(10, 10).unwrap();
let frame2 = BrailleGrid::new(20, 20).unwrap();
let changed = renderer.count_changed_cells(&frame2, &frame1);
assert_eq!(changed, 400); }
#[test]
fn test_cells_differ_detects_dot_change() {
let frame1 = BrailleGrid::new(10, 10).unwrap();
let mut frame2 = BrailleGrid::new(10, 10).unwrap();
frame2.set_dot(0, 0).unwrap();
assert!(DifferentialRenderer::cells_differ(&frame2, &frame1, 0, 0));
assert!(!DifferentialRenderer::cells_differ(&frame2, &frame1, 1, 1));
}
#[test]
fn test_cells_differ_detects_color_change() {
use crate::grid::Color;
let mut frame1 = BrailleGrid::new(10, 10).unwrap();
let mut frame2 = BrailleGrid::new(10, 10).unwrap();
frame1.set_dot(0, 0).unwrap();
frame2.set_dot(0, 0).unwrap();
frame2.set_cell_color(0, 0, Color::rgb(255, 0, 0)).unwrap();
assert!(DifferentialRenderer::cells_differ(&frame2, &frame1, 0, 0));
}
#[test]
fn test_clone() {
let mut renderer = DifferentialRenderer::new();
renderer.last_frame = Some(BrailleGrid::new(10, 10).unwrap());
let cloned = renderer.clone();
assert!(cloned.has_previous_frame());
}
}