use crate::widget::Rect;
const EMPTY_RECT: Rect = Rect {
x: 0,
y: 0,
width: 0,
height: 0,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PresentPlan<'a> {
None,
FullFrame,
Rects(&'a [Rect]),
}
pub trait InvalidationSource {
fn collect_dirty(&mut self, sink: &mut dyn FnMut(Rect));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidationList<const N: usize> {
screen: Rect,
rects: [Rect; N],
len: usize,
full_frame: bool,
}
impl<const N: usize> InvalidationList<N> {
pub const fn new(screen: Rect) -> Self {
Self {
screen,
rects: [EMPTY_RECT; N],
len: 0,
full_frame: false,
}
}
pub const fn with_size(width: i32, height: i32) -> Self {
Self::new(Rect {
x: 0,
y: 0,
width,
height,
})
}
pub const fn screen(&self) -> Rect {
self.screen
}
pub const fn len(&self) -> usize {
self.len
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub const fn is_full_frame(&self) -> bool {
self.full_frame
}
pub fn push(&mut self, rect: Rect) {
if self.full_frame {
return;
}
let Some(clipped) = self.screen.intersect(rect) else {
return;
};
let merged = absorb_overlaps(&mut self.rects, &mut self.len, clipped);
if self.len == N {
self.mark_full_frame();
} else {
self.rects[self.len] = merged;
self.len += 1;
}
}
pub fn push_opt(&mut self, rect: Option<Rect>) {
if let Some(rect) = rect {
self.push(rect);
}
}
pub fn extend_from_slice(&mut self, rects: &[Rect]) {
for &rect in rects {
self.push(rect);
}
}
pub fn gather(&mut self, source: &mut dyn InvalidationSource) {
source.collect_dirty(&mut |rect| self.push(rect));
}
pub fn mark_full_frame(&mut self) {
self.full_frame = true;
self.len = 0;
}
pub fn clear(&mut self) {
self.len = 0;
self.full_frame = false;
}
pub fn plan(&self) -> PresentPlan<'_> {
if self.full_frame {
PresentPlan::FullFrame
} else if self.len == 0 {
PresentPlan::None
} else {
PresentPlan::Rects(&self.rects[..self.len])
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferedInvalidation<const N: usize, const K: usize> {
screen: Rect,
rects: [Rect; N],
ages: [usize; N],
len: usize,
full_frame_age: usize,
}
impl<const N: usize, const K: usize> BufferedInvalidation<N, K> {
const RETAIN: usize = if K == 0 { 1 } else { K };
pub const fn new(screen: Rect) -> Self {
Self {
screen,
rects: [EMPTY_RECT; N],
ages: [0; N],
len: 0,
full_frame_age: 0,
}
}
pub const fn with_size(width: i32, height: i32) -> Self {
Self::new(Rect {
x: 0,
y: 0,
width,
height,
})
}
pub const fn screen(&self) -> Rect {
self.screen
}
pub const fn len(&self) -> usize {
self.len
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub const fn is_full_frame(&self) -> bool {
self.full_frame_age > 0
}
pub fn push(&mut self, rect: Rect) {
let Some(clipped) = self.screen.intersect(rect) else {
return;
};
let merged = self.absorb_overlaps(clipped);
if self.len == N {
self.mark_full_frame();
} else {
self.rects[self.len] = merged;
self.ages[self.len] = Self::RETAIN;
self.len += 1;
}
}
pub fn push_opt(&mut self, rect: Option<Rect>) {
if let Some(rect) = rect {
self.push(rect);
}
}
pub fn extend_from_slice(&mut self, rects: &[Rect]) {
for &rect in rects {
self.push(rect);
}
}
pub fn gather(&mut self, source: &mut dyn InvalidationSource) {
source.collect_dirty(&mut |rect| self.push(rect));
}
pub fn mark_full_frame(&mut self) {
self.full_frame_age = Self::RETAIN;
self.len = 0;
}
pub fn clear(&mut self) {
self.len = 0;
self.full_frame_age = 0;
}
pub fn plan(&self) -> PresentPlan<'_> {
if self.full_frame_age > 0 {
PresentPlan::FullFrame
} else if self.len == 0 {
PresentPlan::None
} else {
PresentPlan::Rects(&self.rects[..self.len])
}
}
pub fn finish_present(&mut self) {
if self.full_frame_age > 0 {
self.full_frame_age -= 1;
}
let mut keep = 0;
for i in 0..self.len {
let age = self.ages[i] - 1;
if age > 0 {
self.rects[keep] = self.rects[i];
self.ages[keep] = age;
keep += 1;
}
}
self.len = keep;
}
fn absorb_overlaps(&mut self, mut rect: Rect) -> Rect {
let mut i = 0;
while i < self.len {
if self.rects[i].intersect(rect).is_some() {
rect = rect.union(self.rects[i]);
self.rects.copy_within(i + 1..self.len, i);
self.ages.copy_within(i + 1..self.len, i);
self.len -= 1;
i = 0;
} else {
i += 1;
}
}
rect
}
}
fn absorb_overlaps<const N: usize>(rects: &mut [Rect; N], len: &mut usize, mut rect: Rect) -> Rect {
let mut i = 0;
while i < *len {
if rects[i].intersect(rect).is_some() {
rect = rect.union(rects[i]);
rects.copy_within(i + 1..*len, i);
*len -= 1;
i = 0;
} else {
i += 1;
}
}
rect
}
#[cfg(test)]
mod tests {
use alloc::boxed::Box;
use alloc::vec::Vec;
use super::*;
use crate::anim::{Animations, Tween};
fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
#[test]
fn push_clips_to_screen_bounds() {
let mut list = InvalidationList::<4>::with_size(100, 50);
list.push(rect(-10, -10, 30, 30));
list.push(rect(90, 40, 30, 30));
assert_eq!(
list.plan(),
PresentPlan::Rects(&[rect(0, 0, 20, 20), rect(90, 40, 10, 10)])
);
}
#[test]
fn push_drops_degenerate_and_outside_rects() {
let mut list = InvalidationList::<4>::with_size(100, 50);
list.push(rect(10, 10, 0, 20)); list.push(rect(10, 10, 20, -5)); list.push(rect(200, 200, 10, 10)); list.push(rect(-50, 0, 50, 50));
assert_eq!(list.plan(), PresentPlan::None);
assert!(list.is_empty());
}
#[test]
fn empty_list_plans_none() {
let list = InvalidationList::<4>::with_size(100, 100);
assert_eq!(list.plan(), PresentPlan::None);
}
#[test]
fn overlapping_rects_merge_and_cascade() {
let mut list = InvalidationList::<4>::with_size(100, 100);
list.push(rect(0, 0, 10, 10));
list.push(rect(30, 0, 10, 10));
list.push(rect(5, 0, 30, 10));
assert_eq!(list.plan(), PresentPlan::Rects(&[rect(0, 0, 40, 10)]));
}
#[test]
fn merge_is_deterministic_for_same_input_sequence() {
let sequence = [
rect(0, 0, 10, 10),
rect(50, 50, 10, 10),
rect(5, 5, 10, 10),
rect(80, 0, 40, 10),
rect(-5, -5, 8, 8),
];
let mut a = InvalidationList::<8>::with_size(100, 100);
let mut b = InvalidationList::<8>::with_size(100, 100);
for &r in &sequence {
a.push(r);
}
for &r in &sequence {
b.push(r);
}
assert_eq!(a.plan(), b.plan());
assert!(matches!(a.plan(), PresentPlan::Rects(_)));
}
#[test]
fn capacity_overflow_promotes_to_full_frame() {
let mut list = InvalidationList::<2>::with_size(100, 100);
list.push(rect(0, 0, 10, 10));
list.push(rect(20, 0, 10, 10));
assert_eq!(list.len(), 2);
list.push(rect(40, 0, 10, 10));
assert_eq!(list.plan(), PresentPlan::FullFrame);
assert!(list.is_full_frame());
list.push(rect(0, 0, 5, 5));
assert_eq!(list.plan(), PresentPlan::FullFrame);
list.clear();
assert_eq!(list.plan(), PresentPlan::None);
}
#[test]
fn multi_rect_present_order_is_stable() {
let mut list = InvalidationList::<4>::with_size(200, 200);
list.push(rect(100, 100, 10, 10));
list.push(rect(0, 0, 10, 10));
list.push(rect(50, 50, 10, 10));
assert_eq!(
list.plan(),
PresentPlan::Rects(&[
rect(100, 100, 10, 10),
rect(0, 0, 10, 10),
rect(50, 50, 10, 10),
])
);
}
#[test]
fn mark_full_frame_subsumes_rects() {
let mut list = InvalidationList::<4>::with_size(100, 100);
list.push(rect(0, 0, 10, 10));
list.mark_full_frame();
assert_eq!(list.plan(), PresentPlan::FullFrame);
assert!(list.is_empty());
}
#[test]
fn push_opt_none_is_noop() {
let mut list = InvalidationList::<4>::with_size(100, 100);
list.push_opt(None);
assert_eq!(list.plan(), PresentPlan::None);
list.push_opt(Some(rect(0, 0, 10, 10)));
assert_eq!(list.plan(), PresentPlan::Rects(&[rect(0, 0, 10, 10)]));
}
#[test]
fn extend_from_slice_ingests_animation_dirty_rects() {
let mut anims = Animations::new();
anims.register(
Tween::new(0, 8, 4),
Box::new(|value| {
Some(Rect {
x: value,
y: 0,
width: 10,
height: 10,
})
}),
);
anims.tick();
let reported: Vec<Rect> = anims.dirty_rects().to_vec();
assert_eq!(reported.len(), 1);
let mut list = InvalidationList::<4>::with_size(100, 100);
list.extend_from_slice(anims.dirty_rects());
assert_eq!(list.plan(), PresentPlan::Rects(&reported));
}
#[test]
fn invalidation_source_gathers_into_list() {
struct TwoRects;
impl InvalidationSource for TwoRects {
fn collect_dirty(&mut self, sink: &mut dyn FnMut(Rect)) {
sink(Rect {
x: 0,
y: 0,
width: 10,
height: 10,
});
sink(Rect {
x: 20,
y: 0,
width: 10,
height: 10,
});
}
}
let mut list = InvalidationList::<4>::with_size(100, 100);
list.gather(&mut TwoRects);
assert_eq!(
list.plan(),
PresentPlan::Rects(&[rect(0, 0, 10, 10), rect(20, 0, 10, 10)])
);
}
#[test]
fn buffered_rect_persists_exactly_k_presents() {
let mut buf = BufferedInvalidation::<4, 2>::with_size(100, 100);
buf.push(rect(10, 10, 5, 5));
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(10, 10, 5, 5)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(10, 10, 5, 5)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_full_frame_persists_k_presents() {
let mut buf = BufferedInvalidation::<4, 2>::with_size(100, 100);
buf.mark_full_frame();
assert_eq!(buf.plan(), PresentPlan::FullFrame);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::FullFrame);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_overflow_promotes_full_frame_for_k_presents() {
let mut buf = BufferedInvalidation::<2, 2>::with_size(100, 100);
buf.push(rect(0, 0, 10, 10));
buf.push(rect(20, 0, 10, 10));
buf.push(rect(40, 0, 10, 10));
assert_eq!(buf.plan(), PresentPlan::FullFrame);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::FullFrame);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_push_during_live_full_frame_reaches_all_buffers() {
let mut buf = BufferedInvalidation::<4, 2>::with_size(100, 100);
buf.mark_full_frame(); buf.finish_present();
buf.push(rect(0, 0, 10, 10));
assert_eq!(buf.plan(), PresentPlan::FullFrame);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(0, 0, 10, 10)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_interleaved_pushes_across_presents() {
let mut buf = BufferedInvalidation::<4, 2>::with_size(100, 100);
buf.push(rect(0, 0, 10, 10));
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(0, 0, 10, 10)]));
buf.finish_present();
buf.push(rect(50, 0, 10, 10)); assert_eq!(
buf.plan(),
PresentPlan::Rects(&[rect(0, 0, 10, 10), rect(50, 0, 10, 10)])
);
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(50, 0, 10, 10)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_merge_resets_retention_to_k() {
let mut buf = BufferedInvalidation::<4, 2>::with_size(100, 100);
buf.push(rect(0, 0, 10, 10));
buf.finish_present();
buf.push(rect(5, 0, 10, 10));
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(0, 0, 15, 10)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(0, 0, 15, 10)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
#[test]
fn buffered_clips_and_drops_like_unbuffered() {
let mut buf = BufferedInvalidation::<4, 1>::with_size(100, 50);
buf.push(rect(-10, -10, 30, 30)); buf.push(rect(200, 200, 10, 10)); buf.push_opt(None);
assert_eq!(buf.plan(), PresentPlan::Rects(&[rect(0, 0, 20, 20)]));
buf.finish_present();
assert_eq!(buf.plan(), PresentPlan::None);
}
}