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::interaction::{HitRegion, InteractionLayer, WidgetAction, WidgetId, WidgetRole};
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);
});
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedEffectPaneKey {
pub kind: EffectKind,
pub text: String,
pub width: u16,
pub height: u16,
pub duration: Option<usize>,
pub seed: Option<u64>,
pub palette: Option<Vec<Color>>,
pub palette_angle_bits: Option<u32>,
}
struct CachedEffectPaneState {
key: CachedEffectPaneKey,
widget: RetainedEffectWidget,
}
pub struct CachedEffectPane {
pub kind: EffectKind,
pub text: String,
pub duration: Option<usize>,
pub seed: Option<u64>,
pub palette: Option<(Vec<Color>, f32)>,
pub header: Option<String>,
pub footer: Option<String>,
pub label_color: Color,
pub region_id: Option<WidgetId>,
state: RefCell<Option<CachedEffectPaneState>>,
}
impl CachedEffectPane {
pub fn new(kind: EffectKind, text: &str) -> Self {
Self {
kind,
text: text.to_string(),
duration: None,
seed: None,
palette: None,
header: None,
footer: None,
label_color: Color::rgb(139, 148, 158),
region_id: None,
state: RefCell::new(None),
}
}
pub fn with_duration(mut self, duration: usize) -> Self {
self.duration = Some(duration);
self.invalidate();
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self.invalidate();
self
}
pub fn with_palette(mut self, colors: Vec<Color>, angle: f32) -> Self {
self.palette = Some((colors, angle));
self.invalidate();
self
}
pub fn with_muted_palette(self, accent: Color) -> Self {
self.with_palette(vec![accent.dim(0.65), accent.dim(0.35), accent], 45.0)
}
pub fn with_header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
pub fn with_footer(mut self, footer: impl Into<String>) -> Self {
self.footer = Some(footer.into());
self
}
pub fn with_label_color(mut self, color: Color) -> Self {
self.label_color = color;
self
}
pub fn with_region_id(mut self, id: impl Into<WidgetId>) -> Self {
self.region_id = Some(id.into());
self
}
pub fn invalidate(&self) {
*self.state.borrow_mut() = None;
}
pub fn cache_key_for_area(&self, area: Rect) -> CachedEffectPaneKey {
CachedEffectPaneKey {
kind: self.kind,
text: self.text.clone(),
width: area.width,
height: area.height,
duration: self.duration,
seed: self.seed,
palette: self.palette.as_ref().map(|(colors, _)| colors.clone()),
palette_angle_bits: self.palette.as_ref().map(|(_, angle)| angle.to_bits()),
}
}
pub fn current_cache_key(&self) -> Option<CachedEffectPaneKey> {
self.state.borrow().as_ref().map(|state| state.key.clone())
}
pub fn cache_hit(&self, area: Rect) -> bool {
self.current_cache_key() == Some(self.cache_key_for_area(area))
}
pub fn frame_count_for_area(&self, area: Rect) -> usize {
self.with_widget_for_area(area, |widget| widget.frame_count())
}
pub fn render_frame(&self, buffer: &mut Buffer, area: Rect, frame_index: usize) {
if area.is_empty() {
return;
}
self.with_widget_for_area(area, |widget| {
widget.render_frame_into(buffer, area, frame_index);
});
self.render_labels(buffer, area);
}
pub fn render_frame_with_interaction(
&self,
buffer: &mut Buffer,
area: Rect,
frame_index: usize,
layer: &mut InteractionLayer,
) {
self.render_frame(buffer, area, frame_index);
if area.is_empty() {
return;
}
let region_id = self
.region_id
.clone()
.unwrap_or_else(|| WidgetId::new("cached-effect"));
layer.push_region(
HitRegion::new(region_id, area)
.with_role(WidgetRole::Effect)
.with_label(self.kind.name())
.with_action(WidgetAction::Focus),
);
}
fn with_widget_for_area<R>(&self, area: Rect, f: impl FnOnce(&RetainedEffectWidget) -> R) -> R {
let key = self.cache_key_for_area(area);
let needs_rebuild = self
.state
.borrow()
.as_ref()
.map(|state| state.key != key)
.unwrap_or(true);
if needs_rebuild {
let mut widget = RetainedEffectWidget::new(self.kind, &self.text)
.with_size(area.width as usize, area.height as usize);
if let Some(duration) = self.duration {
widget = widget.with_duration(duration);
}
if let Some(seed) = self.seed {
widget = widget.with_seed(seed);
}
if let Some((colors, angle)) = self.palette.clone() {
widget = widget.with_gradient_colors(colors, angle);
}
*self.state.borrow_mut() = Some(CachedEffectPaneState { key, widget });
}
let state = self.state.borrow();
f(&state.as_ref().expect("cached effect pane state").widget)
}
fn render_labels(&self, buffer: &mut Buffer, area: Rect) {
if let Some(header) = &self.header {
buffer.set_str(
area.x as usize,
area.y as usize,
&crate::sanitize::truncate_str(header, area.width as usize),
self.label_color,
None,
);
}
if let Some(footer) = &self.footer {
let y = area.bottom().saturating_sub(1) as usize;
buffer.set_str(
area.x as usize,
y,
&crate::sanitize::truncate_str(footer, area.width as usize),
self.label_color,
None,
);
}
}
}
impl Widget for CachedEffectPane {
fn render(&self, buffer: &mut Buffer, area: Rect) {
self.render_frame(buffer, area, 0);
}
}
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
);
}
#[test]
fn cached_effect_pane_reuses_size_aware_player() {
let pane = CachedEffectPane::new(EffectKind::Matrix, "hi")
.with_duration(4)
.with_seed(7)
.with_region_id("effect:pane");
let mut buffer = Buffer::new(16, 4);
let mut layer = InteractionLayer::new();
pane.render_frame_with_interaction(&mut buffer, Rect::new(0, 0, 16, 4), 1, &mut layer);
assert!(pane.cache_hit(Rect::new(0, 0, 16, 4)));
assert!(!pane.cache_hit(Rect::new(0, 0, 12, 4)));
assert_eq!(layer.hit_test(1, 1).unwrap().id.as_ref(), "effect:pane");
assert!(pane.frame_count_for_area(Rect::new(0, 0, 16, 4)) > 0);
}
}