use ratatui::layout::Rect;
use std::collections::HashMap;
pub struct Queued {
pub rect: Rect,
pub key: String,
pub idx: usize,
pub in_place: bool,
}
#[derive(Default)]
pub struct Blitter {
area: Rect,
queue: Vec<Queued>,
last: HashMap<Rect, (String, usize)>,
}
impl Blitter {
pub fn push(&mut self, rect: Rect, key: &str, idx: usize) {
self.queue.push(Queued {
rect,
key: key.to_owned(),
idx,
in_place: false,
});
}
pub fn shift_up(&mut self, top: u16, h: u16, k: u16) -> bool {
let (end, cut) = (top + h, top + k);
let mut next = HashMap::with_capacity(self.last.len());
for (r, id) in &self.last {
let (y0, y1) = (r.y, r.y + r.height);
if y1 <= top || y0 >= end {
next.insert(*r, id.clone()); } else if y0 < top || y1 > end {
return false; } else if y0 >= cut {
next.insert(Rect { y: y0 - k, ..*r }, id.clone());
} }
self.last = next;
true
}
pub fn take_emits(&mut self, area: Rect) -> Vec<Queued> {
if area != self.area {
self.area = area;
self.last.clear();
}
let mut next = HashMap::with_capacity(self.queue.len());
let mut emits = Vec::new();
for q in std::mem::take(&mut self.queue) {
let ident = (q.key, q.idx);
let prev = self.last.get(&q.rect);
let same = prev == Some(&ident);
let in_place = prev.is_some_and(|p| p.0 == ident.0);
next.insert(q.rect, ident.clone());
if !same {
emits.push(Queued {
rect: q.rect,
key: ident.0,
idx: ident.1,
in_place,
});
}
}
self.last = next;
emits
}
}
#[cfg(test)]
mod tests {
use super::*;
const A: Rect = Rect::new(0, 0, 80, 24);
fn r(x: u16, y: u16) -> Rect {
Rect::new(x, y, 2, 2)
}
fn frame(b: &mut Blitter, area: Rect, ps: &[(Rect, &str, usize)]) -> usize {
for (rect, key, idx) in ps {
b.push(*rect, key, *idx);
}
b.take_emits(area).len()
}
fn in_place(b: &mut Blitter, ps: &[(Rect, &str, usize)]) -> Vec<bool> {
for (rect, key, idx) in ps {
b.push(*rect, key, *idx);
}
b.take_emits(A).iter().map(|q| q.in_place).collect()
}
#[test]
fn only_a_frame_advance_is_in_place() {
let mut b = Blitter::default();
assert_eq!(in_place(&mut b, &[(r(1, 1), "k", 0)]), [false]); assert_eq!(in_place(&mut b, &[(r(1, 1), "k", 1)]), [true]); assert_eq!(in_place(&mut b, &[(r(1, 2), "k", 1)]), [false]); assert_eq!(in_place(&mut b, &[(r(1, 2), "j", 1)]), [false]); assert_eq!(in_place(&mut b, &[]), Vec::<bool>::new());
assert_eq!(in_place(&mut b, &[(r(1, 2), "j", 2)]), [false]); }
#[test]
fn scroll_moves_known_placements() {
let mut b = Blitter::default();
frame(
&mut b,
A,
&[(r(1, 2), "k", 0), (r(1, 5), "j", 0), (r(1, 20), "f", 0)],
);
assert!(b.shift_up(2, 10, 3));
assert_eq!(
frame(
&mut b,
A,
&[(r(1, 2), "j", 0), (r(1, 20), "f", 0), (r(1, 9), "n", 0)]
),
1, );
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 3), "k", 0)]);
assert!(b.shift_up(2, 10, 2)); assert_eq!(frame(&mut b, A, &[(r(1, 1), "k", 0)]), 1);
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert!(!b.shift_up(2, 10, 2));
}
#[test]
fn unchanged_skips() {
let mut b = Blitter::default();
assert_eq!(frame(&mut b, A, &[(r(1, 1), "k", 0)]), 1);
assert_eq!(frame(&mut b, A, &[(r(1, 1), "k", 0)]), 0);
}
#[test]
fn moved_emits() {
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert_eq!(frame(&mut b, A, &[(r(1, 2), "k", 0)]), 1);
}
#[test]
fn frame_advance_emits() {
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert_eq!(frame(&mut b, A, &[(r(1, 1), "k", 1)]), 1);
}
#[test]
fn other_emote_emits() {
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert_eq!(frame(&mut b, A, &[(r(1, 1), "j", 0)]), 1);
}
#[test]
fn overwritten_rect_emits_when_it_returns() {
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert_eq!(frame(&mut b, A, &[]), 0);
assert_eq!(frame(&mut b, A, &[(r(1, 1), "k", 0)]), 1);
}
#[test]
fn resize_reemits() {
let mut b = Blitter::default();
frame(&mut b, A, &[(r(1, 1), "k", 0)]);
assert_eq!(
frame(&mut b, Rect::new(0, 0, 90, 24), &[(r(1, 1), "k", 0)]),
1
);
}
}