use unicode_segmentation::UnicodeSegmentation;
use crate::TICK;
use crate::paint::{Ink, Palette, Rgb};
use crate::shimmer::{Intensity, Spell};
pub(crate) const TICK_SECS: f64 = TICK.as_secs_f64();
const HISTORY: usize = 128;
pub(crate) trait Shader: Send + Sync {
fn tick(&mut self, _stage: &Stage) {}
fn idle(&mut self, _stage: &Stage) {}
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas);
fn bullet(&self, stage: &Stage, palette: Palette) -> Ink;
}
#[derive(Default)]
pub(crate) struct Canvas {
pub(crate) mark: Vec<Ink>,
pub(crate) letters: Vec<Ink>,
pub(crate) tail: Vec<Ink>,
pub(crate) backdrop: Vec<Rgb>,
}
#[derive(Default)]
pub(crate) struct Caption {
text: String,
ends: Vec<usize>,
}
impl Caption {
fn new(text: &str) -> Self {
Self {
text: text.to_owned(),
ends: text
.grapheme_indices( true)
.map(|(at, letter)| at + letter.len())
.collect(),
}
}
pub(crate) fn len(&self) -> usize {
self.ends.len()
}
pub(crate) fn get(&self, i: usize) -> Option<&str> {
let end = *self.ends.get(i)?;
let start = i.checked_sub(1).map_or(0, |before| self.ends[before]);
Some(&self.text[start..end])
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &str> {
self.ends.iter().scan(0, |start, &end| {
let letter = &self.text[*start..end];
*start = end;
Some(letter)
})
}
}
pub(crate) struct Stage {
pub(crate) t: f64,
pub(crate) spell: Spell,
pub(crate) spell_t: f64,
pub(crate) intensity: Intensity,
pub(crate) quanta: u64,
pub(crate) quiet: f64,
pub(crate) caption: Caption,
pub(crate) caption_t: f64,
pub(crate) captions: i64,
history: [u32; HISTORY],
newest: usize,
}
impl Default for Stage {
fn default() -> Self {
Self {
t: 0.0,
spell: Spell::Idle,
spell_t: 0.0,
intensity: Intensity::default(),
quanta: 0,
quiet: 0.0,
caption: Caption::default(),
caption_t: 0.0,
captions: 0,
history: [0; HISTORY],
newest: 0,
}
}
}
impl Stage {
pub(crate) fn busy(&self) -> bool {
self.spell == Spell::Busy
}
pub(crate) fn fresh(&self) -> u32 {
self.history[self.newest]
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "A window selects whole ticks of history; negative windows select none and long ones all."
)]
pub(crate) fn quanta_in(&self, window: f64) -> f64 {
let ticks = ((window / TICK_SECS).round() as usize).min(HISTORY);
(0..ticks)
.map(|back| f64::from(self.history[(self.newest + HISTORY - back) % HISTORY]))
.sum()
}
pub(crate) fn flow(&self, window: f64) -> f64 {
self.quanta_in(window) / window
}
pub(crate) fn letters(&self, mut paint: impl FnMut(usize, &str) -> Ink) -> Vec<Ink> {
self.caption
.iter()
.enumerate()
.map(|(i, letter)| paint(i, letter))
.collect()
}
pub(crate) fn cast(&mut self, spell: Spell) -> bool {
let changed = spell != self.spell;
if changed {
self.spell = spell;
self.spell_t = 0.0;
}
changed
}
pub(crate) fn show(&mut self, caption: &str) {
if self.caption.text != caption {
self.caption = Caption::new(caption);
self.caption_t = 0.0;
self.captions += 1;
}
}
pub(crate) fn advance(&mut self, quanta: u32) {
self.t += TICK_SECS;
self.spell_t += TICK_SECS;
self.caption_t += TICK_SECS;
self.quanta += u64::from(quanta);
self.quiet = if quanta > 0 {
0.0
} else {
self.quiet + TICK_SECS
};
self.newest = (self.newest + 1) % HISTORY;
self.history[self.newest] = quanta;
}
}