use std::cell::RefCell;
use aisling::effects::EffectKind;
use aisling::loaders::{LoaderKind, LoaderProgress};
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::effects::{EffectPlayer, LoaderPlayer};
use crate::widgets::Widget;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RetainedEffectCacheKey {
pub width: u16,
pub height: u16,
pub frame: usize,
}
struct CachedEffectFrame {
width: u16,
height: u16,
frame: usize,
buffer: Buffer,
}
pub struct RetainedEffectWidget {
pub player: EffectPlayer,
cache: RefCell<Option<CachedEffectFrame>>,
}
impl RetainedEffectWidget {
pub fn new(kind: EffectKind, text: &str) -> Self {
Self::from_player(EffectPlayer::new(kind, text))
}
pub fn from_player(player: EffectPlayer) -> Self {
Self {
player,
cache: RefCell::new(None),
}
}
pub fn with_accent(mut self, accent: Color) -> Self {
self.player = self.player.with_accent(accent);
self.invalidate();
self
}
pub fn with_duration(mut self, duration: usize) -> Self {
self.player = self.player.with_duration(duration);
self.invalidate();
self
}
pub fn with_size(mut self, width: usize, height: usize) -> Self {
self.player = self.player.with_size(width, height);
self.invalidate();
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.player = self.player.with_seed(seed);
self.invalidate();
self
}
pub fn with_gradient_colors(mut self, colors: Vec<Color>, angle: f32) -> Self {
self.player = self.player.with_gradient_colors(colors, angle);
self.invalidate();
self
}
pub fn advance(&mut self) {
self.player.advance();
}
pub fn advance_n(&mut self, n: usize) {
self.player.advance_n(n);
}
pub fn set_frame(&mut self, frame: usize) {
self.player.set_frame(frame);
}
pub fn frame_count(&self) -> usize {
self.player.total_frames()
}
pub fn invalidate(&self) {
*self.cache.borrow_mut() = None;
}
pub fn current_cache_key(&self) -> Option<RetainedEffectCacheKey> {
self.cache
.borrow()
.as_ref()
.map(|cached| RetainedEffectCacheKey {
width: cached.width,
height: cached.height,
frame: cached.frame,
})
}
pub fn cache_key_for(&self, width: u16, height: u16, frame: usize) -> RetainedEffectCacheKey {
let frame = if self.player.total_frames() == 0 {
0
} else {
frame % self.player.total_frames()
};
RetainedEffectCacheKey {
width,
height,
frame,
}
}
pub fn cache_hit(&self, width: u16, height: u16, frame: usize) -> bool {
let key = self.cache_key_for(width, height, frame);
self.current_cache_key() == Some(key)
}
pub fn with_cached_buffer<R>(
&self,
width: u16,
height: u16,
f: impl FnOnce(&Buffer) -> R,
) -> R {
let frame = self.player.current_frame_index();
let needs_rebuild = match self.cache.borrow().as_ref() {
Some(cached) => {
cached.width != width || cached.height != height || cached.frame != frame
}
None => true,
};
if needs_rebuild {
let mut buffer = Buffer::new(width as usize, height as usize);
self.player
.render_to_buffer(&mut buffer, Rect::new(0, 0, width, height));
*self.cache.borrow_mut() = Some(CachedEffectFrame {
width,
height,
frame,
buffer,
});
}
let cache = self.cache.borrow();
f(&cache
.as_ref()
.expect("retained effect cache must exist")
.buffer)
}
pub fn with_cached_frame_buffer<R>(
&self,
width: u16,
height: u16,
frame: usize,
f: impl FnOnce(&Buffer) -> R,
) -> R {
let frame = self.cache_key_for(width, height, frame).frame;
let needs_rebuild = !self.cache_hit(width, height, frame);
if needs_rebuild {
let mut buffer = Buffer::new(width as usize, height as usize);
self.player
.render_frame_to_buffer(frame, &mut buffer, Rect::new(0, 0, width, height));
*self.cache.borrow_mut() = Some(CachedEffectFrame {
width,
height,
frame,
buffer,
});
}
let cache = self.cache.borrow();
f(&cache
.as_ref()
.expect("retained effect cache must exist")
.buffer)
}
pub fn render_frame_into(&self, buffer: &mut Buffer, area: Rect, frame_index: usize) {
if area.is_empty() {
return;
}
self.with_cached_frame_buffer(area.width, area.height, frame_index, |cached| {
buffer.copy_from_at(cached, area.x as usize, area.y as usize);
});
}
pub fn render_cached_only(&self, buffer: &mut Buffer, area: Rect, frame_index: usize) -> bool {
if area.is_empty() || !self.cache_hit(area.width, area.height, frame_index) {
return false;
}
let cache = self.cache.borrow();
let cached = &cache
.as_ref()
.expect("retained effect cache must exist")
.buffer;
buffer.copy_from_at(cached, area.x as usize, area.y as usize);
true
}
}
impl Widget for RetainedEffectWidget {
fn render(&self, buffer: &mut Buffer, area: Rect) {
if area.is_empty() {
return;
}
self.with_cached_buffer(area.width, area.height, |cached| {
buffer.copy_from_at(cached, area.x as usize, area.y as usize);
});
}
}
struct CachedLoaderFrame {
width: u16,
height: u16,
tick: usize,
progress: LoaderProgress,
buffer: Buffer,
}
pub struct RetainedLoaderWidget {
pub player: LoaderPlayer,
pub tick: usize,
pub progress: LoaderProgress,
cache: RefCell<Option<CachedLoaderFrame>>,
}
impl RetainedLoaderWidget {
pub fn new(kind: LoaderKind) -> Self {
Self::from_player(LoaderPlayer::new(kind))
}
pub fn from_player(player: LoaderPlayer) -> Self {
Self {
player,
tick: 0,
progress: LoaderProgress::new(0.0),
cache: RefCell::new(None),
}
}
pub fn with_accent(mut self, accent: Color) -> Self {
self.player = self.player.with_accent(accent);
self.invalidate();
self
}
pub fn with_size(mut self, width: usize, height: usize) -> Self {
self.player = self.player.with_size(width, height);
self.invalidate();
self
}
pub fn with_label(mut self, label: String) -> Self {
self.player = self.player.with_label(label);
self.invalidate();
self
}
pub fn with_unit(mut self, unit: &str) -> Self {
self.player = self.player.with_unit(unit);
self.invalidate();
self
}
pub fn with_fraction(mut self, fraction: bool) -> Self {
self.player = self.player.with_fraction(fraction);
self.invalidate();
self
}
pub fn with_gradient_colors(mut self, colors: Vec<Color>, angle: f32) -> Self {
self.player = self.player.with_gradient_colors(colors, angle);
self.invalidate();
self
}
pub fn with_tick(mut self, tick: usize) -> Self {
self.tick = tick;
self
}
pub fn with_progress(mut self, progress: LoaderProgress) -> Self {
self.progress = progress;
self
}
pub fn set_tick(&mut self, tick: usize) {
self.tick = tick;
}
pub fn advance(&mut self) {
self.tick = self.tick.wrapping_add(1);
}
pub fn set_progress(&mut self, progress: LoaderProgress) {
self.progress = progress;
}
pub fn invalidate(&self) {
*self.cache.borrow_mut() = None;
}
pub fn with_cached_buffer<R>(
&self,
width: u16,
height: u16,
f: impl FnOnce(&Buffer) -> R,
) -> R {
let needs_rebuild = match self.cache.borrow().as_ref() {
Some(cached) => {
cached.width != width
|| cached.height != height
|| cached.tick != self.tick
|| cached.progress != self.progress
}
None => true,
};
if needs_rebuild {
let mut buffer = Buffer::new(width as usize, height as usize);
self.player.render(
self.tick,
self.progress,
&mut buffer,
Rect::new(0, 0, width, height),
);
*self.cache.borrow_mut() = Some(CachedLoaderFrame {
width,
height,
tick: self.tick,
progress: self.progress,
buffer,
});
}
let cache = self.cache.borrow();
f(&cache
.as_ref()
.expect("retained loader cache must exist")
.buffer)
}
}
impl Widget for RetainedLoaderWidget {
fn render(&self, buffer: &mut Buffer, area: Rect) {
if area.is_empty() {
return;
}
self.with_cached_buffer(area.width, area.height, |cached| {
buffer.copy_from_at(cached, area.x as usize, area.y as usize);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::Widget;
#[test]
fn retained_effect_renders_cached_frame() {
let widget = RetainedEffectWidget::new(EffectKind::Matrix, "hi").with_size(8, 2);
let mut buffer = Buffer::new(8, 2);
widget.render(&mut buffer, Rect::new(0, 0, 8, 2));
assert_eq!(widget.with_cached_buffer(8, 2, |cached| cached.width()), 8);
assert!(widget.cache_hit(8, 2, 0));
assert_eq!(widget.frame_count(), widget.player.total_frames());
}
#[test]
fn retained_effect_can_render_specific_cached_frame_without_state_change() {
let widget = RetainedEffectWidget::new(EffectKind::Matrix, "hi").with_size(8, 2);
let mut buffer = Buffer::new(8, 2);
widget.render_frame_into(&mut buffer, Rect::new(0, 0, 8, 2), 1);
assert_eq!(widget.player.current_frame_index(), 0);
assert!(widget.render_cached_only(&mut buffer, Rect::new(0, 0, 8, 2), 1));
assert!(!widget.render_cached_only(&mut buffer, Rect::new(0, 0, 8, 2), 2));
}
#[test]
fn retained_loader_renders_cached_frame() {
let widget = RetainedLoaderWidget::new(LoaderKind::Bar)
.with_size(12, 2)
.with_progress(LoaderProgress::new(0.5));
let mut buffer = Buffer::new(12, 2);
widget.render(&mut buffer, Rect::new(0, 0, 12, 2));
assert_eq!(
widget.with_cached_buffer(12, 2, |cached| cached.width()),
12
);
}
}