tachyonfx/
effect.rs

1use alloc::boxed::Box;
2use core::fmt::Debug;
3
4use ratatui::{buffer::Buffer, layout::Rect};
5
6use crate::{
7    pattern::AnyPattern, shader::Shader, widget::EffectSpan, CellFilter, ColorSpace, Duration,
8    EffectTimer,
9};
10
11/// Represents an effect that can be applied to terminal cells.
12/// The `Effect` struct wraps a shader, allowing it to be configured
13/// and applied to a specified area and cell selection.
14#[derive(Debug)]
15pub struct Effect {
16    shader: Box<dyn Shader>,
17}
18
19impl Effect {
20    /// Creates a new `Effect` with the specified shader.
21    ///
22    /// # Arguments
23    /// * `shader` - The shader to be used for the effect. It must implement the `Shader`
24    ///   trait and have a static lifetime.
25    ///
26    /// # Returns
27    /// * A new `Effect` instance.
28    pub fn new<S>(shader: S) -> Self
29    where
30        S: Shader + 'static,
31    {
32        Self { shader: Box::new(shader) }
33    }
34
35    /// Creates a new `Effect` with the specified area.
36    ///
37    /// # Arguments
38    /// * `area` - The rectangular area where the effect will be applied.
39    ///
40    /// # Returns
41    /// * A new `Effect` instance with the specified area.
42    ///
43    /// # Example
44    /// ```
45    /// use tachyonfx::{Effect, EffectTimer, fx, Interpolation};
46    /// use ratatui::layout::Rect;
47    ///
48    /// fx::dissolve(EffectTimer::from_ms(120, Interpolation::CircInOut))
49    ///     .with_area(Rect::new(0, 0, 10, 10));
50    /// ```
51    pub fn with_area(mut self, area: Rect) -> Self {
52        self.shader.set_area(area);
53        self
54    }
55
56    /// Creates a new `Effect` with the specified cell filter.
57    ///
58    /// # Arguments
59    /// * `mode` - The cell filter to be used for the effect.
60    ///
61    /// # Returns
62    /// * A new `Effect` instance with the specified filter.
63    ///
64    /// # Notes
65    /// This method only applies the filter if the effect doesn't already have a filter
66    /// set, preserving any existing filters during effect composition.
67    ///
68    /// # Example
69    /// ```
70    /// use ratatui::style::Color;
71    /// use tachyonfx::{Effect, CellFilter, fx, Interpolation};
72    /// use tachyonfx::color_from_hsl;
73    ///
74    /// let color = color_from_hsl(180.0, 85.0, 62.0);
75    /// let shader = fx::fade_to_fg(color, (300, Interpolation::SineIn))
76    ///     .with_filter(CellFilter::Text);
77    /// ```
78    pub fn with_filter(mut self, mode: CellFilter) -> Self {
79        self.filter(mode);
80        self
81    }
82
83    #[deprecated(since = "0.11.0", note = "Use `with_filter` instead")]
84    pub fn with_cell_selection(&self, mode: CellFilter) -> Self {
85        self.clone().with_filter(mode)
86    }
87
88    /// Gets the current color space used for color interpolation.
89    /// Returns the default color space (HSL) if not supported.
90    ///
91    /// # Returns
92    /// * The `ColorSpace` currently in use by this effect.
93    pub fn color_space(&self) -> ColorSpace {
94        self.shader.color_space()
95    }
96
97    /// Sets the color space used for color interpolation.
98    ///
99    /// # Arguments
100    /// * `color_space` - The color space to use for color interpolation.
101    pub fn set_color_space(&mut self, color_space: ColorSpace) {
102        self.shader.set_color_space(color_space);
103    }
104
105    /// Creates a new `Effect` with the specified color space.
106    ///
107    /// # Arguments
108    /// * `color_space` - The color space to use for color interpolation.
109    ///
110    /// # Returns
111    /// * A new `Effect` instance with the specified color space.
112    ///
113    /// # Example
114    /// ```no_run
115    /// use ratatui::style::Color;
116    /// use tachyonfx::{ColorSpace, fx, Interpolation};
117    ///
118    /// let effect = fx::fade_to_fg(Color::Red, (300, Interpolation::SineIn))
119    ///     .with_color_space(ColorSpace::Rgb);
120    /// ```
121    pub fn with_color_space(mut self, color_space: ColorSpace) -> Self {
122        self.set_color_space(color_space);
123        self
124    }
125
126    /// Creates a new `Effect` with the shader's reverse flag toggled.
127    ///
128    /// # Returns
129    /// * A new `Effect` instance with the shader's reverse flag toggled.
130    pub fn reversed(mut self) -> Self {
131        self.reverse();
132        self
133    }
134}
135
136impl Clone for Effect {
137    fn clone(&self) -> Self {
138        Self { shader: self.shader.clone_box() }
139    }
140}
141
142impl Effect {
143    /// Returns the name of the underlying shader.
144    ///
145    /// # Returns
146    /// * The name of the shader as a static string.
147    pub fn name(&self) -> &'static str {
148        self.shader.name()
149    }
150
151    /// Processes the effect for the given duration. This:
152    /// 1. Updates the shader's timer with the given duration
153    /// 2. Executes the shader effect
154    /// 3. Returns any overflow duration
155    ///
156    /// # Arguments
157    /// * `duration` - The duration to process the effect for.
158    /// * `buf` - A mutable reference to the `Buffer` where the effect will be applied.
159    /// * `area` - The rectangular area within the buffer where the effect will be
160    ///   applied. If the effect has its own area set, that takes precedence.
161    ///
162    /// # Returns
163    /// * An `Option` containing the overflow duration if the effect is done, or `None` if
164    ///   it is still running.
165    ///
166    /// # Example
167    /// ```no_run
168    /// use ratatui::buffer::Buffer;
169    /// use ratatui::layout::Rect;
170    /// use tachyonfx::{Effect, fx, Interpolation, Duration};
171    ///
172    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
173    /// let area = Rect::new(0, 0, 10, 10);
174    /// let mut buffer = Buffer::empty(area);
175    /// let overflow = effect.process(Duration::from_millis(50), &mut buffer, area);
176    /// ```
177    pub fn process(
178        &mut self,
179        duration: Duration,
180        buf: &mut Buffer,
181        area: Rect,
182    ) -> Option<Duration> {
183        let area = self.shader.area().unwrap_or(area);
184        if let Some(processor) = self.shader.filter_processor_mut() {
185            processor.update(buf, area);
186        }
187
188        self.shader.process(duration, buf, area)
189    }
190
191    /// Returns true if the effect is done.
192    ///
193    /// # Returns
194    /// * `true` if the effect is done, `false` otherwise.
195    pub fn done(&self) -> bool {
196        self.shader.done()
197    }
198
199    /// Returns true if the effect is still running.
200    ///
201    /// # Returns
202    /// * `true` if the effect is running, `false` otherwise.
203    pub fn running(&self) -> bool {
204        self.shader.running()
205    }
206
207    /// Returns the area where the effect is applied.
208    ///
209    /// # Returns
210    /// * An `Option` containing the rectangular area if set, or `None` if not set.
211    pub fn area(&self) -> Option<Rect> {
212        self.shader.area()
213    }
214
215    /// Sets the area where the effect will be applied.
216    ///
217    /// # Arguments
218    /// * `area` - The rectangular area to set.
219    pub fn set_area(&mut self, area: Rect) {
220        self.shader.set_area(area)
221    }
222
223    /// Sets the cell selection strategy for the effect. Only applies the filter
224    /// if the effect doesn't already have one set.
225    ///
226    /// # Arguments
227    /// * `strategy` - The cell filter strategy to set.
228    ///
229    /// # Example
230    /// ```no_run
231    /// use tachyonfx::{CellFilter, fx, Interpolation};
232    ///
233    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
234    /// effect.filter(CellFilter::Not(CellFilter::Text.into()));
235    /// ```
236    pub fn filter(&mut self, strategy: CellFilter) {
237        self.shader.propagate_filter(strategy)
238    }
239
240    /// Reverses the effect's playback direction.
241    ///
242    /// This toggles the effect to play in the opposite direction from its
243    /// current state. Can be used to create back-and-forth animations.
244    pub fn reverse(&mut self) {
245        self.shader.reverse()
246    }
247
248    /// Returns the timer associated with this effect.
249    ///
250    /// This method is primarily used for visualization purposes, such as in the
251    /// `EffectTimeline` widget. It provides information about the duration and timing
252    /// of the effect.
253    ///
254    /// # Returns
255    /// An `Option<EffectTimer>`:
256    /// - `Some(EffectTimer)` if the effect has an associated timer.
257    /// - `None` if the effect doesn't have a specific duration (e.g., for indefinite
258    ///   effects).
259    ///
260    /// # Notes
261    /// - For composite effects (like parallel or sequential effects), this may return an
262    ///   approximation of the total duration based on the timers of child effects.
263    /// - Some effects may modify the returned timer to reflect their specific behavior
264    ///   (e.g., a ping-pong effect might double the duration).
265    /// - The returned timer should reflect the total expected duration of the effect,
266    ///   which may differ from the current remaining time.
267    pub fn timer(&self) -> Option<EffectTimer> {
268        self.shader.timer()
269    }
270
271    /// Returns a mutable reference to the effect's timer, if any.
272    ///
273    /// # Returns
274    /// * An `Option` containing a mutable reference to the effect's `EffectTimer`, or
275    ///   `None` if not applicable.
276    ///
277    /// # Example
278    /// ```no_run
279    /// use tachyonfx::{fx, Interpolation};
280    ///
281    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
282    /// if let Some(timer) = effect.timer_mut() {
283    ///     timer.reset();
284    /// }
285    /// ```
286    pub fn timer_mut(&mut self) -> Option<&mut EffectTimer> {
287        self.shader.timer_mut()
288    }
289
290    /// Returns the cell selection strategy for the effect, if any.
291    ///
292    /// # Returns
293    /// * An `Option` containing the effect's `CellFilter`, or `None` if not applicable.
294    pub fn cell_filter(&self) -> Option<&CellFilter> {
295        self.shader.cell_filter()
296    }
297
298    /// Resets the effect. Used by effects like ping_pong and repeat to reset
299    /// the hosted effect to its initial state.
300    pub fn reset(&mut self) {
301        self.shader.reset()
302    }
303
304    /// Sets a pattern for spatial alpha progression on pattern-compatible effects.
305    /// This is a no-op for effects that don't support patterns.
306    ///
307    /// # Arguments
308    /// * `pattern` - An AnyPattern enum containing the pattern to apply
309    pub(crate) fn set_pattern(&mut self, pattern: AnyPattern) {
310        self.shader.set_pattern(pattern);
311    }
312
313    /// Applies a pattern to this effect for spatial alpha progression.
314    /// This is a no-op for effects that don't support patterns.
315    ///
316    /// # Arguments
317    /// * `pattern` - A pattern that implements `Into<AnyPattern>`
318    ///
319    /// # Returns
320    /// * The same effect with the pattern applied (if supported)
321    ///
322    /// # Example
323    /// ```
324    /// use tachyonfx::{fx, pattern};
325    ///
326    /// let effect = fx::dissolve(1000)
327    ///     .with_pattern(pattern::RadialPattern::center());
328    /// ```
329    pub fn with_pattern<P>(mut self, pattern: P) -> Self
330    where
331        P: Into<AnyPattern>,
332    {
333        let any_pattern = pattern.into();
334        self.set_pattern(any_pattern);
335        self
336    }
337
338    /// Creates an `EffectSpan` representation of this effect for timeline visualization.
339    ///
340    /// # Arguments
341    /// * `offset` - The time offset when this effect should start in the timeline
342    ///
343    /// # Returns
344    /// * An `EffectSpan` that can be used in timeline widgets
345    pub fn as_effect_span(&self, offset: Duration) -> EffectSpan
346    where
347        Self: Sized + Clone,
348    {
349        self.shader.as_ref().as_effect_span(offset)
350    }
351
352    /// Attempts to convert this effect to a DSL effect expression.
353    ///
354    /// # Returns
355    ///
356    /// Returns a `Result` containing either:
357    /// - `Ok(EffectExpression)` if conversion is successful
358    /// - `Err(DslError::EffectExpressionNotSupported)` containing the effect name if this
359    ///   effect type doesn't support conversion to the DSL format
360    ///
361    /// # Errors
362    ///
363    /// This method returns an error if the underlying shader doesn't support DSL
364    /// conversion.
365    #[cfg(feature = "dsl")]
366    pub fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
367        self.shader.to_dsl()
368    }
369}
370
371/// Trait for converting shader types into Effect instances.
372pub trait IntoEffect {
373    /// Converts this shader into an Effect.
374    fn into_effect(self) -> Effect;
375}
376
377impl<S> IntoEffect for S
378where
379    S: Shader + 'static,
380{
381    fn into_effect(self) -> Effect {
382        Effect::new(self)
383    }
384}
385
386/// Extension trait for shader filter propagation logic.
387pub(crate) trait ShaderExt {
388    /// Propagates the cell filter to the shader if it is not already set.
389    ///
390    /// This method only applies the filter if the shader doesn't already have one,
391    /// preserving existing filters during effect composition.
392    fn propagate_filter(&mut self, cell_filter: CellFilter);
393}
394
395impl<S: Shader + 'static> ShaderExt for S {
396    fn propagate_filter(&mut self, cell_filter: CellFilter) {
397        if self.cell_filter().is_none() {
398            self.filter(cell_filter);
399        }
400    }
401}
402
403impl ShaderExt for dyn Shader {
404    fn propagate_filter(&mut self, cell_filter: CellFilter) {
405        if self.cell_filter().is_none() {
406            self.filter(cell_filter);
407        }
408    }
409}
410
411// PatternedEffect is no longer needed since Effect now has .with_pattern() directly