use crate::backend::{Cursor, CursorStyle, DrawCell, Input, Output};
use crate::color::Style;
use crate::event::{Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use crate::grid::{Pos, Size};
use crate::tile::Tile;
use alloc::vec::Vec;
use core::time::Duration;
use ixy::HasSize;
#[must_use]
pub fn fnv1a(bytes: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET_BASIS;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
pub trait Observable: Output {
fn snapshot(&mut self) -> u64;
}
const fn cell(pos: Pos, tile: &Tile) -> DrawCell<'_> {
DrawCell::new(pos, tile)
}
fn draw_one<B: Output>(backend: &mut B, pos: Pos, tile: &Tile) -> Result<(), B::Error> {
backend.draw_layers(core::iter::once(cell(pos, tile)))?;
backend.flush()
}
fn expect<T, E: core::fmt::Debug>(result: Result<T, E>) -> T {
match result {
Ok(value) => value,
Err(error) => panic!("backend Output call failed: {error:?}"),
}
}
pub fn assert_output_contract<B: Observable, F: FnMut(Size) -> B>(mut make: F) {
let size = Size::new(4, 3);
let a = Tile::new('A', Style::new());
let b = Tile::new('B', Style::new());
{
let mut backend = make(size);
assert_eq!(
backend.size(),
size,
"a freshly made backend must report the size it was made with"
);
let grown = Size::new(size.width() + 2, size.height() + 1);
backend.resize(grown);
assert_eq!(
backend.size(),
grown,
"Output::resize(size) must update what Output::size() reports (retroglyph#763)"
);
}
{
let mut backend = make(size);
expect(draw_one(&mut backend, Pos::new(0, 0), &a));
let first_paint = backend.snapshot();
expect(backend.clear());
let _ = backend.snapshot(); expect(draw_one(&mut backend, Pos::new(0, 0), &a));
let second_paint = backend.snapshot();
assert_eq!(
second_paint, first_paint,
"drawing identical content after clear() must repaint it, not silently skip it \
because it matches an internal shadow copy from before the clear (retroglyph#763)"
);
}
{
let mut backend = make(size);
expect(draw_one(&mut backend, Pos::new(0, 0), &b));
let _ = backend.snapshot();
expect(backend.clear());
let _ = backend.snapshot();
expect(draw_one(&mut backend, Pos::new(0, 0), &a));
let after_clear = backend.snapshot();
drop(backend);
let mut fresh = make(size);
expect(draw_one(&mut fresh, Pos::new(0, 0), &a));
let from_fresh = fresh.snapshot();
assert_eq!(
after_clear, from_fresh,
"after clear(), drawing the same content a fresh backend would draw must produce \
the same digest; a mismatch means clear() left stale secondary state (tracked SGR \
attributes, damage flags, sprite layers, ...) behind (retroglyph#763)"
);
}
{
let mut backend = make(size);
expect(draw_one(&mut backend, Pos::new(0, 0), &b));
let _ = backend.snapshot();
let grown = Size::new(size.width() + 2, size.height() + 1);
backend.resize(grown);
let _ = backend.snapshot();
expect(draw_one(&mut backend, Pos::new(0, 0), &a));
let after_resize = backend.snapshot();
drop(backend);
let mut fresh = make(grown);
expect(draw_one(&mut fresh, Pos::new(0, 0), &a));
let from_fresh = fresh.snapshot();
assert_eq!(
after_resize, from_fresh,
"after resize(size), drawing the same content a fresh backend of the new size would \
draw must produce the same digest; a mismatch means resize() left stale shadow \
state behind (retroglyph#763)"
);
}
{
let mut backend = make(size);
let far = Pos::new(size.width() + 50, size.height() + 50);
expect(draw_one(&mut backend, far, &b));
let out_of_range = backend.snapshot();
drop(backend);
let mut fresh = make(size);
expect(fresh.draw_layers(core::iter::empty()));
expect(fresh.flush());
let nothing_drawn = fresh.snapshot();
assert_eq!(
out_of_range, nothing_drawn,
"a DrawCell positioned outside size() must not panic and must be silently dropped, \
not sent to the display (retroglyph#763)"
);
}
}
pub fn assert_cursor_contract<B: Observable + Cursor, F: FnMut(Size) -> B>(mut make: F) {
let size = Size::new(5, 1);
let a = Tile::new('A', Style::new());
let c = Tile::new('C', Style::new());
let b = Tile::new('B', Style::new());
let mut reference = make(size);
expect(draw_one(&mut reference, Pos::new(0, 0), &a));
expect(draw_one(&mut reference, Pos::new(4, 0), &c));
let _ = reference.snapshot();
expect(draw_one(&mut reference, Pos::new(1, 0), &b));
let reference_delta = reference.snapshot();
drop(reference);
let mut backend = make(size);
expect(draw_one(&mut backend, Pos::new(0, 0), &a));
backend.set_cursor_position(Pos::new(4, 0));
let _ = backend.flush();
let _ = backend.snapshot();
expect(draw_one(&mut backend, Pos::new(1, 0), &b));
let via_external_write = backend.snapshot();
assert_eq!(
via_external_write, reference_delta,
"an external Cursor::set_cursor_position call must keep the backend's own tracked \
cursor in sync with reality, the same as an ordinary draw does: the next draw must \
still emit whatever cursor-move is needed to reach its position (retroglyph#713)"
);
}
const CURSOR_STYLE_VARIANTS: [CursorStyle; 6] = [
CursorStyle::BlinkingBlock,
CursorStyle::SteadyBlock,
CursorStyle::BlinkingUnderline,
CursorStyle::SteadyUnderline,
CursorStyle::BlinkingBar,
CursorStyle::SteadyBar,
];
pub fn assert_cursor_style_contract<B: Observable + Cursor, F: FnMut(Size) -> B>(mut make: F) {
let size = Size::new(5, 1);
let mut backend = make(size);
let _ = backend.snapshot();
let mut digests = Vec::with_capacity(CURSOR_STYLE_VARIANTS.len());
for style in CURSOR_STYLE_VARIANTS {
backend.set_cursor_style(style);
let _ = backend.flush();
digests.push(backend.snapshot());
}
for (i, &a) in digests.iter().enumerate() {
for (j, &b) in digests.iter().enumerate().skip(i + 1) {
assert_ne!(
a, b,
"CursorStyle::{:?} and CursorStyle::{:?} must produce distinct backend effects; \
a match arm has collided or fallen through (retroglyph#920)",
CURSOR_STYLE_VARIANTS[i], CURSOR_STYLE_VARIANTS[j]
);
}
}
}
pub fn assert_input_contract<B: Input, F: FnMut() -> B>(mut make: F) {
const fn moved(x: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(x, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
})
}
let mut backend = make();
for x in 0..32u16 {
backend.push_event(moved(x));
}
assert_eq!(
backend.poll_event(Duration::ZERO),
Some(moved(31)),
"a burst of consecutive Mouse(Moved) pushes must coalesce to the latest one (retroglyph#763)"
);
assert_eq!(
backend.poll_event(Duration::ZERO),
None,
"the coalesced burst must have collapsed to exactly one queued event"
);
backend.push_event(moved(0));
backend.push_event(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos::new(0, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
backend.push_event(moved(1));
assert!(matches!(
backend.poll_event(Duration::ZERO),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
}))
));
assert!(matches!(
backend.poll_event(Duration::ZERO),
Some(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
..
}))
));
assert_eq!(backend.poll_event(Duration::ZERO), Some(moved(1)));
assert_eq!(backend.poll_event(Duration::ZERO), None);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Headless;
use alloc::string::String;
#[test]
fn fnv1a_is_deterministic_and_input_sensitive() {
assert_eq!(fnv1a(b"retroglyph"), fnv1a(b"retroglyph"));
assert_ne!(fnv1a(b"retroglyph"), fnv1a(b"retroglyph!"));
assert_ne!(fnv1a(b""), fnv1a(b"\0"));
}
struct HeadlessObserver {
backend: Headless,
previous: String,
}
impl HeadlessObserver {
fn new(width: u16, height: u16) -> Self {
let backend = Headless::new(width, height);
let previous = backend.format_view();
Self { backend, previous }
}
}
impl Output for HeadlessObserver {
type Error = core::convert::Infallible;
fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = DrawCell<'a>>,
{
self.backend.draw_layers(content)
}
fn flush(&mut self) -> Result<(), Self::Error> {
self.backend.flush()
}
fn size(&self) -> Size {
self.backend.size()
}
fn clear(&mut self) -> Result<(), Self::Error> {
self.backend.clear()
}
fn resize(&mut self, size: Size) {
self.backend.resize(size);
}
}
impl Cursor for HeadlessObserver {
fn set_cursor_visible(&mut self, visible: bool) {
self.backend.set_cursor_visible(visible);
}
fn set_cursor_position(&mut self, position: Pos) {
self.backend.set_cursor_position(position);
}
}
impl Observable for HeadlessObserver {
fn snapshot(&mut self) -> u64 {
let current = self.backend.format_view();
let mut hash = fnv1a(b"headless-diff");
for (index, (was, now)) in self.previous.chars().zip(current.chars()).enumerate() {
if was != now {
hash ^= fnv1a(&(index as u64).to_ne_bytes());
hash ^= fnv1a(&(now as u32).to_ne_bytes());
}
}
self.previous = current;
hash
}
}
#[test]
fn headless_satisfies_the_output_contract() {
assert_output_contract(|size| HeadlessObserver::new(size.width(), size.height()));
}
#[test]
fn headless_satisfies_the_cursor_contract() {
assert_cursor_contract(|size| HeadlessObserver::new(size.width(), size.height()));
}
#[test]
fn headless_satisfies_the_input_contract() {
assert_input_contract(|| Headless::new(10, 10));
}
}