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