use alloc::boxed::Box;
use bon::Builder;
use ratatui_core::{buffer::Buffer, layout::Rect, style::Color};
use crate::{
cell_filter::FilterProcessor,
default_shader_impl,
effect_timer::EffectTimer,
pattern::{AnyPattern, InstancedPattern, Pattern},
shader::Shader,
CellFilter, ColorCache, ColorSpace, Duration,
};
#[derive(Builder, Clone, Debug)]
pub(super) struct FadeColors {
fg: Option<Color>,
bg: Option<Color>,
#[builder(into)]
timer: EffectTimer,
area: Option<Rect>,
cell_filter: Option<FilterProcessor>,
color_space: ColorSpace,
#[builder(default)]
pattern: AnyPattern,
}
impl Shader for FadeColors {
default_shader_impl!(area, timer, filter, color_space, clone);
fn name(&self) -> &'static str {
if self.timer.is_reversed() {
"fade_from"
} else {
"fade_to"
}
}
fn execute(&mut self, _: Duration, area: Rect, buf: &mut Buffer) {
let global_alpha = self.timer.alpha();
let fg = self.fg;
let bg = self.bg;
let color_space = self.color_space;
let mut pattern = self.pattern.clone().for_frame(global_alpha, area);
let cell_iter = self.cell_iter(buf, area);
let mut color_cache: ColorCache<(Color, u8), 8> = ColorCache::new();
cell_iter.for_each_cell(move |pos, cell| {
let alpha = pattern.map_alpha(pos);
let cache_key = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
if let Some(fg) = fg.as_ref() {
let color = color_cache.memoize_fg(cell.fg, (*fg, cache_key), |c| {
color_space.lerp(c, fg, alpha)
});
cell.set_fg(color);
}
if let Some(bg) = bg.as_ref() {
let color = color_cache.memoize_bg(cell.bg, (*bg, cache_key), |c| {
color_space.lerp(c, bg, alpha)
});
cell.set_bg(color);
}
});
}
fn set_pattern(&mut self, pattern: AnyPattern) {
self.pattern = pattern;
}
#[cfg(feature = "dsl")]
fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
use crate::dsl::DslFormat;
let s = if let Some(bg) = self.bg {
format!(
"fx::{}({}, {}, {})",
self.name(),
self.fg.unwrap().dsl_format(),
bg.dsl_format(),
self.timer.dsl_format(),
)
} else {
format!(
"fx::{}_fg({}, {})",
self.name(),
self.fg.unwrap().dsl_format(),
self.timer.dsl_format()
)
};
crate::dsl::EffectExpression::parse(&s)
}
}
#[cfg(test)]
mod plain_test {
use ratatui_core::{buffer::Buffer, layout::Rect, style::Color};
use crate::{fx, pattern::SweepPattern, ColorSpace, Duration, ToRgbComponents};
#[test]
fn test_fade_with_sweep_patterns() {
let area = Rect::new(0, 0, 10, 3);
let test_fade_at_progress = |pattern: SweepPattern, progress_ms: u32| -> Buffer {
let mut fade = fx::fade_to_fg(Color::Red, 1000).with_pattern(pattern);
let mut buf = Buffer::empty(area);
for y in 0..3 {
for x in 0..10 {
buf[(x, y)].fg = Color::White;
}
}
fade.process(Duration::from_millis(progress_ms as _), &mut buf, area);
buf
};
let left_25 = test_fade_at_progress(SweepPattern::left_to_right(5), 250);
let left_75 = test_fade_at_progress(SweepPattern::left_to_right(5), 750);
let right_25 = test_fade_at_progress(SweepPattern::right_to_left(5), 250);
let right_75 = test_fade_at_progress(SweepPattern::right_to_left(5), 750);
let left_25_left = left_25[(2, 0)].fg;
let left_25_right = left_25[(7, 0)].fg;
let left_75_left = left_75[(2, 0)].fg;
let left_75_right = left_75[(7, 0)].fg;
let right_25_left = right_25[(2, 0)].fg;
let right_25_right = right_25[(7, 0)].fg;
let right_75_left = right_75[(2, 0)].fg;
let right_75_right = right_75[(7, 0)].fg;
assert_ne!(
left_25_left, left_25_right,
"Left-to-right at 25% should show gradient"
);
assert_ne!(
right_25_left, right_25_right,
"Right-to-left at 25% should show gradient"
);
assert_ne!(
left_25_left, right_25_left,
"Different directions should affect left side differently at 25%"
);
assert_ne!(
left_25_right, right_25_right,
"Different directions should affect right side differently at 25%"
);
assert_ne!(
left_75_left, left_75_right,
"Left-to-right at 75% should show gradient"
);
assert_ne!(
right_75_left, right_75_right,
"Right-to-left at 75% should show gradient"
);
}
#[test]
fn test_fade_reversal_works_correctly() {
let area = Rect::new(0, 0, 1, 1);
let mut buf1 = Buffer::empty(area);
let mut buf2 = Buffer::empty(area);
buf1[(0, 0)].fg = Color::White;
buf2[(0, 0)].fg = Color::White;
let mut normal_fade = fx::fade_to_fg(Color::Red, 1000);
let mut reversed_fade = fx::fade_from_fg(Color::Red, 1000);
normal_fade.process(Duration::from_millis(250), &mut buf1, area);
reversed_fade.process(Duration::from_millis(250), &mut buf2, area);
let normal_color = buf1[(0, 0)].fg;
let reversed_color = buf2[(0, 0)].fg;
assert_ne!(
normal_color, reversed_color,
"fade_to_fg and fade_from_fg should produce different results"
);
}
#[test]
fn test_fade_in_with_sweep_pattern() {
let mut effect =
fx::fade_from_fg(Color::Red, 1000).with_pattern(SweepPattern::left_to_right(20));
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
for y in 0..5 {
for x in 0..20 {
buf[(x, y)].fg = Color::White;
}
}
effect.process(Duration::from_millis(500), &mut buf, area);
let left_color = buf.cell((0, 0)).unwrap().fg;
let middle_color = buf.cell((10, 0)).unwrap().fg;
let right_color = buf.cell((19, 0)).unwrap().fg;
let colors_differ = left_color != middle_color || middle_color != right_color;
assert!(
colors_differ,
"Sweep pattern should create different colors at different positions. \
Left: {left_color:?}, Middle: {middle_color:?}, Right: {right_color:?}"
);
}
#[test]
fn test_fade_over_buffer_reset_cells() {
use ratatui_core::{buffer::Buffer, layout::Rect, style::Color};
use crate::{fx, Duration};
let area = Rect::new(0, 0, 1, 1);
let mut buf = Buffer::empty(area);
let mut fade_effect = fx::fade_to_fg(Color::Black, 1000).with_color_space(ColorSpace::Rgb);
fade_effect.process(Duration::from_millis(500), &mut buf, area);
let cell = &buf[(0, 0)];
let (r, g, b) = Color::White.to_rgb();
assert_eq!(cell.fg, Color::Rgb(r / 2, g / 2, b / 2));
}
}
#[cfg(test)]
#[cfg(feature = "dsl")]
mod dsl_tests {
use indoc::indoc;
use ratatui_core::style::Color;
use crate::{effect_timer::EffectTimer, fx, Interpolation::QuadOut};
#[test]
fn to_dsl_fade_to_fg() {
let dsl = fx::fade_to_fg(Color::from_u32(0), EffectTimer::from_ms(1000, QuadOut))
.to_dsl()
.unwrap()
.to_string();
assert_eq!(dsl, indoc! {
"fx::fade_to_fg(Color::from_u32(0), EffectTimer::from_ms(1000, Interpolation::QuadOut))"
});
}
#[test]
fn to_dsl_fade_to() {
let dsl = fx::fade_to(
Color::from_u32(0),
Color::from_u32(0),
EffectTimer::from_ms(1000, QuadOut),
)
.to_dsl()
.unwrap()
.to_string();
assert_eq!(dsl, indoc! {
"fx::fade_to(
Color::from_u32(0),
Color::from_u32(0),
EffectTimer::from_ms(1000, Interpolation::QuadOut)
)"
});
}
#[test]
fn to_dsl_fade_from_fg() {
let dsl = fx::fade_from_fg(Color::from_u32(0), EffectTimer::from_ms(1000, QuadOut))
.to_dsl()
.unwrap()
.to_string();
assert_eq!(dsl, indoc! {
"fx::fade_from_fg(Color::from_u32(0), EffectTimer::from_ms(1000, Interpolation::QuadOut))"
});
}
#[test]
fn to_dsl_fade_from() {
let dsl = fx::fade_from(
Color::from_u32(0),
Color::from_u32(0),
EffectTimer::from_ms(1000, QuadOut),
)
.to_dsl()
.unwrap()
.to_string();
assert_eq!(dsl, indoc! {
"fx::fade_from(
Color::from_u32(0),
Color::from_u32(0),
EffectTimer::from_ms(1000, Interpolation::QuadOut)
)"
});
}
}